diff --git a/packages/devtools_app/integration_test/test/live_connection/memory_screen_helpers.dart b/packages/devtools_app/integration_test/test/live_connection/memory_screen_helpers.dart index 87efb707ff5..2753d9704ad 100644 --- a/packages/devtools_app/integration_test/test/live_connection/memory_screen_helpers.dart +++ b/packages/devtools_app/integration_test/test/live_connection/memory_screen_helpers.dart @@ -3,9 +3,9 @@ // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/framework/scaffold/bottom_pane.dart'; import 'package:devtools_app/src/screens/memory/panes/control/widgets/primary_controls.dart'; import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart'; -import 'package:devtools_app/src/shared/console/widgets/console_pane.dart'; import 'package:devtools_test/helpers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -45,7 +45,7 @@ Future prepareMemoryUI( // but not too big to make classes in snapshot hidden. const dragDistance = -320.0; await tester.drag( - find.byType(ConsolePaneHeader), + find.byKey(BottomPane.splitterKey), const Offset(0, dragDistance), ); await tester.pumpAndSettle(); diff --git a/packages/devtools_app/lib/src/framework/scaffold/bottom_pane.dart b/packages/devtools_app/lib/src/framework/scaffold/bottom_pane.dart new file mode 100644 index 00000000000..bba85e2633b --- /dev/null +++ b/packages/devtools_app/lib/src/framework/scaffold/bottom_pane.dart @@ -0,0 +1,38 @@ +// Copyright 2025 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 'package:flutter/material.dart'; + +import '../../shared/ui/tab.dart'; + +/// A widget that displays a tabbed view at the bottom of the DevTools screen. +/// +/// This widget is used to host views like the console and the AI Assistant. +class BottomPane extends StatelessWidget { + const BottomPane({super.key, required this.screenId, required this.tabs}) + : assert(tabs.length > 0); + + static const splitterKey = Key('Bottom Pane Splitter'); + + final String screenId; + final List tabs; + + @override + Widget build(BuildContext context) { + return AnalyticsTabbedView( + gaScreen: screenId, + tabs: tabs + .map((tabbedPane) => (tab: tabbedPane.tab, tabView: tabbedPane)) + .toList(), + staticSingleTab: true, + ); + } +} + +/// An interface for a widget that should be displayed as a tab in the +/// [BottomPane]. +abstract class TabbedPane implements Widget { + /// The tab to display for this pane. + DevToolsTab get tab; +} diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart index 823ad421da7..978f7b3556b 100644 --- a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart +++ b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import '../../app.dart'; import '../../extensions/extension_settings.dart'; import '../../screens/debugger/debugger_screen.dart'; +import '../../shared/ai_assistant/widgets/ai_assistant_pane.dart'; import '../../shared/analytics/prompt.dart'; import '../../shared/config_specific/drag_and_drop/drag_and_drop.dart'; import '../../shared/config_specific/import_export/import_export.dart'; @@ -25,6 +26,7 @@ import '../../shared/primitives/query_parameters.dart'; import '../../shared/title.dart'; import 'about_dialog.dart'; import 'app_bar.dart'; +import 'bottom_pane.dart'; import 'report_feedback_button.dart'; import 'settings_dialog.dart'; import 'status_line.dart'; @@ -310,10 +312,16 @@ class DevToolsScaffoldState extends State return Provider.value( value: _importController, builder: (context, _) { - final showConsole = + final isConnectedAppView = serviceConnection.serviceManager.connectedAppInitialized && - !offlineDataController.showingOfflineData.value && - _currentScreen.showConsole(widget.embedMode); + !offlineDataController.showingOfflineData.value; + final showConsole = + isConnectedAppView && _currentScreen.showConsole(widget.embedMode); + final showAiAssistant = + FeatureFlags.aiAssistant.isEnabled && + isConnectedAppView && + _currentScreen.showAiAssistant(); + final showBottomPane = showConsole || showAiAssistant; final containsSingleSimpleScreen = widget.screens.length == 1 && widget.screens.first is SimpleScreen; final showAppBar = @@ -345,20 +353,24 @@ class DevToolsScaffoldState extends State body: OutlineDecoration.onlyTop( child: Padding( padding: widget.appPadding, - child: showConsole + child: showBottomPane ? SplitPane( axis: Axis.vertical, - splitters: [ConsolePaneHeader()], initialFractions: const [0.8, 0.2], - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: intermediateSpacing, - ), - child: content, + splitters: const [ + DefaultSplitter( + key: BottomPane.splitterKey, + isHorizontal: true, ), - RoundedOutlinedBorder.onlyBottom( - child: const ConsolePane(), + ], + children: [ + content, + BottomPane( + screenId: _currentScreen.screenId, + tabs: [ + if (showConsole) const ConsolePane(), + if (showAiAssistant) const AiAssistantPane(), + ], ), ], ) diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart index b8b7744cbd2..710743dee72 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart +++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart @@ -25,6 +25,9 @@ class InspectorScreen extends Screen { @override bool showConsole(EmbedMode embedMode) => !embedMode.embedded; + @override + bool showAiAssistant() => true; + @override String get docPageId => screenId; diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart index cd526324e2f..5171c5e6736 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -39,6 +39,9 @@ class NetworkScreen extends Screen { @override String get docPageId => screenId; + @override + bool showAiAssistant() => true; + @override Widget buildScreenBody(BuildContext context) => const NetworkScreenBody(); diff --git a/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart b/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart new file mode 100644 index 00000000000..b3d2128f4f6 --- /dev/null +++ b/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart @@ -0,0 +1,29 @@ +// Copyright 2025 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 'package:flutter/material.dart'; + +import '../../../framework/scaffold/bottom_pane.dart'; +import '../../ui/tab.dart'; + +class AiAssistantPane extends StatelessWidget implements TabbedPane { + const AiAssistantPane({super.key}); + + @override + DevToolsTab get tab => + DevToolsTab.create(tabName: _tabName, gaPrefix: _gaPrefix); + + static const _tabName = 'AI Assistant'; + + static const _gaPrefix = 'aiAssistant'; + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Expanded(child: Center(child: Text('TODO: Implement AI Assistant.'))), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart b/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart index 5a40db7b21f..4d661cc881d 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart @@ -6,8 +6,10 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../../framework/scaffold/bottom_pane.dart'; import '../../globals.dart'; import '../../ui/common_widgets.dart'; +import '../../ui/tab.dart'; import '../console.dart'; import '../console_service.dart'; import 'evaluate.dart'; @@ -15,31 +17,8 @@ import 'help_dialog.dart'; // TODO(devoncarew): Show some small UI indicator when we receive stdout/stderr. -class ConsolePaneHeader extends AreaPaneHeader { - ConsolePaneHeader({super.key}) - : super( - title: const Text('Console'), - roundedTopBorder: true, - actions: [ - const ConsoleHelpLink(), - const SizedBox(width: densePadding), - CopyToClipboardControl( - dataProvider: () => - serviceConnection.consoleService.stdio.value.join('\n'), - buttonKey: ConsolePane.copyToClipboardButtonKey, - ), - const SizedBox(width: densePadding), - DeleteControl( - buttonKey: ConsolePane.clearStdioButtonKey, - tooltip: 'Clear console output', - onPressed: () => serviceConnection.consoleService.clearStdio(), - ), - ], - ); -} - /// Display the stdout and stderr output from the process under debug. -class ConsolePane extends StatelessWidget { +class ConsolePane extends StatelessWidget implements TabbedPane { const ConsolePane({super.key}); static const copyToClipboardButtonKey = Key( @@ -47,6 +26,17 @@ class ConsolePane extends StatelessWidget { ); static const clearStdioButtonKey = Key('console_clear_stdio_button'); + static const _tabName = 'Console'; + + static const _gaPrefix = 'consolePane'; + + @override + DevToolsTab get tab => DevToolsTab.create( + tabName: _tabName, + gaPrefix: _gaPrefix, + trailing: const _ConsoleActions(), + ); + ValueListenable> get stdio => serviceConnection.consoleService.stdio; @@ -61,10 +51,30 @@ class ConsolePane extends StatelessWidget { footer = const ExpressionEvalField(); } - return Column( - children: [ - Expanded( - child: Console(lines: stdio, footer: footer), + return Console(lines: stdio, footer: footer); + } +} + +class _ConsoleActions extends StatelessWidget { + const _ConsoleActions(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const ConsoleHelpLink(), + const SizedBox(width: densePadding), + CopyToClipboardControl( + dataProvider: () => + serviceConnection.consoleService.stdio.value.join('\n'), + buttonKey: ConsolePane.copyToClipboardButtonKey, + ), + const SizedBox(width: densePadding), + DeleteControl( + buttonKey: ConsolePane.clearStdioButtonKey, + tooltip: 'Clear console output', + onPressed: () => serviceConnection.consoleService.clearStdio(), ), ], ); diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index e15d825a808..88a8b248878 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -85,6 +85,14 @@ extension FeatureFlags on Never { enabled: true, ); + /// Flag to enable the AI Assistant. + /// + /// https://github.com/flutter/devtools/issues/9590 + static final aiAssistant = BooleanFeatureFlag( + name: 'aiAssistant', + enabled: enableExperiments, + ); + /// A set of all the boolean feature flags for debugging purposes. /// /// When adding a new boolean flag, you are responsible for adding it to this @@ -95,6 +103,7 @@ extension FeatureFlags on Never { devToolsExtensions, dapDebugging, inspectorV2, + aiAssistant, }; /// A set of all the Flutter channel feature flags for debugging purposes. diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index c3b49317b6f..0cdc840653a 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -290,6 +290,9 @@ abstract class Screen { /// Whether to show the console for this screen. bool showConsole(EmbedMode embedMode) => false; + /// Whether to show the AI Assistant for this screen. + bool showAiAssistant() => false; + /// Which keyboard shortcuts should be enabled for this screen. ShortcutsConfiguration buildKeyboardShortcuts(BuildContext context) => ShortcutsConfiguration.empty(); diff --git a/packages/devtools_app/lib/src/shared/ui/tab.dart b/packages/devtools_app/lib/src/shared/ui/tab.dart index ebd6e2a9625..6961b55d0ad 100644 --- a/packages/devtools_app/lib/src/shared/ui/tab.dart +++ b/packages/devtools_app/lib/src/shared/ui/tab.dart @@ -81,6 +81,7 @@ class AnalyticsTabbedView extends StatefulWidget { this.onTabChanged, this.initialSelectedIndex, this.analyticsSessionIdentifier, + this.staticSingleTab = false, }) : trailingWidgets = List.generate( tabs.length, (index) => tabs[index].tab.trailing ?? const SizedBox(), @@ -106,6 +107,10 @@ class AnalyticsTabbedView extends StatefulWidget { /// events. final String? analyticsSessionIdentifier; + /// When there is only a single tab, whether to display that tab as a static + /// title instead of in a [TabBar]. + final bool staticSingleTab; + /// Whether to send analytics events to GA. /// /// Only set this to false if [AnalyticsTabbedView] is being used for @@ -202,13 +207,10 @@ class _AnalyticsTabbedViewState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: TabBar( - labelColor: Theme.of(context).colorScheme.onSurface, - controller: _tabController, - tabs: widget.tabs.map((t) => t.tab).toList(), - isScrollable: true, - ), + _AnalyticsTabBar( + tabs: widget.tabs.map((t) => t.tab).toList(), + tabController: _tabController, + staticSingleTab: widget.staticSingleTab, ), widget.trailingWidgets[_currentTabControllerIndex], ], @@ -233,3 +235,34 @@ class _AnalyticsTabbedViewState extends State ); } } + +/// A [TabBar] used by [AnalyticsTabbedView]. +/// +/// When there is only a single tab and [staticSingleTab] is true, this tab bar +/// will be displayed as a static title. +class _AnalyticsTabBar extends StatelessWidget { + const _AnalyticsTabBar({ + required this.tabs, + required this.tabController, + required this.staticSingleTab, + }); + + static const _tabPadding = 14.0; + + final List tabs; + final TabController? tabController; + final bool staticSingleTab; + + @override + Widget build(BuildContext context) => (staticSingleTab && tabs.length == 1) + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: _tabPadding), + child: tabs.first, + ) + : TabBar( + labelColor: Theme.of(context).colorScheme.onSurface, + controller: tabController, + tabs: tabs, + isScrollable: true, + ); +} diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart new file mode 100644 index 00000000000..8b3d04f0cb4 --- /dev/null +++ b/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart @@ -0,0 +1,159 @@ +// Copyright 2025 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 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/framework/scaffold/scaffold.dart'; +import 'package:devtools_app/src/shared/ai_assistant/widgets/ai_assistant_pane.dart'; +import 'package:devtools_app/src/shared/feature_flags.dart'; +import 'package:devtools_app/src/shared/framework/framework_controller.dart'; +import 'package:devtools_app/src/shared/managers/survey.dart'; +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:devtools_test/helpers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + late MockServiceConnectionManager mockServiceConnection; + late MockServiceManager mockServiceManager; + + setUp(() { + mockServiceConnection = createMockServiceConnectionWithDefaults(); + mockServiceManager = + mockServiceConnection.serviceManager as MockServiceManager; + when( + mockServiceManager.connectedState, + ).thenReturn(ValueNotifier(const ConnectedState(false))); + final mockErrorBadgeManager = MockErrorBadgeManager(); + when( + mockServiceConnection.errorBadgeManager, + ).thenReturn(mockErrorBadgeManager); + when( + mockErrorBadgeManager.errorCountNotifier(any), + ).thenReturn(ValueNotifier(0)); + + setGlobal(ServiceConnectionManager, mockServiceConnection); + setGlobal(FrameworkController, FrameworkController()); + setGlobal(SurveyService, SurveyService()); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(NotificationService, NotificationService()); + setGlobal(BannerMessagesController, BannerMessagesController()); + }); + + Future pumpScaffold( + WidgetTester tester, { + required Screen screen, + bool withConnectedApp = true, + bool withOfflineData = false, + }) async { + if (withOfflineData) { + final offlineController = MockOfflineDataController(); + offlineController.showingOfflineData.value = true; + setGlobal(OfflineDataController, offlineController); + } + + MockConnectedApp? connectedApp; + if (withConnectedApp) { + connectedApp = MockConnectedApp(); + mockConnectedApp(connectedApp); + } + when( + mockServiceManager.connectedAppInitialized, + ).thenReturn(withConnectedApp); + when(mockServiceManager.connectedApp).thenReturn(connectedApp); + + await tester.pumpWidget( + wrapWithControllers( + DevToolsScaffold(screens: [screen]), + analytics: AnalyticsController( + enabled: false, + shouldShowConsentMessage: false, + consentMessage: 'fake message', + ), + ), + ); + } + + group('AI Assistant pane', () { + testWidgets('is visible for supported screens', ( + WidgetTester tester, + ) async { + FeatureFlags.aiAssistant.setEnabledForTests(true); + + await pumpScaffold(tester, screen: const _TestScreenWithAi()); + + expect(find.byType(AiAssistantPane), findsOneWidget); + }); + + testWidgets('is not visible for unsupported screens', ( + WidgetTester tester, + ) async { + FeatureFlags.aiAssistant.setEnabledForTests(true); + + await pumpScaffold(tester, screen: const _TestScreenWithoutAi()); + + expect(find.byType(AiAssistantPane), findsNothing); + }); + + testWidgets('is not visible when app is not connected', ( + WidgetTester tester, + ) async { + FeatureFlags.aiAssistant.setEnabledForTests(true); + + await pumpScaffold( + tester, + screen: const _TestScreenWithAi(), + withConnectedApp: false, + ); + + expect(find.byType(AiAssistantPane), findsNothing); + }); + + testWidgets('is not visible when feature flag is disabled', ( + WidgetTester tester, + ) async { + FeatureFlags.aiAssistant.setEnabledForTests(false); + + await pumpScaffold(tester, screen: const _TestScreenWithAi()); + + expect(find.byType(AiAssistantPane), findsNothing); + }); + + testWidgets('is not visible when in offline mode', ( + WidgetTester tester, + ) async { + FeatureFlags.aiAssistant.setEnabledForTests(true); + + await pumpScaffold( + tester, + screen: const _TestScreenWithAi(), + withOfflineData: true, + ); + + expect(find.byType(AiAssistantPane), findsNothing); + }); + }); +} + +class _TestScreenWithAi extends Screen { + const _TestScreenWithAi() + : super('test_screen_with_ai', showFloatingDebuggerControls: false); + + @override + bool showAiAssistant() => true; + + @override + Widget buildScreenBody(BuildContext context) => const SizedBox(); +} + +class _TestScreenWithoutAi extends Screen { + const _TestScreenWithoutAi() + : super('test_screen_without_ai', showFloatingDebuggerControls: false); + + @override + Widget buildScreenBody(BuildContext context) => const SizedBox(); +} diff --git a/packages/devtools_app/test/screens/debugger/debugger_console_test.dart b/packages/devtools_app/test/screens/debugger/debugger_console_test.dart index 3552e909eb2..392bc0db2e4 100644 --- a/packages/devtools_app/test/screens/debugger/debugger_console_test.dart +++ b/packages/devtools_app/test/screens/debugger/debugger_console_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/framework/scaffold/bottom_pane.dart'; import 'package:devtools_app/src/shared/console/widgets/console_pane.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; @@ -51,8 +52,12 @@ void main() { wrapWithControllers( Row( children: [ - Flexible(child: ConsolePaneHeader()), - const Expanded(child: ConsolePane()), + Expanded( + child: BottomPane( + screenId: 'debugger', + tabs: const [ConsolePane()], + ), + ), ], ), debugger: controller, diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart index 63bc0942f43..daa2f649724 100644 --- a/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart +++ b/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/framework/scaffold/bottom_pane.dart'; import 'package:devtools_app/src/screens/debugger/debugger_model.dart'; import 'package:devtools_app/src/shared/console/widgets/console_pane.dart'; import 'package:devtools_app_shared/ui.dart'; @@ -85,8 +86,12 @@ void main() { wrapWithControllers( Row( children: [ - Flexible(child: ConsolePaneHeader()), - const Expanded(child: ConsolePane()), + Expanded( + child: BottomPane( + screenId: 'debugger', + tabs: const [ConsolePane()], + ), + ), ], ), debugger: controller, diff --git a/packages/devtools_app/test/shared/ansi_output_test.dart b/packages/devtools_app/test/shared/ansi_output_test.dart index e0de53d7af3..639593cdf9d 100644 --- a/packages/devtools_app/test/shared/ansi_output_test.dart +++ b/packages/devtools_app/test/shared/ansi_output_test.dart @@ -252,12 +252,7 @@ void main() { ) async { await tester.pumpWidget( wrapWithControllers( - Row( - children: [ - Flexible(child: ConsolePaneHeader()), - const Expanded(child: ConsolePane()), - ], - ), + const Row(children: [Expanded(child: ConsolePane())]), debugger: controller, ), ); diff --git a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart index 1ac9a1a0dac..826708d5cec 100644 --- a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart +++ b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart @@ -21,6 +21,7 @@ void main() { expect(FeatureFlags.devToolsExtensions.isEnabled, isExternalBuild); expect(FeatureFlags.dapDebugging.isEnabled, false); expect(FeatureFlags.inspectorV2.isEnabled, true); + expect(FeatureFlags.aiAssistant.isEnabled, false); }); group('FlutterChannelFeatureFlag', () { diff --git a/packages/devtools_app_shared/lib/src/ui/split_pane.dart b/packages/devtools_app_shared/lib/src/ui/split_pane.dart index b870cec24ac..59e5ef576dc 100644 --- a/packages/devtools_app_shared/lib/src/ui/split_pane.dart +++ b/packages/devtools_app_shared/lib/src/ui/split_pane.dart @@ -295,7 +295,8 @@ final class _SplitPaneState extends State { } } -final class DefaultSplitter extends StatelessWidget { +final class DefaultSplitter extends StatelessWidget + implements PreferredSizeWidget { const DefaultSplitter({super.key, required this.isHorizontal}); static const iconSize = 24.0; @@ -303,6 +304,9 @@ final class DefaultSplitter extends StatelessWidget { final bool isHorizontal; + @override + Size get preferredSize => const Size(splitterWidth, iconSize); + @override Widget build(BuildContext context) { return Transform.rotate(