blob: 7ebbc86602d9ea331b3b6bd99d8b5f7a5d072e05 [file] [log] [blame]
// Copyright 2023 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:devtools_app/src/shared/primitives/list_queue_value_notifier.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/material.dart';
import 'editor_service/simulated_editor.dart';
import 'shared/common_ui.dart';
/// A simple UI that acts as a stand-in host editor to simplify the development
/// workflow when working on embedded tooling.
///
/// Uses a [SimulatedEditor] to provide functionality over DTD (or legacy
/// `postMessage`).
class MockEditorWidget extends StatefulWidget {
const MockEditorWidget({
super.key,
required this.editor,
required this.clientLog,
this.child,
});
/// The fake editor API we can use to simulate an editor.
final SimulatedEditor editor;
/// A stream of protocol traffic between the sidebar and DTD.
final Stream<String> clientLog;
final Widget? child;
@override
State<MockEditorWidget> createState() => _MockEditorWidgetState();
}
class _MockEditorWidgetState extends State<MockEditorWidget>
with AutoDisposeMixin {
SimulatedEditor get editor => widget.editor;
Stream<String> get clientLog => widget.clientLog;
Stream<String> get editorLog => editor.log;
/// The number of communication messages to keep in the logs.
static const maxLogEvents = 20;
/// The last [maxLogEvents] communication messages sent between the sidebar
/// and DTD.
final clientLogRing = ListQueueValueNotifier<String>(ListQueue());
/// The last [maxLogEvents] communication messages sent between the editor
/// and DTD.
final editorLogRing = ListQueueValueNotifier<String>(ListQueue());
/// Flutter icon for the sidebar.
final sidebarImageBytes = base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAABF1BMVEUAAAD///////+/v//MzMzb29vf39/j4+PV1erY2Njb29vS4eHX1+TZ2ebW1uDY2OLW3d3Y2N7Z2d/a2uDV2+DW2+DX3OHZ2eLZ2d7V2t/Y2OHX29/X29/Z2eDW2eDW2uDX2uHW2d/X2uDY2+HW2d/W2+HW2eHX2d/W2+DW2eDX2eHX2uHX29/X2d/Y2uDY2uDW2uDX2uDX2+DX2+DX2eDX2t/Y2+DX29/Y2eDW2eDX2uDX2uDW2d/X2uDX2uDY2uDX2uHX2eDX2uDX2uHY2t/X2+DX2uDY2uDX2uDX2uDX2+DW2uDX2eDX2uDX2uDX2uDX2eDX2uDX2uDX2uDX2uDX2uDX2uDX2uDX2uDX2uDX2uDX2uDX2uANs9umAAAAXHRSTlMAAgMEBQcICQwNDhETFBkaJScoKTEyMzU2Nzs/QElKS0xQU1VYXV5fY2RlbXh5e3yDi4yNjpmboaKjpKepqrO1ub7AwcLEzM/R2Nnc4OPk5efr7O3w8vT3+Pn7/A+G+WEAAAABYktHRAH/Ai3eAAAA0UlEQVQoz2NgQAKythCgwYAKFCLtTIHAO0YbVVw23AREqUTroYlH0FrcGK94FJq4HExcH5c4t5IyGAiCxeUjDUGUWrQOr0cMBJiDJYwiJYCkarQOt5sXP5Al4OvKBZZgsgqRBJsDERf0c+GE2sFsE2IAVy/k78wBt53ZJkYXKi4c4MCO5C4mCR53Tz4gQyTIng3VyVoxSiDK04cVLY6YLEOlQE4PN2NElzEPkwFS0qHWLNhlxIPt2LDLiAY6cmDaoygmJqYe4cSJLmMBDStNIAcAHhssjDYY1ccAAAAASUVORK5CYII=',
);
@override
void initState() {
super.initState();
// Listen to the log streams to maintain our buffer and trigger rebuilds.
autoDisposeStreamSubscription(
clientLog.listen((log) {
clientLogRing.add(log);
while (clientLogRing.length > maxLogEvents) {
clientLogRing.removeFirst();
}
}),
);
autoDisposeStreamSubscription(
editorLog.listen((log) {
editorLogRing.add(log);
while (editorLogRing.length > maxLogEvents) {
editorLogRing.removeFirst();
}
}),
);
}
@override
Widget build(BuildContext context) {
final editorTheme = VsCodeTheme.of(context);
final theme = Theme.of(context);
return SplitPane(
axis: Axis.horizontal,
initialFractions: const [0.25, 0.75],
minSizes: const [200, 200],
children: [
Row(
children: [
SizedBox(
width: 48,
child: Container(
alignment: Alignment.topCenter,
padding: const EdgeInsets.only(top: 60),
constraints: const BoxConstraints.expand(width: 48),
color: editorTheme.activityBarBackgroundColor,
child: Image.memory(sidebarImageBytes),
),
),
Expanded(
child: Container(
color: editorTheme.sidebarBackgroundColor,
child: widget.child ?? const Placeholder(),
),
),
],
),
SplitPane(
axis: Axis.vertical,
initialFractions: const [0.5, 0.5],
minSizes: const [200, 200],
children: [
Container(
color: editorTheme.editorBackgroundColor,
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Mock Editor', style: theme.textTheme.headlineMedium),
const SizedBox(height: defaultSpacing),
const Text(
'Use these buttons to simulate actions that would usually occur in the IDE.',
),
const SizedBox(height: defaultSpacing),
Row(
children: [
const Text('Editor: '),
ElevatedButton(
onPressed:
editor.connected
? null
: _withUpdate(editor.connectEditor),
child: const Text('Connect'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
editor.connected
? _withUpdate(editor.disconnectEditor)
: null,
child: const Text('Disconnect'),
),
],
),
const SizedBox(height: defaultSpacing),
Row(
children: [
const Text('Devices: '),
ElevatedButton(
onPressed: editor.connectDevices,
child: const Text('Connect'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed: editor.disconnectDevices,
child: const Text('Disconnect'),
),
],
),
const SizedBox(height: defaultSpacing),
const Text('Debug Sessions: '),
const SizedBox(height: denseSpacing),
Row(
children: [
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'macos',
flutterMode: 'debug',
),
child: const Text('Desktop debug'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'macos',
flutterMode: 'profile',
),
child: const Text('Desktop profile'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'macos',
flutterMode: 'release',
),
child: const Text('Desktop release'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'macos',
flutterMode: 'jit_release',
),
child: const Text('Desktop jit_release'),
),
],
),
const SizedBox(height: denseSpacing),
Row(
children: [
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'chrome',
flutterMode: 'debug',
),
child: const Text('Web debug'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'chrome',
flutterMode: 'profile',
),
child: const Text('Web profile'),
),
const SizedBox(width: denseSpacing),
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Flutter',
deviceId: 'chrome',
flutterMode: 'release',
),
child: const Text('Web release'),
),
],
),
const SizedBox(height: denseSpacing),
Row(
children: [
ElevatedButton(
onPressed:
() => editor.startSession(
debuggerType: 'Dart',
deviceId: 'macos',
),
child: const Text('Dart CLI'),
),
],
),
const SizedBox(height: denseSpacing),
ElevatedButton(
onPressed: () => editor.stopAllSessions(),
style: theme.elevatedButtonTheme.style!.copyWith(
backgroundColor: const WidgetStatePropertyAll(Colors.red),
),
child: const Text('Stop All'),
),
],
),
),
DefaultTabController(
length: 2,
child: Container(
color: editorTheme.editorBackgroundColor,
padding: const EdgeInsets.all(10),
child: Column(
children: [
const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Client/Sidebar Log'),
Tab(text: 'Server Log'),
],
),
Expanded(
child: TabBarView(
children: [
for (final logRing in [clientLogRing, editorLogRing])
ValueListenableBuilder(
valueListenable: logRing,
builder: (context, logRing, _) {
return ListView.builder(
itemCount: logRing.length,
itemBuilder:
(context, index) =>
OutlineDecoration.onlyBottom(
child: Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(
vertical: denseSpacing,
),
child: Text(
logRing.elementAt(index),
style:
Theme.of(
context,
).fixedFontStyle,
),
),
),
);
},
),
],
),
),
],
),
),
),
],
),
],
);
}
/// Returns a function that calls [f] and then once it completes, [setState].
Future<void> Function() _withUpdate<T>(FutureOr<T> Function() f) {
return () async {
await f();
setState(() {});
};
}
}