diff --git a/build/native_assets/linux/native_assets.json b/build/native_assets/linux/native_assets.json new file mode 100644 index 00000000000..523bfc7c60f --- /dev/null +++ b/build/native_assets/linux/native_assets.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/build/unit_test_assets/AssetManifest.bin b/build/unit_test_assets/AssetManifest.bin new file mode 100644 index 00000000000..86d111f09a9 Binary files /dev/null and b/build/unit_test_assets/AssetManifest.bin differ diff --git a/build/unit_test_assets/FontManifest.json b/build/unit_test_assets/FontManifest.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/build/unit_test_assets/FontManifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/build/unit_test_assets/NOTICES.Z b/build/unit_test_assets/NOTICES.Z new file mode 100644 index 00000000000..8876bc12f7f Binary files /dev/null and b/build/unit_test_assets/NOTICES.Z differ diff --git a/build/unit_test_assets/NativeAssetsManifest.json b/build/unit_test_assets/NativeAssetsManifest.json new file mode 100644 index 00000000000..523bfc7c60f --- /dev/null +++ b/build/unit_test_assets/NativeAssetsManifest.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/build/unit_test_assets/shaders/ink_sparkle.frag b/build/unit_test_assets/shaders/ink_sparkle.frag new file mode 100644 index 00000000000..ebd80664494 Binary files /dev/null and b/build/unit_test_assets/shaders/ink_sparkle.frag differ diff --git a/build/unit_test_assets/shaders/stretch_effect.frag b/build/unit_test_assets/shaders/stretch_effect.frag new file mode 100644 index 00000000000..3c938de1c44 Binary files /dev/null and b/build/unit_test_assets/shaders/stretch_effect.frag differ diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart index 83043cdb869..7e554e86bea 100644 --- a/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart +++ b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart @@ -73,7 +73,7 @@ class EvalTester { Future selectWidgetTreeNode(Finder finder) async { await tapAndPump( find.descendant( - of: find.byKey(InspectorScreenBodyState.summaryTreeKey), + of: find.byKey(InspectorScreenBodyState.inspectorTreeKey), matching: finder, ), ); diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index 5917216e754..e3657941a56 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -25,10 +25,10 @@ export 'src/screens/deep_link_validation/deep_links_screen.dart'; export 'src/screens/dtd/dtd_tools_controller.dart'; export 'src/screens/dtd/dtd_tools_screen.dart'; export 'src/screens/inspector/inspector_controller.dart'; +export 'src/screens/inspector/inspector_screen.dart'; export 'src/screens/inspector/inspector_screen_body.dart'; +export 'src/screens/inspector/inspector_screen_controller.dart'; export 'src/screens/inspector/inspector_tree_controller.dart'; -export 'src/screens/inspector_shared/inspector_screen.dart'; -export 'src/screens/inspector_shared/inspector_screen_controller.dart'; export 'src/screens/logging/log_details_controller.dart'; export 'src/screens/logging/logging_controller.dart'; export 'src/screens/logging/logging_screen.dart'; @@ -76,7 +76,7 @@ export 'src/shared/analytics/analytics_controller.dart'; export 'src/shared/charts/treemap.dart'; export 'src/shared/console/console_service.dart'; export 'src/shared/console/eval/eval_service.dart'; -export 'src/shared/console/eval/inspector_tree.dart'; +export 'src/shared/console/eval/inspector_tree_v2.dart'; export 'src/shared/console/primitives/simple_items.dart'; export 'src/shared/console/widgets/description.dart'; export 'src/shared/diagnostics/diagnostics_node.dart'; diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 43c72cd6b7c..5c0a0f16465 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -31,8 +31,8 @@ import 'screens/deep_link_validation/deep_links_controller.dart'; import 'screens/deep_link_validation/deep_links_screen.dart'; import 'screens/dtd/dtd_tools_controller.dart'; import 'screens/dtd/dtd_tools_screen.dart'; -import 'screens/inspector_shared/inspector_screen.dart'; -import 'screens/inspector_shared/inspector_screen_controller.dart'; +import 'screens/inspector/inspector_screen.dart'; +import 'screens/inspector/inspector_screen_controller.dart'; import 'screens/logging/logging_controller.dart'; import 'screens/logging/logging_screen.dart'; import 'screens/memory/framework/memory_controller.dart'; @@ -684,7 +684,7 @@ List defaultScreens({ HomeScreen(sampleData: sampleData), ), // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up after - // Inspector V2 has been released. + // Inspector has been released. DevToolsScreen( InspectorScreen(), createController: (_) => InspectorScreenController(), diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart deleted file mode 100644 index f5db041746e..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2021 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_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/console/eval/inspector_tree.dart'; -import '../../shared/primitives/diagnostics_text_styles.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/ui/common_widgets.dart'; - -class InspectorBreadcrumbNavigator extends StatelessWidget { - const InspectorBreadcrumbNavigator({ - super.key, - required this.items, - required this.onTap, - }); - - /// Max number of visible breadcrumbs including root item but not 'more' item. - /// E.g. value 5 means root and 4 breadcrumbs can be displayed, other - /// breadcrumbs (if any) will be replaced by '...' item. - static const _maxNumberOfBreadcrumbs = 5; - - final List items; - final void Function(InspectorTreeNode?) onTap; - - @override - Widget build(BuildContext context) { - if (items.isEmpty) { - return const SizedBox(); - } - - final breadcrumbs = _generateBreadcrumbs(items); - return SizedBox( - height: Breadcrumb.height, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Row( - children: breadcrumbs.map((item) { - if (item.isChevron) { - return const Icon(Icons.chevron_right, size: defaultIconSize); - } - - return Flexible( - child: _InspectorBreadcrumb( - data: item, - onTap: () => onTap(item.node), - ), - ); - }).toList(), - ), - ), - ); - } - - List<_InspectorBreadcrumbData> _generateBreadcrumbs( - List nodes, - ) { - final lastNode = nodes.safeLast; - final items = nodes.map((node) { - return _InspectorBreadcrumbData.wrap( - node: node, - isSelected: node == lastNode, - ); - }).toList(); - List<_InspectorBreadcrumbData> breadcrumbs; - breadcrumbs = items.length > _maxNumberOfBreadcrumbs - ? [ - items[0], - _InspectorBreadcrumbData.more(), - ...items.sublist( - items.length - _maxNumberOfBreadcrumbs + 1, - items.length, - ), - ] - : items; - - return breadcrumbs.joinWith(_InspectorBreadcrumbData.chevron()); - } -} - -class _InspectorBreadcrumb extends StatelessWidget { - const _InspectorBreadcrumb({required this.data, required this.onTap}); - - static const _iconScale = 0.75; - - final _InspectorBreadcrumbData data; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final text = Text( - data.text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: DiagnosticsTextStyles.regular( - Theme.of(context).colorScheme, - ).copyWith(fontSize: 11), - ); - - final icon = data.icon == null - ? null - : Transform.scale( - scale: _iconScale, - child: Padding( - padding: const EdgeInsets.only(right: iconPadding), - child: data.icon, - ), - ); - - return InkWell( - onTap: data.isClickable ? onTap : null, - borderRadius: defaultBorderRadius, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: densePadding, - vertical: borderPadding, - ), - decoration: BoxDecoration( - borderRadius: defaultBorderRadius, - color: data.isSelected - ? Theme.of(context).colorScheme.selectedRowBackgroundColor - : Colors.transparent, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) icon, - Flexible(child: text), - ], - ), - ), - ); - } -} - -class _InspectorBreadcrumbData { - const _InspectorBreadcrumbData._({ - required this.node, - required this.isSelected, - required this.alternativeText, - required this.alternativeIcon, - }); - - factory _InspectorBreadcrumbData.wrap({ - required InspectorTreeNode node, - required bool isSelected, - }) { - return _InspectorBreadcrumbData._( - node: node, - isSelected: isSelected, - alternativeText: null, - alternativeIcon: null, - ); - } - - /// Construct a special item for showing '…' symbol between other items - factory _InspectorBreadcrumbData.more() { - return const _InspectorBreadcrumbData._( - node: null, - isSelected: false, - alternativeText: _ellipsisValue, - alternativeIcon: null, - ); - } - - factory _InspectorBreadcrumbData.chevron() { - return const _InspectorBreadcrumbData._( - node: null, - isSelected: false, - alternativeText: null, - alternativeIcon: _breadcrumbSeparatorIcon, - ); - } - - static const _ellipsisValue = '…'; - static const _breadcrumbSeparatorIcon = Icons.chevron_right; - - final InspectorTreeNode? node; - final IconData? alternativeIcon; - final String? alternativeText; - final bool isSelected; - - String get text => alternativeText ?? node?.diagnostic?.description ?? ''; - - Widget? get icon { - if (alternativeIcon != null) { - return const Icon(_breadcrumbSeparatorIcon, size: defaultIconSize); - } - - return node?.diagnostic?.icon; - } - - bool get isChevron => - node == null && alternativeIcon == _breadcrumbSeparatorIcon; - - bool get isEllipsis => node == null && alternativeText == _ellipsisValue; - - bool get isClickable => !isSelected && !isEllipsis; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart index 9ddc8196e90..f8694bc92e6 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -26,36 +26,47 @@ import '../../service/service_extensions.dart' as extensions; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; +import '../../shared/console/eval/inspector_tree_v2.dart'; import '../../shared/console/primitives/simple_items.dart'; import '../../shared/diagnostics/diagnostics_node.dart'; import '../../shared/diagnostics/inspector_service.dart'; import '../../shared/diagnostics/primitives/instance_ref.dart'; import '../../shared/globals.dart'; +import '../../shared/managers/notifications.dart'; import '../../shared/primitives/query_parameters.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_screen.dart'; +import 'inspector_data_models.dart'; +import 'inspector_screen.dart'; import 'inspector_tree_controller.dart'; final _log = Logger('inspector_controller'); +/// Data pattern containing the properties and render properties for a widget +/// tree node. +typedef WidgetTreeNodeProperties = ({ + /// Properties defined directly on the widget. + List widgetProperties, + + /// Properties defined on the widget's render object. + List renderProperties, + + /// Layout properties for the widget. + LayoutProperties? layoutProperties, +}); + /// This class is based on the InspectorPanel class from the Flutter IntelliJ /// plugin with some refactors to make it more of a true controller than a view. class InspectorController extends DisposableController with AutoDisposeControllerMixin implements InspectorServiceClient { - InspectorController({ - required this.inspectorTree, - InspectorTreeController? detailsTree, - required this.treeType, - this.parent, - this.isSummaryTree = true, - }) : assert((detailsTree != null) == isSummaryTree) { - unawaited(_init(detailsTree: detailsTree)); + InspectorController({required this.inspectorTree, required this.treeType}) { + unawaited(init()); } - Future _init({InspectorTreeController? detailsTree}) async { + @override + Future init() async { + super.init(); _refreshRateLimiter = RateLimiter(refreshFramesPerSecond, refresh); inspectorTree.config = InspectorTreeConfig( @@ -64,15 +75,6 @@ class InspectorController extends DisposableController onExpand: _onExpand, onClientActiveChange: _onClientChange, ); - details = isSummaryTree - ? InspectorController( - inspectorTree: detailsTree!, - treeType: treeType, - parent: this, - isSummaryTree: false, - ) - : null; - await serviceConnection.serviceManager.onServiceAvailable; if (inspectorService is InspectorService) { @@ -84,38 +86,42 @@ class InspectorController extends DisposableController serviceConnection.inspectorService as InspectorService, 'selection', ); + _layoutGroups = InspectorObjectGroupManager( + serviceConnection.inspectorService as InspectorService, + 'layout', + ); } addAutoDisposeListener( serviceConnection.serviceManager.isolateManager.mainIsolate, () { - final isolate = + final newIsolate = serviceConnection.serviceManager.isolateManager.mainIsolate.value; - if (isolate != _mainIsolate) { - onIsolateStopped(); + if (_mainIsolate == newIsolate) return; + // First deactivate the current widget tree. + setActivate(false); + if (newIsolate != null) { + // Then reactivate it with the new isolate. + setActivate(true); } - _mainIsolate = isolate; + _mainIsolate = newIsolate; }, ); - // This logic only needs to be run once so run it in the outermost - // controller. - if (parent == null) { - // If select mode is available, enable the on device inspector as it - // won't interfere with users. - addAutoDisposeListener(_supportsToggleSelectWidgetMode, () { - if (_supportsToggleSelectWidgetMode.value) { - safeUnawaited( - serviceConnection.serviceManager.serviceExtensionManager - .setServiceExtensionState( - extensions.enableOnDeviceInspector.extension, - enabled: true, - value: true, - ), - ); - } - }); - } + // If select mode is available, enable the on device inspector as it + // won't interfere with users. + addAutoDisposeListener(_supportsToggleSelectWidgetMode, () { + if (_supportsToggleSelectWidgetMode.value) { + safeUnawaited( + serviceConnection.serviceManager.serviceExtensionManager + .setServiceExtensionState( + extensions.enableOnDeviceInspector.extension, + enabled: true, + value: true, + ), + ); + } + }); addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { if (serviceConnection.serviceManager.connectedState.value.connected) { @@ -130,6 +136,17 @@ class InspectorController extends DisposableController } serviceConnection.consoleService.ensureServiceInitialized(); + + final vmService = serviceConnection.serviceManager.service; + if (vmService != null) { + autoDisposeStreamSubscription( + vmService.onIsolateEvent.listen(_maybeAutoRefreshInspector), + ); + + autoDisposeStreamSubscription( + vmService.onExtensionEvent.listen(_maybeAutoRefreshInspector), + ); + } } void _handleConnectionStart() { @@ -147,9 +164,7 @@ class InspectorController extends DisposableController void _handleConnectionStop() { setActivate(false); - if (isSummaryTree) { - dispose(); - } + dispose(); } IsolateRef? _mainIsolate; @@ -159,7 +174,7 @@ class InspectorController extends DisposableController .serviceExtensionManager .hasServiceExtension(extensions.toggleSelectWidgetMode.extension); - void _onClientChange(bool added) { + Future _onClientChange(bool added) async { if (!added && _clientCount == 0) { // Don't try to remove clients if there are none return; @@ -168,10 +183,10 @@ class InspectorController extends DisposableController _clientCount += added ? 1 : -1; assert(_clientCount >= 0); if (_clientCount == 1) { - setVisibleToUser(true); + await setVisibleToUser(true); setActivate(true); } else if (_clientCount == 0) { - setVisibleToUser(false); + await setVisibleToUser(false); } } @@ -186,13 +201,6 @@ class InspectorController extends DisposableController /// for now mainly to minimize risk. static const refreshFramesPerSecond = 5.0; - final bool isSummaryTree; - - /// Parent InspectorController if this is a details subtree. - InspectorController? parent; - - InspectorController? details; - InspectorTreeController inspectorTree; final FlutterTreeType treeType; @@ -209,10 +217,12 @@ class InspectorController extends DisposableController /// selection is. /// /// This group needs to be kept separate from treeGroups as the selection is - /// shared more with the details subtree. + /// shared with the widget details. /// TODO(jacobr): is there a way we can unify the selection and tree groups? InspectorObjectGroupManager? _selectionGroups; + InspectorObjectGroupManager? _layoutGroups; + /// Node being highlighted due to the current hover. InspectorTreeNode? get currentShowNode => inspectorTree.hover; @@ -229,6 +239,19 @@ class InspectorController extends DisposableController ValueListenable get selectedNode => _selectedNode; final _selectedNode = ValueNotifier(null); + ValueListenable get selectedNodeProperties => + _selectedNodeProperties; + final _selectedNodeProperties = ValueNotifier(( + widgetProperties: [], + renderProperties: [], + layoutProperties: null, + )); + + /// Whether the implementation widgets are hidden in the widget tree. + ValueListenable get implementationWidgetsHidden => + _implementationWidgetsHidden; + final _implementationWidgetsHidden = ValueNotifier(true); + InspectorTreeNode? lastExpanded; bool isActive = false; @@ -241,8 +264,6 @@ class InspectorController extends DisposableController bool highlightNodesShownInBothTrees = false; - bool get detailsSubtree => parent != null; - RemoteDiagnosticsNode? get selectedDiagnostic => selectedNode.value?.diagnostic; @@ -259,16 +280,14 @@ class InspectorController extends DisposableController return treeType; } - void setVisibleToUser(bool visible) { + Future setVisibleToUser(bool visible) async { if (visibleToUser == visible) { return; } visibleToUser = visible; if (visibleToUser) { - if (parent == null) { - unawaited(maybeLoadUI()); - } + await refreshInspector(); } else { shutdownTree(false); } @@ -291,13 +310,6 @@ class InspectorController extends DisposableController } bool highlightShowNode(InspectorTreeNode? node) { - if (node == null && parent != null) { - // If nothing is highlighted, highlight the node selected in the parent - // tree so user has context of where the node selected in the parent is - // in the details tree. - node = findMatchingInspectorTreeNode(parent?.selectedDiagnostic); - } - currentShowNode = node; return true; } @@ -326,14 +338,7 @@ class InspectorController extends DisposableController return Future.value(); } - // TODO(jacobr): refresh the tree as well as just the properties. - final detailsLocal = details; - if (detailsLocal == null) return _waitForPendingUpdateDone(); - - return [ - _waitForPendingUpdateDone(), - detailsLocal._waitForPendingUpdateDone(), - ].wait; + return _waitForPendingUpdateDone(); } // Note that this may be called after the controller is disposed. We need to handle nulls in the fields. @@ -354,6 +359,9 @@ class InspectorController extends DisposableController inspectorTree.root = inspectorTree.createNode(); programmaticSelectionChangeInProgress = false; valueToInspectorTreeNode.clear(); + // Mark tree as inactive so that it will be re-loaded the next time it is + // opened: + isActive = false; } void onIsolateStopped() { @@ -368,7 +376,7 @@ class InspectorController extends DisposableController if (!visibleToUser || disposed) { return; } - await _recomputeTreeRoot(null, null, false); + await _recomputeTreeRoot(null); if (disposed) { return; } @@ -378,13 +386,29 @@ class InspectorController extends DisposableController return _waitForPendingUpdateDone(); } - void filterErrors() { - if (isSummaryTree) { - serviceConnection.errorBadgeManager.filterErrors( - InspectorScreen.id, - (id) => hasDiagnosticsValue(InspectorInstanceRef(id)), + Future refreshInspector({bool isManualRefresh = false}) async { + // If the user is manually refreshing the inspector before the first load + // has completed, this could indicate a slow load time or that the inspector + // failed to load the tree once available. + if (isManualRefresh && !firstInspectorTreeLoadCompleted) { + // We do not want to complete this timing operation because the manual + // refresh will skew the results. + ga.cancelTimingOperation(InspectorScreen.id, gac.pageReady); + ga.select( + gac.inspector, + gac.refreshEmptyTree, + screenMetricsProvider: () => InspectorScreenMetrics.v2(), ); + firstInspectorTreeLoadCompleted = true; } + await onForceRefresh(); + } + + void filterErrors() { + serviceConnection.errorBadgeManager.filterErrors( + InspectorScreen.id, + (id) => hasDiagnosticsValue(InspectorInstanceRef(id)), + ); } void setActivate(bool enabled) { @@ -404,10 +428,6 @@ class InspectorController extends DisposableController } Future maybeLoadUI() async { - if (parent != null) { - // The parent controller will drive loading the UI. - return; - } if (!visibleToUser || !isActive) { return; } @@ -417,10 +437,7 @@ class InspectorController extends DisposableController // We need to start by querying the inspector service to find out the // current state of the UI. final inspectorRef = DevToolsQueryParams.load().inspectorRef; - await updateSelectionFromService( - firstFrame: true, - inspectorRef: inspectorRef, - ); + await updateSelectionFromService(inspectorRef: inspectorRef); } else { if (disposed) return; if (inspectorService is InspectorService) { @@ -434,13 +451,42 @@ class InspectorController extends DisposableController } } + bool _receivedIsolateReloadEvent = false; + bool _receivedFlutterNavigationEvent = false; + + Future _maybeAutoRefreshInspector(Event event) async { + if (!preferences.inspector.autoRefreshEnabled.value) return; + + // It is not sufficent to wait for the navigation and isolate reload events + // only, because Flutter might not have re-painted the app. Instead, we need + // to wait for the first frame AFTER the isolate reload or navigation event + // in order to request the new tree. + if (event.kind == EventKind.kExtension) { + final extensionEventKind = event.extensionKind; + if (extensionEventKind == 'Flutter.Navigation') { + _receivedFlutterNavigationEvent = true; + } + if ((_receivedFlutterNavigationEvent || _receivedIsolateReloadEvent) && + extensionEventKind == 'Flutter.Frame') { + _refreshingAfterNavigationEvent = _receivedFlutterNavigationEvent; + _receivedFlutterNavigationEvent = false; + _receivedIsolateReloadEvent = false; + await refreshInspector(); + } + } + + if (event.kind == EventKind.kIsolateReload) { + serviceConnection.errorBadgeManager.clearErrors(InspectorScreen.id); + _receivedIsolateReloadEvent = true; + } + } + Future _recomputeTreeRoot( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, { - int subtreeDepth = 2, + RemoteDiagnosticsNode? newSelection, { + bool? hideImplementationWidgets, }) async { assert(!disposed); + hideImplementationWidgets ??= _implementationWidgetsHidden.value; final treeGroups = _treeGroups; if (disposed || treeGroups == null) { return; @@ -449,9 +495,11 @@ class InspectorController extends DisposableController treeGroups.cancelNext(); try { final group = treeGroups.next; - final node = await (detailsSubtree - ? group.getDetailsSubtree(subtreeRoot, subtreeDepth: subtreeDepth) - : group.getRoot(treeType, isSummaryTree: true)); + final node = await group.getRoot( + treeType, + isSummaryTree: hideImplementationWidgets, + includeFullDetails: false, + ); if (node == null || group.disposed || disposed) { return; } @@ -465,11 +513,13 @@ class InspectorController extends DisposableController inspectorTree.createNode(), node, expandChildren: true, - expandProperties: false, ); inspectorTree.root = rootNode; - - refreshSelection(newSelection, detailsSelection, setSubtreeRoot); + final selectedNode = _determineNewSelection( + newSelection ?? selectedDiagnostic, + ); + refreshSelection(selectedNode); + _implementationWidgetsHidden.value = hideImplementationWidgets; } catch (error, st) { _log.shout(error, error, st); treeGroups.cancelNext(); @@ -477,29 +527,118 @@ class InspectorController extends DisposableController } } - void _clearValueToInspectorTreeNodeMapping() { - valueToInspectorTreeNode.clear(); - } + var _refreshingAfterNavigationEvent = false; - /// Show the details subtree starting with node subtreeRoot highlighting - /// node subtreeSelection. - void _showDetailSubtrees( - RemoteDiagnosticsNode? subtreeRoot, - RemoteDiagnosticsNode? subtreeSelection, + RemoteDiagnosticsNode? _determineNewSelection( + RemoteDiagnosticsNode? previousSelection, ) { - this.subtreeRoot = subtreeRoot; - details?.setSubtreeRoot(subtreeRoot, subtreeSelection); + if (previousSelection == null) return null; + if (valueToInspectorTreeNode.containsKey(previousSelection.valueRef)) { + return previousSelection; + } + + // TODO(https://github.com/flutter/devtools/issues/8481): Consider using a + // variation of a path-finding algorithm to determine the new selection, + // instead of looking for the first matching descendant. + final (closestUnchangedAncestor, distanceToAncestor) = + _findClosestUnchangedAncestor(previousSelection); + if (closestUnchangedAncestor == null) return inspectorTree.root?.diagnostic; + + // TODO(elliette): This might cause a race event that will set this to false + // for a subsequent navigate event. Consider passing the value of + // _refreshingAfterNavigationEvent through the method chain from where the + // navigation event is detected. This would require updating the interface + // of InspectorServiceClient.onForceRefresh, or refactoring to avoid doing + // so. + if (_refreshingAfterNavigationEvent) { + _refreshingAfterNavigationEvent = false; + return closestUnchangedAncestor; + } + + const distanceOffset = 3; + final matchingDescendant = _findMatchingDescendant( + of: closestUnchangedAncestor, + matching: previousSelection, + inRange: Range( + distanceToAncestor - distanceOffset, + distanceToAncestor + distanceOffset, + ), + ); + + return matchingDescendant ?? closestUnchangedAncestor; + } + + (RemoteDiagnosticsNode?, int) _findClosestUnchangedAncestor( + RemoteDiagnosticsNode node, [ + int distanceToAncestor = 1, + ]) { + final inspectorTreeNode = valueToInspectorTreeNode[node.valueRef]; + if (inspectorTreeNode != null) { + return (inspectorTreeNode.diagnostic, distanceToAncestor); + } + + final ancestor = node.parent; + if (ancestor == null) return (null, distanceToAncestor); + return _findClosestUnchangedAncestor(ancestor, distanceToAncestor++); + } + + RemoteDiagnosticsNode? _findMatchingDescendant({ + required RemoteDiagnosticsNode of, + required RemoteDiagnosticsNode matching, + required Range inRange, + int currentDistance = 1, + }) { + if (currentDistance > inRange.end) return null; + + if (inRange.contains(currentDistance)) { + if (of.description == matching.description) { + return of; + } + } + + final children = of.childrenNow; + final distance = currentDistance++; + for (final child in children) { + final matchingDescendant = _findMatchingDescendant( + of: child, + matching: matching, + inRange: inRange, + currentDistance: distance, + ); + if (matchingDescendant != null) return matchingDescendant; + } + + return null; + } + + Future toggleImplementationWidgetsVisibility() async { + final root = inspectorTree.root?.diagnostic; + if (root != null) { + final currentSelectedNode = selectedNode.value; + await _recomputeTreeRoot( + root, + hideImplementationWidgets: !_implementationWidgetsHidden.value, + ); + // Persist the selected node after refreshing the widget tree: + refreshSelection(currentSelectedNode?.diagnostic); + + // If the user is searching the tree, refresh the search matches. + inspectorTree.refreshSearchMatches(); + } + } + + void _clearValueToInspectorTreeNodeMapping() { + valueToInspectorTreeNode.clear(); } void setSubtreeRoot( RemoteDiagnosticsNode? node, RemoteDiagnosticsNode? selection, ) { - assert(detailsSubtree); selection ??= node; if (node != null && node == subtreeRoot) { // Select the new node in the existing subtree. - applyNewSelection(selection, null, false); + applyNewSelection(selection); return; } subtreeRoot = node; @@ -511,7 +650,7 @@ class InspectorController extends DisposableController // Clear now to eliminate frame of highlighted nodes flicker. _clearValueToInspectorTreeNodeMapping(); - unawaited(_recomputeTreeRoot(selection, null, false)); + unawaited(_recomputeTreeRoot(selection)); } InspectorTreeNode? getSubtreeRootNode() { @@ -521,33 +660,27 @@ class InspectorController extends DisposableController return valueToInspectorTreeNode[subtreeRoot!.valueRef]; } - void refreshSelection( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, - ) { + void refreshSelection(RemoteDiagnosticsNode? newSelection) { newSelection ??= selectedDiagnostic; - setSelectedNode(findMatchingInspectorTreeNode(newSelection)); - syncSelectionHelper( - maybeRerootDetailsTree: setSubtreeRoot, - selection: newSelection, - detailsSelection: detailsSelection, - ); + final matchingNode = findMatchingInspectorTreeNode(newSelection); + if (matchingNode != null) { + setSelectedNode(matchingNode); + syncSelectionHelper(selection: matchingNode.diagnostic); - final detailsLocal = details; - if (detailsLocal != null) { - if (subtreeRoot != null && getSubtreeRootNode() == null) { - subtreeRoot = newSelection; - detailsLocal.setSubtreeRoot(newSelection, detailsSelection); - } + syncTreeSelection(); } - syncTreeSelection(); } void syncTreeSelection() { programmaticSelectionChangeInProgress = true; - inspectorTree.selection = selectedNode.value; - inspectorTree.expandPath(selectedNode.value); + inspectorTree.refreshTree( + updateTreeAction: () { + inspectorTree + ..setSelectedNode(selectedNode.value) + ..expandPath(selectedNode.value); + return true; + }, + ); programmaticSelectionChangeInProgress = false; animateTo(selectedNode.value); } @@ -572,15 +705,6 @@ class InspectorController extends DisposableController return valueToInspectorTreeNode[node.valueRef]; } - void maybeUpdateValueUI(InspectorInstanceRef valueRef) { - final node = valueToInspectorTreeNode[valueRef]; - if (node == null) { - // The value isn't shown in the parent tree. Nothing to do. - return; - } - inspectorTree.nodeChanged(node); - } - @override void onFlutterFrame() { flutterAppFrameReady = true; @@ -602,22 +726,10 @@ class InspectorController extends DisposableController // Don't do anything. We will update the view once it is visible again. return; } - if (detailsSubtree) { - // Wait for the master to update. - return; - } - unawaited(updateSelectionFromService(firstFrame: false)); + unawaited(updateSelectionFromService()); } - Future updateSelectionFromService({ - required bool firstFrame, - String? inspectorRef, - }) async { - if (parent != null) { - // If we have a parent controller we should wait for the parent to update - // our selection rather than updating it our self. - return; - } + Future updateSelectionFromService({String? inspectorRef}) async { final selectionGroups = _selectionGroups; if (selectionGroups == null) { // Already disposed. Ignore this requested to update selection. @@ -639,41 +751,32 @@ class InspectorController extends DisposableController final pendingSelectionFuture = group.getSelection( selectedDiagnostic, treeType, - restrictToLocalProject: isSummaryTree, + // If implementation widgets are hidden, the only widgets in the tree are + // those that were created by the local project. + restrictToLocalProject: implementationWidgetsHidden.value, ); - final pendingDetailsFuture = isSummaryTree - ? group.getSelection(selectedDiagnostic, treeType) - : null; - try { final newSelection = await pendingSelectionFuture; - if (disposed || group.disposed) return; - RemoteDiagnosticsNode? detailsSelection; - if (pendingDetailsFuture != null) { - detailsSelection = await pendingDetailsFuture; - if (disposed || group.disposed) return; - } + if (disposed || group.disposed) return; - if (!firstFrame && - detailsSelection?.valueRef == details?.selectedDiagnostic?.valueRef && - newSelection?.valueRef == selectedDiagnostic?.valueRef) { - // No need to change the selection as it didn't actually change. - selectionGroups.cancelNext(); - return; - } selectionGroups.promoteNext(); subtreeRoot = newSelection; - applyNewSelection(newSelection, detailsSelection, true); + applyNewSelection(newSelection); + + await _maybeShowNotificationForSelectedNode( + selectedNode: newSelection, + group: group, + ); // Send an event that a widget was selected on the device. ga.select( gac.inspector, gac.onDeviceSelection, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), + screenMetricsProvider: () => InspectorScreenMetrics.v2(), ); } catch (error, st) { if (selectionGroups.next == group) { @@ -683,22 +786,16 @@ class InspectorController extends DisposableController } } - void applyNewSelection( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, - ) { + void applyNewSelection(RemoteDiagnosticsNode? newSelection) { final nodeInTree = findMatchingInspectorTreeNode(newSelection); if (nodeInTree == null) { // The tree has probably changed since we last updated. Do a full refresh // so that the tree includes the new node we care about. - unawaited( - _recomputeTreeRoot(newSelection, detailsSelection, setSubtreeRoot), - ); + unawaited(_recomputeTreeRoot(newSelection)); } - refreshSelection(newSelection, detailsSelection, setSubtreeRoot); + refreshSelection(newSelection); } void animateTo(InspectorTreeNode? node) { @@ -718,18 +815,147 @@ class InspectorController extends DisposableController lastExpanded = null; // New selected node takes precedence. endShowNode(); - final detailsLocal = details; - final parantLocal = parent; - if (detailsLocal != null) { - detailsLocal.endShowNode(); - } else if (parantLocal != null) { - parantLocal.endShowNode(); - } _updateSelectedErrorFromNode(_selectedNode.value); + unawaited(_loadPropertiesForNode(_selectedNode.value)); + + /// If the user selects a hidden implementation widget, first expand that + /// widget's hideable group before scrolling. + final diagnostic = _selectedNode.value?.diagnostic; + if (diagnostic != null && diagnostic.isHidden) { + inspectorTree.refreshTree( + updateTreeAction: () { + diagnostic.hideableGroupLeader?.toggleHiddenGroup(); + return true; + }, + ); + } + animateTo(selectedNode.value); } + static const _implementationWidgetMessage = + 'Selected an implementation widget'; + + static const _notificationDuration = Duration(seconds: 4); + + Future _maybeShowNotificationForSelectedNode({ + required RemoteDiagnosticsNode? selectedNode, + required ObjectGroup group, + }) async { + if (selectedNode == null || + !implementationWidgetsHidden.value || + _selectionIsOutOfDate(selectedNode)) { + return; + } + + final possibleImplementationWidget = await group.getSelection( + selectedDiagnostic, + treeType, + ); + + // Return early if we have a new selected node. + if (_selectionIsOutOfDate(selectedNode)) return; + + final isImplementationWidget = + possibleImplementationWidget != null && + !possibleImplementationWidget.isCreatedByLocalProject; + if (isImplementationWidget) { + final selectedWidgetName = selectedNode.description ?? ''; + final implementationWidgetName = + possibleImplementationWidget.description ?? ''; + + // Return early if we have a new selected node. + if (_selectionIsOutOfDate(selectedNode)) return; + + // Show a notification that the user selected an implementation widget, + // e.g. "Selected an implementation widget of Text: RichText." + final messageDetails = selectedWidgetName.isEmpty + ? '' + : ' of $selectedWidgetName${implementationWidgetName.isEmpty ? '' : ': $implementationWidgetName'}'; + notificationService.pushNotification( + NotificationMessage( + '$_implementationWidgetMessage$messageDetails.', + duration: _notificationDuration, + ), + allowDuplicates: false, + ); + } + } + + bool _selectionIsOutOfDate(RemoteDiagnosticsNode selected) { + return selected.valueRef != selectedNode.value?.diagnostic?.valueRef; + } + + Future _loadPropertiesForNode(InspectorTreeNode? node) async { + final widgetProperties = []; + final renderProperties = []; + LayoutProperties? layoutProperties; + final diagnostic = node?.diagnostic; + final objectGroupApi = diagnostic?.objectGroupApi; + if (diagnostic != null && objectGroupApi != null) { + try { + // Fetch widget properties: + final wProperties = await diagnostic.getProperties(objectGroupApi); + // Check if the selected node has changed, and if so return early: + if (_selectedNode.value != node) { + return; + } + widgetProperties.addAll( + wProperties.where((p) => p.propertyType != 'RenderObject'), + ); + renderProperties.addAll( + wProperties.where((p) => p.propertyType == 'RenderObject'), + ); + // Fetch layout properties: + layoutProperties = await _loadLayoutPropertiesForNode( + diagnostic, + forFlexLayout: false, + ); + // Fetch RenderObject properties: + for (final renderObject in renderProperties) { + final rProperties = await renderObject.getProperties(objectGroupApi); + // Check if the selected node has changed, and if so return early: + if (_selectedNode.value != node) { + return; + } + renderProperties.addAll(rProperties); + } + } catch (e, st) { + _log.warning(e, st); + } + } + _selectedNodeProperties.value = ( + widgetProperties: widgetProperties, + renderProperties: renderProperties, + layoutProperties: layoutProperties, + ); + } + + Future _loadLayoutPropertiesForNode( + RemoteDiagnosticsNode diagnostic, { + required bool forFlexLayout, + }) async { + try { + _layoutGroups?.cancelNext(); + final manager = _layoutGroups!; + final nextObjectGroup = manager.next; + final node = await nextObjectGroup.getLayoutExplorerNode( + diagnostic.layoutRootNode(forFlexLayout: forFlexLayout), + ); + if (node == null || node.renderObject == null) return null; + + if (!nextObjectGroup.disposed) { + assert(manager.next == nextObjectGroup); + manager.promoteNext(); + } + return node.computeLayoutProperties(forFlexLayout: forFlexLayout); + } catch (e, st) { + _log.warning(e, st); + return null; + } + } + /// Update the index of the selected error based on a node that has been /// selected in the tree. void _updateSelectedErrorFromNode(InspectorTreeNode? node) { @@ -774,10 +1000,7 @@ class InspectorController extends DisposableController .value; unawaited( - updateSelectionFromService( - firstFrame: false, - inspectorRef: errors.keys.elementAt(index), - ), + updateSelectionFromService(inspectorRef: errors.keys.elementAt(index)), ); } @@ -802,7 +1025,13 @@ class InspectorController extends DisposableController } } - void selectionChanged() { + /// Handles updating the widget tree when the selecected widget changes. + /// + /// [notifyFlutterInspector] determines whether a request should be sent to + /// the Widget Inspector in the Flutter framework to update the on-device + /// selection. This should only be true if the the selection was changed due + /// to a user action in DevTools (e.g. clicking on a widget in the tree). + void selectionChanged({bool notifyFlutterInspector = false}) { if (!visibleToUser) { return; } @@ -818,79 +1047,30 @@ class InspectorController extends DisposableController setSelectedNode(node); unawaited(_addNodeToConsole(node)); - // Don't reroot if the selected value is already visible in the details tree. - final maybeReroot = - isSummaryTree && - details != null && - selectedDiagnostic != null && - !details!.hasDiagnosticsValue(selectedDiagnostic!.valueRef); syncSelectionHelper( - maybeRerootDetailsTree: maybeReroot, selection: selectedDiagnostic, - detailsSelection: selectedDiagnostic, + notifyFlutterInspector: notifyFlutterInspector, ); - - if (!maybeReroot) { - final parantLocal = parent; - final detailsLocal = details; - - if (isSummaryTree && detailsLocal != null) { - detailsLocal.selectAndShowNode(selectedDiagnostic); - } else if (parantLocal != null) { - parantLocal.selectAndShowNode( - firstAncestorInParentTree(selectedNode.value), - ); - } - } } } - RemoteDiagnosticsNode? firstAncestorInParentTree(InspectorTreeNode? node) { - final parentLocal = parent; - - if (parentLocal == null) { - return node?.diagnostic; - } - while (node != null) { - final diagnostic = node.diagnostic; - if (diagnostic != null && - parentLocal.hasDiagnosticsValue(diagnostic.valueRef)) { - return parentLocal.findDiagnosticsValue(diagnostic.valueRef); - } - node = node.parent; - } - return null; - } - + /// Syncs the selection state after a new widgets was selected. + /// + /// [notifyFlutterInspector] determines whether a request should be sent to + /// the Widget Inspector in the Flutter framework to update the on-device + /// selection. This should only be true if the the selection was changed due + /// to a user action in DevTools (e.g. clicking on a widget in the tree). void syncSelectionHelper({ - required bool maybeRerootDetailsTree, required RemoteDiagnosticsNode? selection, - required RemoteDiagnosticsNode? detailsSelection, + bool notifyFlutterInspector = false, }) { if (selection != null) { if (selection.isCreatedByLocalProject) { _navigateTo(selection); } } - if (detailsSubtree || details == null) { - if (selection != null) { - var toSelect = selectedNode.value; - - while (toSelect != null && toSelect.diagnostic!.isProperty) { - toSelect = toSelect.parent; - } - - if (toSelect != null) { - final diagnosticToSelect = toSelect.diagnostic!; - unawaited(diagnosticToSelect.setSelectionInspector(true)); - } - } - } - if (maybeRerootDetailsTree) { - _showDetailSubtrees(selection, detailsSelection); - } else if (selection != null) { - // We can't rely on the details tree to update the selection on the server in this case. + if (notifyFlutterInspector && selection != null) { unawaited(selection.setSelectionInspector(true)); } } @@ -908,14 +1088,18 @@ class InspectorController extends DisposableController if (serviceConnection.inspectorService != null) { shutdownTree(false); } + _treeGroups?.clear(false); _treeGroups = null; _selectionGroups?.clear(false); _selectionGroups = null; - details?.dispose(); + _layoutGroups?.clear(false); + _layoutGroups = null; _refreshRateLimiter.dispose(); _selectedNode.dispose(); + _selectedNodeProperties.dispose(); + _implementationWidgetsHidden.dispose(); _selectedErrorIndex.dispose(); super.dispose(); } @@ -930,21 +1114,4 @@ class InspectorController extends DisposableController valueToInspectorTreeNode[valueRef] = node; } } - - Future expandAllNodesInDetailsTree() async { - final detailsLocal = details!; - await detailsLocal._recomputeTreeRoot( - inspectorTree.selection?.diagnostic, - detailsLocal.inspectorTree.selection?.diagnostic ?? - detailsLocal.inspectorTree.root?.diagnostic, - false, - subtreeDepth: maxJsInt, - ); - } - - void collapseDetailsToSelected() { - final detailsLocal = details!; - detailsLocal.inspectorTree.collapseToSelected(); - detailsLocal.animateTo(detailsLocal.inspectorTree.selection); - } } diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart similarity index 93% rename from packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart rename to packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart index d1f810a2605..ff4fe1d3bed 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart @@ -13,14 +13,14 @@ import '../../shared/analytics/constants.dart' as gac; import '../../shared/feature_flags.dart'; import '../../shared/globals.dart'; import '../../shared/ui/common_widgets.dart'; -import '../inspector_shared/inspector_settings_dialog.dart'; -import '../inspector_v2/inspector_controller.dart' as v2; +import 'inspector_controller.dart'; +import 'inspector_settings_dialog.dart'; /// Control buttons for the inspector panel. class InspectorControls extends StatelessWidget { const InspectorControls({super.key, this.controller}); - final v2.InspectorController? controller; + final InspectorController? controller; static const minScreenWidthForTextBeforeTruncating = 800.0; static const minScreenWidthForText = 550.0; @@ -30,8 +30,8 @@ class InspectorControls extends StatelessWidget { final activeButtonColor = Theme.of( context, ).colorScheme.activeToggleButtonColor; - final isInspectorV2 = - controller != null && FeatureFlags.inspectorV2.isEnabled; + final isInspector = + controller != null && FeatureFlags.inspector.isEnabled; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -49,13 +49,13 @@ class InspectorControls extends StatelessWidget { : extensions.toggleOnDeviceWidgetInspector, ], minScreenWidthForText: minScreenWidthForText, - minScreenWidthForTextBeforeTruncating: isInspectorV2 + minScreenWidthForTextBeforeTruncating: isInspector ? minScreenWidthForTextBeforeTruncating : null, ); }, ), - if (isInspectorV2) ...[ + if (isInspector) ...[ const SizedBox(width: defaultSpacing), ShowImplementationWidgetsButton(controller: controller!), ], @@ -113,7 +113,7 @@ class InspectorServiceExtensionButtonGroup extends StatelessWidget { class ShowImplementationWidgetsButton extends StatelessWidget { const ShowImplementationWidgetsButton({super.key, required this.controller}); - final v2.InspectorController controller; + final InspectorController controller; @override Widget build(BuildContext context) { diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart index a12ed91f76e..188dabd14b6 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart @@ -1,8 +1,8 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. -/// @docImport 'layout_explorer/ui/overflow_indicator_painter.dart'; +/// @docImport '../inspector/layout_explorer/ui/overflow_indicator_painter.dart'; library; import 'dart:math' as math; @@ -40,6 +40,7 @@ const overflowEpsilon = 0.1; /// * (largestRenderSize - smallestRenderSize) /// / (largestSize - smallestSize) + smallestRenderSize /// ``` +/// /// Explanation: /// - The computation formula for transforming size to renderSize is based on these two things: /// - smallest element will be rendered to [smallestRenderSize] @@ -61,6 +62,7 @@ const overflowEpsilon = 0.1; /// ``` /// sum(renderSize) = maxSizeAvailable /// ``` +/// List computeRenderSizes({ required Iterable sizes, required double smallestSize, @@ -101,6 +103,26 @@ List computeRenderSizes({ return renderSizes; } +/// Data pattern containing a widget's widths or heights. +typedef WidgetSizes = ({ + /// Whether this record represents a widget's widths or heights. + SizeType type, + + /// Either the widget's left (if [type] is [SizeType.widths]) or top (if + /// [type] is [SizeType.heights]) padding. + double paddingA, + + /// Either the widget's width (if [type] is [SizeType.widths]) or height (if + /// [type] is [SizeType.heights]). + double widgetSize, + + /// Either the widget's right (if [type] is [SizeType.widths]) or bottom (if + /// [type] is [SizeType.heights]) padding. + double paddingB, +}); + +enum SizeType { widths, heights } + // TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed /// Represents parsed layout information for a specific [RemoteDiagnosticsNode]. class LayoutProperties { @@ -114,6 +136,7 @@ class LayoutProperties { children = copyLevel == 0 ? [] : node.childrenNow + .where((child) => child.size != null) .map( (child) => LayoutProperties(child, copyLevel: copyLevel - 1), ) @@ -227,6 +250,45 @@ class LayoutProperties { return heightUsed > parentHeight + overflowEpsilon; } + LayoutProperties? get parentLayoutProperties { + final parentElement = node.parentRenderElement; + // Fall back to this node's properties if there is no parent. + if (parentElement == null) return this; + final parentProperties = parentElement.computeLayoutProperties( + forFlexLayout: false, + ); + return parentProperties ?? this; + } + + WidgetSizes? get widgetWidths => _widgetSizes(SizeType.widths); + + WidgetSizes? get widgetHeights => _widgetSizes(SizeType.heights); + + WidgetSizes? _widgetSizes(SizeType type) { + if (parentLayoutProperties == null) return null; + final parentProperties = parentLayoutProperties!; + + final parentData = node.parentData; + final parentSize = parentProperties.size; + + switch (type) { + case SizeType.heights: + return ( + type: type, + paddingA: parentData.offset.dy, + widgetSize: size.height, + paddingB: parentSize.height - (size.height + parentData.offset.dy), + ); + case SizeType.widths: + return ( + type: type, + paddingA: parentData.offset.dx, + widgetSize: size.width, + paddingB: parentSize.width - (size.width + parentData.offset.dx), + ); + } + } + static String describeAxis(double min, double max, String axis) { if (min == max) return '$axis=${min.toStringAsFixed(1)}'; return '${min.toStringAsFixed(1)}<=$axis<=${max.toStringAsFixed(1)}'; @@ -287,6 +349,37 @@ extension MainAxisAlignmentExtension on MainAxisAlignment { } } +/// Encapsulation of [widths] and [heights] for the layout. +class LayoutWidthsAndHeights { + LayoutWidthsAndHeights({required this.widths, required this.heights}); + + final WidgetSizes widths; + final WidgetSizes heights; + + double get widgetWidth => widths.widgetSize; + + double get widgetHeight => heights.widgetSize; + + double get leftPadding => widths.paddingA; + + double get rightPadding => widths.paddingB; + + double get topPadding => heights.paddingA; + + double get bottomPadding => heights.paddingB; + + bool get hasLeftPadding => leftPadding > 0; + + bool get hasRightPadding => rightPadding > 0; + + bool get hasTopPadding => topPadding > 0; + + bool get hasBottomPadding => bottomPadding > 0; + + bool get hasAnyPadding => + hasLeftPadding || hasRightPadding || hasTopPadding || hasBottomPadding; +} + /// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed. class FlexLayoutProperties extends LayoutProperties { FlexLayoutProperties({ diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart new file mode 100644 index 00000000000..790e0438f4c --- /dev/null +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart @@ -0,0 +1,36 @@ +// Copyright 2024 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_shared/shared.dart'; +import 'package:flutter/material.dart'; + +import '../../shared/framework/screen.dart'; +import '../../shared/globals.dart'; +import '../inspector/inspector_screen_body.dart'; +import 'inspector_screen_controller.dart'; + +class InspectorScreen extends Screen { + InspectorScreen() : super.fromMetaData(ScreenMetaData.inspector); + + static const minScreenWidthForText = 900.0; + + static final id = ScreenMetaData.inspector.id; + + // There is not enough room to safely show the console in the embed view of + // the DevTools and IDEs have their own consoles. + @override + bool showConsole(EmbedMode embedMode) => !embedMode.embedded; + + @override + bool showAiAssistant() => true; + + @override + String get docPageId => screenId; + + @override + Widget buildScreenBody(BuildContext context) { + final controller = screenControllers.lookup(); + return InspectorScreenBody(controller: controller.inspectorController); + } +} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart index 5ab577d564d..b43e9ff50a9 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -9,24 +9,21 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; -import '../../service/service_extension_widgets.dart'; -import '../../service/service_extensions.dart' as extensions; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; +import '../../shared/console/eval/inspector_tree_v2.dart'; import '../../shared/globals.dart'; -import '../../shared/managers/banner_messages.dart'; import '../../shared/managers/error_badge_manager.dart'; import '../../shared/primitives/blocking_action_mixin.dart'; import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/search.dart'; -import '../inspector_shared/inspector_controls.dart'; -import '../inspector_shared/inspector_screen.dart'; -import '../inspector_shared/inspector_settings_dialog.dart'; +import '../../shared/utils/utils.dart'; import 'inspector_controller.dart'; -import 'inspector_screen_details_tab.dart'; +import 'inspector_controls.dart'; +import 'inspector_screen.dart'; import 'inspector_tree_controller.dart'; +import 'widget_details.dart'; class InspectorScreenBody extends StatefulWidget { const InspectorScreenBody({super.key, required this.controller}); @@ -41,15 +38,12 @@ class InspectorScreenBodyState extends State with BlockingActionMixin, AutoDisposeMixin { InspectorController get controller => widget.controller; - InspectorTreeController get _summaryTreeController => + InspectorTreeController get _inspectorTreeController => controller.inspectorTree; - InspectorTreeController get _detailsTreeController => - controller.details!.inspectorTree; - bool searchVisible = false; - SearchControllerMixin get searchController => _summaryTreeController; + SearchControllerMixin get searchController => _inspectorTreeController; /// Indicates whether search can be closed. The value is set to true when /// search target type dropdown is displayed @@ -58,10 +52,8 @@ class InspectorScreenBodyState extends State SearchTargetType searchTarget = SearchTargetType.widget; - static const summaryTreeKey = Key('Summary Tree'); - static const detailsTreeKey = Key('Details Tree'); + static const inspectorTreeKey = Key('Inspector Tree'); static const minScreenWidthForText = 900.0; - static const serviceExtensionButtonsIncludeTextWidth = 1200.0; @override void initState() { @@ -103,7 +95,7 @@ class InspectorScreenBodyState extends State addAutoDisposeListener(preferences.inspector.pubRootDirectories, () { if (serviceConnection.serviceManager.connectedState.value.connected && controller.firstInspectorTreeLoadCompleted) { - _refreshInspector(); + safeUnawaited(controller.refreshInspector()); } }); @@ -111,55 +103,45 @@ class InspectorScreenBodyState extends State ga.timeStart(InspectorScreen.id, gac.pageReady); } - _summaryTreeController.setSearchTarget(searchTarget); - - _showLegacyInspectorWarning(context); + _inspectorTreeController.setSearchTarget(searchTarget); } @override Widget build(BuildContext context) { - final summaryTree = _buildSummaryTreeColumn(); - - final detailsTree = InspectorTree( - key: detailsTreeKey, - controller: controller, - treeController: _detailsTreeController, - summaryTreeController: _summaryTreeController, - screenId: InspectorScreen.id, - ); + final inspectorTree = _buildInspectorTreeColumn(); final splitAxis = SplitPane.axisFor(context, 0.85); final widgetTrees = SplitPane( axis: splitAxis, initialFractions: const [0.33, 0.67], children: [ - summaryTree, - InspectorDetails(detailsTree: detailsTree, controller: controller), + inspectorTree, + WidgetDetails(controller: controller), ], ); return Column( children: [ - const InspectorControls(), + InspectorControls(controller: controller), const SizedBox(height: intermediateSpacing), Expanded(child: widgetTrees), ], ); } - Widget _buildSummaryTreeColumn() { + Widget _buildInspectorTreeColumn() { return LayoutBuilder( builder: (context, constraints) { return RoundedOutlinedBorder( child: Column( children: [ - InspectorSummaryTreeControls( + InspectorTreeControls( isSearchVisible: searchVisible, constraints: constraints, - onRefreshInspectorPressed: _refreshInspector, + onRefreshInspectorPressed: _manualInspectorRefresh, onSearchVisibleToggle: _onSearchVisibleToggle, searchFieldBuilder: () => StatelessSearchField( - controller: _summaryTreeController, + controller: _inspectorTreeController, searchFieldEnabled: true, shouldRequestFocus: searchVisible, supportsNavigation: true, @@ -182,10 +164,9 @@ class InspectorScreenBodyState extends State return Stack( children: [ InspectorTree( - key: summaryTreeKey, + key: inspectorTreeKey, controller: controller, - treeController: _summaryTreeController, - isSummaryTree: true, + treeController: _inspectorTreeController, widgetErrors: inspectableErrors, screenId: InspectorScreen.id, ), @@ -218,76 +199,25 @@ class InspectorScreenBodyState extends State setState(() { searchVisible = !searchVisible; }); - _summaryTreeController.resetSearch(); - } - - void _showLegacyInspectorWarning(BuildContext context) { - if (context.mounted) { - pushLegacyInspectorWarning(InspectorScreen.id); - } - } - - List getServiceExtensionWidgets() { - return [ - ServiceExtensionButtonGroup( - minScreenWidthForText: serviceExtensionButtonsIncludeTextWidth, - extensions: [ - extensions.slowAnimations, - extensions.debugPaint, - extensions.debugPaintBaselines, - extensions.repaintRainbow, - extensions.invertOversizedImages, - ], - ), - const SizedBox(width: defaultSpacing), - SettingsOutlinedButton( - gaScreen: gac.inspector, - gaSelection: gac.inspectorSettings, - tooltip: 'Flutter Inspector Settings', - onPressed: () { - unawaited( - showDialog( - context: context, - builder: (context) => const FlutterInspectorSettingsDialog(), - ), - ); - }, - ), - // TODO(jacobr): implement TogglePlatformSelector. - // TogglePlatformSelector().selector - ]; + _inspectorTreeController.resetSearch(); } - void _refreshInspector() { + void _manualInspectorRefresh() { ga.select( gac.inspector, gac.refresh, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), + screenMetricsProvider: () => InspectorScreenMetrics.v2(), ); unawaited( blockWhileInProgress(() async { - // If the user is force refreshing the inspector before the first load has - // completed, this could indicate a slow load time or that the inspector - // failed to load the tree once available. - if (!controller.firstInspectorTreeLoadCompleted) { - // We do not want to complete this timing operation because the force - // refresh will skew the results. - ga.cancelTimingOperation(InspectorScreen.id, gac.pageReady); - ga.select( - gac.inspector, - gac.refreshEmptyTree, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), - ); - controller.firstInspectorTreeLoadCompleted = true; - } - await controller.onForceRefresh(); + await controller.refreshInspector(isManualRefresh: true); }), ); } } -class InspectorSummaryTreeControls extends StatelessWidget { - const InspectorSummaryTreeControls({ +class InspectorTreeControls extends StatelessWidget { + const InspectorTreeControls({ super.key, required this.constraints, required this.isSearchVisible, diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_controller.dart new file mode 100644 index 00000000000..1a82aa0e2e9 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_controller.dart @@ -0,0 +1,47 @@ +// Copyright 2024 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 '../../shared/analytics/metrics.dart'; +import '../../shared/console/primitives/simple_items.dart'; +import '../../shared/framework/screen.dart'; +import '../../shared/framework/screen_controllers.dart'; +import '../inspector/inspector_controller.dart'; +import '../inspector/inspector_tree_controller.dart'; + +/// Screen controller for the Inspector screen. +/// +/// This controller can be accessed from anywhere in DevTools, as long as it was +/// first registered, by +/// calling `screenControllers.lookup()`. +/// +/// The controller lifecycle is managed by the [ScreenControllers] class. The +/// `init` method is called lazily upon the first controller access from +/// `screenControllers`. The `dispose` method is called by `screenControllers` +/// when DevTools is destroying a set of DevTools screen controllers. +class InspectorScreenController extends DevToolsScreenController { + @override + final screenId = ScreenMetaData.inspector.id; + + late InspectorController inspectorController; + late InspectorTreeController inspectorTreeController; + + @override + void init() { + super.init(); + inspectorTreeController = InspectorTreeController( + gaId: InspectorScreenMetrics.summaryTreeGaId, + ); + inspectorController = InspectorController( + inspectorTree: inspectorTreeController, + treeType: FlutterTreeType.widget, + ); + } + + @override + void dispose() { + inspectorTreeController.dispose(); + inspectorController.dispose(); + super.dispose(); + } +} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart deleted file mode 100644 index ae8cc53e551..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2019 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 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/globals.dart'; -import '../../shared/preferences/preferences.dart'; -import '../../shared/primitives/blocking_action_mixin.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/tab.dart'; -import 'inspector_controller.dart'; -import 'inspector_screen_body.dart'; -import 'layout_explorer/layout_explorer.dart'; - -class InspectorDetails extends StatelessWidget { - const InspectorDetails({ - required this.detailsTree, - required this.controller, - super.key, - }); - - final Widget detailsTree; - final InspectorController controller; - - @override - Widget build(BuildContext context) { - final tabs = [ - ( - tab: _buildTab(tabName: InspectorDetailsViewType.layoutExplorer.key), - tabView: LayoutExplorerTab(controller: controller), - ), - ( - tab: _buildTab( - tabName: InspectorDetailsViewType.widgetDetailsTree.key, - trailing: InspectorExpandCollapseButtons(controller: controller), - ), - tabView: detailsTree, - ), - ]; - return ValueListenableBuilder( - valueListenable: preferences.inspector.defaultDetailsView, - builder: (BuildContext context, value, Widget? child) { - int defaultInspectorViewIndex = 0; - - if (preferences.inspector.defaultDetailsView.value == - InspectorDetailsViewType.widgetDetailsTree) { - defaultInspectorViewIndex = 1; - } - - return AnalyticsTabbedView( - tabs: tabs, - gaScreen: gac.inspector, - initialSelectedIndex: defaultInspectorViewIndex, - ); - }, - ); - } - - DevToolsTab _buildTab({required String tabName, Widget? trailing}) { - return DevToolsTab.create( - tabName: tabName, - gaPrefix: 'inspectorDetailsTab', - trailing: trailing, - ); - } -} - -class InspectorExpandCollapseButtons extends StatefulWidget { - const InspectorExpandCollapseButtons({super.key, required this.controller}); - - final InspectorController controller; - - @override - State createState() => - _InspectorExpandCollapseButtonsState(); -} - -class _InspectorExpandCollapseButtonsState - extends State - with BlockingActionMixin { - bool get enableButtons => !actionInProgress; - - @override - Widget build(BuildContext context) { - return Container( - alignment: Alignment.centerRight, - decoration: BoxDecoration( - border: Border(left: defaultBorderSide(Theme.of(context))), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - child: GaDevToolsButton( - icon: Icons.unfold_more, - onPressed: enableButtons ? _onExpandClick : null, - label: 'Expand all', - minScreenWidthForText: - InspectorScreenBodyState.minScreenWidthForText, - gaScreen: gac.inspector, - gaSelection: gac.expandAll, - outlined: false, - ), - ), - const SizedBox(width: denseSpacing), - SizedBox( - child: GaDevToolsButton( - icon: Icons.unfold_less, - onPressed: enableButtons ? _onCollapseClick : null, - label: 'Collapse to selected', - minScreenWidthForText: - InspectorScreenBodyState.minScreenWidthForText, - gaScreen: gac.inspector, - gaSelection: gac.collapseAll, - outlined: false, - ), - ), - ], - ), - ); - } - - void _onExpandClick() { - unawaited( - blockWhileInProgress(() async { - ga.select(gac.inspector, gac.expandAll); - await widget.controller.expandAllNodesInDetailsTree(); - }), - ); - } - - void _onCollapseClick() { - ga.select(gac.inspector, gac.collapseAll); - widget.controller.collapseDetailsToSelected(); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_settings_dialog.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_settings_dialog.dart new file mode 100644 index 00000000000..a185822be0f --- /dev/null +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_settings_dialog.dart @@ -0,0 +1,127 @@ +// Copyright 2024 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 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:vm_service/vm_service.dart' hide Stack; + +import '../../shared/analytics/constants.dart' as gac; +import '../../shared/globals.dart'; +import '../../shared/primitives/simple_items.dart'; +import '../../shared/ui/common_widgets.dart'; +import '../../shared/ui/editable_list.dart'; + +class FlutterInspectorSettingsDialog extends StatefulWidget { + const FlutterInspectorSettingsDialog({super.key}); + + @override + State createState() => + _FlutterInspectorSettingsDialogState(); +} + +class _FlutterInspectorSettingsDialogState + extends State + with AutoDisposeMixin { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const dialogHeight = 500.0; + + return DevToolsDialog( + title: const DialogTitleText('Flutter Inspector Settings'), + content: SizedBox( + width: defaultDialogWidth, + height: dialogHeight, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...dialogSubHeader(theme, 'General'), + CheckboxSetting( + notifier: + preferences.inspector.hoverEvalModeEnabled + as ValueNotifier, + title: 'Enable hover inspection', + description: + 'Hovering over any widget displays its properties and values.', + gaItem: gac.inspectorHoverEvalMode, + ), + const SizedBox(height: largeSpacing), + CheckboxSetting( + notifier: + preferences.inspector.autoRefreshEnabled + as ValueNotifier, + title: 'Enable widget tree auto-refreshing', + description: + 'The widget tree will automatically refresh after a hot-reload or navigation event.', + gaItem: gac.inspectorAutoRefreshEnabled, + ), + const SizedBox(height: largeSpacing), + ...dialogSubHeader(theme, 'Package Directories'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + 'Widgets in these directories will show up in your summary tree.', + style: theme.subtleTextStyle, + ), + ), + MoreInfoLink( + url: DocLinks.inspectorPackageDirectories.value, + gaScreenName: gac.inspector, + gaSelectedItemDescription: + gac.InspectorDocs.packageDirectoriesDocs.name, + ), + ], + ), + Text( + '(e.g. /absolute/path/to/myPackage/)', + style: theme.subtleTextStyle, + ), + const SizedBox(height: denseSpacing), + const Expanded(child: PubRootDirectorySection()), + ], + ), + ), + actions: const [DialogCloseButton()], + ); + } +} + +class PubRootDirectorySection extends StatelessWidget { + const PubRootDirectorySection({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: + serviceConnection.serviceManager.isolateManager.mainIsolate, + builder: (_, _, _) { + return SizedBox( + height: 200.0, + child: EditableList( + gaScreen: gac.inspector, + gaRefreshSelection: gac.refreshPubRoots, + entries: preferences.inspector.pubRootDirectories, + textFieldLabel: 'Enter a new package directory', + isRefreshing: preferences.inspector.isRefreshingPubRootDirectories, + onEntryAdded: (p0) => unawaited( + preferences.inspector.addPubRootDirectories([ + p0, + ], shouldCache: true), + ), + onEntryRemoved: (p0) => + unawaited(preferences.inspector.removePubRootDirectories([p0])), + onRefreshTriggered: () => + unawaited(preferences.inspector.loadPubRootDirectories()), + ), + ); + }, + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart index 35b5d9b66cd..a29c12f22e2 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -16,7 +16,7 @@ import 'package:logging/logging.dart'; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; +import '../../shared/console/eval/inspector_tree_v2.dart'; import '../../shared/console/widgets/description.dart'; import '../../shared/diagnostics/diagnostics_node.dart'; import '../../shared/globals.dart'; @@ -29,7 +29,6 @@ import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/search.dart'; import '../../shared/ui/utils.dart'; import '../../shared/utils/utils.dart'; -import 'inspector_breadcrumbs.dart'; import 'inspector_controller.dart'; final _log = Logger('inspector_tree_controller'); @@ -90,11 +89,17 @@ class _InspectorTreeRowState extends State<_InspectorTreeRowWidget> void onExpandChanged(bool expanded) { setState(() { final row = widget.row; - if (expanded) { - widget.inspectorTreeState.treeController!.onExpandRow(row); - } else { - widget.inspectorTreeState.treeController!.onCollapseRow(row); - } + final treeController = widget.inspectorTreeState.treeController!; + treeController.refreshTree( + updateTreeAction: () { + if (expanded) { + treeController.onExpandRow(row); + } else { + treeController.onCollapseRow(row); + } + return true; + }, + ); }); } @@ -115,7 +120,8 @@ class InspectorTreeController extends DisposableController /// [InspectorTreeController]. final int? gaId; - InspectorTreeNode createNode() => InspectorTreeNode(); + InspectorTreeNode createNode() => + InspectorTreeNode(whenDirty: _handleDirtyNode); SearchTargetType _searchTarget = SearchTargetType.widget; int _rootSetCount = 0; @@ -127,14 +133,20 @@ class InspectorTreeController extends DisposableController gac.inspector, gac.inspectorTreeControllerInitialized, nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( + screenMetricsProvider: () => InspectorScreenMetrics.v2( inspectorTreeControllerId: gaId, rootSetCount: _rootSetCount, - rowCount: _root?.subtreeSize, + rowCount: _rowsInTree.value.length, ), ); } + @override + void dispose() { + _rowsInTree.dispose(); + super.dispose(); + } + void addClient(InspectorControllerClient value) { final firstClient = _clients.isEmpty; _clients.add(value); @@ -150,14 +162,6 @@ class InspectorTreeController extends DisposableController } } - // Method defined to avoid a direct Flutter dependency. - void setState(VoidCallback fn) { - fn(); - for (final client in _clients) { - client.onChanged(); - } - } - void requestFocus() { for (final client in _clients) { client.requestFocus(); @@ -168,23 +172,21 @@ class InspectorTreeController extends DisposableController InspectorTreeNode? _root; set root(InspectorTreeNode? node) { - if (disposed) return; + if (node != null) { + _updateRows(node: node, updateSearchableRows: true); + } + _root = node; - setState(() { - _root = node; - _populateSearchableCachedRows(); - - ga.select( - gac.inspector, - gac.inspectorTreeControllerRootChange, - nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( - inspectorTreeControllerId: gaId, - rootSetCount: ++_rootSetCount, - rowCount: _root?.subtreeSize, - ), - ); - }); + ga.select( + gac.inspector, + gac.inspectorTreeControllerRootChange, + nonInteraction: true, + screenMetricsProvider: () => InspectorScreenMetrics.v2( + inspectorTreeControllerId: gaId, + rootSetCount: ++_rootSetCount, + rowCount: _rowsInTree.value.length, + ), + ); } InspectorTreeNode? get selection => _selection; @@ -192,18 +194,31 @@ class InspectorTreeController extends DisposableController late final InspectorTreeConfig config; - set selection(InspectorTreeNode? node) { - if (node == _selection) return; + /// Refreshes the tree's rows if the return value of the [updateTreeAction] + /// callback is true. + void refreshTree({required bool Function() updateTreeAction}) { + final requiresRefresh = updateTreeAction(); + if (requiresRefresh) { + _updateRows(); + } + } - setState(() { - _selection?.selected = false; - _selection = node; - _selection?.selected = true; - final configLocal = config; - if (configLocal.onSelectionChange != null) { - configLocal.onSelectionChange!(); - } - }); + bool setSelectedNode( + InspectorTreeNode? node, { + bool notifyFlutterInspector = false, + }) { + if (node == _selection) return false; + + _selection?.selected = false; + _selection = node; + _selection?.selected = true; + final configLocal = config; + if (configLocal.onSelectionChange != null) { + configLocal.onSelectionChange!( + notifyFlutterInspector: notifyFlutterInspector, + ); + } + return true; } InspectorTreeNode? get hover => _hover; @@ -211,62 +226,82 @@ class InspectorTreeController extends DisposableController double? lastContentWidth; - final cachedRows = []; InspectorTreeRow? _cachedSelectedRow; /// All cached rows of the tree. /// - /// Similar to [cachedRows] but: + /// Similar to [rowsInTree] but: /// * contains every row in the tree (including collapsed rows) /// * items don't change when nodes are expanded or collapsed /// * items are populated only when root is changed final _searchableCachedRows = []; - void setSearchTarget(SearchTargetType searchTarget) { - _searchTarget = searchTarget; - refreshSearchMatches(); - } + /// All the rows that should be displayed in the tree. + /// + /// The rows can be updated with a call to [_updateRows]. + ValueListenable> get rowsInTree => _rowsInTree; + final _rowsInTree = ValueNotifier>([]); - // TODO: we should add a listener instead that clears the cache when the - // root is marked as dirty. - void _maybeClearCache() { - final rootLocal = root; - if (rootLocal != null && rootLocal.isDirty) { - cachedRows.clear(); - _cachedSelectedRow = null; - rootLocal.isDirty = false; - lastContentWidth = null; - } - } + /// Map from node to the index for that node's row in [rowsInTree]. + final _nodeToRowIndex = {}; - void _populateSearchableCachedRows() { - _searchableCachedRows.clear(); - for (int i = 0; i < numRows; i++) { - _searchableCachedRows.add(getCachedRow(i)); - } - } + /// Rebuilds the tree and updates [rowsInTree] with the new values. + /// + /// If [updateSearchableRows] is true, also updates [_searchableCachedRows] + /// with the new values. + void _updateRows({ + InspectorTreeNode? node, + bool updateSearchableRows = false, + }) { + if (disposed) return; + + // TODO(elliette): Consider only updating an [InspectorTreeNode]'s branch + // when it is marked as dirty, instead of the entire tree. See: + // https://github.com/flutter/devtools/issues/7980 + node ??= root; + if (node == null) return; - InspectorTreeRow? getCachedRow(int index) { - if (index < 0) return null; + final rows = _buildRows(node); + _rowsInTree.value = rows; - _maybeClearCache(); - while (cachedRows.length <= index) { - cachedRows.add(null); + // Build the reverse node-to-index map for faster lookups: + for (int i = 0; i < _rowsInTree.value.length; i++) { + final row = _rowsInTree.value[i]; + final node = row.node; + _nodeToRowIndex[node] = i; } - cachedRows[index] ??= root?.getRow(index); - final cachedRow = cachedRows[index]; - cachedRow?.isSearchMatch = - _searchableCachedRows.safeGet(index)?.isSearchMatch ?? false; + if (updateSearchableRows) { + final searchableRows = _buildRows( + node, + includeHiddenRows: true, + includeCollapsedRows: true, + ); - if (cachedRow?.isSelected == true) { - _cachedSelectedRow = cachedRow; + _searchableCachedRows + ..clear() + ..addAll(searchableRows); } - return cachedRow; } - double getRowOffset(int index) { - return (getCachedRow(index)?.depth ?? 0) * inspectorColumnWidth; + /// Resets the state if the root has been marked as dirty. + void _handleDirtyNode(InspectorTreeNode node) { + if (node == root) { + _cachedSelectedRow = null; + lastContentWidth = null; + _updateRows(); + } + } + + void setSearchTarget(SearchTargetType searchTarget) { + _searchTarget = searchTarget; + refreshSearchMatches(); + } + + InspectorTreeRow? rowAtIndex(int index) => _rowsInTree.value.safeGet(index); + + double rowOffset(int index) { + return (rowAtIndex(index)?.depth ?? 0) * inspectorColumnIndent; } List getPathFromSelectedRowToRoot() { @@ -286,10 +321,8 @@ class InspectorTreeController extends DisposableController if (node == _hover) { return; } - setState(() { - _hover = node; - // TODO(jacobr): we could choose to repaint only a portion of the UI - }); + + _hover = node; } void navigateUp() { @@ -302,6 +335,13 @@ class InspectorTreeController extends DisposableController void navigateLeft() { final selectionLocal = selection; + final diagnostic = selectionLocal?.diagnostic; + + final toggledHideableGroup = _maybeToggleHideableGroup( + diagnostic, + showGroup: false, + ); + if (toggledHideableGroup) return; // This logic is consistent with how IntelliJ handles tree navigation on // on left arrow key press. @@ -310,57 +350,96 @@ class InspectorTreeController extends DisposableController return; } - if (selectionLocal.isExpanded) { - setState(() { - selectionLocal.isExpanded = false; - }); - return; - } - if (selectionLocal.parent != null) { - selection = selectionLocal.parent; - } + refreshTree( + updateTreeAction: () { + if (selectionLocal.isExpanded) { + selectionLocal.isExpanded = false; + return true; + } + if (selectionLocal.parent != null) { + return setSelectedNode(selectionLocal.parent); + } + return false; + }, + ); } void navigateRight() { + final selectionLocal = selection; + final diagnostic = selectionLocal?.diagnostic; + + final toggledHideableGroup = _maybeToggleHideableGroup( + diagnostic, + showGroup: true, + ); + if (toggledHideableGroup) return; + // This logic is consistent with how IntelliJ handles tree navigation on // on right arrow key press. - final selectionLocal = selection; - if (selectionLocal == null || selectionLocal.isExpanded) { _navigateHelper(1); return; } - setState(() { - selectionLocal.isExpanded = true; - }); + selectionLocal.isExpanded = true; + _updateRows(); } void _navigateHelper(int indexOffset) { - if (numRows == 0) return; + if (_numRows == 0) return; + + refreshTree( + updateTreeAction: () { + final nodeToSelect = selection == null + ? root + : rowAtIndex( + (_rowIndexFromNode(selection!) + indexOffset).clamp( + 0, + _numRows - 1, + ), + )?.node; + setSelectedNode(nodeToSelect); + return true; + }, + ); + } - if (selection == null) { - selection = root; - return; + /// Given [showGroup], toggles the visibility of a hideable group. + /// + /// Returns a [bool] representing whether or not the group was toggled. + bool _maybeToggleHideableGroup( + RemoteDiagnosticsNode? diagnostic, { + required bool showGroup, + }) { + final isHideableGroupLeader = + diagnostic != null && diagnostic.isHideableGroupLeader; + final shouldToggle = + isHideableGroupLeader && + (showGroup ? diagnostic.groupIsHidden : !diagnostic.groupIsHidden); + + if (shouldToggle) { + refreshTree( + updateTreeAction: () { + diagnostic.toggleHiddenGroup(); + return true; + }, + ); + return true; } - final rootLocal = root!; - - selection = rootLocal - .getRow( - (rootLocal.getRowIndex(selection!) + indexOffset).clamp( - 0, - numRows - 1, - ), - ) - ?.node; + return false; } static const horizontalPadding = 10.0; + /// Returns the indentation of a row at the given [depth] in the inspector. + /// + /// This indentation roughly corresponds to the center of the icon next to the + /// widget name. double getDepthIndent(int depth) { - return (depth + 1) * inspectorColumnWidth + horizontalPadding; + // Note: depth is 0-based, therefore add 1. + return (depth + 1) * inspectorColumnIndent + horizontalPadding; } double rowYTop(int index) { @@ -368,27 +447,15 @@ class InspectorTreeController extends DisposableController } void nodeChanged(InspectorTreeNode node) { - setState(() { - node.isDirty = true; - }); + node.isDirty = true; } void removeNodeFromParent(InspectorTreeNode node) { - setState(() { - node.parent?.removeChild(node); - }); - } - - void appendChild(InspectorTreeNode node, InspectorTreeNode child) { - setState(() { - node.appendChild(child); - }); + node.parent?.removeChild(node); } void expandPath(InspectorTreeNode? node) { - setState(() { - _expandPath(node); - }); + _expandPath(node); } void _expandPath(InspectorTreeNode? node) { @@ -400,50 +467,95 @@ class InspectorTreeController extends DisposableController } } - void collapseToSelected() { - setState(() { - _collapseAllNodes(root!); - if (selection == null) return; - _expandPath(selection); - }); + void toggleHiddenGroup(InspectorTreeNode? node) { + final diagnostic = node?.diagnostic; + if (diagnostic != null) { + diagnostic.toggleHiddenGroup(); + } } - void _collapseAllNodes(InspectorTreeNode root) { - root.isExpanded = false; - root.children.forEach(_collapseAllNodes); - } + int get _numRows => _rowsInTree.value.length; - int get numRows => root?.subtreeSize ?? 0; + int _rowIndexFromNode(InspectorTreeNode node) => _nodeToRowIndex[node] ?? -1; - int getRowIndex(double y) => max(0, y ~/ inspectorRowHeight); + int _rowIndexFromOffset(double y) => max(0, y ~/ inspectorRowHeight); + + List _buildRows( + InspectorTreeNode node, { + bool includeHiddenRows = false, + bool includeCollapsedRows = false, + }) { + final rows = []; + + void buildRowsHelper( + InspectorTreeNode node, { + required int depth, + required List ticks, + }) { + final currentIdx = rows.length; + final isHidden = node.diagnostic?.isHidden ?? false; + if (!isHidden || includeHiddenRows) { + rows.add( + InspectorTreeRow( + node: node, + index: currentIdx, + ticks: ticks, + depth: depth, + lineToParent: + !node.isProperty && + currentIdx != 0 && + node.parent!.showLinesToChildren, + hasSingleChild: node.children.length == 1, + ), + ); + } + + if (!node.isExpanded && !includeCollapsedRows) return; + final children = node.children; + final parentDepth = depth; + final childrenDepth = children.length > 1 ? parentDepth + 1 : parentDepth; + for (final child in children) { + final shouldAddTick = + children.length > 1 && + children.last != child && + !children.last.isProperty && + node.diagnostic?.shouldIndent == true; + + buildRowsHelper( + child, + depth: childrenDepth, + ticks: [...ticks, if (shouldAddTick) parentDepth], + ); + } + } + + buildRowsHelper(node, depth: 0, ticks: []); + return rows; + } InspectorTreeRow? getRowForNode(InspectorTreeNode node) { final rootLocal = root; if (rootLocal == null) return null; - return getCachedRow(rootLocal.getRowIndex(node)); + return rowAtIndex(_rowIndexFromNode(node)); } - InspectorTreeRow? getRow(Offset offset) { + InspectorTreeRow? rowForOffset(Offset offset) { final rootLocal = root; if (rootLocal == null) return null; - final row = getRowIndex(offset.dy); - return row < rootLocal.subtreeSize ? getCachedRow(row) : null; + final row = _rowIndexFromOffset(offset.dy); + return row < _rowsInTree.value.length ? rowAtIndex(row) : null; } void onExpandRow(InspectorTreeRow row) { - setState(() { - final onExpand = config.onExpand; - row.node.isExpanded = true; - if (onExpand != null) { - onExpand(row.node); - } - }); + final onExpand = config.onExpand; + row.node.isExpanded = true; + if (onExpand != null) { + onExpand(row.node); + } } void onCollapseRow(InspectorTreeRow row) { - setState(() { - row.node.isExpanded = false; - }); + row.node.isExpanded = false; } void onSelectRow(InspectorTreeRow row) { @@ -451,12 +563,16 @@ class InspectorTreeController extends DisposableController } void onSelectNode(InspectorTreeNode? node) { - selection = node; + setSelectedNode(node, notifyFlutterInspector: true); ga.select( gac.inspector, gac.treeNodeSelection, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), + screenMetricsProvider: () => InspectorScreenMetrics.v2(), ); + final diagnostic = node?.diagnostic; + if (diagnostic != null && diagnostic.groupIsHidden) { + diagnostic.hideableGroupLeader?.toggleHiddenGroup(); + } expandPath(node); } @@ -480,7 +596,7 @@ class InspectorTreeController extends DisposableController void scrollToRect(Rect targetRect) { for (final client in _clients) { - client.scrollToRect(targetRect); + client.waitForClientsThenScrollToRect(targetRect); } } @@ -495,8 +611,8 @@ class InspectorTreeController extends DisposableController double get maxRowIndent { if (lastContentWidth == null) { double maxIndent = 0; - for (int i = 0; i < numRows; i++) { - final row = getCachedRow(i); + for (int i = 0; i < _numRows; i++) { + final row = rowAtIndex(i); if (row != null) { maxIndent = max(maxIndent, getDepthIndent(row.depth)); } @@ -551,13 +667,17 @@ class InspectorTreeController extends DisposableController InspectorTreeNode node, RemoteDiagnosticsNode diagnosticsNode, { required bool expandChildren, - required bool expandProperties, + RemoteDiagnosticsNode? hideableGroupLeader, }) { node.diagnostic = diagnosticsNode; final configLocal = config; if (configLocal.onNodeAdded != null) { configLocal.onNodeAdded!(node, diagnosticsNode); } + final inHideableGroup = diagnosticsNode.inHideableGroup; + if (inHideableGroup && hideableGroupLeader != null) { + hideableGroupLeader.addHideableGroupSubordinate(diagnosticsNode); + } if (diagnosticsNode.hasChildren || diagnosticsNode.inlineProperties.isNotEmpty) { @@ -570,7 +690,9 @@ class InspectorTreeController extends DisposableController node, node.diagnostic!.childrenNow, expandChildren: expandChildren && styleIsMultiline, - expandProperties: expandProperties && styleIsMultiline, + hideableGroupLeader: inHideableGroup + ? (hideableGroupLeader ?? diagnosticsNode) + : null, ); } else { node.clearChildren(); @@ -585,38 +707,42 @@ class InspectorTreeController extends DisposableController InspectorTreeNode treeNode, List? children, { required bool expandChildren, - required bool expandProperties, + RemoteDiagnosticsNode? hideableGroupLeader, }) { treeNode.isExpanded = expandChildren; if (treeNode.children.isNotEmpty) { // Only case supported is this is the loading node. assert(treeNode.children.length == 1); - removeNodeFromParent(treeNode.children.first); + refreshTree( + updateTreeAction: () { + removeNodeFromParent(treeNode.children.first); + return true; + }, + ); } final inlineProperties = parent.inlineProperties; for (final property in inlineProperties) { - appendChild( - treeNode, + treeNode.appendChild( setupInspectorTreeNode( createNode(), property, // We are inside a property so only expand children if // expandProperties is true. - expandChildren: expandProperties, - expandProperties: expandProperties, + expandChildren: false, ), ); } if (children != null) { for (final child in children) { - appendChild( - treeNode, + treeNode.appendChild( setupInspectorTreeNode( createNode(), child, expandChildren: expandChildren, - expandProperties: expandProperties, + hideableGroupLeader: child.inHideableGroup + ? hideableGroupLeader + : null, ), ); } @@ -631,17 +757,16 @@ class InspectorTreeController extends DisposableController try { final children = await diagnostic.children; if (treeNode.hasPlaceholderChildren || treeNode.children.isEmpty) { - setupChildren( - diagnostic, - treeNode, - children, - expandChildren: true, - expandProperties: false, + setupChildren(diagnostic, treeNode, children, expandChildren: true); + refreshTree( + updateTreeAction: () { + nodeChanged(treeNode); + if (treeNode == selection) { + expandPath(treeNode); + } + return true; + }, ); - nodeChanged(treeNode); - if (treeNode == selection) { - expandPath(treeNode); - } } } catch (e, st) { _log.shout(e, e, st); @@ -652,7 +777,12 @@ class InspectorTreeController extends DisposableController /* Search support */ @override void onMatchChanged(int index) { - onSelectRow(searchMatches.value[index]); + refreshTree( + updateTreeAction: () { + onSelectRow(searchMatches.value[index]); + return true; + }, + ); } @override @@ -734,10 +864,10 @@ extension RemoteDiagnosticsNodeExtension on RemoteDiagnosticsNode { } abstract class InspectorControllerClient { - void onChanged(); - void scrollToRect(Rect rect); + void waitForClientsThenScrollToRect(Rect rect, {int retries}); + void requestFocus(); } @@ -746,24 +876,14 @@ class InspectorTree extends StatefulWidget { super.key, required this.controller, required this.treeController, - this.summaryTreeController, - this.isSummaryTree = false, this.widgetErrors, this.screenId, - }) : assert(isSummaryTree == (summaryTreeController == null)); + }); final InspectorController controller; final InspectorTreeController? treeController; - /// Stores the summary tree controller when this instance of [InspectorTree] - /// is for the details tree (i.e. when [isSummaryTree] is false). - /// - /// This value should be null when this instance of [InspectorTree] is for the - /// summary tree itself. - final InspectorTreeController? summaryTreeController; - - final bool isSummaryTree; final LinkedHashMap? widgetErrors; final String? screenId; @@ -799,9 +919,7 @@ class _InspectorTreeState extends State _scrollControllerY = ScrollController(); // TODO(devoncarew): Commented out as per flutter/devtools/pull/2001. //_scrollControllerY.addListener(_onScrollYChange); - if (widget.isSummaryTree) { - _constraintDisplayController = longAnimationController(this); - } + _constraintDisplayController = longAnimationController(this); _focusNode = FocusNode(debugLabel: 'inspector-tree'); autoDisposeFocusNode(_focusNode); final mainIsolateState = @@ -813,6 +931,10 @@ class _InspectorTreeState extends State readyWhen: (triggerValue) => !triggerValue, ); } + + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.animateTo(controller.selectedNode.value); + }); } @override @@ -861,6 +983,19 @@ class _InspectorTreeState extends State // ); // } + @override + Future waitForClientsThenScrollToRect( + Rect rect, { + int retries = 5, + }) async { + if (_scrollControllerY.hasClients || _scrollControllerX.hasClients) { + return scrollToRect(rect); + } + if (retries == 0) return; + await Future.delayed(const Duration(milliseconds: 20)); + return waitForClientsThenScrollToRect(rect, retries: retries - 1); + } + @override Future scrollToRect(Rect rect) async { if (rect == _currentAnimateTarget) { @@ -884,9 +1019,15 @@ class _InspectorTreeState extends State safeViewportHeight, ); - final isRectInViewPort = - viewPortInScrollControllerSpace.contains(rect.topLeft) && - viewPortInScrollControllerSpace.contains(rect.bottomRight); + // Decide to scroll based on whether the middle of the center-left half of + // the row is visible. See https://github.com/flutter/devtools/pull/8367. + final centerLeftHalf = Offset( + (rect.centerLeft.dx + rect.center.dx) / 2, + rect.center.dy, + ); + final isRectInViewPort = viewPortInScrollControllerSpace.contains( + centerLeftHalf, + ); if (isRectInViewPort) { // The rect is already in view, don't scroll return; @@ -951,7 +1092,7 @@ class _InspectorTreeState extends State required double initialX, int padCount = _scrollPadCount, }) { - return initialX - inspectorColumnWidth * padCount; + return initialX - inspectorColumnIndent * padCount; } /// Pad [initialY] so that a row would be placed in the vertical center of @@ -988,11 +1129,6 @@ class _InspectorTreeState extends State treeController?.addClient(this); } - @override - void onChanged() { - setState(() {}); - } - @override Widget build(BuildContext context) { super.build(context); @@ -1001,113 +1137,105 @@ class _InspectorTreeState extends State // Indicate the tree is loading. return const CenteredCircularProgressIndicator(); } - if (treeControllerLocal.numRows == 0) { - // This works around a bug when Scrollbars are present on a short lived - // widget. - return const SizedBox(); - } - if (!controller.firstInspectorTreeLoadCompleted && widget.isSummaryTree) { - final screenId = widget.screenId; - if (screenId != null) { - ga.timeEnd( - screenId, - gac.pageReady, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( - rowCount: treeControllerLocal.numRows, - ), - ); - unawaited( - serviceConnection.sendDwdsEvent( - screen: screenId, - action: gac.pageReady, - ), - ); - } - controller.firstInspectorTreeLoadCompleted = true; - } - return LayoutBuilder( - builder: (context, constraints) { - final viewportWidth = constraints.maxWidth; - final tree = Scrollbar( - thumbVisibility: true, - controller: _scrollControllerX, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _scrollControllerX, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: - treeControllerLocal.rowWidth + - treeControllerLocal.maxRowIndent, + return ValueListenableBuilder>( + valueListenable: treeControllerLocal.rowsInTree, + builder: (context, rows, _) { + // Note: The inspector rows contain only the fake root node when the + // inspector tree is shutdown. + if (rows.length <= 1) { + // This works around a bug when Scrollbars are present on a short lived + // widget. + return const SizedBox(child: CenteredCircularProgressIndicator()); + } + + if (!controller.firstInspectorTreeLoadCompleted) { + final screenId = widget.screenId; + if (screenId != null) { + ga.timeEnd( + screenId, + gac.pageReady, + screenMetricsProvider: () => + InspectorScreenMetrics.v2(rowCount: rows.length), + ); + unawaited( + serviceConnection.sendDwdsEvent( + screen: screenId, + action: gac.pageReady, ), - // TODO(kenz): this scrollbar needs to be sticky to the right side of - // the visible container - right now it is lined up to the right of - // the widest row (which is likely not visible). This may require some - // refactoring. - child: GestureDetector( - onTap: _focusNode.requestFocus, - child: Focus( - onKeyEvent: _handleKeyEvent, - autofocus: widget.isSummaryTree, - focusNode: _focusNode, - child: OffsetScrollbar( - isAlwaysShown: true, - axis: Axis.vertical, - controller: _scrollControllerY, - offsetController: _scrollControllerX, - offsetControllerViewportDimension: viewportWidth, - child: ListView.custom( - itemExtent: inspectorRowHeight, - childrenDelegate: SliverChildBuilderDelegate(( - context, - index, - ) { - if (index == treeControllerLocal.numRows) { - return const SizedBox(height: inspectorRowHeight); - } - final row = treeControllerLocal.getCachedRow(index)!; - final inspectorRef = row.node.diagnostic?.valueRef.id; - return _InspectorTreeRowWidget( - key: PageStorageKey(row.node), - inspectorTreeState: this, - row: row, - scrollControllerX: _scrollControllerX, - viewportWidth: viewportWidth, - error: - widget.widgetErrors != null && - inspectorRef != null - ? widget.widgetErrors![inspectorRef] - : null, - ); - }, childCount: treeControllerLocal.numRows + 1), - controller: _scrollControllerY, + ); + } + controller.firstInspectorTreeLoadCompleted = true; + } + + return LayoutBuilder( + builder: (context, constraints) { + final viewportWidth = constraints.maxWidth; + final tree = Scrollbar( + thumbVisibility: true, + controller: _scrollControllerX, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _scrollControllerX, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + treeControllerLocal.rowWidth + + treeControllerLocal.maxRowIndent, + ), + // TODO(kenz): this scrollbar needs to be sticky to the right side of + // the visible container - right now it is lined up to the right of + // the widest row (which is likely not visible). This may require some + // refactoring. + child: GestureDetector( + onTap: _focusNode.requestFocus, + child: Focus( + onKeyEvent: _handleKeyEvent, + autofocus: true, + focusNode: _focusNode, + child: OffsetScrollbar( + isAlwaysShown: true, + axis: Axis.vertical, + controller: _scrollControllerY, + offsetController: _scrollControllerX, + offsetControllerViewportDimension: viewportWidth, + child: ListView.custom( + itemExtent: inspectorRowHeight, + childrenDelegate: SliverChildBuilderDelegate(( + context, + index, + ) { + if (index == rows.length) { + return const SizedBox(height: inspectorRowHeight); + } + final row = treeControllerLocal.rowAtIndex(index)!; + final inspectorRef = + row.node.diagnostic?.valueRef.id; + return _InspectorTreeRowWidget( + key: PageStorageKey(row.node), + inspectorTreeState: this, + row: row, + scrollControllerX: _scrollControllerX, + viewportWidth: viewportWidth, + error: + widget.widgetErrors != null && + inspectorRef != null + ? widget.widgetErrors![inspectorRef] + : null, + ); + }, childCount: rows.length + 1), + controller: _scrollControllerY, + ), + ), ), ), ), ), - ), - ), - ); - - final shouldShowBreadcrumbs = !widget.isSummaryTree; - if (shouldShowBreadcrumbs) { - final inspectorTreeController = widget.summaryTreeController!; - - final parents = inspectorTreeController - .getPathFromSelectedRowToRoot(); - return Column( - children: [ - InspectorBreadcrumbNavigator( - items: parents, - onTap: (node) => inspectorTreeController.onSelectNode(node), - ), - Expanded(child: tree), - ], - ); - } + ); - return tree; + return tree; + }, + ); }, ); } @@ -1120,6 +1248,20 @@ Paint _defaultPaint(ColorScheme colorScheme) => Paint() ..color = colorScheme.treeGuidelineColor ..strokeWidth = chartLineStrokeWidth; +/// The distance (on the x-axis) between the center of the widget icon and the +/// start of the row, as determined by a percentage of the +/// [inspectorColumnIndent]. +const _iconCenterToRowStartXDistancePercentage = 0.41; + +/// The distance (on the y-axis) between the bottom of the widget icon and the +/// top of the row, as determined by a percentage of the [inspectorRowHeight]. +const _iconBottomToRowTopYDistancePercentage = 0.75; + +/// The distance (on the y-axis) between the top of the child widget's icon and +/// the top of the current row, as determined by a percentage of the +/// [inspectorRowHeight]. +const _childIconTopToRowTopYDistancePercentage = 1.25; + /// Custom painter that draws lines indicating how parent and child rows are /// connected to each other. /// @@ -1138,38 +1280,76 @@ class _RowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - double currentX = 0; final paint = _defaultPaint(colorScheme); final node = row.node; final showExpandCollapse = node.showExpandCollapse; + const distanceFromIconCenterToRowStart = + inspectorColumnIndent * _iconCenterToRowStartXDistancePercentage; for (final tick in row.ticks) { - currentX = _controller.getDepthIndent(tick) - inspectorColumnWidth * 0.5; + final expandCollapseX = + _controller.getDepthIndent(tick) - distanceFromIconCenterToRowStart; // Draw a vertical line for each tick identifying a connection between // an ancestor of this node and some other node in the tree. canvas.drawLine( - Offset(currentX, 0.0), - Offset(currentX, inspectorRowHeight), + Offset(expandCollapseX, 0.0), + Offset(expandCollapseX, inspectorRowHeight), paint, ); } // If this row is itself connected to a parent then draw the L shaped line // to make that connection. if (row.lineToParent) { - currentX = + final parentExpandCollapseX = _controller.getDepthIndent(row.depth - 1) - - inspectorColumnWidth * 0.5; + distanceFromIconCenterToRowStart; final width = showExpandCollapse - ? inspectorColumnWidth * 0.5 - : inspectorColumnWidth; + ? inspectorColumnIndent * 0.45 + : inspectorColumnIndent * 0.6; canvas.drawLine( - Offset(currentX, 0.0), - Offset(currentX, inspectorRowHeight * 0.5), + Offset(parentExpandCollapseX, 0.0), + Offset(parentExpandCollapseX, inspectorRowHeight * 0.5), paint, ); canvas.drawLine( - Offset(currentX, inspectorRowHeight * 0.5), - Offset(currentX + width, inspectorRowHeight * 0.5), + Offset(parentExpandCollapseX, inspectorRowHeight * 0.5), + Offset(parentExpandCollapseX + width, inspectorRowHeight * 0.5), + paint, + ); + } + + // Draw a straight vertical line from current node's icon to the icon below + // it if either the current node: + // 1. is expanded (meaning its child is visible) and it only has one child + // (because multiple children get indented). + // 2. is NOT the first node in a hidden group of which the last hidden node + // in that group is childless (meaning that last node is at the end of a + // branch and therefore has nothing below it). + final expandedWithSingleChild = row.hasSingleChild && node.isExpanded; + final subordinates = + node.diagnostic?.hideableGroupSubordinates ?? []; + final groupIsHidden = node.diagnostic?.groupIsHidden ?? false; + final lastHiddenSubordinateHasNoChildren = + groupIsHidden && + subordinates.isNotEmpty && + subordinates.last.childrenNow.isEmpty; + if (expandedWithSingleChild && !lastHiddenSubordinateHasNoChildren) { + const distanceFromIconCenterToRowStart = + inspectorColumnIndent * _iconCenterToRowStartXDistancePercentage; + final iconCenterX = + _controller.getDepthIndent(row.depth) - + distanceFromIconCenterToRowStart; + // Draw a line from the bottom of the current row's icon to the top of the + // child row's icon: + canvas.drawLine( + Offset( + iconCenterX, + inspectorRowHeight * _iconBottomToRowTopYDistancePercentage, + ), + Offset( + iconCenterX, + inspectorRowHeight * _childIconTopToRowTopYDistancePercentage, + ), paint, ); } @@ -1224,7 +1404,7 @@ class InspectorRowContent extends StatelessWidget { @override Widget build(BuildContext context) { final currentX = - controller.getDepthIndent(row.depth) - inspectorColumnWidth; + controller.getDepthIndent(row.depth) - inspectorColumnIndent; final theme = Theme.of(context); final colorScheme = theme.colorScheme; @@ -1236,14 +1416,16 @@ class InspectorRowContent extends StatelessWidget { } final node = row.node; - + final diagnostic = node.diagnostic; + final isHideableGroupLeader = diagnostic?.isHideableGroupLeader ?? false; + const expandCollapseWidth = 14.0; Widget rowWidget = Padding( padding: EdgeInsets.only(left: currentX), child: ValueListenableBuilder( valueListenable: controller.searchNotifier, builder: (context, searchValue, _) { return Opacity( - opacity: searchValue.isEmpty || row.isSearchMatch ? 1 : 0.2, + opacity: searchValue.isEmpty || row.isSearchMatch ? 1 : 0.6, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1259,7 +1441,7 @@ class InspectorRowContent extends StatelessWidget { ), ) : const SizedBox( - width: defaultSpacing, + width: expandCollapseWidth, height: defaultSpacing, ), Expanded( @@ -1267,11 +1449,16 @@ class InspectorRowContent extends StatelessWidget { color: backgroundColor, child: InkWell( onTap: () { - controller.onSelectRow(row); - // TODO(gmoothart): It may be possible to capture the tap - // and request focus directly from the InspectorTree. Then - // we wouldn't need this. - controller.requestFocus(); + controller.refreshTree( + updateTreeAction: () { + controller.onSelectRow(row); + // TODO(gmoothart): It may be possible to capture the tap + // and request focus directly from the InspectorTree. Then + // we wouldn't need this. + controller.requestFocus(); + return true; + }, + ); }, child: SizedBox( height: inspectorRowHeight, @@ -1280,6 +1467,7 @@ class InspectorRowContent extends StatelessWidget { isSelected: row.isSelected, searchValue: searchValue, errorText: error?.errorMessage, + emphasizeNodesFromLocalProject: true, nodeDescriptionHighlightStyle: searchValue.isEmpty || !row.isSearchMatch ? DiagnosticsTextStyles.regular( @@ -1288,6 +1476,29 @@ class InspectorRowContent extends StatelessWidget { : row.isSelected ? theme.searchMatchHighlightStyleFocused : theme.searchMatchHighlightStyle, + actionLabel: isHideableGroupLeader + ? diagnostic!.groupIsHidden + ? '(expand)' + : '(collapse)' + : null, + actionCallback: isHideableGroupLeader + ? () { + controller.refreshTree( + updateTreeAction: () { + controller.toggleHiddenGroup(node); + return true; + }, + ); + } + : null, + customDescription: + isHideableGroupLeader && diagnostic!.groupIsHidden + ? '${diagnostic.hideableGroupSubordinates!.length + 1} more widgets...' + : null, + customIconName: + isHideableGroupLeader && diagnostic!.groupIsHidden + ? 'HiddenGroup' + : null, ), ), ), diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart index ff579921994..bfd22ddd984 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart @@ -1,91 +1,34 @@ -// Copyright 2021 The Flutter Authors +// Copyright 2024 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:math' as math; - -import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/primitives/math_utils.dart'; import '../../../../shared/primitives/utils.dart'; +import '../../inspector_controller.dart'; import '../../inspector_data_models.dart'; import '../ui/free_space.dart'; -import '../ui/layout_explorer_widget.dart'; import '../ui/theme.dart'; import '../ui/utils.dart'; import '../ui/widget_constraints.dart'; import '../ui/widgets_theme.dart'; -/// Layout visualizer for a widget with a box-layout. -class BoxLayoutExplorerWidget extends LayoutExplorerWidget { - const BoxLayoutExplorerWidget(super.inspectorController, {super.key}); - - static bool shouldDisplay(RemoteDiagnosticsNode _) { - // Pretend this layout explorer is always available. This layout explorer - // will gracefully fall back to an error message if the required properties - // are not needed. - // TODO(jacobr) pass a RemoteDiagnosticsNode to this method that contains - // the layout explorer related supplemental properties so that we can - // accurately determine whether the widget uses box layout. - return true; - } - - @override - State createState() => - BoxLayoutExplorerWidgetState(); -} - -class BoxLayoutExplorerWidgetState - extends - LayoutExplorerWidgetState { - @override - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node) { - final nodeLocal = node; - if (nodeLocal == null) return null; - if (!shouldDisplay(nodeLocal)) return null; - return node; - } - - @override - bool shouldDisplay(RemoteDiagnosticsNode node) { - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return false; - return BoxLayoutExplorerWidget.shouldDisplay(selectedNodeLocal); - } - - @override - AnimatedLayoutProperties computeAnimatedProperties( - LayoutProperties nextProperties, - ) { - return AnimatedLayoutProperties( - // If an animation is in progress, freeze it and start animating from there, else start a fresh animation from widget.properties. - animatedProperties?.copyWith() ?? properties!, - nextProperties, - changeAnimation, - ); - } +class BoxLayoutExplorerWidget extends StatelessWidget { + const BoxLayoutExplorerWidget( + this.inspectorController, { + super.key, + required this.layoutProperties, + required this.selectedNode, + }); - @override - LayoutProperties computeLayoutProperties(RemoteDiagnosticsNode node) => - LayoutProperties(node); - - @override - void updateHighlighted(LayoutProperties? newProperties) { - setState(() { - // This implementation will need to change if we support showing more than - // a single widget in the box visualization for the layout explorer. - highlighted = newProperties != null && selectedNode == newProperties.node - ? newProperties - : null; - }); - } + final InspectorController inspectorController; + final LayoutProperties? layoutProperties; + final RemoteDiagnosticsNode? selectedNode; @override Widget build(BuildContext context) { - if (properties == null) { + if (layoutProperties == null) { final selectedNodeLocal = selectedNode; return Center( child: Text( @@ -95,292 +38,257 @@ class BoxLayoutExplorerWidgetState ), ); } - return Container( - margin: const EdgeInsets.all(denseSpacing), - child: AnimatedBuilder( - animation: changeController, - builder: (context, _) { - return LayoutBuilder(builder: _buildLayout); - }, - ), - ); + return LayoutBuilder(builder: _buildLayout); } - /// Simplistic layout algorithm to roughly match minFraction restrictions for - /// each sizes attempting to render a stylized version of the original layout. - /// TODO(jacobr): see if we can unify with the stylized version of the overall - /// layout used for Flex. Our constraints are quite different as we can - /// guarantee that the entire layout fits without scrolling while in the Flex - /// case that would be difficult. - /// - /// The overall layout will expand to use the full availableSize treating null - /// values in [sizes] as an indication that the items should have zero size. - /// On the other hand, a non-null size indicates that the minFractions - /// constraints should be obeyed. This is needed to ensure that negative sizes - /// are visualized reasonably. - /// The minFractions aren't exactly obeyed but they are approximated in a way - /// that keeps this algorithm simple and has the nice property that an initial - /// value much smaller than the minSize results in a slightly smaller value - /// than a value that is almost minSize. - /// In the most extreme case an item will get not minFraction but will instead - /// get the slightly smaller value of minFraction / (1 + minFraction) - /// which is close enough for the simple values we need this for. - static List minFractionLayout({ - required double availableSize, - required List sizes, - required List minFractions, + List _paddingWidgets({ + required LayoutProperties childProperties, + required LayoutProperties parentProperties, + required LayoutWidthsAndHeights widthsAndHeights, + required LayoutWidthsAndHeights displayWidthsAndHeights, + required ColorScheme colorScheme, + required Color widgetColor, }) { - assert(sizes.length == minFractions.length); - final length = sizes.length; - double total = 1.0; // This isn't set to zero to avoid divide by zero bugs. - final fractions = minFractions.toList(); - for (final size in sizes) { - if (size != null) { - total += math.max(0, size); - } - } - - double totalFraction = 0.0; - for (int i = 0; i < length; i++) { - final size = sizes[i]; - if (size != null) { - fractions[i] = math.max(size / total, minFractions[i]); - totalFraction += fractions[i]; - } else { - fractions[i] = 0.0; - } - } - if (totalFraction != 1.0) { - for (int i = 0; i < length; i++) { - fractions[i] = fractions[i] / totalFraction; - } - } - final output = []; - for (final fraction in fractions) { - output.add(fraction * availableSize); - } - return output; + if (!widthsAndHeights.hasAnyPadding) return []; + + final LayoutWidthsAndHeights( + :topPadding, + :bottomPadding, + :leftPadding, + :rightPadding, + :hasTopPadding, + :hasBottomPadding, + :hasLeftPadding, + :hasRightPadding, + ) = widthsAndHeights; + + final displayWidgetHeight = displayWidthsAndHeights.widgetHeight; + final displayWidgetWidth = displayWidthsAndHeights.widgetWidth; + final displayTopPadding = displayWidthsAndHeights.topPadding; + final displayBottomPadding = displayWidthsAndHeights.bottomPadding; + final displayLeftPadding = displayWidthsAndHeights.leftPadding; + final displayRightPadding = displayWidthsAndHeights.rightPadding; + + final parentHeight = parentProperties.size.height; + final parentWidth = parentProperties.size.width; + + return [ + LayoutExplorerBackground(colorScheme: colorScheme), + PositionedBackgroundLabel( + labelText: _describeBoxName(parentProperties), + labelColor: widgetColor, + hasTopPadding: hasTopPadding, + hasBottomPadding: hasBottomPadding, + hasLeftPadding: hasLeftPadding, + hasRightPadding: hasRightPadding, + ), + if (hasLeftPadding) + PaddingVisualizerWidget( + RenderProperties( + axis: Axis.horizontal, + size: Size(displayLeftPadding, displayWidgetHeight), + offset: Offset(0, displayTopPadding), + realSize: Size(leftPadding, parentHeight), + layoutProperties: childProperties, + isFreeSpace: true, + ), + horizontal: true, + ), + if (hasTopPadding) + PaddingVisualizerWidget( + RenderProperties( + axis: Axis.horizontal, + size: Size(displayWidgetWidth, displayTopPadding), + offset: Offset(displayLeftPadding, 0), + realSize: Size(parentWidth, topPadding), + layoutProperties: childProperties, + isFreeSpace: true, + ), + horizontal: false, + ), + if (hasRightPadding) + PaddingVisualizerWidget( + RenderProperties( + axis: Axis.horizontal, + size: Size(displayRightPadding, displayWidgetHeight), + offset: Offset( + displayLeftPadding + displayWidgetWidth, + displayTopPadding, + ), + realSize: Size(rightPadding, parentHeight), + layoutProperties: childProperties, + isFreeSpace: true, + ), + horizontal: true, + ), + if (hasBottomPadding) + PaddingVisualizerWidget( + RenderProperties( + axis: Axis.horizontal, + size: Size(displayWidgetWidth, displayBottomPadding), + offset: Offset( + displayLeftPadding, + displayTopPadding + displayWidgetHeight, + ), + realSize: Size(parentWidth, bottomPadding), + layoutProperties: childProperties, + isFreeSpace: true, + ), + horizontal: false, + ), + ]; } - Widget _buildChild(BuildContext context) { - final propertiesLocal = properties!; + Widget _buildLayout(BuildContext context, BoxConstraints constraints) { + final propertiesLocal = layoutProperties!; final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final parentProperties = - this.parentProperties ?? - propertiesLocal; // Fall back to this node's properties if there is no parent. + final parentProperties = propertiesLocal.parentLayoutProperties!; - final parentSize = parentProperties.size; - final offset = propertiesLocal.node.parentData; - - return LayoutBuilder( + final child = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // Subtract out one pixel border on each side. final availableHeight = constraints.maxHeight - 2; final availableWidth = constraints.maxWidth - 2; - final minFractions = [0.2, 0.5, 0.2]; - // TODO(polinach, jacobr): consider using zeros for zero values, - // without replacing them with nulls. - // See https://github.com/flutter/devtools/issues/3931. - double? nullOutZero(double value) => value != 0.0 ? value : null; - final widths = [ - nullOutZero(offset.offset.dx), - propertiesLocal.size.width, - nullOutZero( - parentSize.width - (propertiesLocal.size.width + offset.offset.dx), - ), - ]; - final heights = [ - nullOutZero(offset.offset.dy), - propertiesLocal.size.height, - nullOutZero( - parentSize.height - - (propertiesLocal.size.height + offset.offset.dy), - ), - ]; - // 3 element array with [left padding, widget width, right padding]. - final displayWidths = minFractionLayout( + final widgetWidths = propertiesLocal.widgetWidths; + final widgetHeights = propertiesLocal.widgetHeights; + + final widthsAndHeights = LayoutWidthsAndHeights( + widths: widgetWidths!, + heights: widgetHeights!, + ); + + final displayWidths = _simpleFractionalLayout( availableSize: availableWidth, - sizes: widths, - minFractions: minFractions, + sizes: widgetWidths, ); - // 3 element array with [top padding, widget height, bottom padding]. - final displayHeights = minFractionLayout( + final displayHeights = _simpleFractionalLayout( availableSize: availableHeight, - sizes: heights, - minFractions: minFractions, + sizes: widgetHeights, ); - final widgetWidth = displayWidths[1]; - final widgetHeight = displayHeights[1]; - final safeParentSize = parentSize; - final width0 = widths[0]; - final width2 = widths[2]; - final height0 = heights[0]; - final height2 = heights[2]; - return Container( - width: constraints.maxWidth, - height: constraints.maxHeight, - decoration: BoxDecoration( - border: Border.all( - color: WidgetTheme.fromName( - propertiesLocal.node.description, - ).color, - ), - ), - child: Stack( - children: [ - LayoutExplorerBackground(colorScheme: colorScheme), - // Left padding. - if (width0 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidths[0], widgetHeight), - offset: Offset(0, displayHeights[0]), - realSize: Size(width0, safeParentSize.height), - layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: true, - ), - // Top padding. - if (height0 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, displayHeights[0]), - offset: Offset(displayWidths[0], 0), - realSize: Size(safeParentSize.width, height0), - layoutProperties: propertiesLocal, - isFreeSpace: true, + final displayWidthsAndHeights = LayoutWidthsAndHeights( + widths: displayWidths, + heights: displayHeights, + ); + + final widgetColor = WidgetTheme.fromName( + propertiesLocal.node.description, + ).color; + return Column( + children: [ + Container( + width: constraints.maxWidth, + height: constraints.maxHeight, + decoration: BoxDecoration(border: Border.all(color: widgetColor)), + child: Stack( + children: [ + ..._paddingWidgets( + childProperties: propertiesLocal, + parentProperties: parentProperties, + widthsAndHeights: widthsAndHeights, + displayWidthsAndHeights: displayWidthsAndHeights, + colorScheme: colorScheme, + widgetColor: widgetColor, ), - horizontal: false, - ), - // Right padding. - if (width2 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidths[2], widgetHeight), - offset: Offset( - displayWidths[0] + displayWidths[1], - displayHeights[0], - ), - realSize: Size(width2, safeParentSize.height), + BoxChildVisualizer( + isSelected: true, layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: true, - ), - // Bottom padding. - if (height2 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, displayHeights[2]), - offset: Offset( - displayWidths[0], - displayHeights[0] + displayHeights[1], + renderProperties: RenderProperties( + axis: Axis.horizontal, + size: Size( + displayWidthsAndHeights.widgetWidth, + displayWidthsAndHeights.widgetHeight, + ), + offset: Offset( + displayWidthsAndHeights.leftPadding, + displayWidthsAndHeights.topPadding, + ), + realSize: propertiesLocal.size, + layoutProperties: propertiesLocal, ), - realSize: Size(safeParentSize.width, height2), - layoutProperties: propertiesLocal, - isFreeSpace: true, ), - horizontal: false, - ), - BoxChildVisualizer( - isSelected: true, - state: this, - layoutProperties: propertiesLocal, - renderProperties: RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, widgetHeight), - offset: Offset(displayWidths[0], displayHeights[0]), - realSize: propertiesLocal.size, - layoutProperties: propertiesLocal, - ), + ], ), - ], - ), + ), + ], ); }, ); - } - - LayoutProperties? get parentProperties { - final parentElement = properties?.node.parentRenderElement; - if (parentElement == null) return null; - return computeLayoutProperties(parentElement); - } - Widget _buildLayout(BuildContext context, BoxConstraints constraints) { - final maxHeight = constraints.maxHeight; - final maxWidth = constraints.maxWidth; - - Widget widget = _buildChild(context); - final parentProperties = this.parentProperties; - if (parentProperties != null) { - // Wrap with a widget visualizer for the parent if there is a valid parent. - widget = WidgetVisualizer( - // TODO(jacobr): this node's name can be misleading more often than - // in the flex case the widget doesn't have its own RenderObject. - // Consider showing the true ancestor for the summary tree that first - // has a different render object. - title: describeBoxName(parentProperties), - largeTitle: true, - layoutProperties: parentProperties, - isSelected: false, - child: VisualizeWidthAndHeightWithConstraints( - properties: parentProperties, - warnIfUnconstrained: false, - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: widget, - ), - ), - ); - } return Container( - constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), - child: widget, + constraints: BoxConstraints( + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + ), + child: child, ); } } -String describeBoxName(LayoutProperties properties) { - // Displaying a high quality name is more ambiguous for the Box case than the - // Flex case because the RenderObject for each widget is often quite - // different than the user expected as not all widgets have RenderObjects. - // As a compromise we currently show 'WidgetName - RenderObjectName'. - // This is clearer but risks more confusion - - // Widget name. - var title = properties.node.description ?? ''; - final renderDescription = properties.node.renderObject?.description; - // TODO(jacobr): consider de-emphasizing the render object name by putting it - // in more transparent text or just calling the widget Parent instead of - // surfacing a widget name. - if (renderDescription != null) { - // Name of the associated RenderObject if one is available. - title += ' - $renderDescription'; +String _describeBoxName(LayoutProperties properties) => + properties.node.description ?? ''; + +/// Represents a box widget and its surrounding padding. +class BoxChildAndPaddingVisualizer extends StatelessWidget { + const BoxChildAndPaddingVisualizer({ + super.key, + required this.layoutProperties, + required this.renderProperties, + required this.isSelected, + }); + + final bool isSelected; + final LayoutProperties layoutProperties; + final RenderProperties renderProperties; + + LayoutProperties? get properties => renderProperties.layoutProperties; + + @override + Widget build(BuildContext context) { + final renderSize = renderProperties.size; + final renderOffset = renderProperties.offset; + + final propertiesLocal = properties!; + + return Positioned( + top: renderOffset.dy, + left: renderOffset.dx, + child: SizedBox( + width: safePositiveDouble(renderSize.width), + height: safePositiveDouble(renderSize.height), + child: WidgetVisualizer( + isSelected: isSelected, + layoutProperties: layoutProperties, + title: _describeBoxName(propertiesLocal), + // TODO(jacobr): consider surfacing the overflow size information + // if we determine + // overflowSide: properties.overflowSide, + + // We only show one child at a time so a large title is safe. + largeTitle: true, + child: VisualizeWidthAndHeightWithConstraints( + arrowHeadSize: arrowHeadSize, + properties: propertiesLocal, + warnIfUnconstrained: false, + child: const SizedBox.shrink(), + ), + ), + ), + ); } - return title; } /// Widget that represents and visualize a direct child of Flex widget. class BoxChildVisualizer extends StatelessWidget { const BoxChildVisualizer({ super.key, - required this.state, required this.layoutProperties, required this.renderProperties, required this.isSelected, }); - final BoxLayoutExplorerWidgetState state; - final bool isSelected; final LayoutProperties layoutProperties; final RenderProperties renderProperties; @@ -392,55 +300,69 @@ class BoxChildVisualizer extends StatelessWidget { final renderSize = renderProperties.size; final renderOffset = renderProperties.offset; - Widget buildEntranceAnimation(BuildContext _, Widget? child) { - final size = renderSize; - // TODO(jacobr): does this entrance animation really add value. - return Opacity( - opacity: min([state.entranceCurve.value * 5, 1.0]), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: math.max(0.0, (renderSize.width - size.width) / 2), - vertical: math.max(0.0, (renderSize.height - size.height) / 2), - ), - child: child, - ), - ); - } - final propertiesLocal = properties!; return Positioned( top: renderOffset.dy, left: renderOffset.dx, - child: InkWell( - onTap: () => unawaited(state.onTap(propertiesLocal)), - onDoubleTap: () => state.onDoubleTap(propertiesLocal), - child: SizedBox( - width: safePositiveDouble(renderSize.width), - height: safePositiveDouble(renderSize.height), - child: AnimatedBuilder( - animation: state.entranceController, - builder: buildEntranceAnimation, - child: WidgetVisualizer( - isSelected: isSelected, - layoutProperties: layoutProperties, - title: describeBoxName(propertiesLocal), - // TODO(jacobr): consider surfacing the overflow size information - // if we determine - // overflowSide: properties.overflowSide, - - // We only show one child at a time so a large title is safe. - largeTitle: true, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - warnIfUnconstrained: false, - child: const SizedBox.shrink(), - ), - ), + child: SizedBox( + width: safePositiveDouble(renderSize.width), + height: safePositiveDouble(renderSize.height), + child: WidgetVisualizer( + isSelected: isSelected, + layoutProperties: layoutProperties, + title: _describeBoxName(propertiesLocal), + // TODO(jacobr): consider surfacing the overflow size information + // if we determine + // overflowSide: properties.overflowSide, + + // We only show one child at a time so a large title is safe. + largeTitle: true, + child: VisualizeWidthAndHeightWithConstraints( + arrowHeadSize: arrowHeadSize, + properties: propertiesLocal, + warnIfUnconstrained: false, + child: const SizedBox.shrink(), ), ), ), ); } } + +/// The percent of the visualizer dedicating to a single padding block when +/// the box child has multiple padding blocks. +const _narrowPaddingVisualizerPercent = 0.3; + +/// The percent of the visualizer dedicating to a single padding block when +/// the box child has only one padding block. +const _widePaddingVisualizerPercent = 0.35; + +/// Simplistic layout algorithm that will return [WidgetSizes] for the widget +/// display based on the display's [availableSize] and the real widget's +/// [WidgetSizes]. +/// +/// Uses a constant `paddingFraction` for the display padding, regardless of +/// the actual size. +WidgetSizes _simpleFractionalLayout({ + required double availableSize, + required WidgetSizes sizes, +}) { + final paddingASize = sizes.paddingA; + final paddingBSize = sizes.paddingB; + + final paddingFraction = paddingASize > 0 && paddingBSize > 0 + ? _narrowPaddingVisualizerPercent + : _widePaddingVisualizerPercent; + + final paddingAFraction = paddingASize > 0 ? paddingFraction : 0.0; + final paddingBFraction = paddingBSize > 0 ? paddingFraction : 0.0; + final widgetFraction = 1 - paddingAFraction - paddingBFraction; + + return ( + type: sizes.type, + paddingA: paddingAFraction * availableSize, + widgetSize: widgetFraction * availableSize, + paddingB: paddingBFraction * availableSize, + ); +} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart index c00d352397b..3c3cb55e2ec 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -286,6 +286,7 @@ class FlexLayoutExplorerWidgetState child: InkWell( onTap: () => unawaited(onTap(propertiesLocal)), child: WidgetVisualizer( + isFlex: true, title: flexType, layoutProperties: propertiesLocal, isSelected: highlighted == properties, @@ -752,6 +753,7 @@ class FlexChildVisualizer extends StatelessWidget { animation: state.entranceController, builder: buildEntranceAnimation, child: WidgetVisualizer( + isFlex: true, isSelected: isSelected, layoutProperties: layoutProperties, title: propertiesLocal.description ?? '', diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart index cc3b2019ca3..5e67d1e3117 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart deleted file mode 100644 index 116a3c6c849..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2019 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_shared/utils.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../shared/diagnostics/diagnostics_node.dart'; -import '../inspector_controller.dart'; -import '../layout_explorer/box/box.dart'; -import '../layout_explorer/flex/flex.dart'; - -/// Tab that acts as a proxy to decide which widget to be displayed -class LayoutExplorerTab extends StatefulWidget { - const LayoutExplorerTab({super.key, required this.controller}); - - final InspectorController controller; - - @override - State createState() => _LayoutExplorerTabState(); -} - -class _LayoutExplorerTabState extends State - with AutomaticKeepAliveClientMixin, AutoDisposeMixin { - InspectorController get controller => widget.controller; - - RemoteDiagnosticsNode? get selected => - controller.selectedNode.value?.diagnostic; - - RemoteDiagnosticsNode? previousSelection; - - Widget rootWidget(RemoteDiagnosticsNode? node) { - if (node != null && FlexLayoutExplorerWidget.shouldDisplay(node)) { - return FlexLayoutExplorerWidget(controller); - } - if (node != null && BoxLayoutExplorerWidget.shouldDisplay(node)) { - return BoxLayoutExplorerWidget(controller); - } - return Center( - child: Text( - node != null - ? 'Currently, Layout Explorer only supports Box and Flex-based widgets.' - : 'Select a widget to view its layout.', - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - ), - ); - } - - void onSelectionChanged() { - if (rootWidget(previousSelection).runtimeType != - rootWidget(selected).runtimeType) { - setState(() => previousSelection = selected); - } - } - - @override - void initState() { - super.initState(); - addAutoDisposeListener(controller.selectedNode, onSelectionChanged); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return rootWidget(selected); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart index 7fcf002900a..3adb4e615cb 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart index cc5e40cc5df..7dfb1b8e2c1 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart index d515ed554fc..aeb3766ae9e 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -96,9 +96,8 @@ class PaddingVisualizerWidget extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final heightDescription = - 'h=${toStringAsFixed(renderProperties.realHeight)}'; - final widthDescription = 'w=${toStringAsFixed(renderProperties.realWidth)}'; + final heightDescription = toStringAsFixed(renderProperties.realHeight); + final widthDescription = toStringAsFixed(renderProperties.realWidth); final widthWidget = Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart index ac4229c49b7..1a9840f9ead 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. @@ -188,7 +188,7 @@ abstract class LayoutExplorerWidgetState< // update selected widget and trigger selection listener event to change focus. void refreshSelection(RemoteDiagnosticsNode node) { - inspectorController.refreshSelection(node, node, true); + inspectorController.refreshSelection(node); } Future onTap(LayoutProperties properties) async { diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart index 298b7782ef8..2295666ceb5 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart index 974a0a5be75..666a6d902ca 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart @@ -1,4 +1,4 @@ -// Copyright 2021 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart index 798ea7a45f7..859b6508cad 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart @@ -1,4 +1,4 @@ -// Copyright 2021 The Flutter Authors +// Copyright 2024 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. @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import '../../../../shared/diagnostics/diagnostics_node.dart'; import '../../../../shared/primitives/utils.dart'; import '../../inspector_data_models.dart'; +import 'dimension.dart'; import 'overflow_indicator_painter.dart'; import 'theme.dart'; import 'widgets_theme.dart'; @@ -118,6 +119,7 @@ class WidgetVisualizer extends StatelessWidget { required this.child, this.overflowSide, this.largeTitle = false, + this.isFlex = false, }); final LayoutProperties layoutProperties; @@ -126,6 +128,7 @@ class WidgetVisualizer extends StatelessWidget { final Widget? hint; final bool isSelected; final bool largeTitle; + final bool isFlex; final OverflowSide? overflowSide; @@ -139,14 +142,12 @@ class WidgetVisualizer extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final colorScheme = theme.colorScheme; final properties = layoutProperties; final borderColor = WidgetTheme.fromName(properties.node.description).color; final boxAdjust = isSelected ? _selectedPadding : 0.0; return LayoutBuilder( builder: (context, constraints) { - final hintLocal = hint; return OverflowBox( minWidth: constraints.minWidth + boxAdjust, maxWidth: constraints.maxWidth + boxAdjust, @@ -167,7 +168,7 @@ class WidgetVisualizer extends StatelessWidget { ? [ BoxShadow( color: Colors.black.withAlpha(255 ~/ 2), - blurRadius: 20, + blurRadius: 10, ), ] : null, @@ -192,43 +193,19 @@ class WidgetVisualizer extends StatelessWidget { ? _overflowIndicatorSize : 0.0, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Container( - constraints: BoxConstraints( - maxWidth: largeTitle - ? defaultMaxRenderWidth - : minRenderWidth * - widgetTitleMaxWidthPercentage, - ), - decoration: BoxDecoration(color: borderColor), - padding: const EdgeInsets.all(4.0), - child: Center( - child: Text( - title, - style: theme.regularTextStyleWithColor( - colorScheme.widgetNameColor, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - if (hintLocal != null) Flexible(child: hintLocal), - ], + child: isFlex + ? FlexWidgetVisualizer( + title: title, + largeTitle: largeTitle, + borderColor: borderColor, + hint: hint, + child: child, + ) + : BoxWidgetVisualizer( + borderColor: borderColor, + title: title, + properties: properties, ), - ), - Expanded(child: child), - ], - ), ), ], ), @@ -239,6 +216,148 @@ class WidgetVisualizer extends StatelessWidget { } } +/// Visualizer display for a widget in a flex layout. +class FlexWidgetVisualizer extends StatelessWidget { + const FlexWidgetVisualizer({ + super.key, + required this.largeTitle, + required this.borderColor, + required this.title, + required this.hint, + required this.child, + }); + + final bool largeTitle; + final Color borderColor; + final String title; + final Widget? hint; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final hintLocal = hint; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Container( + constraints: BoxConstraints( + maxWidth: largeTitle + ? defaultMaxRenderWidth + : minRenderWidth * widgetTitleMaxWidthPercentage, + ), + decoration: BoxDecoration(color: borderColor), + padding: const EdgeInsets.all(densePadding), + child: Center( + child: Text( + title, + style: theme.regularTextStyleWithColor( + colorScheme.widgetNameColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + if (hintLocal != null) Flexible(child: hintLocal), + ], + ), + ), + Expanded(child: child), + ], + ); + } +} + +/// Visualizer display for a widget in a box layout. +class BoxWidgetVisualizer extends StatelessWidget { + const BoxWidgetVisualizer({ + super.key, + required this.borderColor, + required this.title, + required this.properties, + }); + + final Color borderColor; + final String title; + final LayoutProperties properties; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: WidgetLabel(labelColor: borderColor, labelText: title), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + dimensionDescription( + TextSpan(text: properties.describeHeight()), + false, + theme.colorScheme, + ), + dimensionDescription( + TextSpan(text: properties.describeWidth()), + false, + theme.colorScheme, + ), + ], + ), + ), + ], + ); + } +} + +/// A label for the widget in the layout explorer. +class WidgetLabel extends StatelessWidget { + const WidgetLabel({ + super.key, + required this.labelColor, + required this.labelText, + this.positionedAtBottom = false, + }); + + final Color labelColor; + final String labelText; + final bool positionedAtBottom; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + decoration: BoxDecoration(color: labelColor), + padding: EdgeInsets.fromLTRB( + densePadding, + positionedAtBottom ? borderPadding : 0.0, + densePadding, + positionedAtBottom ? 0.0 : borderPadding, + ), + child: Text( + labelText, + style: theme.regularTextStyleWithColor(colorScheme.widgetNameColor), + overflow: TextOverflow.ellipsis, + ), + ); + } +} + class AnimatedLayoutProperties implements LayoutProperties { AnimatedLayoutProperties(this.begin, this.end, this.animation) @@ -265,6 +384,15 @@ class AnimatedLayoutProperties end.parent = parent; } + @override + LayoutProperties? get parentLayoutProperties => null; + + @override + WidgetSizes? get widgetWidths => null; + + @override + WidgetSizes? get widgetHeights => null; + @override List get children { return _children; @@ -437,3 +565,51 @@ class LayoutExplorerBackground extends StatelessWidget { ); } } + +/// Builds and positions a label for the [LayoutExplorerBackground] as +/// determined by the widget's padding. +class PositionedBackgroundLabel extends StatelessWidget { + const PositionedBackgroundLabel({ + super.key, + required this.labelText, + required this.labelColor, + required this.hasTopPadding, + required this.hasBottomPadding, + required this.hasLeftPadding, + required this.hasRightPadding, + }); + + final String labelText; + final Color labelColor; + final bool hasTopPadding; + final bool hasBottomPadding; + final bool hasLeftPadding; + final bool hasRightPadding; + + @override + Widget build(BuildContext context) { + return Column( + // Push to the bottom if there is no padding on the top. + mainAxisAlignment: !hasTopPadding && hasBottomPadding + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Row( + // Push to the right if there is no padding on the left. + mainAxisAlignment: (!hasLeftPadding && hasRightPadding) + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: WidgetLabel( + labelColor: labelColor, + labelText: labelText, + positionedAtBottom: !hasTopPadding && hasBottomPadding, + ), + ), + ], + ), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart index 45de4048974..465a2cb6440 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors +// Copyright 2024 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. diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart index 4abdb55e500..a492a1cddbb 100644 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart +++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart @@ -1,4 +1,4 @@ -// Copyright 2021 The Flutter Authors +// Copyright 2024 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. @@ -192,7 +192,6 @@ class WidgetTheme { 'PageView': WidgetTheme(iconAsset: WidgetIcons.pageView), 'Material': WidgetTheme(iconAsset: WidgetIcons.material), 'AppBar': WidgetTheme(iconAsset: WidgetIcons.appBar), - 'HiddenGroup': WidgetTheme(iconAsset: WidgetIcons.hidden), }; } @@ -246,5 +245,4 @@ class WidgetIcons { static const materialApp = 'icons/inspector/widget_icons/materialapp.png'; static const bottomNavigationBar = 'icons/inspector/widget_icons/bottomnavigationbar.png'; - static const hidden = 'icons/inspector/widget_icons/onedot.png'; } diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/widget_details.dart b/packages/devtools_app/lib/src/screens/inspector/widget_details.dart similarity index 100% rename from packages/devtools_app/lib/src/screens/inspector_v2/widget_details.dart rename to packages/devtools_app/lib/src/screens/inspector/widget_details.dart diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/widget_properties/properties_view.dart b/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart similarity index 100% rename from packages/devtools_app/lib/src/screens/inspector_v2/widget_properties/properties_view.dart rename to packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart 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 deleted file mode 100644 index 710743dee72..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024 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_shared/shared.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/feature_flags.dart'; -import '../../shared/framework/screen.dart'; -import '../../shared/globals.dart'; -import '../inspector/inspector_screen_body.dart' as legacy; -import '../inspector_v2/inspector_screen_body.dart' as v2; -import 'inspector_screen_controller.dart'; - -class InspectorScreen extends Screen { - InspectorScreen() : super.fromMetaData(ScreenMetaData.inspector); - - static const minScreenWidthForText = 900.0; - - static final id = ScreenMetaData.inspector.id; - - // There is not enough room to safely show the console in the embed view of - // the DevTools and IDEs have their own consoles. - @override - bool showConsole(EmbedMode embedMode) => !embedMode.embedded; - - @override - bool showAiAssistant() => true; - - @override - String get docPageId => screenId; - - @override - Widget buildScreenBody(BuildContext context) => - const InspectorScreenSwitcher(); -} - -class InspectorScreenSwitcher extends StatefulWidget { - const InspectorScreenSwitcher({super.key}); - - @override - State createState() => - _InspectorScreenSwitcherState(); -} - -class _InspectorScreenSwitcherState extends State - with AutoDisposeMixin { - late InspectorScreenController controller; - - bool get shouldShowInspectorV2 => - FeatureFlags.inspectorV2.isEnabled && - !preferences.inspector.legacyInspectorEnabled.value; - - @override - void initState() { - super.initState(); - controller = screenControllers.lookup(); - addAutoDisposeListener( - preferences.inspector.legacyInspectorEnabled, - () async { - controller.legacyInspectorController.setVisibleToUser( - !shouldShowInspectorV2, - ); - await controller.v2InspectorController.setVisibleToUser( - shouldShowInspectorV2, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: preferences.inspector.legacyInspectorEnabled, - builder: (context, _, _) { - if (shouldShowInspectorV2) { - return v2.InspectorScreenBody( - controller: controller.v2InspectorController, - ); - } - - return legacy.InspectorScreenBody( - controller: controller.legacyInspectorController, - ); - }, - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart deleted file mode 100644 index 5ca8073fa6f..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2024 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 '../../shared/analytics/metrics.dart'; -import '../../shared/console/primitives/simple_items.dart'; -import '../../shared/framework/screen.dart'; -import '../../shared/framework/screen_controllers.dart'; -import '../inspector/inspector_controller.dart' as legacy; -import '../inspector/inspector_tree_controller.dart' as legacy; -import '../inspector_v2/inspector_controller.dart' as v2; -import '../inspector_v2/inspector_tree_controller.dart' as v2; - -/// Screen controller for the Inspector screen. -/// -/// This controller can be accessed from anywhere in DevTools, as long as it was -/// first registered, by -/// calling `screenControllers.lookup()`. -/// -/// The controller lifecycle is managed by the [ScreenControllers] class. The -/// `init` method is called lazily upon the first controller access from -/// `screenControllers`. The `dispose` method is called by `screenControllers` -/// when DevTools is destroying a set of DevTools screen controllers. -class InspectorScreenController extends DevToolsScreenController { - @override - final screenId = ScreenMetaData.inspector.id; - - late v2.InspectorController v2InspectorController; - late v2.InspectorTreeController v2InspectorTreeController; - - late legacy.InspectorController legacyInspectorController; - late legacy.InspectorTreeController legacyInspectorTreeController; - late legacy.InspectorTreeController legacyDetailsTreeController; - - @override - void init() { - super.init(); - v2InspectorTreeController = v2.InspectorTreeController( - gaId: InspectorScreenMetrics.summaryTreeGaId, - ); - v2InspectorController = v2.InspectorController( - inspectorTree: v2InspectorTreeController, - treeType: FlutterTreeType.widget, - ); - - // TODO(elliette): Remove legacy inspector. - legacyInspectorTreeController = legacy.InspectorTreeController( - gaId: InspectorScreenMetrics.summaryTreeGaId, - ); - legacyDetailsTreeController = legacy.InspectorTreeController( - gaId: InspectorScreenMetrics.detailsTreeGaId, - ); - legacyInspectorController = legacy.InspectorController( - inspectorTree: legacyInspectorTreeController, - detailsTree: legacyDetailsTreeController, - treeType: FlutterTreeType.widget, - ); - } - - @override - void dispose() { - v2InspectorTreeController.dispose(); - v2InspectorController.dispose(); - - legacyInspectorTreeController.dispose(); - legacyDetailsTreeController.dispose(); - legacyInspectorController.dispose(); - super.dispose(); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart deleted file mode 100644 index 823a3dd3c6a..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2024 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 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; -import 'package:vm_service/vm_service.dart' hide Stack; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/feature_flags.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/banner_messages.dart'; -import '../../shared/preferences/preferences.dart'; -import '../../shared/primitives/simple_items.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/editable_list.dart'; -import 'inspector_screen.dart'; - -class FlutterInspectorSettingsDialog extends StatefulWidget { - const FlutterInspectorSettingsDialog({super.key}); - - @override - State createState() => - _FlutterInspectorSettingsDialogState(); -} - -class _FlutterInspectorSettingsDialogState - extends State - with AutoDisposeMixin { - @override - void initState() { - super.initState(); - addAutoDisposeListener(preferences.inspector.legacyInspectorEnabled, () { - if (!preferences.inspector.legacyInspectorEnabled.value) { - bannerMessages.removeMessageByKey( - LegacyInspectorWarningMessage.buildKey(InspectorScreen.id), - InspectorScreen.id, - ); - } - }); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - const dialogHeight = 500.0; - - return ValueListenableBuilder( - valueListenable: preferences.inspector.legacyInspectorEnabled, - builder: (context, legacyInspectorEnabled, _) { - final inspectorV2Enabled = !legacyInspectorEnabled; - return DevToolsDialog( - title: const DialogTitleText('Flutter Inspector Settings'), - content: SizedBox( - width: defaultDialogWidth, - height: dialogHeight, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...dialogSubHeader(theme, 'General'), - CheckboxSetting( - notifier: - preferences.inspector.hoverEvalModeEnabled - as ValueNotifier, - title: 'Enable hover inspection', - description: - 'Hovering over any widget displays its properties and values.', - gaItem: gac.inspectorHoverEvalMode, - ), - const SizedBox(height: largeSpacing), - if (inspectorV2Enabled) ...[ - CheckboxSetting( - notifier: - preferences.inspector.autoRefreshEnabled - as ValueNotifier, - title: 'Enable widget tree auto-refreshing', - description: - 'The widget tree will automatically refresh after a hot-reload or navigation event.', - gaItem: gac.inspectorAutoRefreshEnabled, - ), - ] else ...[ - const InspectorDefaultDetailsViewOption(), - ], - const SizedBox(height: largeSpacing), - // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up - // after Inspector V2 has been released. - if (FeatureFlags.inspectorV2.isEnabled) - Flexible( - child: CheckboxSetting( - notifier: - preferences.inspector.legacyInspectorEnabled - as ValueNotifier, - title: 'Use legacy inspector', - description: - 'Disable the redesigned Flutter inspector. Please know that ' - 'the legacy inspector will be removed in a future release.', - gaItem: gac.inspectorV2Disabled, - ), - ), - const SizedBox(height: largeSpacing), - ...dialogSubHeader(theme, 'Package Directories'), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - 'Widgets in these directories will show up in your summary tree.', - style: theme.subtleTextStyle, - ), - ), - MoreInfoLink( - url: DocLinks.inspectorPackageDirectories.value, - gaScreenName: gac.inspector, - gaSelectedItemDescription: - gac.InspectorDocs.packageDirectoriesDocs.name, - ), - ], - ), - Text( - '(e.g. /absolute/path/to/myPackage/)', - style: theme.subtleTextStyle, - ), - const SizedBox(height: denseSpacing), - const Expanded(child: PubRootDirectorySection()), - ], - ), - ), - actions: const [DialogCloseButton()], - ); - }, - ); - } -} - -class InspectorDefaultDetailsViewOption extends StatelessWidget { - const InspectorDefaultDetailsViewOption({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: preferences.inspector.defaultDetailsView, - builder: (context, selection, _) { - final theme = Theme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Select the default tab for the inspector.', - style: theme.subtleTextStyle, - ), - const SizedBox(height: denseSpacing), - RadioGroup( - groupValue: selection, - onChanged: _onChanged, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Radio( - value: InspectorDetailsViewType.layoutExplorer, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - Text(InspectorDetailsViewType.layoutExplorer.key), - const SizedBox(width: denseSpacing), - const Radio( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: InspectorDetailsViewType.widgetDetailsTree, - ), - Text(InspectorDetailsViewType.widgetDetailsTree.key), - ], - ), - ), - ], - ); - }, - ); - } - - void _onChanged(InspectorDetailsViewType? value) { - if (value != null) { - preferences.inspector.setDefaultInspectorDetailsView(value); - final item = value.name == InspectorDetailsViewType.layoutExplorer.name - ? gac.defaultDetailsViewToLayoutExplorer - : gac.defaultDetailsViewToWidgetDetails; - ga.select(gac.inspector, item); - } - } -} - -class PubRootDirectorySection extends StatelessWidget { - const PubRootDirectorySection({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: - serviceConnection.serviceManager.isolateManager.mainIsolate, - builder: (_, _, _) { - return SizedBox( - height: 200.0, - child: EditableList( - gaScreen: gac.inspector, - gaRefreshSelection: gac.refreshPubRoots, - entries: preferences.inspector.pubRootDirectories, - textFieldLabel: 'Enter a new package directory', - isRefreshing: preferences.inspector.isRefreshingPubRootDirectories, - onEntryAdded: (p0) => unawaited( - preferences.inspector.addPubRootDirectories([ - p0, - ], shouldCache: true), - ), - onEntryRemoved: (p0) => - unawaited(preferences.inspector.removePubRootDirectories([p0])), - onRefreshTriggered: () => - unawaited(preferences.inspector.loadPubRootDirectories()), - ), - ); - }, - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart deleted file mode 100644 index 32de022bcdf..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart +++ /dev/null @@ -1,1117 +0,0 @@ -// Copyright 2024 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. - -/// This library must not have direct dependencies on dart:html. -/// -/// This allows tests of the complicated logic in this class to run on the VM -/// and will help simplify porting this code to work with Hummingbird. -/// -/// This code is directly based on -/// src/io/flutter/view/InspectorPanel.java -/// with some refactors to make the code more of a controller than a combination -/// of view and controller. View specific portions of InspectorPanel.java have -/// been moved to inspector.dart. -library; - -import 'dart:async'; - -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:vm_service/vm_service.dart'; - -import '../../service/service_extensions.dart' as extensions; -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree_v2.dart'; -import '../../shared/console/primitives/simple_items.dart'; -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/diagnostics/inspector_service.dart'; -import '../../shared/diagnostics/primitives/instance_ref.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/notifications.dart'; -import '../../shared/primitives/query_parameters.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_screen.dart'; -import 'inspector_data_models.dart'; -import 'inspector_tree_controller.dart'; - -final _log = Logger('inspector_controller'); - -/// Data pattern containing the properties and render properties for a widget -/// tree node. -typedef WidgetTreeNodeProperties = ({ - /// Properties defined directly on the widget. - List widgetProperties, - - /// Properties defined on the widget's render object. - List renderProperties, - - /// Layout properties for the widget. - LayoutProperties? layoutProperties, -}); - -/// This class is based on the InspectorPanel class from the Flutter IntelliJ -/// plugin with some refactors to make it more of a true controller than a view. -class InspectorController extends DisposableController - with AutoDisposeControllerMixin - implements InspectorServiceClient { - InspectorController({required this.inspectorTree, required this.treeType}) { - unawaited(init()); - } - - @override - Future init() async { - super.init(); - _refreshRateLimiter = RateLimiter(refreshFramesPerSecond, refresh); - - inspectorTree.config = InspectorTreeConfig( - onNodeAdded: _onNodeAdded, - onSelectionChange: selectionChanged, - onExpand: _onExpand, - onClientActiveChange: _onClientChange, - ); - await serviceConnection.serviceManager.onServiceAvailable; - - if (inspectorService is InspectorService) { - _treeGroups = InspectorObjectGroupManager( - serviceConnection.inspectorService as InspectorService, - 'tree', - ); - _selectionGroups = InspectorObjectGroupManager( - serviceConnection.inspectorService as InspectorService, - 'selection', - ); - _layoutGroups = InspectorObjectGroupManager( - serviceConnection.inspectorService as InspectorService, - 'layout', - ); - } - - addAutoDisposeListener( - serviceConnection.serviceManager.isolateManager.mainIsolate, - () { - final newIsolate = - serviceConnection.serviceManager.isolateManager.mainIsolate.value; - if (_mainIsolate == newIsolate) return; - // First deactivate the current widget tree. - setActivate(false); - if (newIsolate != null) { - // Then reactivate it with the new isolate. - setActivate(true); - } - _mainIsolate = newIsolate; - }, - ); - - // If select mode is available, enable the on device inspector as it - // won't interfere with users. - addAutoDisposeListener(_supportsToggleSelectWidgetMode, () { - if (_supportsToggleSelectWidgetMode.value) { - safeUnawaited( - serviceConnection.serviceManager.serviceExtensionManager - .setServiceExtensionState( - extensions.enableOnDeviceInspector.extension, - enabled: true, - value: true, - ), - ); - } - }); - - addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { - if (serviceConnection.serviceManager.connectedState.value.connected) { - _handleConnectionStart(); - } else { - _handleConnectionStop(); - } - }); - - if (serviceConnection.serviceManager.connectedAppInitialized) { - _handleConnectionStart(); - } - - serviceConnection.consoleService.ensureServiceInitialized(); - - final vmService = serviceConnection.serviceManager.service; - if (vmService != null) { - autoDisposeStreamSubscription( - vmService.onIsolateEvent.listen(_maybeAutoRefreshInspector), - ); - - autoDisposeStreamSubscription( - vmService.onExtensionEvent.listen(_maybeAutoRefreshInspector), - ); - } - } - - void _handleConnectionStart() { - // Clear any existing badge/errors for older errors that were collected. - // Do this in a post frame callback so that we are not trying to clear the - // error notifiers for this screen while the framework is already in the - // process of building widgets. - // TODO(kenz): When this method is called outside createState(), this post - // frame callback can be removed. - WidgetsBinding.instance.addPostFrameCallback((_) { - serviceConnection.errorBadgeManager.clearErrorCount(InspectorScreen.id); - }); - filterErrors(); - } - - void _handleConnectionStop() { - setActivate(false); - dispose(); - } - - IsolateRef? _mainIsolate; - - ValueListenable get _supportsToggleSelectWidgetMode => serviceConnection - .serviceManager - .serviceExtensionManager - .hasServiceExtension(extensions.toggleSelectWidgetMode.extension); - - Future _onClientChange(bool added) async { - if (!added && _clientCount == 0) { - // Don't try to remove clients if there are none - return; - } - - _clientCount += added ? 1 : -1; - assert(_clientCount >= 0); - if (_clientCount == 1) { - await setVisibleToUser(true); - setActivate(true); - } else if (_clientCount == 0) { - await setVisibleToUser(false); - } - } - - int _clientCount = 0; - - /// Maximum frame rate to refresh the inspector at to avoid taxing the - /// physical device with too many requests to recompute properties and trees. - /// - /// A value up to around 30 frames per second could be reasonable for - /// debugging highly interactive cases particularly when the user is on a - /// simulator or high powered native device. The frame rate is set low - /// for now mainly to minimize risk. - static const refreshFramesPerSecond = 5.0; - - InspectorTreeController inspectorTree; - final FlutterTreeType treeType; - - late RateLimiter _refreshRateLimiter; - - InspectorServiceBase get inspectorService => - serviceConnection.inspectorService as InspectorServiceBase; - - /// Groups used to manage and cancel requests to load data to display directly - /// in the tree. - InspectorObjectGroupManager? _treeGroups; - - /// Groups used to manage and cancel requests to determine what the current - /// selection is. - /// - /// This group needs to be kept separate from treeGroups as the selection is - /// shared with the widget details. - /// TODO(jacobr): is there a way we can unify the selection and tree groups? - InspectorObjectGroupManager? _selectionGroups; - - InspectorObjectGroupManager? _layoutGroups; - - /// Node being highlighted due to the current hover. - InspectorTreeNode? get currentShowNode => inspectorTree.hover; - - set currentShowNode(InspectorTreeNode? node) => inspectorTree.hover = node; - - bool flutterAppFrameReady = false; - - bool treeLoadStarted = false; - - RemoteDiagnosticsNode? subtreeRoot; - - bool programmaticSelectionChangeInProgress = false; - - ValueListenable get selectedNode => _selectedNode; - final _selectedNode = ValueNotifier(null); - - ValueListenable get selectedNodeProperties => - _selectedNodeProperties; - final _selectedNodeProperties = ValueNotifier(( - widgetProperties: [], - renderProperties: [], - layoutProperties: null, - )); - - /// Whether the implementation widgets are hidden in the widget tree. - ValueListenable get implementationWidgetsHidden => - _implementationWidgetsHidden; - final _implementationWidgetsHidden = ValueNotifier(true); - - InspectorTreeNode? lastExpanded; - - bool isActive = false; - - final valueToInspectorTreeNode = {}; - - /// When visibleToUser is false we should dispose all allocated objects and - /// not perform any actions. - bool visibleToUser = false; - - bool highlightNodesShownInBothTrees = false; - - RemoteDiagnosticsNode? get selectedDiagnostic => - selectedNode.value?.diagnostic; - - ValueListenable get selectedErrorIndex => _selectedErrorIndex; - final _selectedErrorIndex = ValueNotifier(null); - - /// Tracks whether the first load of the inspector tree has been completed. - /// - /// This field is used to prevent sending multiple analytics events for - /// inspector tree load timing. - bool firstInspectorTreeLoadCompleted = false; - - FlutterTreeType getTreeType() { - return treeType; - } - - Future setVisibleToUser(bool visible) async { - if (visibleToUser == visible) { - return; - } - visibleToUser = visible; - - if (visibleToUser) { - await refreshInspector(); - } else { - shutdownTree(false); - } - } - - bool hasDiagnosticsValue(InspectorInstanceRef ref) { - return valueToInspectorTreeNode.containsKey(ref); - } - - RemoteDiagnosticsNode? findDiagnosticsValue(InspectorInstanceRef ref) { - return valueToInspectorTreeNode[ref]?.diagnostic; - } - - void endShowNode() { - highlightShowNode(null); - } - - bool highlightShowFromNodeInstanceRef(InspectorInstanceRef ref) { - return highlightShowNode(valueToInspectorTreeNode[ref]); - } - - bool highlightShowNode(InspectorTreeNode? node) { - currentShowNode = node; - return true; - } - - InspectorTreeNode? findMatchingInspectorTreeNode( - RemoteDiagnosticsNode? node, - ) { - final valueRef = node?.valueRef; - if (valueRef == null) { - return null; - } - return valueToInspectorTreeNode[valueRef]; - } - - Future _waitForPendingUpdateDone() async { - // Wait for the selection to be resolved followed by waiting for the tree to be computed. - await _selectionGroups?.pendingUpdateDone; - await _treeGroups?.pendingUpdateDone; - // TODO(jacobr): are there race conditions we need to think more carefully about here? - } - - Future refresh() { - if (!visibleToUser) { - // We will refresh again once we are visible. - // There is a risk a refresh got triggered before the view was visble. - return Future.value(); - } - - return _waitForPendingUpdateDone(); - } - - // Note that this may be called after the controller is disposed. We need to handle nulls in the fields. - void shutdownTree(bool isolateStopped) { - // It is critical we clear all data that is kept alive by inspector object - // references in this method as that stale data will trigger inspector - // exceptions. - programmaticSelectionChangeInProgress = true; - _treeGroups?.clear(isolateStopped); - _selectionGroups?.clear(isolateStopped); - - currentShowNode = null; - _selectedNode.value = null; - lastExpanded = null; - - subtreeRoot = null; - - inspectorTree.root = inspectorTree.createNode(); - programmaticSelectionChangeInProgress = false; - valueToInspectorTreeNode.clear(); - // Mark tree as inactive so that it will be re-loaded the next time it is - // opened: - isActive = false; - } - - void onIsolateStopped() { - flutterAppFrameReady = false; - treeLoadStarted = false; - shutdownTree(true); - } - - @override - Future onForceRefresh() async { - assert(!disposed); - if (!visibleToUser || disposed) { - return; - } - await _recomputeTreeRoot(null); - if (disposed) { - return; - } - - filterErrors(); - - return _waitForPendingUpdateDone(); - } - - Future refreshInspector({bool isManualRefresh = false}) async { - // If the user is manually refreshing the inspector before the first load - // has completed, this could indicate a slow load time or that the inspector - // failed to load the tree once available. - if (isManualRefresh && !firstInspectorTreeLoadCompleted) { - // We do not want to complete this timing operation because the manual - // refresh will skew the results. - ga.cancelTimingOperation(InspectorScreen.id, gac.pageReady); - ga.select( - gac.inspector, - gac.refreshEmptyTree, - screenMetricsProvider: () => InspectorScreenMetrics.v2(), - ); - firstInspectorTreeLoadCompleted = true; - } - await onForceRefresh(); - } - - void filterErrors() { - serviceConnection.errorBadgeManager.filterErrors( - InspectorScreen.id, - (id) => hasDiagnosticsValue(InspectorInstanceRef(id)), - ); - } - - void setActivate(bool enabled) { - if (!enabled) { - onIsolateStopped(); - isActive = false; - return; - } - if (isActive) { - // Already activated. - return; - } - - isActive = true; - inspectorService.addClient(this); - unawaited(maybeLoadUI()); - } - - Future maybeLoadUI() async { - if (!visibleToUser || !isActive) { - return; - } - - if (flutterAppFrameReady) { - if (disposed) return; - // We need to start by querying the inspector service to find out the - // current state of the UI. - final inspectorRef = DevToolsQueryParams.load().inspectorRef; - await updateSelectionFromService(inspectorRef: inspectorRef); - } else { - if (disposed) return; - if (inspectorService is InspectorService) { - final widgetTreeReady = await (inspectorService as InspectorService) - .isWidgetTreeReady(); - flutterAppFrameReady = widgetTreeReady; - } - if (isActive && flutterAppFrameReady) { - await maybeLoadUI(); - } - } - } - - bool _receivedIsolateReloadEvent = false; - bool _receivedFlutterNavigationEvent = false; - - Future _maybeAutoRefreshInspector(Event event) async { - if (!preferences.inspector.autoRefreshEnabled.value) return; - - // It is not sufficent to wait for the navigation and isolate reload events - // only, because Flutter might not have re-painted the app. Instead, we need - // to wait for the first frame AFTER the isolate reload or navigation event - // in order to request the new tree. - if (event.kind == EventKind.kExtension) { - final extensionEventKind = event.extensionKind; - if (extensionEventKind == 'Flutter.Navigation') { - _receivedFlutterNavigationEvent = true; - } - if ((_receivedFlutterNavigationEvent || _receivedIsolateReloadEvent) && - extensionEventKind == 'Flutter.Frame') { - _refreshingAfterNavigationEvent = _receivedFlutterNavigationEvent; - _receivedFlutterNavigationEvent = false; - _receivedIsolateReloadEvent = false; - await refreshInspector(); - } - } - - if (event.kind == EventKind.kIsolateReload) { - serviceConnection.errorBadgeManager.clearErrors(InspectorScreen.id); - _receivedIsolateReloadEvent = true; - } - } - - Future _recomputeTreeRoot( - RemoteDiagnosticsNode? newSelection, { - bool? hideImplementationWidgets, - }) async { - assert(!disposed); - hideImplementationWidgets ??= _implementationWidgetsHidden.value; - final treeGroups = _treeGroups; - if (disposed || treeGroups == null) { - return; - } - - treeGroups.cancelNext(); - try { - final group = treeGroups.next; - final node = await group.getRoot( - treeType, - isSummaryTree: hideImplementationWidgets, - includeFullDetails: false, - ); - if (node == null || group.disposed || disposed) { - return; - } - // TODO(jacobr): as a performance optimization we should check if the - // new tree is identical to the existing tree in which case we should - // dispose the new tree and keep the old tree. - treeGroups.promoteNext(); - _clearValueToInspectorTreeNodeMapping(); - - final rootNode = inspectorTree.setupInspectorTreeNode( - inspectorTree.createNode(), - node, - expandChildren: true, - ); - inspectorTree.root = rootNode; - final selectedNode = _determineNewSelection( - newSelection ?? selectedDiagnostic, - ); - refreshSelection(selectedNode); - _implementationWidgetsHidden.value = hideImplementationWidgets; - } catch (error, st) { - _log.shout(error, error, st); - treeGroups.cancelNext(); - return; - } - } - - var _refreshingAfterNavigationEvent = false; - - RemoteDiagnosticsNode? _determineNewSelection( - RemoteDiagnosticsNode? previousSelection, - ) { - if (previousSelection == null) return null; - if (valueToInspectorTreeNode.containsKey(previousSelection.valueRef)) { - return previousSelection; - } - - // TODO(https://github.com/flutter/devtools/issues/8481): Consider using a - // variation of a path-finding algorithm to determine the new selection, - // instead of looking for the first matching descendant. - final (closestUnchangedAncestor, distanceToAncestor) = - _findClosestUnchangedAncestor(previousSelection); - if (closestUnchangedAncestor == null) return inspectorTree.root?.diagnostic; - - // TODO(elliette): This might cause a race event that will set this to false - // for a subsequent navigate event. Consider passing the value of - // _refreshingAfterNavigationEvent through the method chain from where the - // navigation event is detected. This would require updating the interface - // of InspectorServiceClient.onForceRefresh, or refactoring to avoid doing - // so. - if (_refreshingAfterNavigationEvent) { - _refreshingAfterNavigationEvent = false; - return closestUnchangedAncestor; - } - - const distanceOffset = 3; - final matchingDescendant = _findMatchingDescendant( - of: closestUnchangedAncestor, - matching: previousSelection, - inRange: Range( - distanceToAncestor - distanceOffset, - distanceToAncestor + distanceOffset, - ), - ); - - return matchingDescendant ?? closestUnchangedAncestor; - } - - (RemoteDiagnosticsNode?, int) _findClosestUnchangedAncestor( - RemoteDiagnosticsNode node, [ - int distanceToAncestor = 1, - ]) { - final inspectorTreeNode = valueToInspectorTreeNode[node.valueRef]; - if (inspectorTreeNode != null) { - return (inspectorTreeNode.diagnostic, distanceToAncestor); - } - - final ancestor = node.parent; - if (ancestor == null) return (null, distanceToAncestor); - return _findClosestUnchangedAncestor(ancestor, distanceToAncestor++); - } - - RemoteDiagnosticsNode? _findMatchingDescendant({ - required RemoteDiagnosticsNode of, - required RemoteDiagnosticsNode matching, - required Range inRange, - int currentDistance = 1, - }) { - if (currentDistance > inRange.end) return null; - - if (inRange.contains(currentDistance)) { - if (of.description == matching.description) { - return of; - } - } - - final children = of.childrenNow; - final distance = currentDistance++; - for (final child in children) { - final matchingDescendant = _findMatchingDescendant( - of: child, - matching: matching, - inRange: inRange, - currentDistance: distance, - ); - if (matchingDescendant != null) return matchingDescendant; - } - - return null; - } - - Future toggleImplementationWidgetsVisibility() async { - final root = inspectorTree.root?.diagnostic; - if (root != null) { - final currentSelectedNode = selectedNode.value; - await _recomputeTreeRoot( - root, - hideImplementationWidgets: !_implementationWidgetsHidden.value, - ); - // Persist the selected node after refreshing the widget tree: - refreshSelection(currentSelectedNode?.diagnostic); - - // If the user is searching the tree, refresh the search matches. - inspectorTree.refreshSearchMatches(); - } - } - - void _clearValueToInspectorTreeNodeMapping() { - valueToInspectorTreeNode.clear(); - } - - void setSubtreeRoot( - RemoteDiagnosticsNode? node, - RemoteDiagnosticsNode? selection, - ) { - selection ??= node; - if (node != null && node == subtreeRoot) { - // Select the new node in the existing subtree. - applyNewSelection(selection); - return; - } - subtreeRoot = node; - if (node == null) { - // Passing in a null node indicates we should clear the subtree and free any memory allocated. - shutdownTree(false); - return; - } - - // Clear now to eliminate frame of highlighted nodes flicker. - _clearValueToInspectorTreeNodeMapping(); - unawaited(_recomputeTreeRoot(selection)); - } - - InspectorTreeNode? getSubtreeRootNode() { - if (subtreeRoot == null) { - return null; - } - return valueToInspectorTreeNode[subtreeRoot!.valueRef]; - } - - void refreshSelection(RemoteDiagnosticsNode? newSelection) { - newSelection ??= selectedDiagnostic; - final matchingNode = findMatchingInspectorTreeNode(newSelection); - if (matchingNode != null) { - setSelectedNode(matchingNode); - syncSelectionHelper(selection: matchingNode.diagnostic); - - syncTreeSelection(); - } - } - - void syncTreeSelection() { - programmaticSelectionChangeInProgress = true; - inspectorTree.refreshTree( - updateTreeAction: () { - inspectorTree - ..setSelectedNode(selectedNode.value) - ..expandPath(selectedNode.value); - return true; - }, - ); - programmaticSelectionChangeInProgress = false; - animateTo(selectedNode.value); - } - - void selectAndShowNode(RemoteDiagnosticsNode? node) { - if (node == null) { - return; - } - selectAndShowInspectorInstanceRef(node.valueRef); - } - - void selectAndShowInspectorInstanceRef(InspectorInstanceRef ref) { - final node = valueToInspectorTreeNode[ref]; - if (node == null) { - return; - } - setSelectedNode(node); - syncTreeSelection(); - } - - InspectorTreeNode? getTreeNode(RemoteDiagnosticsNode node) { - return valueToInspectorTreeNode[node.valueRef]; - } - - @override - void onFlutterFrame() { - flutterAppFrameReady = true; - if (!visibleToUser) { - return; - } - - if (!treeLoadStarted) { - treeLoadStarted = true; - // This was the first frame. - unawaited(maybeLoadUI()); - } - _refreshRateLimiter.scheduleRequest(); - } - - @override - void onInspectorSelectionChanged() { - if (!visibleToUser) { - // Don't do anything. We will update the view once it is visible again. - return; - } - unawaited(updateSelectionFromService()); - } - - Future updateSelectionFromService({String? inspectorRef}) async { - final selectionGroups = _selectionGroups; - if (selectionGroups == null) { - // Already disposed. Ignore this requested to update selection. - return; - } - treeLoadStarted = true; - - selectionGroups.cancelNext(); - - final group = selectionGroups.next; - - if (inspectorRef != null) { - await group.setSelectionInspector( - InspectorInstanceRef(inspectorRef), - false, - ); - if (disposed) return; - } - final pendingSelectionFuture = group.getSelection( - selectedDiagnostic, - treeType, - // If implementation widgets are hidden, the only widgets in the tree are - // those that were created by the local project. - restrictToLocalProject: implementationWidgetsHidden.value, - ); - - try { - final newSelection = await pendingSelectionFuture; - - if (disposed || group.disposed) return; - - selectionGroups.promoteNext(); - - subtreeRoot = newSelection; - - applyNewSelection(newSelection); - - await _maybeShowNotificationForSelectedNode( - selectedNode: newSelection, - group: group, - ); - - // Send an event that a widget was selected on the device. - ga.select( - gac.inspector, - gac.onDeviceSelection, - screenMetricsProvider: () => InspectorScreenMetrics.v2(), - ); - } catch (error, st) { - if (selectionGroups.next == group) { - _log.shout(error, error, st); - selectionGroups.cancelNext(); - } - } - } - - void applyNewSelection(RemoteDiagnosticsNode? newSelection) { - final nodeInTree = findMatchingInspectorTreeNode(newSelection); - - if (nodeInTree == null) { - // The tree has probably changed since we last updated. Do a full refresh - // so that the tree includes the new node we care about. - unawaited(_recomputeTreeRoot(newSelection)); - } - - refreshSelection(newSelection); - } - - void animateTo(InspectorTreeNode? node) { - if (node == null) { - return; - } - - inspectorTree.animateToTargets([node]); - } - - void setSelectedNode(InspectorTreeNode? newSelection) { - if (newSelection == selectedNode.value) { - return; - } - - _selectedNode.value = newSelection; - - lastExpanded = null; // New selected node takes precedence. - endShowNode(); - - _updateSelectedErrorFromNode(_selectedNode.value); - unawaited(_loadPropertiesForNode(_selectedNode.value)); - - /// If the user selects a hidden implementation widget, first expand that - /// widget's hideable group before scrolling. - final diagnostic = _selectedNode.value?.diagnostic; - if (diagnostic != null && diagnostic.isHidden) { - inspectorTree.refreshTree( - updateTreeAction: () { - diagnostic.hideableGroupLeader?.toggleHiddenGroup(); - return true; - }, - ); - } - - animateTo(selectedNode.value); - } - - static const _implementationWidgetMessage = - 'Selected an implementation widget'; - - static const _notificationDuration = Duration(seconds: 4); - - Future _maybeShowNotificationForSelectedNode({ - required RemoteDiagnosticsNode? selectedNode, - required ObjectGroup group, - }) async { - if (selectedNode == null || - !implementationWidgetsHidden.value || - _selectionIsOutOfDate(selectedNode)) { - return; - } - - final possibleImplementationWidget = await group.getSelection( - selectedDiagnostic, - treeType, - ); - - // Return early if we have a new selected node. - if (_selectionIsOutOfDate(selectedNode)) return; - - final isImplementationWidget = - possibleImplementationWidget != null && - !possibleImplementationWidget.isCreatedByLocalProject; - if (isImplementationWidget) { - final selectedWidgetName = selectedNode.description ?? ''; - final implementationWidgetName = - possibleImplementationWidget.description ?? ''; - - // Return early if we have a new selected node. - if (_selectionIsOutOfDate(selectedNode)) return; - - // Show a notification that the user selected an implementation widget, - // e.g. "Selected an implementation widget of Text: RichText." - final messageDetails = selectedWidgetName.isEmpty - ? '' - : ' of $selectedWidgetName${implementationWidgetName.isEmpty ? '' : ': $implementationWidgetName'}'; - notificationService.pushNotification( - NotificationMessage( - '$_implementationWidgetMessage$messageDetails.', - duration: _notificationDuration, - ), - allowDuplicates: false, - ); - } - } - - bool _selectionIsOutOfDate(RemoteDiagnosticsNode selected) { - return selected.valueRef != selectedNode.value?.diagnostic?.valueRef; - } - - Future _loadPropertiesForNode(InspectorTreeNode? node) async { - final widgetProperties = []; - final renderProperties = []; - LayoutProperties? layoutProperties; - final diagnostic = node?.diagnostic; - final objectGroupApi = diagnostic?.objectGroupApi; - if (diagnostic != null && objectGroupApi != null) { - try { - // Fetch widget properties: - final wProperties = await diagnostic.getProperties(objectGroupApi); - // Check if the selected node has changed, and if so return early: - if (_selectedNode.value != node) { - return; - } - widgetProperties.addAll( - wProperties.where((p) => p.propertyType != 'RenderObject'), - ); - renderProperties.addAll( - wProperties.where((p) => p.propertyType == 'RenderObject'), - ); - // Fetch layout properties: - layoutProperties = await _loadLayoutPropertiesForNode( - diagnostic, - forFlexLayout: false, - ); - // Fetch RenderObject properties: - for (final renderObject in renderProperties) { - final rProperties = await renderObject.getProperties(objectGroupApi); - // Check if the selected node has changed, and if so return early: - if (_selectedNode.value != node) { - return; - } - renderProperties.addAll(rProperties); - } - } catch (e, st) { - _log.warning(e, st); - } - } - _selectedNodeProperties.value = ( - widgetProperties: widgetProperties, - renderProperties: renderProperties, - layoutProperties: layoutProperties, - ); - } - - Future _loadLayoutPropertiesForNode( - RemoteDiagnosticsNode diagnostic, { - required bool forFlexLayout, - }) async { - try { - _layoutGroups?.cancelNext(); - final manager = _layoutGroups!; - final nextObjectGroup = manager.next; - final node = await nextObjectGroup.getLayoutExplorerNode( - diagnostic.layoutRootNode(forFlexLayout: forFlexLayout), - ); - if (node == null || node.renderObject == null) return null; - - if (!nextObjectGroup.disposed) { - assert(manager.next == nextObjectGroup); - manager.promoteNext(); - } - return node.computeLayoutProperties(forFlexLayout: forFlexLayout); - } catch (e, st) { - _log.warning(e, st); - return null; - } - } - - /// Update the index of the selected error based on a node that has been - /// selected in the tree. - void _updateSelectedErrorFromNode(InspectorTreeNode? node) { - final inspectorRef = node?.diagnostic?.valueRef.id; - - final errors = serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id) - .value; - - // Check whether the node that was just selected has any errors associated - // with it. - var errorIndex = inspectorRef != null - ? errors.keys.toList().indexOf(inspectorRef) - : null; - if (errorIndex == -1) { - errorIndex = null; - } - - _selectedErrorIndex.value = errorIndex; - - if (errorIndex != null) { - // Mark the error as "seen" as this will render slightly differently - // so the user can track which errored nodes they've viewed. - serviceConnection.errorBadgeManager.markErrorAsRead( - InspectorScreen.id, - errors[inspectorRef!]!, - ); - // Also clear the error badge since new errors may have arrived while - // the inspector was visible (normally they're cleared when visiting - // the screen) and visiting an errored node seems an appropriate - // acknowledgement of the errors. - serviceConnection.errorBadgeManager.clearErrorCount(InspectorScreen.id); - } - } - - /// Updates the index of the selected error and selects its node in the tree. - void selectErrorByIndex(int index) { - _selectedErrorIndex.value = index; - - final errors = serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id) - .value; - - unawaited( - updateSelectionFromService(inspectorRef: errors.keys.elementAt(index)), - ); - } - - void _onExpand(InspectorTreeNode node) { - unawaited(inspectorTree.maybePopulateChildren(node)); - } - - Future _addNodeToConsole(InspectorTreeNode node) async { - final valueRef = node.diagnostic!.valueRef; - final isolateRef = inspectorService.isolateRef; - final instanceRef = await node.diagnostic!.objectGroupApi - ?.toObservatoryInstanceRef(valueRef); - if (disposed) return; - - if (instanceRef != null) { - await serviceConnection.consoleService.appendInstanceRef( - value: instanceRef, - diagnostic: node.diagnostic, - isolateRef: isolateRef, - forceScrollIntoView: true, - ); - } - } - - /// Handles updating the widget tree when the selecected widget changes. - /// - /// [notifyFlutterInspector] determines whether a request should be sent to - /// the Widget Inspector in the Flutter framework to update the on-device - /// selection. This should only be true if the the selection was changed due - /// to a user action in DevTools (e.g. clicking on a widget in the tree). - void selectionChanged({bool notifyFlutterInspector = false}) { - if (!visibleToUser) { - return; - } - - final node = inspectorTree.selection; - if (node != null) { - unawaited(inspectorTree.maybePopulateChildren(node)); - } - if (programmaticSelectionChangeInProgress) { - return; - } - if (node != null) { - setSelectedNode(node); - unawaited(_addNodeToConsole(node)); - - syncSelectionHelper( - selection: selectedDiagnostic, - notifyFlutterInspector: notifyFlutterInspector, - ); - } - } - - /// Syncs the selection state after a new widgets was selected. - /// - /// [notifyFlutterInspector] determines whether a request should be sent to - /// the Widget Inspector in the Flutter framework to update the on-device - /// selection. This should only be true if the the selection was changed due - /// to a user action in DevTools (e.g. clicking on a widget in the tree). - void syncSelectionHelper({ - required RemoteDiagnosticsNode? selection, - bool notifyFlutterInspector = false, - }) { - if (selection != null) { - if (selection.isCreatedByLocalProject) { - _navigateTo(selection); - } - } - - if (notifyFlutterInspector && selection != null) { - unawaited(selection.setSelectionInspector(true)); - } - } - - // TODO(jacobr): implement this method and use the parameter. - // ignore: avoid-unused-parameters - void _navigateTo(RemoteDiagnosticsNode diagnostic) { - // TODO(jacobr): dispatch an event over the inspectorService requesting a - // navigate operation. - } - - @override - void dispose() { - assert(!disposed); - if (serviceConnection.inspectorService != null) { - shutdownTree(false); - } - - _treeGroups?.clear(false); - _treeGroups = null; - _selectionGroups?.clear(false); - _selectionGroups = null; - _layoutGroups?.clear(false); - _layoutGroups = null; - - _refreshRateLimiter.dispose(); - _selectedNode.dispose(); - _selectedNodeProperties.dispose(); - _implementationWidgetsHidden.dispose(); - _selectedErrorIndex.dispose(); - super.dispose(); - } - - void _onNodeAdded( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, - ) { - final valueRef = diagnosticsNode.valueRef; - // Properties do not have unique values so should not go in the valueToInspectorTreeNode map. - if (valueRef.id != null && !diagnosticsNode.isProperty) { - valueToInspectorTreeNode[valueRef] = node; - } - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart deleted file mode 100644 index 188dabd14b6..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart +++ /dev/null @@ -1,980 +0,0 @@ -// Copyright 2024 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. - -/// @docImport '../inspector/layout_explorer/ui/overflow_indicator_painter.dart'; -library; - -import 'dart:math' as math; - -import 'package:flutter/rendering.dart'; - -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/primitives/math_utils.dart'; -import '../../shared/primitives/utils.dart'; -import 'layout_explorer/flex/utils.dart'; - -const overflowEpsilon = 0.1; - -/// Compute real widget sizes into rendered sizes to be displayed on the details tab. -/// The sum of the resulting render sizes may or may not be greater than the [maxSizeAvailable] -/// In the case where it is greater, we should render it with scrolling capability. -/// -/// Variables: -/// - [sizes] : real size for widgets that want to be rendered / scaled -/// - [smallestSize] : the smallest element in the array [sizes] -/// - [largestSize] : the largest element in the array [sizes] -/// - [smallestRenderSize] : render size for smallest element -/// - [largestRenderSize] : render size for largest element -/// - [maxSizeAvailable] : maximum size available for rendering the widget -/// - [useMaxSizeAvailable] : flag for forcing the widget dimension to be at least [maxSizeAvailable] -/// -/// if [useMaxSizeAvailable] is set to true, -/// this method will ignore the largestRenderSize -/// and compute its own largestRenderSize to force -/// the sum of the render size to be equals to [maxSizeAvailable] -/// -/// Formula for computing render size: -/// ``` -/// renderSize[i] = (size[i] - smallestSize) -/// * (largestRenderSize - smallestRenderSize) -/// / (largestSize - smallestSize) + smallestRenderSize -/// ``` -/// -/// Explanation: -/// - The computation formula for transforming size to renderSize is based on these two things: -/// - smallest element will be rendered to [smallestRenderSize] -/// - largest element will be rendered to [largestRenderSize] -/// - any other size will be scaled accordingly -/// - The formula above is derived from: -/// ``` -/// (renderSize[i] - smallestRenderSize) / (largestRenderSize - smallestRenderSize) -/// = (size[i] - smallestSize) / (size[i] - smallestSize) -/// ``` -/// -/// Formula for computing forced [largestRenderSize]: -/// ``` -/// largestRenderSize = (maxSizeAvailable - sizes.length * smallestRenderSize) -/// * (largestSize - smallestSize) / sum(s[i] - ss) + smallestRenderSize -/// ``` -/// Explanation: -/// - This formula is derived from the equation: -/// ``` -/// sum(renderSize) = maxSizeAvailable -/// ``` -/// -List computeRenderSizes({ - required Iterable sizes, - required double smallestSize, - required double largestSize, - required double smallestRenderSize, - required double largestRenderSize, - required double maxSizeAvailable, - bool useMaxSizeAvailable = true, -}) { - final n = sizes.length; - - if (smallestSize == largestSize) { - // It means that all widget have the same size - // and we can just divide the size evenly - // but it should be at least as big as [smallestRenderSize] - final renderSize = math.max(smallestRenderSize, maxSizeAvailable / n); - return [for (final _ in sizes) renderSize]; - } - - List transformToRenderSize(double largestRenderSize) => [ - for (final s in sizes) - (s - smallestSize) * - (largestRenderSize - smallestRenderSize) / - (largestSize - smallestSize) + - smallestRenderSize, - ]; - - var renderSizes = transformToRenderSize(largestRenderSize); - - if (useMaxSizeAvailable && sum(renderSizes) < maxSizeAvailable) { - largestRenderSize = - (maxSizeAvailable - n * smallestRenderSize) * - (largestSize - smallestSize) / - sum([for (final s in sizes) s - smallestSize]) + - smallestRenderSize; - renderSizes = transformToRenderSize(largestRenderSize); - } - return renderSizes; -} - -/// Data pattern containing a widget's widths or heights. -typedef WidgetSizes = ({ - /// Whether this record represents a widget's widths or heights. - SizeType type, - - /// Either the widget's left (if [type] is [SizeType.widths]) or top (if - /// [type] is [SizeType.heights]) padding. - double paddingA, - - /// Either the widget's width (if [type] is [SizeType.widths]) or height (if - /// [type] is [SizeType.heights]). - double widgetSize, - - /// Either the widget's right (if [type] is [SizeType.widths]) or bottom (if - /// [type] is [SizeType.heights]) padding. - double paddingB, -}); - -enum SizeType { widths, heights } - -// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed -/// Represents parsed layout information for a specific [RemoteDiagnosticsNode]. -class LayoutProperties { - LayoutProperties(this.node, {int copyLevel = 1}) - : description = node.description, - size = node.size!, - constraints = node.constraints, - isFlex = node.isFlex, - flexFactor = node.flexFactor, - flexFit = node.flexFit, - children = copyLevel == 0 - ? [] - : node.childrenNow - .where((child) => child.size != null) - .map( - (child) => LayoutProperties(child, copyLevel: copyLevel - 1), - ) - .toList(growable: false) { - for (final child in children) { - child.parent = this; - } - } - - LayoutProperties.values({ - required this.node, - required this.children, - required this.constraints, - required this.description, - required this.flexFactor, - required this.isFlex, - required this.size, - required this.flexFit, - }) { - for (final child in children) { - child.parent = this; - } - } - - LayoutProperties? parent; - final RemoteDiagnosticsNode node; - final List children; - final BoxConstraints? constraints; - final String? description; - final num? flexFactor; - final FlexFit? flexFit; - final bool isFlex; - final Size size; - - /// Represents the order of [children] to be displayed. - List get displayChildren => children; - - bool get hasFlexFactor { - final flexFactorLocal = flexFactor; - if (flexFactorLocal == null) return false; - return flexFactorLocal > 0; - } - - int get totalChildren => children.length; - - bool get hasChildren => children.isNotEmpty; - - double get width => size.width; - - double get height => size.height; - - double dimension(Axis axis) => axis == Axis.horizontal ? width : height; - - List childrenDimensions(Axis axis) { - return displayChildren.map((child) => child.dimension(axis)).toList(); - } - - List get childrenWidths => childrenDimensions(Axis.horizontal); - - List get childrenHeights => childrenDimensions(Axis.vertical); - - String describeWidthConstraints() { - final constraintsLocal = constraints; - if (constraintsLocal == null) return ''; - return constraintsLocal.hasBoundedWidth - ? describeAxis( - constraintsLocal.minWidth, - constraintsLocal.maxWidth, - 'w', - ) - : 'width is unconstrained'; - } - - String describeHeightConstraints() { - final constraintsLocal = constraints; - if (constraintsLocal == null) return ''; - return constraintsLocal.hasBoundedHeight - ? describeAxis( - constraintsLocal.minHeight, - constraintsLocal.maxHeight, - 'h', - ) - : 'height is unconstrained'; - } - - String describeWidth() => 'w=${toStringAsFixed(size.width)}'; - - String describeHeight() => 'h=${toStringAsFixed(size.height)}'; - - bool get isOverflowWidth { - final parentWidth = parent?.width; - if (parentWidth == null) return false; - final parentData = node.parentData; - double widthUsed = width; - - widthUsed += parentData.offset.dx; - - // TODO(jacobr): certain widgets may allow overflow so this may false - // positive a bit for cases like Stack. - return widthUsed > parentWidth + overflowEpsilon; - } - - bool get isOverflowHeight { - final parentHeight = parent?.height; - if (parentHeight == null) return false; - final parentData = node.parentData; - double heightUsed = height; - - heightUsed += parentData.offset.dy; - - return heightUsed > parentHeight + overflowEpsilon; - } - - LayoutProperties? get parentLayoutProperties { - final parentElement = node.parentRenderElement; - // Fall back to this node's properties if there is no parent. - if (parentElement == null) return this; - final parentProperties = parentElement.computeLayoutProperties( - forFlexLayout: false, - ); - return parentProperties ?? this; - } - - WidgetSizes? get widgetWidths => _widgetSizes(SizeType.widths); - - WidgetSizes? get widgetHeights => _widgetSizes(SizeType.heights); - - WidgetSizes? _widgetSizes(SizeType type) { - if (parentLayoutProperties == null) return null; - final parentProperties = parentLayoutProperties!; - - final parentData = node.parentData; - final parentSize = parentProperties.size; - - switch (type) { - case SizeType.heights: - return ( - type: type, - paddingA: parentData.offset.dy, - widgetSize: size.height, - paddingB: parentSize.height - (size.height + parentData.offset.dy), - ); - case SizeType.widths: - return ( - type: type, - paddingA: parentData.offset.dx, - widgetSize: size.width, - paddingB: parentSize.width - (size.width + parentData.offset.dx), - ); - } - } - - static String describeAxis(double min, double max, String axis) { - if (min == max) return '$axis=${min.toStringAsFixed(1)}'; - return '${min.toStringAsFixed(1)}<=$axis<=${max.toStringAsFixed(1)}'; - } - - LayoutProperties copyWith({ - List? children, - BoxConstraints? constraints, - String? description, - int? flexFactor, - FlexFit? flexFit, - bool? isFlex, - Size? size, - }) { - return LayoutProperties.values( - node: node, - children: children ?? this.children, - constraints: constraints ?? this.constraints, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - isFlex: isFlex ?? this.isFlex, - size: size ?? this.size, - flexFit: flexFit ?? this.flexFit, - ); - } -} - -/// Enum object to represent which side of the widget is overflowing. -/// -/// See also: -/// * [OverflowIndicatorPainter] -enum OverflowSide { right, bottom } - -// TODO(jacobr): is it possible to overflow on multiple sides? -// TODO(jacobr): do we need to worry about overflowing on the left side in RTL -// layouts? We need to audit the Flutter semantics for determining overflow to -// make sure we are consistent. -extension LayoutPropertiesExtension on LayoutProperties { - OverflowSide? get overflowSide { - if (isOverflowWidth) return OverflowSide.right; - if (isOverflowHeight) return OverflowSide.bottom; - return null; - } -} - -final _flexLayoutExpando = Expando(); - -extension MainAxisAlignmentExtension on MainAxisAlignment { - MainAxisAlignment get reversed { - switch (this) { - case MainAxisAlignment.start: - return MainAxisAlignment.end; - case MainAxisAlignment.end: - return MainAxisAlignment.start; - default: - return this; - } - } -} - -/// Encapsulation of [widths] and [heights] for the layout. -class LayoutWidthsAndHeights { - LayoutWidthsAndHeights({required this.widths, required this.heights}); - - final WidgetSizes widths; - final WidgetSizes heights; - - double get widgetWidth => widths.widgetSize; - - double get widgetHeight => heights.widgetSize; - - double get leftPadding => widths.paddingA; - - double get rightPadding => widths.paddingB; - - double get topPadding => heights.paddingA; - - double get bottomPadding => heights.paddingB; - - bool get hasLeftPadding => leftPadding > 0; - - bool get hasRightPadding => rightPadding > 0; - - bool get hasTopPadding => topPadding > 0; - - bool get hasBottomPadding => bottomPadding > 0; - - bool get hasAnyPadding => - hasLeftPadding || hasRightPadding || hasTopPadding || hasBottomPadding; -} - -/// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed. -class FlexLayoutProperties extends LayoutProperties { - FlexLayoutProperties({ - required super.size, - required super.children, - required super.node, - super.constraints, - super.isFlex = false, - super.description, - super.flexFactor, - super.flexFit, - this.direction = Axis.vertical, - this.mainAxisAlignment, - this.crossAxisAlignment, - this.mainAxisSize, - required this.textDirection, - required this.verticalDirection, - this.textBaseline, - }) : super.values(); - - FlexLayoutProperties._fromNode( - super.node, { - this.direction = Axis.vertical, - this.mainAxisAlignment, - this.mainAxisSize, - this.crossAxisAlignment, - required this.textDirection, - required this.verticalDirection, - this.textBaseline, - }); - - factory FlexLayoutProperties.fromDiagnostics(RemoteDiagnosticsNode node) { - // Cache the properties on an expando so that local tweaks to - // FlexLayoutProperties persist across multiple lookups from an - // RemoteDiagnosticsNode. - return _flexLayoutExpando[node] ??= _buildNode(node); - } - - @override - FlexLayoutProperties copyWith({ - Size? size, - List? children, - BoxConstraints? constraints, - bool? isFlex, - String? description, - num? flexFactor, - FlexFit? flexFit, - Axis? direction, - MainAxisAlignment? mainAxisAlignment, - MainAxisSize? mainAxisSize, - CrossAxisAlignment? crossAxisAlignment, - TextDirection? textDirection, - VerticalDirection? verticalDirection, - TextBaseline? textBaseline, - }) { - return FlexLayoutProperties( - size: size ?? this.size, - children: children ?? this.children, - node: node, - constraints: constraints ?? this.constraints, - isFlex: isFlex ?? this.isFlex, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - flexFit: flexFit ?? this.flexFit, - direction: direction ?? this.direction, - mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment, - mainAxisSize: mainAxisSize ?? this.mainAxisSize, - crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment, - textDirection: textDirection ?? this.textDirection, - verticalDirection: verticalDirection ?? this.verticalDirection, - textBaseline: textBaseline ?? this.textBaseline, - ); - } - - static FlexLayoutProperties _buildNode(RemoteDiagnosticsNode node) { - final renderObjectJson = node.renderObject!.json; - final properties = (renderObjectJson['properties'] as List) - .cast>(); - - final data = { - for (final property in properties) - property['name']: property['description'] as String?, - }; - - return FlexLayoutProperties._fromNode( - node, - direction: _directionNamesToValues[data['direction']] ?? Axis.vertical, - mainAxisAlignment: - _mainAxisAlignmentNamesToValues[data['mainAxisAlignment']], - mainAxisSize: _mainAxisSizeNamesToValues[data['mainAxisSize']], - crossAxisAlignment: - _crossAxisAlignmentNamesToValues[data['crossAxisAlignment']], - textDirection: - _textDirectionNamesToValues[data['textDirection']] ?? - TextDirection.ltr, - verticalDirection: - _verticalDirectionNamesToValues[data['verticalDirection']] ?? - VerticalDirection.down, - textBaseline: _textBaselineNamesToValues[data['textBaseline']], - ); - } - - final Axis direction; - final MainAxisAlignment? mainAxisAlignment; - final CrossAxisAlignment? crossAxisAlignment; - final MainAxisSize? mainAxisSize; - final TextDirection textDirection; - final VerticalDirection verticalDirection; - final TextBaseline? textBaseline; - - List? _displayChildren; - - @override - List get displayChildren { - final displayChildren = _displayChildren; - if (displayChildren != null) return displayChildren; - return _displayChildren = startIsTopLeft - ? children - : children.reversed.toList(); - } - - int? _totalFlex; - - bool get isMainAxisHorizontal => direction == Axis.horizontal; - - bool get isMainAxisVertical => direction == Axis.vertical; - - String get horizontalDirectionDescription { - return direction == Axis.horizontal ? 'Main Axis' : 'Cross Axis'; - } - - String get verticalDirectionDescription { - return direction == Axis.vertical ? 'Main Axis' : 'Cross Axis'; - } - - String get type => direction.flexType; - - num get totalFlex { - if (children.isEmpty) return 0; - _totalFlex ??= children - .map((child) => child.flexFactor ?? 0) - .reduce((value, element) => value + element) - .toInt(); - return _totalFlex!; - } - - Axis get crossAxisDirection { - return direction == Axis.horizontal ? Axis.vertical : Axis.horizontal; - } - - double get mainAxisDimension => dimension(direction); - - double get crossAxisDimension => dimension(crossAxisDirection); - - @override - bool get isOverflowWidth { - if (direction == Axis.horizontal) { - return width + overflowEpsilon < sum(childrenWidths); - } - return width + overflowEpsilon < max(childrenWidths); - } - - @override - bool get isOverflowHeight { - if (direction == Axis.vertical) { - return height + overflowEpsilon < sum(childrenHeights); - } - return height + overflowEpsilon < max(childrenHeights); - } - - bool get startIsTopLeft { - switch (direction) { - case Axis.horizontal: - switch (textDirection) { - case TextDirection.ltr: - return true; - case TextDirection.rtl: - return false; - } - case Axis.vertical: - switch (verticalDirection) { - case VerticalDirection.down: - return true; - case VerticalDirection.up: - return false; - } - } - } - - /// render properties for laying out rendered Flex & Flex children widgets - /// the computation is similar to [RenderFlex].performLayout() method - List childrenRenderProperties({ - required double smallestRenderWidth, - required double largestRenderWidth, - required double smallestRenderHeight, - required double largestRenderHeight, - required double Function(Axis) maxSizeAvailable, - }) { - /// calculate the render empty spaces - final freeSpace = dimension(direction) - sum(childrenDimensions(direction)); - final displayMainAxisAlignment = startIsTopLeft - ? mainAxisAlignment - : mainAxisAlignment?.reversed; - - double leadingSpace(double freeSpace) { - if (children.isEmpty) return 0.0; - switch (displayMainAxisAlignment) { - case MainAxisAlignment.start: - case MainAxisAlignment.end: - return freeSpace; - case MainAxisAlignment.center: - return freeSpace * 0.5; - case MainAxisAlignment.spaceBetween: - return 0.0; - case MainAxisAlignment.spaceAround: - final spaceBetweenChildren = freeSpace / children.length; - return spaceBetweenChildren * 0.5; - case MainAxisAlignment.spaceEvenly: - return freeSpace / (children.length + 1); - default: - return 0.0; - } - } - - double betweenSpace(double freeSpace) { - if (children.isEmpty) return 0.0; - switch (displayMainAxisAlignment) { - case MainAxisAlignment.start: - case MainAxisAlignment.end: - case MainAxisAlignment.center: - return 0.0; - case MainAxisAlignment.spaceBetween: - if (children.length == 1) return freeSpace; - return freeSpace / (children.length - 1); - case MainAxisAlignment.spaceAround: - return freeSpace / children.length; - case MainAxisAlignment.spaceEvenly: - return freeSpace / (children.length + 1); - default: - return 0.0; - } - } - - double smallestRenderSize(Axis axis) { - return axis == Axis.horizontal - ? smallestRenderWidth - : smallestRenderHeight; - } - - double largestRenderSize(Axis axis) { - final lrs = axis == Axis.horizontal - ? largestRenderWidth - : largestRenderHeight; - // use all the space when visualizing cross axis - return (axis == direction) ? lrs : maxSizeAvailable(axis); - } - - List renderSizes(Axis axis) { - final sizes = childrenDimensions(axis); - if (freeSpace > 0.0 && axis == direction) { - /// include free space in the computation - sizes.add(freeSpace); - } - final smallestSize = min(sizes); - final largestSize = max(sizes); - if (axis == direction || - (crossAxisAlignment != CrossAxisAlignment.stretch && - smallestSize != largestSize)) { - return computeRenderSizes( - sizes: sizes, - smallestSize: smallestSize, - largestSize: largestSize, - smallestRenderSize: smallestRenderSize(axis), - largestRenderSize: largestRenderSize(axis), - maxSizeAvailable: maxSizeAvailable(axis), - ); - } else { - // uniform cross axis sizes. - double size = crossAxisAlignment == CrossAxisAlignment.stretch - ? maxSizeAvailable(axis) - : largestSize / - math.max(dimension(axis), 1.0) * - maxSizeAvailable(axis); - size = math.max(size, smallestRenderSize(axis)); - return sizes.map((_) => size).toList(); - } - } - - final widths = renderSizes(Axis.horizontal); - final heights = renderSizes(Axis.vertical); - - final renderFreeSpace = freeSpace > 0.0 - ? (isMainAxisHorizontal ? widths.last : heights.last) - : 0.0; - - final renderLeadingSpace = leadingSpace(renderFreeSpace); - final renderBetweenSpace = betweenSpace(renderFreeSpace); - - final childrenRenderProps = []; - - double lastMainAxisOffset() { - if (childrenRenderProps.isEmpty) return 0.0; - return childrenRenderProps.last.mainAxisOffset; - } - - double lastMainAxisDimension() { - if (childrenRenderProps.isEmpty) return 0.0; - return childrenRenderProps.last.mainAxisDimension; - } - - double space(int index) { - if (index == 0) { - if (displayMainAxisAlignment == MainAxisAlignment.start) return 0.0; - return renderLeadingSpace; - } - return renderBetweenSpace; - } - - double calculateMainAxisOffset(int i) { - return lastMainAxisOffset() + lastMainAxisDimension() + space(i); - } - - double calculateCrossAxisOffset(int i) { - final maxDimension = maxSizeAvailable(crossAxisDirection); - final usedDimension = crossAxisDirection == Axis.horizontal - ? widths[i] - : heights[i]; - - if (crossAxisAlignment == CrossAxisAlignment.start || - crossAxisAlignment == CrossAxisAlignment.stretch || - maxDimension == usedDimension) { - return 0.0; - } - final emptySpace = math.max(0.0, maxDimension - usedDimension); - if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace; - return emptySpace * 0.5; - } - - for (var i = 0; i < children.length; ++i) { - childrenRenderProps.add( - RenderProperties( - axis: direction, - size: Size(widths[i], heights[i]), - offset: Offset.zero, - realSize: displayChildren[i].size, - layoutProperties: displayChildren[i], - ) - ..mainAxisOffset = calculateMainAxisOffset(i) - ..crossAxisOffset = calculateCrossAxisOffset(i), - ); - } - - final spaces = []; - final actualLeadingSpace = leadingSpace(freeSpace); - final actualBetweenSpace = betweenSpace(freeSpace); - final renderPropsWithFullCrossAxisDimension = - RenderProperties( - axis: direction, - isFreeSpace: true, - layoutProperties: this, - ) - ..crossAxisDimension = maxSizeAvailable(crossAxisDirection) - ..crossAxisRealDimension = dimension(crossAxisDirection) - ..crossAxisOffset = 0.0; - if (actualLeadingSpace > 0.0 && - displayMainAxisAlignment != MainAxisAlignment.start) { - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisOffset = 0.0 - ..mainAxisDimension = renderLeadingSpace - ..mainAxisRealDimension = actualLeadingSpace, - ); - } - if (actualBetweenSpace > 0.0) { - for (var i = 0; i < childrenRenderProps.length - 1; ++i) { - final child = childrenRenderProps[i]; - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisDimension = renderBetweenSpace - ..mainAxisRealDimension = actualBetweenSpace - ..mainAxisOffset = child.mainAxisOffset + child.mainAxisDimension, - ); - } - } - if (actualLeadingSpace > 0.0 && - displayMainAxisAlignment != MainAxisAlignment.end) { - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisOffset = - childrenRenderProps.last.mainAxisDimension + - childrenRenderProps.last.mainAxisOffset - ..mainAxisDimension = renderLeadingSpace - ..mainAxisRealDimension = actualLeadingSpace, - ); - } - return [...childrenRenderProps, ...spaces]; - } - - List crossAxisSpaces({ - required List childrenRenderProperties, - required double Function(Axis) maxSizeAvailable, - }) { - if (crossAxisAlignment == CrossAxisAlignment.stretch) return []; - final spaces = []; - for (var i = 0; i < children.length; ++i) { - if (dimension(crossAxisDirection) == - displayChildren[i].dimension(crossAxisDirection) || - childrenRenderProperties[i].crossAxisDimension == - maxSizeAvailable(crossAxisDirection)) { - continue; - } - - final renderProperties = childrenRenderProperties[i]; - final space = renderProperties.copyWith(isFreeSpace: true); - - space.crossAxisRealDimension = - crossAxisDimension - space.crossAxisRealDimension; - space.crossAxisDimension = - maxSizeAvailable(crossAxisDirection) - space.crossAxisDimension; - if (space.crossAxisDimension <= 0.0) continue; - if (crossAxisAlignment == CrossAxisAlignment.center) { - space.crossAxisDimension *= 0.5; - final crossAxisRealDimension = space.crossAxisRealDimension; - space.crossAxisRealDimension = crossAxisRealDimension * 0.5; - spaces.add(space.copyWith()..crossAxisOffset = 0.0); - spaces.add( - space.copyWith() - ..crossAxisOffset = - renderProperties.crossAxisDimension + - renderProperties.crossAxisOffset, - ); - } else { - space.crossAxisOffset = crossAxisAlignment == CrossAxisAlignment.end - ? 0 - : renderProperties.crossAxisDimension; - spaces.add(space); - } - } - return spaces; - } - - static final _directionNamesToValues = Axis.values.asNameMap(); - static final _mainAxisAlignmentNamesToValues = MainAxisAlignment.values - .asNameMap(); - static final _mainAxisSizeNamesToValues = MainAxisSize.values.asNameMap(); - static final _crossAxisAlignmentNamesToValues = CrossAxisAlignment.values - .asNameMap(); - static final _textDirectionNamesToValues = TextDirection.values.asNameMap(); - static final _verticalDirectionNamesToValues = VerticalDirection.values - .asNameMap(); - static final _textBaselineNamesToValues = TextBaseline.values.asNameMap(); -} - -/// Information for rendering a [LayoutProperties] node. -class RenderProperties { - RenderProperties({ - required this.axis, - required this.layoutProperties, - this.isFreeSpace = false, - Size? size, - Offset? offset, - Size? realSize, - }) : width = size?.width ?? 0.0, - height = size?.height ?? 0.0, - realWidth = realSize?.width ?? 0.0, - realHeight = realSize?.height ?? 0.0, - dx = offset?.dx ?? 0.0, - dy = offset?.dy ?? 0.0; - - final Axis axis; - - /// Represents which node is rendered for this object. - final LayoutProperties layoutProperties; - - final bool isFreeSpace; - - double dx, dy; - double width, height; - double realWidth, realHeight; - - Size get size => Size(width, height); - - Size get realSize => Size(realWidth, realHeight); - - Offset get offset => Offset(dx, dy); - - double get mainAxisDimension => axis == Axis.horizontal ? width : height; - - set mainAxisDimension(double dim) { - if (axis == Axis.horizontal) { - width = dim; - } else { - height = dim; - } - } - - double get crossAxisDimension => axis == Axis.horizontal ? height : width; - - set crossAxisDimension(double dim) { - if (axis == Axis.horizontal) { - height = dim; - } else { - width = dim; - } - } - - double get mainAxisOffset => axis == Axis.horizontal ? dx : dy; - - set mainAxisOffset(double offset) { - if (axis == Axis.horizontal) { - dx = offset; - } else { - dy = offset; - } - } - - double get crossAxisOffset => axis == Axis.horizontal ? dy : dx; - - set crossAxisOffset(double offset) { - if (axis == Axis.horizontal) { - dy = offset; - } else { - dx = offset; - } - } - - double get mainAxisRealDimension => - axis == Axis.horizontal ? realWidth : realHeight; - - set mainAxisRealDimension(double newVal) { - if (axis == Axis.horizontal) { - realWidth = newVal; - } else { - realHeight = newVal; - } - } - - double get crossAxisRealDimension => - axis == Axis.horizontal ? realHeight : realWidth; - - set crossAxisRealDimension(double newVal) { - if (axis == Axis.horizontal) { - realHeight = newVal; - } else { - realWidth = newVal; - } - } - - RenderProperties copyWith({bool? isFreeSpace}) { - return RenderProperties( - axis: axis, - size: size, - offset: offset, - realSize: realSize, - layoutProperties: layoutProperties, - isFreeSpace: isFreeSpace ?? this.isFreeSpace, - ); - } - - @override - int get hashCode => - axis.hashCode ^ - size.hashCode ^ - offset.hashCode ^ - realSize.hashCode ^ - isFreeSpace.hashCode; - - @override - bool operator ==(Object other) { - return other is RenderProperties && - axis == other.axis && - size.closeTo(other.size) && - offset.closeTo(other.offset) && - realSize.closeTo(other.realSize) && - isFreeSpace == other.isFreeSpace; - } - - @override - String toString() { - return '{ axis: $axis, size: $size, offset: $offset, realSize: $realSize, isFreeSpace: $isFreeSpace }'; - } -} - -bool _closeTo(double a, double b, {int precision = 1}) { - return a.toStringAsPrecision(precision) == b.toStringAsPrecision(precision); -} - -extension on Size { - bool closeTo(Size other) { - return _closeTo(width, other.width) && _closeTo(height, other.height); - } -} - -extension on Offset { - bool closeTo(Offset other) { - return _closeTo(dx, other.dx) && _closeTo(dy, other.dy); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart deleted file mode 100644 index bcb6070927b..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart +++ /dev/null @@ -1,388 +0,0 @@ -// Copyright 2024 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 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree_v2.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/error_badge_manager.dart'; -import '../../shared/primitives/blocking_action_mixin.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/search.dart'; -import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_controls.dart'; -import '../inspector_shared/inspector_screen.dart'; -import 'inspector_controller.dart'; -import 'inspector_tree_controller.dart'; -import 'widget_details.dart'; - -class InspectorScreenBody extends StatefulWidget { - const InspectorScreenBody({super.key, required this.controller}); - - final InspectorController controller; - - @override - InspectorScreenBodyState createState() => InspectorScreenBodyState(); -} - -class InspectorScreenBodyState extends State - with BlockingActionMixin, AutoDisposeMixin { - InspectorController get controller => widget.controller; - - InspectorTreeController get _inspectorTreeController => - controller.inspectorTree; - - bool searchVisible = false; - - SearchControllerMixin get searchController => _inspectorTreeController; - - /// Indicates whether search can be closed. The value is set to true when - /// search target type dropdown is displayed - /// TODO(https://github.com/flutter/devtools/issues/3489) use this variable when adding the scope dropdown - bool searchPreventClose = false; - - SearchTargetType searchTarget = SearchTargetType.widget; - - static const inspectorTreeKey = Key('Inspector Tree'); - static const minScreenWidthForText = 900.0; - - @override - void initState() { - super.initState(); - ga.screen(InspectorScreen.id); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (serviceConnection.inspectorService == null) { - // The app must not be a Flutter app. - return; - } - - cancelListeners(); - searchVisible = searchController.search.isNotEmpty; - addAutoDisposeListener(searchController.searchFieldFocusNode, () { - final searchFieldFocusNode = searchController.searchFieldFocusNode; - if (searchFieldFocusNode == null) return; - // Close the search once focus is lost and following conditions are met: - // 1. Search string is empty. - // 2. [searchPreventClose] == false (this is set true when searchTargetType Dropdown is opened). - if (!searchFieldFocusNode.hasFocus && - searchController.search.isEmpty && - !searchPreventClose) { - setState(() { - searchVisible = false; - }); - } - - // Reset [searchPreventClose] state to false after the search field gains focus. - // Focus is returned automatically once the Dropdown menu is closed. - if (searchFieldFocusNode.hasFocus) { - searchPreventClose = false; - } - }); - addAutoDisposeListener(preferences.inspector.pubRootDirectories, () { - if (serviceConnection.serviceManager.connectedState.value.connected && - controller.firstInspectorTreeLoadCompleted) { - safeUnawaited(controller.refreshInspector()); - } - }); - - if (!controller.firstInspectorTreeLoadCompleted) { - ga.timeStart(InspectorScreen.id, gac.pageReady); - } - - _inspectorTreeController.setSearchTarget(searchTarget); - } - - @override - Widget build(BuildContext context) { - final inspectorTree = _buildInspectorTreeColumn(); - - final splitAxis = SplitPane.axisFor(context, 0.85); - final widgetTrees = SplitPane( - axis: splitAxis, - initialFractions: const [0.33, 0.67], - children: [ - inspectorTree, - WidgetDetails(controller: controller), - ], - ); - return Column( - children: [ - InspectorControls(controller: controller), - const SizedBox(height: intermediateSpacing), - Expanded(child: widgetTrees), - ], - ); - } - - Widget _buildInspectorTreeColumn() { - return LayoutBuilder( - builder: (context, constraints) { - return RoundedOutlinedBorder( - child: Column( - children: [ - InspectorTreeControls( - isSearchVisible: searchVisible, - constraints: constraints, - onRefreshInspectorPressed: _manualInspectorRefresh, - onSearchVisibleToggle: _onSearchVisibleToggle, - searchFieldBuilder: () => - StatelessSearchField( - controller: _inspectorTreeController, - searchFieldEnabled: true, - shouldRequestFocus: searchVisible, - supportsNavigation: true, - onClose: _onSearchVisibleToggle, - ), - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id), - builder: (_, LinkedHashMap errors, _) { - final inspectableErrors = - errors.map( - (key, value) => MapEntry( - key, - value as InspectableWidgetError, - ), - ) - as LinkedHashMap; - return Stack( - children: [ - InspectorTree( - key: inspectorTreeKey, - controller: controller, - treeController: _inspectorTreeController, - widgetErrors: inspectableErrors, - screenId: InspectorScreen.id, - ), - if (errors.isNotEmpty) - ValueListenableBuilder( - valueListenable: controller.selectedErrorIndex, - builder: (_, selectedErrorIndex, _) => Positioned( - top: 0, - right: 0, - child: ErrorNavigator( - errors: inspectableErrors, - errorIndex: selectedErrorIndex, - onSelectError: controller.selectErrorByIndex, - ), - ), - ), - ], - ); - }, - ), - ), - ], - ), - ); - }, - ); - } - - void _onSearchVisibleToggle() { - setState(() { - searchVisible = !searchVisible; - }); - _inspectorTreeController.resetSearch(); - } - - void _manualInspectorRefresh() { - ga.select( - gac.inspector, - gac.refresh, - screenMetricsProvider: () => InspectorScreenMetrics.v2(), - ); - unawaited( - blockWhileInProgress(() async { - await controller.refreshInspector(isManualRefresh: true); - }), - ); - } -} - -class InspectorTreeControls extends StatelessWidget { - const InspectorTreeControls({ - super.key, - required this.constraints, - required this.isSearchVisible, - required this.onRefreshInspectorPressed, - required this.onSearchVisibleToggle, - required this.searchFieldBuilder, - }); - - static const _searchBreakpoint = 375.0; - - final bool isSearchVisible; - final BoxConstraints constraints; - final VoidCallback onRefreshInspectorPressed; - final VoidCallback onSearchVisibleToggle; - final Widget Function() searchFieldBuilder; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - _controlsContainer( - context, - Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: denseSpacing), - child: Text( - 'Widget Tree', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ...!isSearchVisible - ? [ - const Spacer(), - ToolbarAction( - icon: Icons.search, - onPressed: onSearchVisibleToggle, - tooltip: 'Search Tree', - ), - ] - : [ - constraints.maxWidth >= _searchBreakpoint - ? _buildSearchControls() - : const Spacer(), - ], - ToolbarAction( - icon: Icons.refresh, - onPressed: onRefreshInspectorPressed, - tooltip: 'Refresh Tree', - ), - ], - ), - ), - if (isSearchVisible && constraints.maxWidth < _searchBreakpoint) - _controlsContainer(context, Row(children: [_buildSearchControls()])), - ], - ); - } - - Container _controlsContainer(BuildContext context, Widget child) { - return Container( - height: defaultHeaderHeight, - decoration: BoxDecoration( - border: Border(bottom: defaultBorderSide(Theme.of(context))), - ), - child: child, - ); - } - - Widget _buildSearchControls() { - return Expanded( - child: SizedBox( - height: defaultTextFieldHeight, - child: searchFieldBuilder(), - ), - ); - } -} - -class ErrorNavigator extends StatelessWidget { - const ErrorNavigator({ - super.key, - required this.errors, - required this.errorIndex, - required this.onSelectError, - }); - - final LinkedHashMap errors; - - final int? errorIndex; - - final void Function(int) onSelectError; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final label = errorIndex != null - ? 'Error ${errorIndex! + 1}/${errors.length}' - : 'Errors: ${errors.length}'; - return Container( - color: colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: defaultSpacing, - vertical: denseSpacing, - ), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Text( - label, - style: TextStyle(color: colorScheme.onErrorContainer), - ), - ), - _ErrorNavigatorButton( - icon: Icons.keyboard_arrow_up, - onPressed: _previousError, - ), - _ErrorNavigatorButton( - icon: Icons.keyboard_arrow_down, - onPressed: _nextError, - ), - ], - ), - ), - ); - } - - void _previousError() { - var newIndex = errorIndex == null ? errors.length - 1 : errorIndex! - 1; - while (newIndex < 0) { - newIndex += errors.length; - } - - onSelectError(newIndex); - } - - void _nextError() { - final newIndex = errorIndex == null ? 0 : (errorIndex! + 1) % errors.length; - - onSelectError(newIndex); - } -} - -class _ErrorNavigatorButton extends StatelessWidget { - const _ErrorNavigatorButton({required this.icon, required this.onPressed}); - - final IconData icon; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - // This is required to force the button size. - height: defaultButtonHeight, - width: defaultButtonHeight, - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - splashRadius: defaultIconSize, - icon: Icon(icon), - color: Theme.of(context).colorScheme.onErrorContainer, - onPressed: onPressed, - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_tree_controller.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_tree_controller.dart deleted file mode 100644 index a29c12f22e2..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_tree_controller.dart +++ /dev/null @@ -1,1542 +0,0 @@ -// Copyright 2024 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:math'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:logging/logging.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree_v2.dart'; -import '../../shared/console/widgets/description.dart'; -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/error_badge_manager.dart'; -import '../../shared/primitives/collapsible_mixin.dart'; -import '../../shared/primitives/diagnostics_text_styles.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/ui/colors.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/search.dart'; -import '../../shared/ui/utils.dart'; -import '../../shared/utils/utils.dart'; -import 'inspector_controller.dart'; - -final _log = Logger('inspector_tree_controller'); - -/// Presents a [InspectorTreeNode]. -class _InspectorTreeRowWidget extends StatefulWidget { - /// Constructs a [_InspectorTreeRowWidget] that presents a line in the - /// Inspector tree. - const _InspectorTreeRowWidget({ - required super.key, - required this.row, - required this.inspectorTreeState, - this.error, - required this.scrollControllerX, - required this.viewportWidth, - }); - - final _InspectorTreeState inspectorTreeState; - - InspectorTreeNode get node => row.node; - final InspectorTreeRow row; - final ScrollController scrollControllerX; - final double viewportWidth; - - /// A [DevToolsError] that applies to the widget in this row. - /// - /// This will be null if there is no error for this row. - final DevToolsError? error; - - @override - _InspectorTreeRowState createState() => _InspectorTreeRowState(); -} - -class _InspectorTreeRowState extends State<_InspectorTreeRowWidget> - with TickerProviderStateMixin, CollapsibleAnimationMixin { - @override - Widget build(BuildContext context) { - return SizedBox( - height: inspectorRowHeight, - child: InspectorRowContent( - row: widget.row, - error: widget.error, - expandArrowAnimation: expandArrowAnimation, - controller: widget.inspectorTreeState.treeController!, - scrollControllerX: widget.scrollControllerX, - viewportWidth: widget.viewportWidth, - onToggle: () { - setExpanded(!isExpanded); - }, - ), - ); - } - - @override - bool get isExpanded => widget.node.isExpanded; - - @override - void onExpandChanged(bool expanded) { - setState(() { - final row = widget.row; - final treeController = widget.inspectorTreeState.treeController!; - treeController.refreshTree( - updateTreeAction: () { - if (expanded) { - treeController.onExpandRow(row); - } else { - treeController.onCollapseRow(row); - } - return true; - }, - ); - }); - } - - @override - bool shouldShow() => widget.node.shouldShow; -} - -class InspectorTreeController extends DisposableController - with SearchControllerMixin { - InspectorTreeController({this.gaId}) { - init(); - } - - /// Clients the controller notifies to trigger changes to the UI. - final _clients = {}; - - /// Identifier used when sending Google Analytics about events in this - /// [InspectorTreeController]. - final int? gaId; - - InspectorTreeNode createNode() => - InspectorTreeNode(whenDirty: _handleDirtyNode); - - SearchTargetType _searchTarget = SearchTargetType.widget; - int _rootSetCount = 0; - - @override - void init() { - super.init(); - ga.select( - gac.inspector, - gac.inspectorTreeControllerInitialized, - nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.v2( - inspectorTreeControllerId: gaId, - rootSetCount: _rootSetCount, - rowCount: _rowsInTree.value.length, - ), - ); - } - - @override - void dispose() { - _rowsInTree.dispose(); - super.dispose(); - } - - void addClient(InspectorControllerClient value) { - final firstClient = _clients.isEmpty; - _clients.add(value); - if (firstClient) { - config.onClientActiveChange?.call(true); - } - } - - void removeClient(InspectorControllerClient value) { - _clients.remove(value); - if (_clients.isEmpty) { - config.onClientActiveChange?.call(false); - } - } - - void requestFocus() { - for (final client in _clients) { - client.requestFocus(); - } - } - - InspectorTreeNode? get root => _root; - InspectorTreeNode? _root; - - set root(InspectorTreeNode? node) { - if (node != null) { - _updateRows(node: node, updateSearchableRows: true); - } - _root = node; - - ga.select( - gac.inspector, - gac.inspectorTreeControllerRootChange, - nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.v2( - inspectorTreeControllerId: gaId, - rootSetCount: ++_rootSetCount, - rowCount: _rowsInTree.value.length, - ), - ); - } - - InspectorTreeNode? get selection => _selection; - InspectorTreeNode? _selection; - - late final InspectorTreeConfig config; - - /// Refreshes the tree's rows if the return value of the [updateTreeAction] - /// callback is true. - void refreshTree({required bool Function() updateTreeAction}) { - final requiresRefresh = updateTreeAction(); - if (requiresRefresh) { - _updateRows(); - } - } - - bool setSelectedNode( - InspectorTreeNode? node, { - bool notifyFlutterInspector = false, - }) { - if (node == _selection) return false; - - _selection?.selected = false; - _selection = node; - _selection?.selected = true; - final configLocal = config; - if (configLocal.onSelectionChange != null) { - configLocal.onSelectionChange!( - notifyFlutterInspector: notifyFlutterInspector, - ); - } - return true; - } - - InspectorTreeNode? get hover => _hover; - InspectorTreeNode? _hover; - - double? lastContentWidth; - - InspectorTreeRow? _cachedSelectedRow; - - /// All cached rows of the tree. - /// - /// Similar to [rowsInTree] but: - /// * contains every row in the tree (including collapsed rows) - /// * items don't change when nodes are expanded or collapsed - /// * items are populated only when root is changed - final _searchableCachedRows = []; - - /// All the rows that should be displayed in the tree. - /// - /// The rows can be updated with a call to [_updateRows]. - ValueListenable> get rowsInTree => _rowsInTree; - final _rowsInTree = ValueNotifier>([]); - - /// Map from node to the index for that node's row in [rowsInTree]. - final _nodeToRowIndex = {}; - - /// Rebuilds the tree and updates [rowsInTree] with the new values. - /// - /// If [updateSearchableRows] is true, also updates [_searchableCachedRows] - /// with the new values. - void _updateRows({ - InspectorTreeNode? node, - bool updateSearchableRows = false, - }) { - if (disposed) return; - - // TODO(elliette): Consider only updating an [InspectorTreeNode]'s branch - // when it is marked as dirty, instead of the entire tree. See: - // https://github.com/flutter/devtools/issues/7980 - node ??= root; - if (node == null) return; - - final rows = _buildRows(node); - _rowsInTree.value = rows; - - // Build the reverse node-to-index map for faster lookups: - for (int i = 0; i < _rowsInTree.value.length; i++) { - final row = _rowsInTree.value[i]; - final node = row.node; - _nodeToRowIndex[node] = i; - } - - if (updateSearchableRows) { - final searchableRows = _buildRows( - node, - includeHiddenRows: true, - includeCollapsedRows: true, - ); - - _searchableCachedRows - ..clear() - ..addAll(searchableRows); - } - } - - /// Resets the state if the root has been marked as dirty. - void _handleDirtyNode(InspectorTreeNode node) { - if (node == root) { - _cachedSelectedRow = null; - lastContentWidth = null; - _updateRows(); - } - } - - void setSearchTarget(SearchTargetType searchTarget) { - _searchTarget = searchTarget; - refreshSearchMatches(); - } - - InspectorTreeRow? rowAtIndex(int index) => _rowsInTree.value.safeGet(index); - - double rowOffset(int index) { - return (rowAtIndex(index)?.depth ?? 0) * inspectorColumnIndent; - } - - List getPathFromSelectedRowToRoot() { - final selectedItem = _cachedSelectedRow?.node; - if (selectedItem == null) return []; - - final pathToRoot = [selectedItem]; - InspectorTreeNode? nextParentNode = selectedItem.parent; - while (nextParentNode != null) { - pathToRoot.add(nextParentNode); - nextParentNode = nextParentNode.parent; - } - return pathToRoot.reversed.toList(); - } - - set hover(InspectorTreeNode? node) { - if (node == _hover) { - return; - } - - _hover = node; - } - - void navigateUp() { - _navigateHelper(-1); - } - - void navigateDown() { - _navigateHelper(1); - } - - void navigateLeft() { - final selectionLocal = selection; - final diagnostic = selectionLocal?.diagnostic; - - final toggledHideableGroup = _maybeToggleHideableGroup( - diagnostic, - showGroup: false, - ); - if (toggledHideableGroup) return; - - // This logic is consistent with how IntelliJ handles tree navigation on - // on left arrow key press. - if (selectionLocal == null) { - _navigateHelper(-1); - return; - } - - refreshTree( - updateTreeAction: () { - if (selectionLocal.isExpanded) { - selectionLocal.isExpanded = false; - return true; - } - if (selectionLocal.parent != null) { - return setSelectedNode(selectionLocal.parent); - } - return false; - }, - ); - } - - void navigateRight() { - final selectionLocal = selection; - final diagnostic = selectionLocal?.diagnostic; - - final toggledHideableGroup = _maybeToggleHideableGroup( - diagnostic, - showGroup: true, - ); - if (toggledHideableGroup) return; - - // This logic is consistent with how IntelliJ handles tree navigation on - // on right arrow key press. - - if (selectionLocal == null || selectionLocal.isExpanded) { - _navigateHelper(1); - return; - } - - selectionLocal.isExpanded = true; - _updateRows(); - } - - void _navigateHelper(int indexOffset) { - if (_numRows == 0) return; - - refreshTree( - updateTreeAction: () { - final nodeToSelect = selection == null - ? root - : rowAtIndex( - (_rowIndexFromNode(selection!) + indexOffset).clamp( - 0, - _numRows - 1, - ), - )?.node; - setSelectedNode(nodeToSelect); - return true; - }, - ); - } - - /// Given [showGroup], toggles the visibility of a hideable group. - /// - /// Returns a [bool] representing whether or not the group was toggled. - bool _maybeToggleHideableGroup( - RemoteDiagnosticsNode? diagnostic, { - required bool showGroup, - }) { - final isHideableGroupLeader = - diagnostic != null && diagnostic.isHideableGroupLeader; - final shouldToggle = - isHideableGroupLeader && - (showGroup ? diagnostic.groupIsHidden : !diagnostic.groupIsHidden); - - if (shouldToggle) { - refreshTree( - updateTreeAction: () { - diagnostic.toggleHiddenGroup(); - return true; - }, - ); - return true; - } - - return false; - } - - static const horizontalPadding = 10.0; - - /// Returns the indentation of a row at the given [depth] in the inspector. - /// - /// This indentation roughly corresponds to the center of the icon next to the - /// widget name. - double getDepthIndent(int depth) { - // Note: depth is 0-based, therefore add 1. - return (depth + 1) * inspectorColumnIndent + horizontalPadding; - } - - double rowYTop(int index) { - return inspectorRowHeight * index; - } - - void nodeChanged(InspectorTreeNode node) { - node.isDirty = true; - } - - void removeNodeFromParent(InspectorTreeNode node) { - node.parent?.removeChild(node); - } - - void expandPath(InspectorTreeNode? node) { - _expandPath(node); - } - - void _expandPath(InspectorTreeNode? node) { - while (node != null) { - if (!node.isExpanded) { - node.isExpanded = true; - } - node = node.parent; - } - } - - void toggleHiddenGroup(InspectorTreeNode? node) { - final diagnostic = node?.diagnostic; - if (diagnostic != null) { - diagnostic.toggleHiddenGroup(); - } - } - - int get _numRows => _rowsInTree.value.length; - - int _rowIndexFromNode(InspectorTreeNode node) => _nodeToRowIndex[node] ?? -1; - - int _rowIndexFromOffset(double y) => max(0, y ~/ inspectorRowHeight); - - List _buildRows( - InspectorTreeNode node, { - bool includeHiddenRows = false, - bool includeCollapsedRows = false, - }) { - final rows = []; - - void buildRowsHelper( - InspectorTreeNode node, { - required int depth, - required List ticks, - }) { - final currentIdx = rows.length; - final isHidden = node.diagnostic?.isHidden ?? false; - if (!isHidden || includeHiddenRows) { - rows.add( - InspectorTreeRow( - node: node, - index: currentIdx, - ticks: ticks, - depth: depth, - lineToParent: - !node.isProperty && - currentIdx != 0 && - node.parent!.showLinesToChildren, - hasSingleChild: node.children.length == 1, - ), - ); - } - - if (!node.isExpanded && !includeCollapsedRows) return; - final children = node.children; - final parentDepth = depth; - final childrenDepth = children.length > 1 ? parentDepth + 1 : parentDepth; - for (final child in children) { - final shouldAddTick = - children.length > 1 && - children.last != child && - !children.last.isProperty && - node.diagnostic?.shouldIndent == true; - - buildRowsHelper( - child, - depth: childrenDepth, - ticks: [...ticks, if (shouldAddTick) parentDepth], - ); - } - } - - buildRowsHelper(node, depth: 0, ticks: []); - return rows; - } - - InspectorTreeRow? getRowForNode(InspectorTreeNode node) { - final rootLocal = root; - if (rootLocal == null) return null; - return rowAtIndex(_rowIndexFromNode(node)); - } - - InspectorTreeRow? rowForOffset(Offset offset) { - final rootLocal = root; - if (rootLocal == null) return null; - final row = _rowIndexFromOffset(offset.dy); - return row < _rowsInTree.value.length ? rowAtIndex(row) : null; - } - - void onExpandRow(InspectorTreeRow row) { - final onExpand = config.onExpand; - row.node.isExpanded = true; - if (onExpand != null) { - onExpand(row.node); - } - } - - void onCollapseRow(InspectorTreeRow row) { - row.node.isExpanded = false; - } - - void onSelectRow(InspectorTreeRow row) { - onSelectNode(row.node); - } - - void onSelectNode(InspectorTreeNode? node) { - setSelectedNode(node, notifyFlutterInspector: true); - ga.select( - gac.inspector, - gac.treeNodeSelection, - screenMetricsProvider: () => InspectorScreenMetrics.v2(), - ); - final diagnostic = node?.diagnostic; - if (diagnostic != null && diagnostic.groupIsHidden) { - diagnostic.hideableGroupLeader?.toggleHiddenGroup(); - } - expandPath(node); - } - - Rect getBoundingBox(InspectorTreeRow row) { - // For future reference: the bounding box likely needs to be in terms of - // positions after the current animations are complete so that computations - // to start animations to show specific widget scroll to where the target - // nodes will be displayed rather than where they are currently displayed. - final diagnostic = row.node.diagnostic; - // The node width is approximated since the widgets are not available at the - // time of calculating the bounding box. - final approximateNodeWidth = - DiagnosticsNodeDescription.approximateNodeWidth(diagnostic); - return Rect.fromLTWH( - getDepthIndent(row.depth), - rowYTop(row.index), - approximateNodeWidth, - inspectorRowHeight, - ); - } - - void scrollToRect(Rect targetRect) { - for (final client in _clients) { - client.waitForClientsThenScrollToRect(targetRect); - } - } - - /// Width each row in the tree should have ignoring its indent. - /// - /// Content in rows should wrap if it exceeds this width. - final rowWidth = 1200; - - /// Maximum indent of the tree in pixels. - double? _maxIndent; - - double get maxRowIndent { - if (lastContentWidth == null) { - double maxIndent = 0; - for (int i = 0; i < _numRows; i++) { - final row = rowAtIndex(i); - if (row != null) { - maxIndent = max(maxIndent, getDepthIndent(row.depth)); - } - } - lastContentWidth = maxIndent + maxIndent; - _maxIndent = maxIndent; - } - return _maxIndent!; - } - - void animateToTargets(List targets) { - Rect? targetRect; - - for (final target in targets) { - final row = getRowForNode(target); - if (row != null) { - final rowRect = getBoundingBox(row); - targetRect = targetRect == null - ? rowRect - : targetRect.expandToInclude(rowRect); - } - } - - if (targetRect == null || targetRect.isEmpty) return; - - scrollToRect(targetRect); - } - - bool expandPropertiesByDefault(DiagnosticsTreeStyle style) { - // This code matches the text style defaults for which styles are - // by default and which aren't. - switch (style) { - case DiagnosticsTreeStyle.none: - case DiagnosticsTreeStyle.singleLine: - case DiagnosticsTreeStyle.errorProperty: - return false; - - case DiagnosticsTreeStyle.sparse: - case DiagnosticsTreeStyle.offstage: - case DiagnosticsTreeStyle.dense: - case DiagnosticsTreeStyle.transition: - case DiagnosticsTreeStyle.error: - case DiagnosticsTreeStyle.whitespace: - case DiagnosticsTreeStyle.flat: - case DiagnosticsTreeStyle.shallow: - case DiagnosticsTreeStyle.truncateChildren: - return true; - } - } - - InspectorTreeNode setupInspectorTreeNode( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, { - required bool expandChildren, - RemoteDiagnosticsNode? hideableGroupLeader, - }) { - node.diagnostic = diagnosticsNode; - final configLocal = config; - if (configLocal.onNodeAdded != null) { - configLocal.onNodeAdded!(node, diagnosticsNode); - } - final inHideableGroup = diagnosticsNode.inHideableGroup; - if (inHideableGroup && hideableGroupLeader != null) { - hideableGroupLeader.addHideableGroupSubordinate(diagnosticsNode); - } - - if (diagnosticsNode.hasChildren || - diagnosticsNode.inlineProperties.isNotEmpty) { - if (diagnosticsNode.childrenReady || !diagnosticsNode.hasChildren) { - final styleIsMultiline = expandPropertiesByDefault( - diagnosticsNode.style, - ); - setupChildren( - diagnosticsNode, - node, - node.diagnostic!.childrenNow, - expandChildren: expandChildren && styleIsMultiline, - hideableGroupLeader: inHideableGroup - ? (hideableGroupLeader ?? diagnosticsNode) - : null, - ); - } else { - node.clearChildren(); - node.appendChild(createNode()); - } - } - return node; - } - - void setupChildren( - RemoteDiagnosticsNode parent, - InspectorTreeNode treeNode, - List? children, { - required bool expandChildren, - RemoteDiagnosticsNode? hideableGroupLeader, - }) { - treeNode.isExpanded = expandChildren; - if (treeNode.children.isNotEmpty) { - // Only case supported is this is the loading node. - assert(treeNode.children.length == 1); - refreshTree( - updateTreeAction: () { - removeNodeFromParent(treeNode.children.first); - return true; - }, - ); - } - final inlineProperties = parent.inlineProperties; - - for (final property in inlineProperties) { - treeNode.appendChild( - setupInspectorTreeNode( - createNode(), - property, - // We are inside a property so only expand children if - // expandProperties is true. - expandChildren: false, - ), - ); - } - if (children != null) { - for (final child in children) { - treeNode.appendChild( - setupInspectorTreeNode( - createNode(), - child, - expandChildren: expandChildren, - hideableGroupLeader: child.inHideableGroup - ? hideableGroupLeader - : null, - ), - ); - } - } - } - - Future maybePopulateChildren(InspectorTreeNode treeNode) async { - final diagnostic = treeNode.diagnostic; - if (diagnostic != null && - diagnostic.hasChildren && - (treeNode.hasPlaceholderChildren || treeNode.children.isEmpty)) { - try { - final children = await diagnostic.children; - if (treeNode.hasPlaceholderChildren || treeNode.children.isEmpty) { - setupChildren(diagnostic, treeNode, children, expandChildren: true); - refreshTree( - updateTreeAction: () { - nodeChanged(treeNode); - if (treeNode == selection) { - expandPath(treeNode); - } - return true; - }, - ); - } - } catch (e, st) { - _log.shout(e, e, st); - } - } - } - - /* Search support */ - @override - void onMatchChanged(int index) { - refreshTree( - updateTreeAction: () { - onSelectRow(searchMatches.value[index]); - return true; - }, - ); - } - - @override - Duration get debounceDelay => const Duration(milliseconds: 300); - - @override - List matchesForSearch( - String search, { - bool searchPreviousMatches = false, - }) { - final matches = []; - - if (searchPreviousMatches) { - final previousMatches = searchMatches.value; - for (final previousMatch in previousMatches) { - if (previousMatch.node.diagnostic!.searchValue.caseInsensitiveContains( - search, - )) { - matches.add(previousMatch); - } - } - - if (matches.isNotEmpty) return matches; - } - - int debugStatsSearchOps = 0; - final debugStatsWidgets = _searchableCachedRows.length; - - final inspectorService = serviceConnection.inspectorService; - if (search.isEmpty || - inspectorService == null || - inspectorService.isDisposed) { - assert(() { - debugPrint('Search completed, no search'); - return true; - }()); - return matches; - } - - assert(() { - debugPrint('Search started: $_searchTarget'); - return true; - }()); - - for (final row in _searchableCachedRows) { - final diagnostic = row!.node.diagnostic; - if (diagnostic == null) continue; - - // Widget search begin - if (_searchTarget == SearchTargetType.widget) { - debugStatsSearchOps++; - if (diagnostic.searchValue.caseInsensitiveContains(search)) { - matches.add(row); - continue; - } - } - // Widget search end - } - - assert(() { - debugPrint( - 'Search completed with $debugStatsWidgets widgets, $debugStatsSearchOps ops', - ); - return true; - }()); - - return matches; - } -} - -extension RemoteDiagnosticsNodeExtension on RemoteDiagnosticsNode { - String get searchValue { - final description = toStringShort(); - final textPreview = json['textPreview']; - return textPreview is String - ? '$description ${textPreview.replaceAll('\n', ' ')}' - : description; - } -} - -abstract class InspectorControllerClient { - void scrollToRect(Rect rect); - - void waitForClientsThenScrollToRect(Rect rect, {int retries}); - - void requestFocus(); -} - -class InspectorTree extends StatefulWidget { - const InspectorTree({ - super.key, - required this.controller, - required this.treeController, - this.widgetErrors, - this.screenId, - }); - - final InspectorController controller; - - final InspectorTreeController? treeController; - - final LinkedHashMap? widgetErrors; - final String? screenId; - - @override - State createState() => _InspectorTreeState(); -} - -// AutomaticKeepAlive is necessary so that the tree does not get recreated when we switch tabs. -class _InspectorTreeState extends State - with - SingleTickerProviderStateMixin, - AutomaticKeepAliveClientMixin, - AutoDisposeMixin - implements InspectorControllerClient { - InspectorController get controller => widget.controller; - InspectorTreeController? get treeController => widget.treeController; - - late ScrollController _scrollControllerY; - late ScrollController _scrollControllerX; - Future? _currentAnimateY; - Rect? _currentAnimateTarget; - - AnimationController? _constraintDisplayController; - late FocusNode _focusNode; - - /// When autoscrolling, the number of rows to pad the target location with. - static const _scrollPadCount = 3; - - @override - void initState() { - super.initState(); - _scrollControllerX = ScrollController(); - _scrollControllerY = ScrollController(); - // TODO(devoncarew): Commented out as per flutter/devtools/pull/2001. - //_scrollControllerY.addListener(_onScrollYChange); - _constraintDisplayController = longAnimationController(this); - _focusNode = FocusNode(debugLabel: 'inspector-tree'); - autoDisposeFocusNode(_focusNode); - final mainIsolateState = - serviceConnection.serviceManager.isolateManager.mainIsolateState; - if (mainIsolateState != null) { - callOnceWhenReady( - trigger: mainIsolateState.isPaused, - callback: _bindToController, - readyWhen: (triggerValue) => !triggerValue, - ); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.animateTo(controller.selectedNode.value); - }); - } - - @override - void didUpdateWidget(InspectorTree oldWidget) { - final oldTreeController = oldWidget.treeController; - if (oldTreeController != widget.treeController) { - oldTreeController?.removeClient(this); - - // TODO(elliette): Figure out if we can remove this. See explanation: - // https://github.com/flutter/devtools/pull/1290/files#r342399899. - cancelListeners(); - - _bindToController(); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - treeController?.removeClient(this); - _scrollControllerX.dispose(); - _scrollControllerY.dispose(); - _constraintDisplayController?.dispose(); - super.dispose(); - } - - @override - void requestFocus() { - _focusNode.requestFocus(); - } - - // TODO(devoncarew): Commented out as per flutter/devtools/pull/2001. - // void _onScrollYChange() { - // if (controller == null) return; - // - // // If the vertical position is already being animated we should not trigger - // // a new animation of the horizontal position as a more direct animation of - // // the horizontal position has already been triggered. - // if (currentAnimateY != null) return; - // - // final x = _computeTargetX(_scrollControllerY.offset); - // _scrollControllerX.animateTo( - // x, - // duration: defaultDuration, - // curve: defaultCurve, - // ); - // } - - @override - Future waitForClientsThenScrollToRect( - Rect rect, { - int retries = 5, - }) async { - if (_scrollControllerY.hasClients || _scrollControllerX.hasClients) { - return scrollToRect(rect); - } - if (retries == 0) return; - await Future.delayed(const Duration(milliseconds: 20)); - return waitForClientsThenScrollToRect(rect, retries: retries - 1); - } - - @override - Future scrollToRect(Rect rect) async { - if (rect == _currentAnimateTarget) { - // We are in the middle of an animation to this exact rectangle. - return; - } - - final initialX = rect.left; - final initialY = rect.top; - final yOffsetAtViewportTop = _scrollControllerY.hasClients - ? _scrollControllerY.offset - : _scrollControllerY.initialScrollOffset; - final xOffsetAtViewportLeft = _scrollControllerX.hasClients - ? _scrollControllerX.offset - : _scrollControllerX.initialScrollOffset; - - final viewPortInScrollControllerSpace = Rect.fromLTWH( - xOffsetAtViewportLeft, - yOffsetAtViewportTop, - safeViewportWidth, - safeViewportHeight, - ); - - // Decide to scroll based on whether the middle of the center-left half of - // the row is visible. See https://github.com/flutter/devtools/pull/8367. - final centerLeftHalf = Offset( - (rect.centerLeft.dx + rect.center.dx) / 2, - rect.center.dy, - ); - final isRectInViewPort = viewPortInScrollControllerSpace.contains( - centerLeftHalf, - ); - if (isRectInViewPort) { - // The rect is already in view, don't scroll - return; - } - - _currentAnimateTarget = rect; - - final targetY = _padTargetY(initialY: initialY); - if (_scrollControllerY.hasClients) { - _currentAnimateY = _scrollControllerY.animateTo( - targetY, - duration: longDuration, - curve: defaultCurve, - ); - } else { - _currentAnimateY = null; - _scrollControllerY = ScrollController(initialScrollOffset: targetY); - } - - final targetX = _padTargetX(initialX: initialX); - if (_scrollControllerX.hasClients) { - unawaited( - _scrollControllerX.animateTo( - targetX, - duration: longDuration, - curve: defaultCurve, - ), - ); - } else { - _scrollControllerX = ScrollController(initialScrollOffset: targetX); - } - - try { - await _currentAnimateY; - } catch (e) { - // Doesn't matter if the animation was cancelled. - } - _currentAnimateY = null; - _currentAnimateTarget = null; - } - - // TODO(jacobr): resolve cases where we need to know the viewport height - // before it is available so we don't need this approximation. - /// Placeholder viewport height to use if we don't yet know the real - /// viewport height. - static const _placeholderViewportSize = Size(1000.0, 1000.0); - - double get safeViewportHeight { - return _scrollControllerY.hasClients - ? _scrollControllerY.position.viewportDimension - : _placeholderViewportSize.height; - } - - double get safeViewportWidth { - return _scrollControllerX.hasClients - ? _scrollControllerX.position.viewportDimension - : _placeholderViewportSize.width; - } - - /// Pad [initialX] with the horizontal indentation of [padCount] rows. - double _padTargetX({ - required double initialX, - int padCount = _scrollPadCount, - }) { - return initialX - inspectorColumnIndent * padCount; - } - - /// Pad [initialY] so that a row would be placed in the vertical center of - /// the screen. - double _padTargetY({required double initialY}) { - return initialY - (safeViewportHeight / 2) + inspectorRowHeight / 2; - } - - /// Handle arrow keys for the InspectorTree. Ignore other key events so that - /// other widgets have a chance to respond to them. - KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) { - if (!event.isKeyDownOrRepeat) return KeyEventResult.ignored; - - final treeControllerLocal = treeController!; - - if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - treeControllerLocal.navigateDown(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - treeControllerLocal.navigateUp(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - treeControllerLocal.navigateLeft(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - treeControllerLocal.navigateRight(); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } - - void _bindToController() { - treeController?.addClient(this); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final treeControllerLocal = treeController; - if (treeControllerLocal == null) { - // Indicate the tree is loading. - return const CenteredCircularProgressIndicator(); - } - - return ValueListenableBuilder>( - valueListenable: treeControllerLocal.rowsInTree, - builder: (context, rows, _) { - // Note: The inspector rows contain only the fake root node when the - // inspector tree is shutdown. - if (rows.length <= 1) { - // This works around a bug when Scrollbars are present on a short lived - // widget. - return const SizedBox(child: CenteredCircularProgressIndicator()); - } - - if (!controller.firstInspectorTreeLoadCompleted) { - final screenId = widget.screenId; - if (screenId != null) { - ga.timeEnd( - screenId, - gac.pageReady, - screenMetricsProvider: () => - InspectorScreenMetrics.v2(rowCount: rows.length), - ); - unawaited( - serviceConnection.sendDwdsEvent( - screen: screenId, - action: gac.pageReady, - ), - ); - } - controller.firstInspectorTreeLoadCompleted = true; - } - - return LayoutBuilder( - builder: (context, constraints) { - final viewportWidth = constraints.maxWidth; - final tree = Scrollbar( - thumbVisibility: true, - controller: _scrollControllerX, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _scrollControllerX, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: - treeControllerLocal.rowWidth + - treeControllerLocal.maxRowIndent, - ), - // TODO(kenz): this scrollbar needs to be sticky to the right side of - // the visible container - right now it is lined up to the right of - // the widest row (which is likely not visible). This may require some - // refactoring. - child: GestureDetector( - onTap: _focusNode.requestFocus, - child: Focus( - onKeyEvent: _handleKeyEvent, - autofocus: true, - focusNode: _focusNode, - child: OffsetScrollbar( - isAlwaysShown: true, - axis: Axis.vertical, - controller: _scrollControllerY, - offsetController: _scrollControllerX, - offsetControllerViewportDimension: viewportWidth, - child: ListView.custom( - itemExtent: inspectorRowHeight, - childrenDelegate: SliverChildBuilderDelegate(( - context, - index, - ) { - if (index == rows.length) { - return const SizedBox(height: inspectorRowHeight); - } - final row = treeControllerLocal.rowAtIndex(index)!; - final inspectorRef = - row.node.diagnostic?.valueRef.id; - return _InspectorTreeRowWidget( - key: PageStorageKey(row.node), - inspectorTreeState: this, - row: row, - scrollControllerX: _scrollControllerX, - viewportWidth: viewportWidth, - error: - widget.widgetErrors != null && - inspectorRef != null - ? widget.widgetErrors![inspectorRef] - : null, - ); - }, childCount: rows.length + 1), - controller: _scrollControllerY, - ), - ), - ), - ), - ), - ), - ); - - return tree; - }, - ); - }, - ); - } - - @override - bool get wantKeepAlive => true; -} - -Paint _defaultPaint(ColorScheme colorScheme) => Paint() - ..color = colorScheme.treeGuidelineColor - ..strokeWidth = chartLineStrokeWidth; - -/// The distance (on the x-axis) between the center of the widget icon and the -/// start of the row, as determined by a percentage of the -/// [inspectorColumnIndent]. -const _iconCenterToRowStartXDistancePercentage = 0.41; - -/// The distance (on the y-axis) between the bottom of the widget icon and the -/// top of the row, as determined by a percentage of the [inspectorRowHeight]. -const _iconBottomToRowTopYDistancePercentage = 0.75; - -/// The distance (on the y-axis) between the top of the child widget's icon and -/// the top of the current row, as determined by a percentage of the -/// [inspectorRowHeight]. -const _childIconTopToRowTopYDistancePercentage = 1.25; - -/// Custom painter that draws lines indicating how parent and child rows are -/// connected to each other. -/// -/// Each rows object contains a list of ticks that indicate the x coordinates of -/// vertical lines connecting other rows need to be drawn within the vertical -/// area of the current row. This approach has the advantage that a row contains -/// all information required to render all content within it but has the -/// disadvantage that the x coordinates of each line connecting rows must be -/// computed in advance. -class _RowPainter extends CustomPainter { - _RowPainter(this.row, this._controller, this.colorScheme); - - final InspectorTreeController _controller; - final InspectorTreeRow row; - final ColorScheme colorScheme; - - @override - void paint(Canvas canvas, Size size) { - final paint = _defaultPaint(colorScheme); - - final node = row.node; - final showExpandCollapse = node.showExpandCollapse; - const distanceFromIconCenterToRowStart = - inspectorColumnIndent * _iconCenterToRowStartXDistancePercentage; - for (final tick in row.ticks) { - final expandCollapseX = - _controller.getDepthIndent(tick) - distanceFromIconCenterToRowStart; - // Draw a vertical line for each tick identifying a connection between - // an ancestor of this node and some other node in the tree. - canvas.drawLine( - Offset(expandCollapseX, 0.0), - Offset(expandCollapseX, inspectorRowHeight), - paint, - ); - } - // If this row is itself connected to a parent then draw the L shaped line - // to make that connection. - if (row.lineToParent) { - final parentExpandCollapseX = - _controller.getDepthIndent(row.depth - 1) - - distanceFromIconCenterToRowStart; - final width = showExpandCollapse - ? inspectorColumnIndent * 0.45 - : inspectorColumnIndent * 0.6; - canvas.drawLine( - Offset(parentExpandCollapseX, 0.0), - Offset(parentExpandCollapseX, inspectorRowHeight * 0.5), - paint, - ); - canvas.drawLine( - Offset(parentExpandCollapseX, inspectorRowHeight * 0.5), - Offset(parentExpandCollapseX + width, inspectorRowHeight * 0.5), - paint, - ); - } - - // Draw a straight vertical line from current node's icon to the icon below - // it if either the current node: - // 1. is expanded (meaning its child is visible) and it only has one child - // (because multiple children get indented). - // 2. is NOT the first node in a hidden group of which the last hidden node - // in that group is childless (meaning that last node is at the end of a - // branch and therefore has nothing below it). - final expandedWithSingleChild = row.hasSingleChild && node.isExpanded; - final subordinates = - node.diagnostic?.hideableGroupSubordinates ?? []; - final groupIsHidden = node.diagnostic?.groupIsHidden ?? false; - final lastHiddenSubordinateHasNoChildren = - groupIsHidden && - subordinates.isNotEmpty && - subordinates.last.childrenNow.isEmpty; - if (expandedWithSingleChild && !lastHiddenSubordinateHasNoChildren) { - const distanceFromIconCenterToRowStart = - inspectorColumnIndent * _iconCenterToRowStartXDistancePercentage; - final iconCenterX = - _controller.getDepthIndent(row.depth) - - distanceFromIconCenterToRowStart; - // Draw a line from the bottom of the current row's icon to the top of the - // child row's icon: - canvas.drawLine( - Offset( - iconCenterX, - inspectorRowHeight * _iconBottomToRowTopYDistancePercentage, - ), - Offset( - iconCenterX, - inspectorRowHeight * _childIconTopToRowTopYDistancePercentage, - ), - paint, - ); - } - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - if (oldDelegate is _RowPainter) { - // TODO(jacobr): check whether the row has different ticks. - return oldDelegate.colorScheme.isLight != colorScheme.isLight; - } - return true; - } -} - -/// Widget defining the contents of a single row in the InspectorTree. -/// -/// This class defines the scaffolding around the rendering of the actual -/// content of a [RemoteDiagnosticsNode] provided by -/// [DiagnosticsNodeDescription] to provide a tree implementation with lines -/// drawn between parent and child nodes when nodes have multiple children. -/// -/// Changes to how the actual content of the node within the row should -/// be implemented by changing [DiagnosticsNodeDescription] instead. -class InspectorRowContent extends StatelessWidget { - const InspectorRowContent({ - super.key, - required this.row, - required this.controller, - required this.onToggle, - required this.expandArrowAnimation, - this.error, - required this.scrollControllerX, - required this.viewportWidth, - }); - - final InspectorTreeRow row; - final InspectorTreeController controller; - final VoidCallback onToggle; - final Animation expandArrowAnimation; - final ScrollController scrollControllerX; - final double viewportWidth; - - /// A [DevToolsError] that applies to the widget in this row. - /// - /// This will be null if there is no error for this row. - final DevToolsError? error; - - /// Whether this row has any error. - bool get hasError => error != null; - - @override - Widget build(BuildContext context) { - final currentX = - controller.getDepthIndent(row.depth) - inspectorColumnIndent; - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - Color? backgroundColor; - if (row.isSelected) { - backgroundColor = hasError - ? colorScheme.errorContainer - : colorScheme.selectedRowBackgroundColor; - } - - final node = row.node; - final diagnostic = node.diagnostic; - final isHideableGroupLeader = diagnostic?.isHideableGroupLeader ?? false; - const expandCollapseWidth = 14.0; - Widget rowWidget = Padding( - padding: EdgeInsets.only(left: currentX), - child: ValueListenableBuilder( - valueListenable: controller.searchNotifier, - builder: (context, searchValue, _) { - return Opacity( - opacity: searchValue.isEmpty || row.isSearchMatch ? 1 : 0.6, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - node.showExpandCollapse - ? InkWell( - onTap: onToggle, - child: RotationTransition( - turns: expandArrowAnimation, - child: const Icon( - Icons.expand_more, - size: defaultIconSize, - ), - ), - ) - : const SizedBox( - width: expandCollapseWidth, - height: defaultSpacing, - ), - Expanded( - child: Container( - color: backgroundColor, - child: InkWell( - onTap: () { - controller.refreshTree( - updateTreeAction: () { - controller.onSelectRow(row); - // TODO(gmoothart): It may be possible to capture the tap - // and request focus directly from the InspectorTree. Then - // we wouldn't need this. - controller.requestFocus(); - return true; - }, - ); - }, - child: SizedBox( - height: inspectorRowHeight, - child: DiagnosticsNodeDescription( - node.diagnostic, - isSelected: row.isSelected, - searchValue: searchValue, - errorText: error?.errorMessage, - emphasizeNodesFromLocalProject: true, - nodeDescriptionHighlightStyle: - searchValue.isEmpty || !row.isSearchMatch - ? DiagnosticsTextStyles.regular( - Theme.of(context).colorScheme, - ) - : row.isSelected - ? theme.searchMatchHighlightStyleFocused - : theme.searchMatchHighlightStyle, - actionLabel: isHideableGroupLeader - ? diagnostic!.groupIsHidden - ? '(expand)' - : '(collapse)' - : null, - actionCallback: isHideableGroupLeader - ? () { - controller.refreshTree( - updateTreeAction: () { - controller.toggleHiddenGroup(node); - return true; - }, - ); - } - : null, - customDescription: - isHideableGroupLeader && diagnostic!.groupIsHidden - ? '${diagnostic.hideableGroupSubordinates!.length + 1} more widgets...' - : null, - customIconName: - isHideableGroupLeader && diagnostic!.groupIsHidden - ? 'HiddenGroup' - : null, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ), - ); - - // Wrap with tooltip if there is an error for this node's widget. - if (hasError) { - rowWidget = DevToolsTooltip( - message: error!.errorMessage, - child: rowWidget, - ); - } - - return CustomPaint( - painter: _RowPainter(row, controller, colorScheme), - size: Size(currentX, inspectorRowHeight), - child: Align( - alignment: Alignment.topLeft, - child: AnimatedBuilder( - animation: scrollControllerX, - builder: (context, child) { - final rowWidth = - scrollControllerX.offset + viewportWidth - defaultSpacing; - return SizedBox( - width: max(rowWidth, currentX + 100), - child: rowWidth > currentX ? child : const SizedBox(), - ); - }, - child: rowWidget, - ), - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/box/box.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/box/box.dart deleted file mode 100644 index bfd22ddd984..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/box/box.dart +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright 2024 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/diagnostics/diagnostics_node.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_controller.dart'; -import '../../inspector_data_models.dart'; -import '../ui/free_space.dart'; -import '../ui/theme.dart'; -import '../ui/utils.dart'; -import '../ui/widget_constraints.dart'; -import '../ui/widgets_theme.dart'; - -class BoxLayoutExplorerWidget extends StatelessWidget { - const BoxLayoutExplorerWidget( - this.inspectorController, { - super.key, - required this.layoutProperties, - required this.selectedNode, - }); - - final InspectorController inspectorController; - final LayoutProperties? layoutProperties; - final RemoteDiagnosticsNode? selectedNode; - - @override - Widget build(BuildContext context) { - if (layoutProperties == null) { - final selectedNodeLocal = selectedNode; - return Center( - child: Text( - '${selectedNodeLocal?.description ?? 'Widget'} has no layout properties to display.', - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - ), - ); - } - return LayoutBuilder(builder: _buildLayout); - } - - List _paddingWidgets({ - required LayoutProperties childProperties, - required LayoutProperties parentProperties, - required LayoutWidthsAndHeights widthsAndHeights, - required LayoutWidthsAndHeights displayWidthsAndHeights, - required ColorScheme colorScheme, - required Color widgetColor, - }) { - if (!widthsAndHeights.hasAnyPadding) return []; - - final LayoutWidthsAndHeights( - :topPadding, - :bottomPadding, - :leftPadding, - :rightPadding, - :hasTopPadding, - :hasBottomPadding, - :hasLeftPadding, - :hasRightPadding, - ) = widthsAndHeights; - - final displayWidgetHeight = displayWidthsAndHeights.widgetHeight; - final displayWidgetWidth = displayWidthsAndHeights.widgetWidth; - final displayTopPadding = displayWidthsAndHeights.topPadding; - final displayBottomPadding = displayWidthsAndHeights.bottomPadding; - final displayLeftPadding = displayWidthsAndHeights.leftPadding; - final displayRightPadding = displayWidthsAndHeights.rightPadding; - - final parentHeight = parentProperties.size.height; - final parentWidth = parentProperties.size.width; - - return [ - LayoutExplorerBackground(colorScheme: colorScheme), - PositionedBackgroundLabel( - labelText: _describeBoxName(parentProperties), - labelColor: widgetColor, - hasTopPadding: hasTopPadding, - hasBottomPadding: hasBottomPadding, - hasLeftPadding: hasLeftPadding, - hasRightPadding: hasRightPadding, - ), - if (hasLeftPadding) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayLeftPadding, displayWidgetHeight), - offset: Offset(0, displayTopPadding), - realSize: Size(leftPadding, parentHeight), - layoutProperties: childProperties, - isFreeSpace: true, - ), - horizontal: true, - ), - if (hasTopPadding) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidgetWidth, displayTopPadding), - offset: Offset(displayLeftPadding, 0), - realSize: Size(parentWidth, topPadding), - layoutProperties: childProperties, - isFreeSpace: true, - ), - horizontal: false, - ), - if (hasRightPadding) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayRightPadding, displayWidgetHeight), - offset: Offset( - displayLeftPadding + displayWidgetWidth, - displayTopPadding, - ), - realSize: Size(rightPadding, parentHeight), - layoutProperties: childProperties, - isFreeSpace: true, - ), - horizontal: true, - ), - if (hasBottomPadding) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidgetWidth, displayBottomPadding), - offset: Offset( - displayLeftPadding, - displayTopPadding + displayWidgetHeight, - ), - realSize: Size(parentWidth, bottomPadding), - layoutProperties: childProperties, - isFreeSpace: true, - ), - horizontal: false, - ), - ]; - } - - Widget _buildLayout(BuildContext context, BoxConstraints constraints) { - final propertiesLocal = layoutProperties!; - - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final parentProperties = propertiesLocal.parentLayoutProperties!; - - final child = LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // Subtract out one pixel border on each side. - final availableHeight = constraints.maxHeight - 2; - final availableWidth = constraints.maxWidth - 2; - - final widgetWidths = propertiesLocal.widgetWidths; - final widgetHeights = propertiesLocal.widgetHeights; - - final widthsAndHeights = LayoutWidthsAndHeights( - widths: widgetWidths!, - heights: widgetHeights!, - ); - - final displayWidths = _simpleFractionalLayout( - availableSize: availableWidth, - sizes: widgetWidths, - ); - final displayHeights = _simpleFractionalLayout( - availableSize: availableHeight, - sizes: widgetHeights, - ); - final displayWidthsAndHeights = LayoutWidthsAndHeights( - widths: displayWidths, - heights: displayHeights, - ); - - final widgetColor = WidgetTheme.fromName( - propertiesLocal.node.description, - ).color; - return Column( - children: [ - Container( - width: constraints.maxWidth, - height: constraints.maxHeight, - decoration: BoxDecoration(border: Border.all(color: widgetColor)), - child: Stack( - children: [ - ..._paddingWidgets( - childProperties: propertiesLocal, - parentProperties: parentProperties, - widthsAndHeights: widthsAndHeights, - displayWidthsAndHeights: displayWidthsAndHeights, - colorScheme: colorScheme, - widgetColor: widgetColor, - ), - BoxChildVisualizer( - isSelected: true, - layoutProperties: propertiesLocal, - renderProperties: RenderProperties( - axis: Axis.horizontal, - size: Size( - displayWidthsAndHeights.widgetWidth, - displayWidthsAndHeights.widgetHeight, - ), - offset: Offset( - displayWidthsAndHeights.leftPadding, - displayWidthsAndHeights.topPadding, - ), - realSize: propertiesLocal.size, - layoutProperties: propertiesLocal, - ), - ), - ], - ), - ), - ], - ); - }, - ); - - return Container( - constraints: BoxConstraints( - maxWidth: constraints.maxWidth, - maxHeight: constraints.maxHeight, - ), - child: child, - ); - } -} - -String _describeBoxName(LayoutProperties properties) => - properties.node.description ?? ''; - -/// Represents a box widget and its surrounding padding. -class BoxChildAndPaddingVisualizer extends StatelessWidget { - const BoxChildAndPaddingVisualizer({ - super.key, - required this.layoutProperties, - required this.renderProperties, - required this.isSelected, - }); - - final bool isSelected; - final LayoutProperties layoutProperties; - final RenderProperties renderProperties; - - LayoutProperties? get properties => renderProperties.layoutProperties; - - @override - Widget build(BuildContext context) { - final renderSize = renderProperties.size; - final renderOffset = renderProperties.offset; - - final propertiesLocal = properties!; - - return Positioned( - top: renderOffset.dy, - left: renderOffset.dx, - child: SizedBox( - width: safePositiveDouble(renderSize.width), - height: safePositiveDouble(renderSize.height), - child: WidgetVisualizer( - isSelected: isSelected, - layoutProperties: layoutProperties, - title: _describeBoxName(propertiesLocal), - // TODO(jacobr): consider surfacing the overflow size information - // if we determine - // overflowSide: properties.overflowSide, - - // We only show one child at a time so a large title is safe. - largeTitle: true, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - warnIfUnconstrained: false, - child: const SizedBox.shrink(), - ), - ), - ), - ); - } -} - -/// Widget that represents and visualize a direct child of Flex widget. -class BoxChildVisualizer extends StatelessWidget { - const BoxChildVisualizer({ - super.key, - required this.layoutProperties, - required this.renderProperties, - required this.isSelected, - }); - - final bool isSelected; - final LayoutProperties layoutProperties; - final RenderProperties renderProperties; - - LayoutProperties? get properties => renderProperties.layoutProperties; - - @override - Widget build(BuildContext context) { - final renderSize = renderProperties.size; - final renderOffset = renderProperties.offset; - - final propertiesLocal = properties!; - - return Positioned( - top: renderOffset.dy, - left: renderOffset.dx, - child: SizedBox( - width: safePositiveDouble(renderSize.width), - height: safePositiveDouble(renderSize.height), - child: WidgetVisualizer( - isSelected: isSelected, - layoutProperties: layoutProperties, - title: _describeBoxName(propertiesLocal), - // TODO(jacobr): consider surfacing the overflow size information - // if we determine - // overflowSide: properties.overflowSide, - - // We only show one child at a time so a large title is safe. - largeTitle: true, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - warnIfUnconstrained: false, - child: const SizedBox.shrink(), - ), - ), - ), - ); - } -} - -/// The percent of the visualizer dedicating to a single padding block when -/// the box child has multiple padding blocks. -const _narrowPaddingVisualizerPercent = 0.3; - -/// The percent of the visualizer dedicating to a single padding block when -/// the box child has only one padding block. -const _widePaddingVisualizerPercent = 0.35; - -/// Simplistic layout algorithm that will return [WidgetSizes] for the widget -/// display based on the display's [availableSize] and the real widget's -/// [WidgetSizes]. -/// -/// Uses a constant `paddingFraction` for the display padding, regardless of -/// the actual size. -WidgetSizes _simpleFractionalLayout({ - required double availableSize, - required WidgetSizes sizes, -}) { - final paddingASize = sizes.paddingA; - final paddingBSize = sizes.paddingB; - - final paddingFraction = paddingASize > 0 && paddingBSize > 0 - ? _narrowPaddingVisualizerPercent - : _widePaddingVisualizerPercent; - - final paddingAFraction = paddingASize > 0 ? paddingFraction : 0.0; - final paddingBFraction = paddingBSize > 0 ? paddingFraction : 0.0; - final widgetFraction = 1 - paddingAFraction - paddingBFraction; - - return ( - type: sizes.type, - paddingA: paddingAFraction * availableSize, - widgetSize: widgetFraction * availableSize, - paddingB: paddingBFraction * availableSize, - ); -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/flex.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/flex.dart deleted file mode 100644 index 3c3cb55e2ec..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/flex.dart +++ /dev/null @@ -1,779 +0,0 @@ -// Copyright 2024 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:math' as math; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/diagnostics/inspector_service.dart'; -import '../../../../shared/primitives/math_utils.dart'; -import '../../../../shared/ui/common_widgets.dart'; -import '../../inspector_data_models.dart'; -import '../ui/arrow.dart'; -import '../ui/free_space.dart'; -import '../ui/layout_explorer_widget.dart'; -import '../ui/theme.dart'; -import '../ui/utils.dart'; -import '../ui/widget_constraints.dart'; -import 'utils.dart'; - -// TODO(kenz): clean up this file so that we use helper widgets instead of -// methods that pass around build context. - -// TODO(kenz): densify the layout explorer visualization for flex widgets. - -const alignmentDropdownMaxSize = 140.0; - -class FlexLayoutExplorerWidget extends LayoutExplorerWidget { - const FlexLayoutExplorerWidget(super.inspectorController, {super.key}); - - static bool shouldDisplay(RemoteDiagnosticsNode node) { - return (node.isFlex) || (node.parent?.isFlex ?? false); - } - - @override - State createState() => - FlexLayoutExplorerWidgetState(); -} - -class FlexLayoutExplorerWidgetState - extends - LayoutExplorerWidgetState< - FlexLayoutExplorerWidget, - FlexLayoutProperties - > { - final scrollController = ScrollController(); - - Axis get direction => properties!.direction; - - ObjectGroup? get objectGroup => - properties!.node.objectGroupApi as ObjectGroup?; - - Color horizontalColor(ColorScheme colorScheme) => - properties!.isMainAxisHorizontal - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor; - - Color verticalColor(ColorScheme colorScheme) => properties!.isMainAxisVertical - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor; - - Color horizontalTextColor(ColorScheme colorScheme) => - properties!.isMainAxisHorizontal - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - - Color verticalTextColor(ColorScheme colorScheme) => - properties!.isMainAxisVertical - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - - String get flexType => properties!.type; - - @override - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node) { - if (node == null) return null; - if (!shouldDisplay(node)) return null; - if (node.isFlex) return node; - return node.parent; - } - - @override - bool shouldDisplay(RemoteDiagnosticsNode node) { - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return false; - return FlexLayoutExplorerWidget.shouldDisplay(selectedNodeLocal); - } - - @override - AnimatedFlexLayoutProperties computeAnimatedProperties( - FlexLayoutProperties nextProperties, - ) { - return AnimatedFlexLayoutProperties( - // If an animation is in progress, freeze it and start animating from there, else start a fresh animation from widget.properties. - animatedProperties?.copyWith() as FlexLayoutProperties? ?? properties!, - nextProperties, - changeAnimation, - ); - } - - @override - FlexLayoutProperties computeLayoutProperties(RemoteDiagnosticsNode node) => - FlexLayoutProperties.fromDiagnostics(node); - - @override - void updateHighlighted(FlexLayoutProperties? newProperties) { - setState(() { - if (selectedNode!.isFlex) { - highlighted = newProperties; - } else { - final idx = - selectedNode?.parent?.childrenNow.indexOf(selectedNode!) ?? -1; - if (newProperties == null) return; - if (idx != -1) highlighted = newProperties.children[idx]; - } - }); - } - - Widget _buildAxisAlignmentDropdown(Axis axis, ThemeData theme) { - final colorScheme = theme.colorScheme; - final color = axis == direction - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - List alignmentEnumEntries; - Object? selected; - final propertiesLocal = properties!; - if (axis == direction) { - alignmentEnumEntries = MainAxisAlignment.values; - selected = propertiesLocal.mainAxisAlignment; - } else { - alignmentEnumEntries = CrossAxisAlignment.values.toList(growable: true); - if (propertiesLocal.textBaseline == null) { - // TODO(albertusangga): Look for ways to visualize baseline when it is null - alignmentEnumEntries.remove(CrossAxisAlignment.baseline); - } - selected = propertiesLocal.crossAxisAlignment; - } - return RotatedBox( - quarterTurns: axis == Axis.vertical ? 3 : 0, - child: Container( - constraints: const BoxConstraints( - maxWidth: alignmentDropdownMaxSize, - maxHeight: defaultButtonHeight, - ), - child: DropdownButton( - value: selected, - isDense: true, - isExpanded: true, - // Avoid showing an underline for the main axis and cross-axis drop downs. - underline: const SizedBox(), - iconEnabledColor: axis == propertiesLocal.direction - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor, - selectedItemBuilder: (context) { - return [ - for (final alignment in alignmentEnumEntries) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Text( - alignment.name, - style: theme.regularTextStyleWithColor(color), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - height: actionsIconSize, - width: actionsIconSize, - child: Image.asset( - (axis == direction) - ? mainAxisAssetImageUrl( - direction, - alignment as MainAxisAlignment, - ) - : crossAxisAssetImageUrl( - direction, - alignment as CrossAxisAlignment, - ), - height: axisAlignmentAssetImageHeight, - fit: BoxFit.fitHeight, - color: color, - ), - ), - ], - ), - ]; - }, - items: [ - for (final alignment in alignmentEnumEntries) - DropdownMenuItem( - value: alignment, - child: Container( - padding: const EdgeInsets.symmetric(vertical: margin), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - child: Text( - alignment.name, - style: theme.regularTextStyleWithColor(color), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - height: actionsIconSize, - width: actionsIconSize, - child: Image.asset( - (axis == direction) - ? mainAxisAssetImageUrl( - direction, - alignment as MainAxisAlignment, - ) - : crossAxisAssetImageUrl( - direction, - alignment as CrossAxisAlignment, - ), - fit: BoxFit.fitHeight, - color: color, - ), - ), - ], - ), - ), - ), - ], - onChanged: (Object? newSelection) async { - // newSelection is an object instead of type here because - // the type is dependent on the `axis` parameter - // if the axis is the main axis the type should be [MainAxisAlignment] - // if the axis is the cross axis the type should be [CrossAxisAlignment] - FlexLayoutProperties changedProperties; - changedProperties = axis == direction - ? propertiesLocal.copyWith( - mainAxisAlignment: newSelection as MainAxisAlignment?, - ) - : propertiesLocal.copyWith( - crossAxisAlignment: newSelection as CrossAxisAlignment?, - ); - final valueRef = propertiesLocal.node.valueRef; - markAsDirty(); - await objectGroup!.invokeSetFlexProperties( - valueRef, - changedProperties.mainAxisAlignment, - changedProperties.crossAxisAlignment, - ); - }, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - if (properties == null) return const SizedBox(); - return Container( - margin: const EdgeInsets.all(margin), - padding: const EdgeInsets.only(bottom: margin, right: margin), - child: AnimatedBuilder( - animation: changeController, - builder: (context, _) { - return LayoutBuilder(builder: _buildLayout); - }, - ), - ); - } - - Widget _buildLayout(BuildContext context, BoxConstraints constraints) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final maxHeight = constraints.maxHeight; - final maxWidth = constraints.maxWidth; - final propertiesLocal = properties!; - final flexDescription = Align( - alignment: Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.only( - top: mainAxisArrowIndicatorSize, - left: crossAxisArrowIndicatorSize + margin, - ), - child: InkWell( - onTap: () => unawaited(onTap(propertiesLocal)), - child: WidgetVisualizer( - isFlex: true, - title: flexType, - layoutProperties: propertiesLocal, - isSelected: highlighted == properties, - overflowSide: propertiesLocal.overflowSide, - hint: Container( - padding: const EdgeInsets.all(4.0), - child: Text( - 'Total Flex Factor: ${propertiesLocal.totalFlex.toInt()}', - style: theme.regularTextStyleWithColor(emphasizedTextColor), - overflow: TextOverflow.ellipsis, - ), - ), - child: VisualizeFlexChildren( - state: this, - properties: propertiesLocal, - children: children, - highlighted: highlighted, - scrollController: scrollController, - direction: direction, - ), - ), - ), - ), - ); - - final verticalAxisDescription = Align( - alignment: Alignment.bottomLeft, - child: Container( - margin: const EdgeInsets.only(top: mainAxisArrowIndicatorSize + margin), - width: crossAxisArrowIndicatorSize, - child: Column( - children: [ - Expanded( - child: ArrowWrapper.unidirectional( - arrowColor: verticalColor(colorScheme), - type: ArrowType.down, - child: Truncateable( - truncate: maxHeight <= minHeightToAllowTruncating, - child: RotatedBox( - quarterTurns: 3, - child: Text( - propertiesLocal.verticalDirectionDescription, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.regularTextStyleWithColor( - verticalTextColor(colorScheme), - ), - ), - ), - ), - ), - ), - Truncateable( - truncate: maxHeight <= minHeightToAllowTruncating, - child: _buildAxisAlignmentDropdown(Axis.vertical, theme), - ), - ], - ), - ), - ); - - final horizontalAxisDescription = Align( - alignment: Alignment.topRight, - child: Container( - margin: const EdgeInsets.only( - left: crossAxisArrowIndicatorSize + margin, - ), - height: mainAxisArrowIndicatorSize, - child: Row( - children: [ - Expanded( - child: ArrowWrapper.unidirectional( - arrowColor: horizontalColor(colorScheme), - type: ArrowType.right, - child: Truncateable( - truncate: maxWidth <= minWidthToAllowTruncating, - child: Text( - propertiesLocal.horizontalDirectionDescription, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.regularTextStyleWithColor( - horizontalTextColor(colorScheme), - ), - ), - ), - ), - ), - Truncateable( - truncate: maxWidth <= minWidthToAllowTruncating, - child: _buildAxisAlignmentDropdown(Axis.horizontal, theme), - ), - ], - ), - ), - ); - - return Container( - constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), - child: Stack( - children: [ - flexDescription, - verticalAxisDescription, - horizontalAxisDescription, - ], - ), - ); - } -} - -class VisualizeFlexChildren extends StatefulWidget { - const VisualizeFlexChildren({ - super.key, - required this.state, - required this.properties, - required this.children, - required this.highlighted, - required this.scrollController, - required this.direction, - }); - - final FlexLayoutProperties properties; - final List children; - final LayoutProperties? highlighted; - final ScrollController scrollController; - final Axis direction; - final FlexLayoutExplorerWidgetState state; - - @override - State createState() => _VisualizeFlexChildrenState(); -} - -class _VisualizeFlexChildrenState extends State { - LayoutProperties? lastHighlighted; - static final selectedChildKey = GlobalKey(debugLabel: 'selectedChild'); - - @override - Widget build(BuildContext context) { - if (lastHighlighted != widget.highlighted) { - lastHighlighted = widget.highlighted; - if (widget.highlighted != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final selectedRenderObject = selectedChildKey.currentContext - ?.findRenderObject(); - if (selectedRenderObject != null && - widget.scrollController.hasClients) { - unawaited( - widget.scrollController.position.ensureVisible( - selectedRenderObject, - alignment: 0.5, - duration: defaultDuration, - ), - ); - } - }); - } - } - - if (!widget.properties.hasChildren) { - return const CenteredMessage(message: 'No Children'); - } - - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - final contents = Container( - decoration: BoxDecoration( - border: Border.all(color: theme.primaryColorLight), - ), - margin: const EdgeInsets.only(top: margin, left: margin), - child: LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final maxHeight = constraints.maxHeight; - - double maxSizeAvailable(Axis axis) { - return axis == Axis.horizontal ? maxWidth : maxHeight; - } - - final childrenAndMainAxisSpacesRenderProps = widget.properties - .childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - - final renderProperties = childrenAndMainAxisSpacesRenderProps - .where((renderProps) => !renderProps.isFreeSpace) - .toList(); - final mainAxisSpaces = childrenAndMainAxisSpacesRenderProps - .where((renderProps) => renderProps.isFreeSpace) - .toList(); - final crossAxisSpaces = widget.properties.crossAxisSpaces( - childrenRenderProperties: renderProperties, - maxSizeAvailable: maxSizeAvailable, - ); - - final childrenRenderWidgets = []; - Widget? selectedWidget; - for (var i = 0; i < widget.children.length; i++) { - final child = widget.children[i]; - final isSelected = widget.highlighted == child; - - final visualizer = FlexChildVisualizer( - key: isSelected ? selectedChildKey : null, - state: widget.state, - layoutProperties: child, - isSelected: isSelected, - renderProperties: renderProperties[i], - ); - - if (isSelected) { - selectedWidget = visualizer; - } else { - childrenRenderWidgets.add(visualizer); - } - } - - // Selected widget needs to be last to draw its border over other children - if (selectedWidget != null) { - childrenRenderWidgets.add(selectedWidget); - } - - final freeSpacesWidgets = [ - for (final renderProperties in [ - ...mainAxisSpaces, - ...crossAxisSpaces, - ]) - FreeSpaceVisualizerWidget(renderProperties), - ]; - return Scrollbar( - thumbVisibility: true, - controller: widget.scrollController, - child: SingleChildScrollView( - scrollDirection: widget.properties.direction, - controller: widget.scrollController, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: maxWidth, - minHeight: maxHeight, - maxWidth: widget.direction == Axis.horizontal - ? sum( - childrenAndMainAxisSpacesRenderProps.map( - (renderSize) => renderSize.width, - ), - ) - : maxWidth, - maxHeight: widget.direction == Axis.vertical - ? sum( - childrenAndMainAxisSpacesRenderProps.map( - (renderSize) => renderSize.height, - ), - ) - : maxHeight, - ).normalize(), - child: Stack( - children: [ - LayoutExplorerBackground(colorScheme: colorScheme), - ...freeSpacesWidgets, - ...childrenRenderWidgets, - ], - ), - ), - ), - ); - }, - ), - ); - return VisualizeWidthAndHeightWithConstraints( - properties: widget.properties, - child: contents, - ); - } -} - -/// Widget that represents and visualize a direct child of Flex widget. -class FlexChildVisualizer extends StatelessWidget { - const FlexChildVisualizer({ - super.key, - required this.state, - required this.layoutProperties, - required this.renderProperties, - required this.isSelected, - }); - - final FlexLayoutExplorerWidgetState state; - - final bool isSelected; - - final LayoutProperties layoutProperties; - - final RenderProperties renderProperties; - - // TODO(polina-c, jacob314): consider refactoring to remove `!`. - FlexLayoutProperties get root => state.properties!; - - LayoutProperties get properties => renderProperties.layoutProperties; - - ObjectGroup? get objectGroup => - properties.node.objectGroupApi as ObjectGroup?; - - void onChangeFlexFactor(int? newFlexFactor) async { - state.markAsDirty(); - await objectGroup!.invokeSetFlexFactor( - properties.node.valueRef, - newFlexFactor, - ); - } - - void onChangeFlexFit(FlexFit? newFlexFit) async { - state.markAsDirty(); - await objectGroup!.invokeSetFlexFit(properties.node.valueRef, newFlexFit!); - } - - Widget _buildFlexFactorChangerDropdown( - int maximumFlexFactor, - ThemeData theme, - ) { - final propertiesLocal = properties; - - Widget buildMenuitemChild(int? flexFactor) { - return Text( - 'flex: $flexFactor', - style: flexFactor == propertiesLocal.flexFactor - ? theme.boldTextStyle.copyWith(color: emphasizedTextColor) - : theme.regularTextStyleWithColor(emphasizedTextColor), - ); - } - - DropdownMenuItem buildMenuItem(int? flexFactor) { - return DropdownMenuItem( - value: flexFactor, - child: buildMenuitemChild(flexFactor), - ); - } - - return DropdownButton( - value: propertiesLocal.flexFactor?.toInt().clamp(0, maximumFlexFactor), - onChanged: onChangeFlexFactor, - iconEnabledColor: textColor, - underline: buildUnderline(), - items: >[ - buildMenuItem(null), - for (var i = 0; i <= maximumFlexFactor; ++i) buildMenuItem(i), - ], - ); - } - - Widget _buildFlexFitChangerDropdown(ThemeData theme) { - Widget flexFitDescription(FlexFit flexFit) => Text( - 'fit: ${flexFit.name}', - style: theme.regularTextStyleWithColor(emphasizedTextColor), - ); - - final propertiesLocal = properties; - - // Disable FlexFit changer if widget is Expanded. - if (propertiesLocal.description == 'Expanded') { - return flexFitDescription(FlexFit.tight); - } - - DropdownMenuItem buildMenuItem(FlexFit flexFit) { - return DropdownMenuItem( - value: flexFit, - child: flexFitDescription(flexFit), - ); - } - - return DropdownButton( - value: propertiesLocal.flexFit, - onChanged: onChangeFlexFit, - underline: buildUnderline(), - iconEnabledColor: emphasizedTextColor, - items: >[ - buildMenuItem(FlexFit.loose), - if (propertiesLocal.description != 'Expanded') - buildMenuItem(FlexFit.tight), - ], - ); - } - - Widget _buildContent(ThemeData theme) { - // TODO(https://github.com/flutter/devtools/issues/4058) allow more dynamic - // flex factor input - final currentFlexFactor = properties.flexFactor?.toInt() ?? 0; - final currentMaxFlexFactor = math.max( - currentFlexFactor, - maximumFlexFactorOptions, - ); - - return Container( - margin: const EdgeInsets.only(top: margin, left: margin), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: _buildFlexFactorChangerDropdown(currentMaxFlexFactor, theme), - ), - if (!properties.hasFlexFactor) - Text( - 'unconstrained ${root.isMainAxisHorizontal ? 'horizontal' : 'vertical'}', - style: theme.regularTextStyle.copyWith( - color: theme.colorScheme.unconstrainedColor, - fontStyle: FontStyle.italic, - ), - maxLines: 2, - softWrap: true, - overflow: TextOverflow.ellipsis, - textScaler: const TextScaler.linear(smallTextScaleFactor), - textAlign: TextAlign.center, - ), - _buildFlexFitChangerDropdown(theme), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final renderSize = renderProperties.size; - final renderOffset = renderProperties.offset; - final propertiesLocal = properties; - final rootLocal = root; - - Widget buildEntranceAnimation(BuildContext _, Widget? child) { - final vertical = rootLocal.isMainAxisVertical; - final horizontal = rootLocal.isMainAxisHorizontal; - - late Size size; - size = propertiesLocal.hasFlexFactor - ? SizeTween( - begin: Size( - horizontal ? minRenderWidth - entranceMargin : renderSize.width, - vertical ? minRenderHeight - entranceMargin : renderSize.height, - ), - end: renderSize, - ).evaluate(state.entranceCurve)! - : renderSize; - // Not-expanded widgets enter much faster. - return Opacity( - opacity: min([state.entranceCurve.value * 5, 1.0]), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: math.max(0.0, (renderSize.width - size.width) / 2), - vertical: math.max(0.0, (renderSize.height - size.height) / 2), - ), - child: child, - ), - ); - } - - return Positioned( - top: renderOffset.dy, - left: renderOffset.dx, - child: GestureDetector( - onTap: () => unawaited(state.onTap(propertiesLocal)), - onDoubleTap: () => state.onDoubleTap(propertiesLocal), - onLongPress: () => state.onDoubleTap(propertiesLocal), - child: SizedBox( - width: renderSize.width, - height: renderSize.height, - child: AnimatedBuilder( - animation: state.entranceController, - builder: buildEntranceAnimation, - child: WidgetVisualizer( - isFlex: true, - isSelected: isSelected, - layoutProperties: layoutProperties, - title: propertiesLocal.description ?? '', - overflowSide: propertiesLocal.overflowSide, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - child: Align( - alignment: Alignment.topRight, - child: _buildContent(Theme.of(context)), - ), - ), - ), - ), - ), - ), - ); - } - - /// define the number of flex factor to be shown in the flex dropdown button - /// for example if it's set to 5 the dropdown will consist of 6 items (null and 0..5) - static const maximumFlexFactorOptions = 5; -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/utils.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/utils.dart deleted file mode 100644 index 5e67d1e3117..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/flex/utils.dart +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2024 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:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../inspector_data_models.dart'; -import '../ui/utils.dart'; - -String crossAxisAssetImageUrl(Axis direction, CrossAxisAlignment alignment) { - return 'assets/img/layout_explorer/cross_axis_alignment/' - '${direction.flexType.toLowerCase()}_${alignment.name}.png'; -} - -String mainAxisAssetImageUrl(Axis direction, MainAxisAlignment alignment) { - return 'assets/img/layout_explorer/main_axis_alignment/' - '${direction.flexType.toLowerCase()}_${alignment.name}.png'; -} - -class AnimatedFlexLayoutProperties - extends AnimatedLayoutProperties - implements FlexLayoutProperties { - AnimatedFlexLayoutProperties(super.begin, super.end, super.animation); - - @override - CrossAxisAlignment? get crossAxisAlignment => end.crossAxisAlignment; - - @override - MainAxisAlignment? get mainAxisAlignment => end.mainAxisAlignment; - - @override - List childrenRenderProperties({ - required double smallestRenderWidth, - required double largestRenderWidth, - required double smallestRenderHeight, - required double largestRenderHeight, - required double Function(Axis) maxSizeAvailable, - }) { - final beginRenderProperties = begin.childrenRenderProperties( - smallestRenderHeight: smallestRenderHeight, - smallestRenderWidth: smallestRenderWidth, - largestRenderHeight: largestRenderHeight, - largestRenderWidth: largestRenderWidth, - maxSizeAvailable: maxSizeAvailable, - ); - final endRenderProperties = end.childrenRenderProperties( - smallestRenderHeight: smallestRenderHeight, - smallestRenderWidth: smallestRenderWidth, - largestRenderHeight: largestRenderHeight, - largestRenderWidth: largestRenderWidth, - maxSizeAvailable: maxSizeAvailable, - ); - final result = []; - for (var i = 0; i < children.length; i++) { - final beginProps = beginRenderProperties[i]; - final endProps = endRenderProperties[i]; - final t = animation.value; - result.add( - RenderProperties( - axis: endProps.axis, - offset: Offset.lerp(beginProps.offset, endProps.offset, t), - size: Size.lerp(beginProps.size, endProps.size, t), - realSize: Size.lerp(beginProps.realSize, endProps.realSize, t), - layoutProperties: AnimatedLayoutProperties( - beginProps.layoutProperties, - endProps.layoutProperties, - animation, - ), - ), - ); - } - // Add in the free space from the end. - // TODO(djshuckerow): We should make free space a part of - // RenderProperties so that we can animate between those. - result.addAll(endRenderProperties.where((prop) => prop.isFreeSpace)); - return result; - } - - @override - double get crossAxisDimension => lerpDouble( - begin.crossAxisDimension, - end.crossAxisDimension, - animation.value, - )!; - - @override - Axis get crossAxisDirection => end.crossAxisDirection; - - @override - List crossAxisSpaces({ - required List childrenRenderProperties, - required double Function(Axis) maxSizeAvailable, - }) { - return end.crossAxisSpaces( - childrenRenderProperties: childrenRenderProperties, - maxSizeAvailable: maxSizeAvailable, - ); - } - - @override - Axis get direction => end.direction; - - @override - String get horizontalDirectionDescription => - end.horizontalDirectionDescription; - - @override - bool get isMainAxisHorizontal => end.isMainAxisHorizontal; - - @override - bool get isMainAxisVertical => end.isMainAxisVertical; - - @override - double get mainAxisDimension => lerpDouble( - begin.mainAxisDimension, - end.mainAxisDimension, - animation.value, - )!; - - @override - MainAxisSize? get mainAxisSize => end.mainAxisSize; - - @override - TextBaseline? get textBaseline => end.textBaseline; - - @override - TextDirection get textDirection => end.textDirection; - - @override - double get totalFlex => - lerpDouble(begin.totalFlex, end.totalFlex, animation.value)!; - - @override - String get type => end.type; - - @override - VerticalDirection get verticalDirection => end.verticalDirection; - - @override - String get verticalDirectionDescription => end.verticalDirectionDescription; - - /// Returns a frozen copy of these FlexLayoutProperties that does not animate. - /// - /// Useful for interrupting an animation with a transition to another [FlexLayoutProperties]. - @override - FlexLayoutProperties copyWith({ - Size? size, - List? children, - BoxConstraints? constraints, - bool? isFlex, - String? description, - num? flexFactor, - FlexFit? flexFit, - Axis? direction, - MainAxisAlignment? mainAxisAlignment, - MainAxisSize? mainAxisSize, - CrossAxisAlignment? crossAxisAlignment, - TextDirection? textDirection, - VerticalDirection? verticalDirection, - TextBaseline? textBaseline, - }) { - return FlexLayoutProperties( - size: size ?? this.size, - children: children ?? this.children, - node: node, - constraints: constraints ?? this.constraints, - isFlex: isFlex ?? this.isFlex, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - direction: direction ?? this.direction, - mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment, - mainAxisSize: mainAxisSize ?? this.mainAxisSize, - crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment, - textDirection: textDirection ?? this.textDirection, - verticalDirection: verticalDirection ?? this.verticalDirection, - textBaseline: textBaseline ?? this.textBaseline, - ); - } - - @override - bool get startIsTopLeft => end.startIsTopLeft; -} - -/// LayoutProperties extension to be reused on LayoutProperties and AnimatedLayoutProperties - -extension AxisExtension on Axis { - String get flexType { - switch (this) { - case Axis.horizontal: - return 'Row'; - case Axis.vertical: - return 'Column'; - } - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/arrow.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/arrow.dart deleted file mode 100644 index 3adb4e615cb..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/arrow.dart +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2024 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:math'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -const defaultArrowColor = Colors.white; -const defaultArrowStrokeWidth = 2.0; -const defaultDistanceToArrow = 4.0; - -enum ArrowType { up, left, right, down } - -Axis axis(ArrowType type) => (type == ArrowType.up || type == ArrowType.down) - ? Axis.vertical - : Axis.horizontal; - -/// Widget that draws a bidirectional arrow around another widget. -/// -/// This widget is typically used to help draw diagrams. -@immutable -class ArrowWrapper extends StatelessWidget { - ArrowWrapper.unidirectional({ - super.key, - this.child, - required ArrowType type, - this.arrowColor = defaultArrowColor, - double? arrowHeadSize, - this.arrowStrokeWidth = defaultArrowStrokeWidth, - this.childMarginFromArrow = defaultDistanceToArrow, - }) : assert(childMarginFromArrow > 0.0), - direction = axis(type), - isBidirectional = false, - startArrowType = type, - endArrowType = type, - arrowHeadSize = arrowHeadSize ?? defaultIconSize; - - const ArrowWrapper.bidirectional({ - super.key, - this.child, - required this.direction, - this.arrowColor = defaultArrowColor, - required this.arrowHeadSize, - this.arrowStrokeWidth = defaultArrowStrokeWidth, - this.childMarginFromArrow = defaultDistanceToArrow, - }) : assert(arrowHeadSize >= 0.0), - assert(childMarginFromArrow >= 0.0), - isBidirectional = true, - startArrowType = direction == Axis.horizontal - ? ArrowType.left - : ArrowType.up, - endArrowType = direction == Axis.horizontal - ? ArrowType.right - : ArrowType.down; - - final Color arrowColor; - final double arrowHeadSize; - final double arrowStrokeWidth; - final Widget? child; - - final Axis direction; - final double childMarginFromArrow; - - final bool isBidirectional; - final ArrowType startArrowType; - final ArrowType endArrowType; - - double get verticalMarginFromArrow { - if (child == null || direction == Axis.horizontal) return 0.0; - return childMarginFromArrow; - } - - double get horizontalMarginFromArrow { - if (child == null || direction == Axis.vertical) return 0.0; - return childMarginFromArrow; - } - - @override - Widget build(BuildContext context) { - return Flex( - direction: direction, - children: [ - Expanded( - child: Container( - margin: EdgeInsets.only( - bottom: verticalMarginFromArrow, - right: horizontalMarginFromArrow, - ), - child: ArrowWidget( - color: arrowColor, - headSize: arrowHeadSize, - strokeWidth: arrowStrokeWidth, - type: startArrowType, - shouldDrawHead: isBidirectional - ? true - : (startArrowType == ArrowType.left || - startArrowType == ArrowType.up), - ), - ), - ), - if (child != null) child!, - Expanded( - child: Container( - margin: EdgeInsets.only( - top: verticalMarginFromArrow, - left: horizontalMarginFromArrow, - ), - child: ArrowWidget( - color: arrowColor, - headSize: arrowHeadSize, - strokeWidth: arrowStrokeWidth, - type: endArrowType, - shouldDrawHead: isBidirectional - ? true - : (endArrowType == ArrowType.right || - endArrowType == ArrowType.down), - ), - ), - ), - ], - ); - } -} - -/// Widget that draws a fully sized, centered, unidirectional arrow according to its constraints -@immutable -class ArrowWidget extends StatelessWidget { - ArrowWidget({ - this.color = defaultArrowColor, - required this.headSize, - super.key, - this.shouldDrawHead = true, - this.strokeWidth = defaultArrowStrokeWidth, - required this.type, - }) : assert(headSize > 0.0), - assert(strokeWidth > 0.0), - _painter = _ArrowPainter( - headSize: headSize, - color: color, - strokeWidth: strokeWidth, - type: type, - shouldDrawHead: shouldDrawHead, - ); - - final Color color; - - /// The arrow head is a Equilateral triangle - final double headSize; - - final double strokeWidth; - - final ArrowType type; - - final CustomPainter _painter; - - final bool shouldDrawHead; - - @override - Widget build(BuildContext context) { - return CustomPaint(painter: _painter, child: Container()); - } -} - -class _ArrowPainter extends CustomPainter { - _ArrowPainter({ - required this.headSize, - this.strokeWidth = defaultArrowStrokeWidth, - this.color = defaultArrowColor, - this.shouldDrawHead = true, - required this.type, - }) : // the height of an equilateral triangle - headHeight = 0.5 * sqrt(3) * headSize; - - final Color color; - final double headSize; - final bool shouldDrawHead; - final double strokeWidth; - final ArrowType type; - - final double headHeight; - - bool headIsGreaterThanConstraint(Size size) { - if (type == ArrowType.left || type == ArrowType.right) { - return headHeight >= (size.width); - } - return headHeight >= (size.height); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => - !(oldDelegate is _ArrowPainter && - headSize == oldDelegate.headSize && - strokeWidth == oldDelegate.strokeWidth && - color == oldDelegate.color && - type == oldDelegate.type); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = strokeWidth; - - final originX = size.width / 2, originY = size.height / 2; - Offset lineStartingPoint = Offset.zero; - Offset lineEndingPoint = Offset.zero; - - if (!headIsGreaterThanConstraint(size) && shouldDrawHead) { - Offset p1, p2, p3; - final headSizeDividedBy2 = headSize / 2; - switch (type) { - case ArrowType.up: - p1 = Offset(originX, 0); - p2 = Offset(originX - headSizeDividedBy2, headHeight); - p3 = Offset(originX + headSizeDividedBy2, headHeight); - break; - case ArrowType.left: - p1 = Offset(0, originY); - p2 = Offset(headHeight, originY - headSizeDividedBy2); - p3 = Offset(headHeight, originY + headSizeDividedBy2); - break; - case ArrowType.right: - final startingX = size.width - headHeight; - p1 = Offset(size.width, originY); - p2 = Offset(startingX, originY - headSizeDividedBy2); - p3 = Offset(startingX, originY + headSizeDividedBy2); - break; - case ArrowType.down: - final startingY = size.height - headHeight; - p1 = Offset(originX, size.height); - p2 = Offset(originX - headSizeDividedBy2, startingY); - p3 = Offset(originX + headSizeDividedBy2, startingY); - break; - } - final path = Path() - ..moveTo(p1.dx, p1.dy) - ..lineTo(p2.dx, p2.dy) - ..lineTo(p3.dx, p3.dy) - ..close(); - canvas.drawPath(path, paint); - - switch (type) { - case ArrowType.up: - lineStartingPoint = Offset(originX, headHeight); - lineEndingPoint = Offset(originX, size.height); - break; - case ArrowType.left: - lineStartingPoint = Offset(headHeight, originY); - lineEndingPoint = Offset(size.width, originY); - break; - case ArrowType.right: - final arrowHeadStartingX = size.width - headHeight; - lineStartingPoint = Offset(0, originY); - lineEndingPoint = Offset(arrowHeadStartingX, originY); - break; - case ArrowType.down: - final headStartingY = size.height - headHeight; - lineStartingPoint = Offset(originX, 0); - lineEndingPoint = Offset(originX, headStartingY); - break; - } - } else { - // draw full line - switch (type) { - case ArrowType.up: - case ArrowType.down: - lineStartingPoint = Offset(originX, 0); - lineEndingPoint = Offset(originX, size.height); - break; - case ArrowType.left: - case ArrowType.right: - lineStartingPoint = Offset(0, originY); - lineEndingPoint = Offset(size.width, originY); - break; - } - } - canvas.drawLine(lineStartingPoint, lineEndingPoint, paint); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/dimension.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/dimension.dart deleted file mode 100644 index 7dfb1b8e2c1..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/dimension.dart +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 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 'theme.dart'; - -/// Text widget for displaying width / height. -Widget dimensionDescription( - TextSpan description, - bool overflow, - ColorScheme colorScheme, -) { - final text = Text.rich( - description, - textAlign: TextAlign.center, - style: overflow - ? overflowingDimensionIndicatorTextStyle(colorScheme) - : dimensionIndicatorTextStyle, - overflow: TextOverflow.ellipsis, - ); - if (overflow) { - return Container( - padding: const EdgeInsets.symmetric( - vertical: minPadding, - horizontal: overflowTextHorizontalPadding, - ), - decoration: BoxDecoration( - color: colorScheme.overflowBackgroundColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: Center(child: text), - ); - } - return text; -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/free_space.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/free_space.dart deleted file mode 100644 index aeb3766ae9e..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/free_space.dart +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2024 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_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import 'arrow.dart'; -import 'dimension.dart'; -import 'theme.dart'; - -class FreeSpaceVisualizerWidget extends StatelessWidget { - const FreeSpaceVisualizerWidget(this.renderProperties, {super.key}); - - final RenderProperties renderProperties; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final heightDescription = - 'h=${toStringAsFixed(renderProperties.realHeight)}'; - final widthDescription = 'w=${toStringAsFixed(renderProperties.realWidth)}'; - final showWidth = - renderProperties.realWidth != renderProperties.layoutProperties.width; - final widthWidget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: widthDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - direction: Axis.horizontal, - arrowHeadSize: arrowHeadSize, - ), - ), - ], - ); - final heightWidget = SizedBox( - width: heightOnlyIndicatorSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: heightDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - direction: Axis.vertical, - arrowHeadSize: arrowHeadSize, - childMarginFromArrow: 0.0, - ), - ), - ], - ), - ); - return Positioned( - top: renderProperties.offset.dy, - left: renderProperties.offset.dx, - child: SizedBox( - width: renderProperties.width, - height: renderProperties.height, - child: DevToolsTooltip( - message: '$widthDescription\n$heightDescription', - child: showWidth ? widthWidget : heightWidget, - ), - ), - ); - } -} - -class PaddingVisualizerWidget extends StatelessWidget { - const PaddingVisualizerWidget( - this.renderProperties, { - required this.horizontal, - super.key, - }); - - final RenderProperties renderProperties; - final bool horizontal; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final heightDescription = toStringAsFixed(renderProperties.realHeight); - final widthDescription = toStringAsFixed(renderProperties.realWidth); - final widthWidget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: widthDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - direction: Axis.horizontal, - arrowHeadSize: arrowHeadSize, - ), - ), - ], - ); - final heightWidget = SizedBox( - width: heightOnlyIndicatorSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: heightDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - direction: Axis.vertical, - arrowHeadSize: arrowHeadSize, - childMarginFromArrow: 0.0, - ), - ), - ], - ), - ); - return Positioned( - top: renderProperties.offset.dy, - left: renderProperties.offset.dx, - child: SizedBox( - width: safePositiveDouble(renderProperties.width), - height: safePositiveDouble(renderProperties.height), - child: horizontal ? widthWidget : heightWidget, - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/layout_explorer_widget.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/layout_explorer_widget.dart deleted file mode 100644 index 1a9840f9ead..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/layout_explorer_widget.dart +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2024 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 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/diagnostics/inspector_service.dart'; -import '../../../../shared/globals.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_controller.dart'; -import '../../inspector_data_models.dart'; -import 'utils.dart'; - -const maxRequestsPerSecond = 3.0; - -/// Base class for layout widgets for all widget types. -abstract class LayoutExplorerWidget extends StatefulWidget { - const LayoutExplorerWidget(this.inspectorController, {super.key}); - - final InspectorController inspectorController; -} - -/// Base class for state objects for layout widgets for all widget types. -abstract class LayoutExplorerWidgetState< - W extends LayoutExplorerWidget, - L extends LayoutProperties -> - extends State - with TickerProviderStateMixin - implements InspectorServiceClient { - LayoutExplorerWidgetState() { - _onSelectionChangedCallback = onSelectionChanged; - } - - late AnimationController entranceController; - late CurvedAnimation entranceCurve; - late AnimationController changeController; - - late CurvedAnimation changeAnimation; - - L? _previousProperties; - - L? _properties; - - InspectorObjectGroupManager? objectGroupManager; - - AnimatedLayoutProperties? get animatedProperties => _animatedProperties; - AnimatedLayoutProperties? _animatedProperties; - - L? get properties => - _previousProperties ?? _animatedProperties as L? ?? _properties; - - RemoteDiagnosticsNode? get selectedNode => - inspectorController.selectedNode.value?.diagnostic; - - InspectorController get inspectorController => widget.inspectorController; - - InspectorService? get inspectorService => - serviceConnection.inspectorService as InspectorService?; - - late RateLimiter rateLimiter; - - late Future Function() _onSelectionChangedCallback; - - Future onSelectionChanged() async { - if (!mounted) return; - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return; - if (!shouldDisplay(selectedNodeLocal)) return; - final prevRootId = id(_properties?.node); - final newRootId = id(getRoot(selectedNodeLocal)); - final shouldFetch = prevRootId != newRootId; - if (shouldFetch) { - _dirty = false; - final newSelection = await fetchLayoutProperties(); - _setProperties(newSelection); - } else { - updateHighlighted(_properties); - } - } - - /// Whether this layout explorer can work with this kind of node. - bool shouldDisplay(RemoteDiagnosticsNode node); - - List get children => properties!.displayChildren; - - LayoutProperties? highlighted; - - /// Returns the root widget to show. - /// - /// For cases such as Flex widgets or in the future ListView widgets we may - /// want to show the layout for all widgets under a root that is the parent - /// of the current widget. - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node); - - Future fetchLayoutProperties() async { - objectGroupManager?.cancelNext(); - final manager = objectGroupManager!; - final nextObjectGroup = manager.next; - final node = await nextObjectGroup.getLayoutExplorerNode( - getRoot(selectedNode), - ); - if (node == null || node.renderObject == null) return null; - - if (!nextObjectGroup.disposed) { - assert(manager.next == nextObjectGroup); - manager.promoteNext(); - } - return computeLayoutProperties(node); - } - - L computeLayoutProperties(RemoteDiagnosticsNode node); - - AnimatedLayoutProperties computeAnimatedProperties(L nextProperties); - - void updateHighlighted(L? newProperties); - - String? id(RemoteDiagnosticsNode? node) => node?.valueRef.id; - - void _registerInspectorControllerService() { - inspectorController.selectedNode.addListener(_onSelectionChangedCallback); - inspectorService?.addClient(this); - } - - void _unregisterInspectorControllerService() { - inspectorController.selectedNode.removeListener( - _onSelectionChangedCallback, - ); - inspectorService?.removeClient(this); - } - - @override - void initState() { - super.initState(); - rateLimiter = RateLimiter(maxRequestsPerSecond, refresh); - _registerInspectorControllerService(); - _initAnimationStates(); - _updateObjectGroupManager(); - // TODO(jacobr): put inspector controller in Controllers and - // update on didChangeDependencies. - _animateProperties(); - } - - @override - void didUpdateWidget(W oldWidget) { - super.didUpdateWidget(oldWidget); - _updateObjectGroupManager(); - _animateProperties(); - if (oldWidget.inspectorController != inspectorController) { - _unregisterInspectorControllerService(); - _registerInspectorControllerService(); - } - } - - @override - void dispose() { - entranceController.dispose(); - changeController.dispose(); - _unregisterInspectorControllerService(); - super.dispose(); - } - - void _animateProperties() { - if (_animatedProperties != null) { - unawaited(changeController.forward()); - } - if (_previousProperties != null) { - unawaited(entranceController.reverse()); - } else { - unawaited(entranceController.forward()); - } - } - - // update selected widget in the device without triggering selection listener event. - // this is required so that we don't change focus - // when tapping on a child is also Flex-based widget. - Future setSelectionInspector(RemoteDiagnosticsNode node) async { - final service = node.objectGroupApi; - if (service != null && service is ObjectGroup) { - await service.setSelectionInspector(node.valueRef, false); - } - } - - // update selected widget and trigger selection listener event to change focus. - void refreshSelection(RemoteDiagnosticsNode node) { - inspectorController.refreshSelection(node); - } - - Future onTap(LayoutProperties properties) async { - setState(() => highlighted = properties); - await setSelectionInspector(properties.node); - } - - void onDoubleTap(LayoutProperties properties) { - refreshSelection(properties.node); - } - - Future refresh() async { - if (!_dirty) return; - _dirty = false; - final updatedProperties = await fetchLayoutProperties(); - if (updatedProperties != null) { - _changeProperties(updatedProperties); - } - } - - void _changeProperties(L nextProperties) { - if (!mounted) return; - updateHighlighted(nextProperties); - setState(() { - _animatedProperties = computeAnimatedProperties(nextProperties); - unawaited(changeController.forward(from: 0.0)); - }); - } - - void _setProperties(L? newProperties) { - if (!mounted) return; - updateHighlighted(newProperties); - if (_properties == newProperties) { - return; - } - setState(() { - _previousProperties ??= _properties; - _properties = newProperties; - }); - _animateProperties(); - } - - void _initAnimationStates() { - entranceController = longAnimationController(this) - ..addStatusListener((status) { - if (status == AnimationStatus.dismissed) { - setState(() { - _previousProperties = null; - unawaited(entranceController.forward()); - }); - } - }); - entranceCurve = defaultCurvedAnimation(entranceController); - changeController = longAnimationController(this) - ..addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() { - _properties = _animatedProperties!.end; - _animatedProperties = null; - changeController.value = 0.0; - }); - } - }); - changeAnimation = defaultCurvedAnimation(changeController); - } - - void _updateObjectGroupManager() { - final service = serviceConnection.inspectorService; - if (service != objectGroupManager?.inspectorService) { - objectGroupManager = InspectorObjectGroupManager( - service as InspectorService, - 'flex-layout', - ); - } - unawaited(onSelectionChanged()); - } - - bool _dirty = false; - - @override - void onFlutterFrame() { - if (!mounted) return; - if (_dirty) { - rateLimiter.scheduleRequest(); - } - } - - // TODO(albertusangga): Investigate why onForceRefresh is not getting called. - @override - Future onForceRefresh() async { - final properties = await fetchLayoutProperties(); - if (properties != null) { - _setProperties(properties); - } - } - - /// Currently this is not working so we should listen to controller selection event instead. - @override - Future onInspectorSelectionChanged() async {} - - /// Register callback to be executed once Flutter frame is ready. - void markAsDirty() { - _dirty = true; - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/overflow_indicator_painter.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/overflow_indicator_painter.dart deleted file mode 100644 index 2295666ceb5..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/overflow_indicator_painter.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2024 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:ui' as ui; - -import 'package:flutter/rendering.dart'; - -import '../../inspector_data_models.dart'; - -/// CustomPainter for drawing [DebugOverflowIndicatorMixin]'s patterned background. -/// Draws overflow pattern on the [OverflowSide] of the widget. -/// -/// [DebugOverflowIndicatorMixin] can not be reused here -/// because it is a mixin on RenderObject and requires real overflows on the widget. -/// -/// If [side] is set to [OverflowSide.right], -/// the pattern will occupy the whole height -/// and the width will be the given [size]. -/// -/// If [side] is set to [OverflowSide.bottom], -/// the pattern will occupy the whole width -/// and the height will be the given [size]. -/// -/// See also: -/// * [DebugOverflowIndicatorMixin] -class OverflowIndicatorPainter extends CustomPainter { - const OverflowIndicatorPainter(this.side, this.size); - - final OverflowSide side; - final double size; - - /// These static variables are taken from [DebugOverflowIndicatorMixin] - /// since all of them are private. - static const black = Color(0xBF000000); - static const yellow = Color(0xBFFFFF00); - static final indicatorPaint = Paint() - ..shader = ui.Gradient.linear( - const Offset(0.0, 0.0), - const Offset(10.0, 10.0), - [black, yellow, yellow, black], - [0.25, 0.25, 0.75, 0.75], - TileMode.repeated, - ); - - @override - void paint(Canvas canvas, Size size) { - final bottomOverflow = OverflowSide.bottom == side; - final width = bottomOverflow ? size.width : this.size; - final height = !bottomOverflow ? size.height : this.size; - - final left = bottomOverflow ? 0.0 : size.width - width; - final top = side == OverflowSide.right ? 0.0 : size.height - height; - final rect = Rect.fromLTWH(left, top, width, height); - canvas.drawRect(rect, indicatorPaint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate is OverflowIndicatorPainter && - (side != oldDelegate.side || size != oldDelegate.size); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/theme.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/theme.dart deleted file mode 100644 index 666a6d902ca..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/theme.dart +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2024 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_shared/ui.dart'; -import 'package:flutter/material.dart'; - -const margin = 6.0; - -const arrowHeadSize = 8.0; -const arrowMargin = 4.0; -const arrowStrokeWidth = 1.5; - -/// Hardcoded sizes for scaling the flex children widget properly. -const minRenderWidth = 250.0; -const minRenderHeight = 250.0; - -const minPadding = 2.0; -const overflowTextHorizontalPadding = 8.0; - -/// The size to shrink a widget by when animating it in. -const entranceMargin = 50.0; - -const defaultMaxRenderWidth = 400.0; -const defaultMaxRenderHeight = 400.0; - -const widgetTitleMaxWidthPercentage = 0.75; - -/// Hardcoded arrow size respective to its cross axis (because it's unconstrained). -const heightAndConstraintIndicatorSize = 48.0; -const widthAndConstraintIndicatorSize = 56.0; -const mainAxisArrowIndicatorSize = 48.0; -const crossAxisArrowIndicatorSize = 48.0; - -const heightOnlyIndicatorSize = 72.0; - -/// Minimum size to display width/height inside the arrow -const minWidthToDisplayWidthInsideArrow = 200.0; -const minHeightToDisplayHeightInsideArrow = 200.0; - -const smallTextScaleFactor = 0.8; - -/// Height for limiting asset image (selected one in the drop down). -const axisAlignmentAssetImageHeight = 24.0; - -const minHeightToAllowTruncating = 375.0; -const minWidthToAllowTruncating = 375.0; - -// Story of Layout colors -const mainAxisLightColor = Color(0xff2c5daa); -const mainAxisDarkColor = Color(0xff2c5daa); - -const textColor = Color(0xff55767f); -const emphasizedTextColor = Color(0xff009aca); - -const crossAxisLightColor = Color(0xff8ac652); -const crossAxisDarkColor = Color(0xff8ac652); - -const mainAxisTextColorLight = Color(0xFF1375bc); -const mainAxisTextColorDark = Color(0xFF1375bc); - -const crossAxisTextColorLight = Color(0xFF66672C); -const crossAxisTextColorsDark = Color(0xFFB3D25A); - -const overflowBackgroundColorDark = Color(0xFFB00020); -const overflowBackgroundColorLight = Color(0xFFB00020); - -const overflowTextColorDark = Color(0xfff5846b); -const overflowTextColorLight = Color(0xffdea089); - -const backgroundColorSelectedDark = Color( - 0x4d474747, -); // TODO(jacobr): we would like Color(0x4dedeeef) but that makes the background show through. -const backgroundColorSelectedLight = Color(0x4dedeeef); - -extension LayoutExplorerColorScheme on ColorScheme { - Color get mainAxisColor => isLight ? mainAxisLightColor : mainAxisDarkColor; - - Color get widgetNameColor => isLight ? Colors.white : Colors.black; - - Color get crossAxisColor => - isLight ? crossAxisLightColor : crossAxisDarkColor; - - Color get mainAxisTextColor => - isLight ? mainAxisTextColorLight : mainAxisTextColorDark; - - Color get crossAxisTextColor => - isLight ? crossAxisTextColorLight : crossAxisTextColorsDark; - - Color get overflowBackgroundColor => - isLight ? overflowBackgroundColorLight : overflowBackgroundColorDark; - - Color get overflowTextColor => - isLight ? overflowTextColorLight : overflowTextColorDark; - - Color get backgroundColorSelected => - isLight ? backgroundColorSelectedLight : backgroundColorSelectedDark; - - Color get unconstrainedColor => - isLight ? unconstrainedLightColor : unconstrainedDarkColor; -} - -const backgroundColorDark = Color(0xff30302f); -const backgroundColorLight = Color(0xffffffff); - -const unconstrainedDarkColor = Color(0xffdea089); -const unconstrainedLightColor = Color(0xfff5846b); - -const widthIndicatorColor = textColor; -const heightIndicatorColor = textColor; - -const negativeSpaceDarkAssetName = - 'assets/img/layout_explorer/negative_space_dark.png'; -const negativeSpaceLightAssetName = - 'assets/img/layout_explorer/negative_space_light.png'; - -const dimensionIndicatorTextStyle = TextStyle( - height: 1.0, - letterSpacing: 1.1, - color: emphasizedTextColor, - fontSize: defaultFontSize, -); - -TextStyle overflowingDimensionIndicatorTextStyle(ColorScheme colorScheme) => - dimensionIndicatorTextStyle.merge( - TextStyle( - fontWeight: FontWeight.bold, - color: colorScheme.overflowTextColor, - ), - ); - -Widget buildUnderline() { - return Container( - height: 1.0, - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: textColor, width: 0.0)), - ), - ); -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart deleted file mode 100644 index 5b4b1ec2d4f..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart +++ /dev/null @@ -1,615 +0,0 @@ -// Copyright 2024 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:ui'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../../inspector/layout_explorer/ui/dimension.dart'; -import '../../inspector_data_models.dart'; -import 'overflow_indicator_painter.dart'; -import 'theme.dart'; -import 'widgets_theme.dart'; - -/// A widget for positioning sized widgets that follows layout as follows: -/// | top | -/// left | center | right -/// | bottom | -@immutable -class BorderLayout extends StatelessWidget { - const BorderLayout({ - super.key, - this.left, - this.leftWidth, - this.top, - this.topHeight, - this.right, - this.rightWidth, - this.bottom, - this.bottomHeight, - this.center, - }) : assert( - left != null || - top != null || - right != null || - bottom != null || - center != null, - ); - - final Widget? center; - final Widget? top; - final Widget? left; - final Widget? right; - final Widget? bottom; - - final double? leftWidth; - final double? rightWidth; - final double? topHeight; - final double? bottomHeight; - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Center( - child: Container( - margin: EdgeInsets.only( - left: leftWidth ?? 0, - right: rightWidth ?? 0, - top: topHeight ?? 0, - bottom: bottomHeight ?? 0, - ), - child: center, - ), - ), - if (top != null) - Align( - alignment: Alignment.topCenter, - child: SizedBox(height: topHeight, child: top), - ), - if (left != null) - Align( - alignment: Alignment.centerLeft, - child: SizedBox(width: leftWidth, child: left), - ), - if (right != null) - Align( - alignment: Alignment.centerRight, - child: SizedBox(width: rightWidth, child: right), - ), - if (bottom != null) - Align( - alignment: Alignment.bottomCenter, - child: SizedBox(height: bottomHeight, child: bottom), - ), - ], - ); - } -} - -@immutable -class Truncateable extends StatelessWidget { - const Truncateable({super.key, this.truncate = false, required this.child}); - - final Widget child; - final bool truncate; - - @override - Widget build(BuildContext context) { - return Flexible(flex: truncate ? 1 : 0, child: child); - } -} - -/// Widget that draws bounding box with the title (usually widget name) in its -/// top left. -/// -/// * [hint] is an optional widget to be placed in the top right of the box. -/// * [child] is an optional widget to be placed in the center of the box. -class WidgetVisualizer extends StatelessWidget { - const WidgetVisualizer({ - super.key, - required this.title, - this.hint, - required this.isSelected, - required this.layoutProperties, - required this.child, - this.overflowSide, - this.largeTitle = false, - this.isFlex = false, - }); - - final LayoutProperties layoutProperties; - final String title; - final Widget child; - final Widget? hint; - final bool isSelected; - final bool largeTitle; - final bool isFlex; - - final OverflowSide? overflowSide; - - static const _overflowIndicatorSize = 20.0; - static const _borderUnselectedWidth = 1.0; - static const _borderSelectedWidth = 3.0; - static const _selectedPadding = 4.0; - - bool get drawOverflow => overflowSide != null; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final properties = layoutProperties; - final borderColor = WidgetTheme.fromName(properties.node.description).color; - final boxAdjust = isSelected ? _selectedPadding : 0.0; - - return LayoutBuilder( - builder: (context, constraints) { - return OverflowBox( - minWidth: constraints.minWidth + boxAdjust, - maxWidth: constraints.maxWidth + boxAdjust, - maxHeight: constraints.maxHeight + boxAdjust, - minHeight: constraints.minHeight + boxAdjust, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: isSelected - ? _borderSelectedWidth - : _borderUnselectedWidth, - ), - color: isSelected - ? theme.canvasColor.brighten() - : theme.canvasColor.darken(), - boxShadow: isSelected - ? [ - BoxShadow( - color: Colors.black.withAlpha(255 ~/ 2), - blurRadius: 10, - ), - ] - : null, - ), - child: Stack( - children: [ - if (drawOverflow) - Positioned.fill( - child: CustomPaint( - painter: OverflowIndicatorPainter( - overflowSide!, - _overflowIndicatorSize, - ), - ), - ), - Container( - margin: EdgeInsets.only( - right: overflowSide == OverflowSide.right - ? _overflowIndicatorSize - : 0.0, - bottom: overflowSide == OverflowSide.bottom - ? _overflowIndicatorSize - : 0.0, - ), - child: isFlex - ? FlexWidgetVisualizer( - title: title, - largeTitle: largeTitle, - borderColor: borderColor, - hint: hint, - child: child, - ) - : BoxWidgetVisualizer( - borderColor: borderColor, - title: title, - properties: properties, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -/// Visualizer display for a widget in a flex layout. -class FlexWidgetVisualizer extends StatelessWidget { - const FlexWidgetVisualizer({ - super.key, - required this.largeTitle, - required this.borderColor, - required this.title, - required this.hint, - required this.child, - }); - - final bool largeTitle; - final Color borderColor; - final String title; - final Widget? hint; - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final hintLocal = hint; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Container( - constraints: BoxConstraints( - maxWidth: largeTitle - ? defaultMaxRenderWidth - : minRenderWidth * widgetTitleMaxWidthPercentage, - ), - decoration: BoxDecoration(color: borderColor), - padding: const EdgeInsets.all(densePadding), - child: Center( - child: Text( - title, - style: theme.regularTextStyleWithColor( - colorScheme.widgetNameColor, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - if (hintLocal != null) Flexible(child: hintLocal), - ], - ), - ), - Expanded(child: child), - ], - ); - } -} - -/// Visualizer display for a widget in a box layout. -class BoxWidgetVisualizer extends StatelessWidget { - const BoxWidgetVisualizer({ - super.key, - required this.borderColor, - required this.title, - required this.properties, - }); - - final Color borderColor; - final String title; - final LayoutProperties properties; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: WidgetLabel(labelColor: borderColor, labelText: title), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - dimensionDescription( - TextSpan(text: properties.describeHeight()), - false, - theme.colorScheme, - ), - dimensionDescription( - TextSpan(text: properties.describeWidth()), - false, - theme.colorScheme, - ), - ], - ), - ), - ], - ); - } -} - -/// A label for the widget in the layout explorer. -class WidgetLabel extends StatelessWidget { - const WidgetLabel({ - super.key, - required this.labelColor, - required this.labelText, - this.positionedAtBottom = false, - }); - - final Color labelColor; - final String labelText; - final bool positionedAtBottom; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Container( - decoration: BoxDecoration(color: labelColor), - padding: EdgeInsets.fromLTRB( - densePadding, - positionedAtBottom ? borderPadding : 0.0, - densePadding, - positionedAtBottom ? 0.0 : borderPadding, - ), - child: Text( - labelText, - style: theme.regularTextStyleWithColor(colorScheme.widgetNameColor), - overflow: TextOverflow.ellipsis, - ), - ); - } -} - -class AnimatedLayoutProperties - implements LayoutProperties { - AnimatedLayoutProperties(this.begin, this.end, this.animation) - : assert(begin.children.length == end.children.length), - _children = [ - for (var i = 0; i < begin.children.length; i++) - AnimatedLayoutProperties( - begin.children[i], - end.children[i], - animation, - ), - ]; - - final T begin; - final T end; - final Animation animation; - final List _children; - - @override - LayoutProperties? get parent => end.parent; - - @override - set parent(LayoutProperties? parent) { - end.parent = parent; - } - - @override - LayoutProperties? get parentLayoutProperties => null; - - @override - WidgetSizes? get widgetWidths => null; - - @override - WidgetSizes? get widgetHeights => null; - - @override - List get children { - return _children; - } - - List _lerpList(List l1, List l2) { - assert(l1.length == l2.length); - if (l1.isEmpty) return []; - final animationLocal = animation; - return [ - for (var i = 0; i < children.length; i++) - lerpDouble(l1[i], l2[i], animationLocal.value)!, - ]; - } - - @override - List childrenDimensions(Axis axis) { - final beginDimensions = begin.childrenDimensions(axis); - final endDimensions = end.childrenDimensions(axis); - return _lerpList(beginDimensions, endDimensions); - } - - @override - List get childrenHeights => - _lerpList(begin.childrenHeights, end.childrenHeights); - - @override - List get childrenWidths => - _lerpList(begin.childrenWidths, end.childrenWidths); - - @override - BoxConstraints? get constraints { - try { - return BoxConstraints.lerp( - begin.constraints, - end.constraints, - animation.value, - ); - } catch (e) { - return end.constraints; - } - } - - @override - String describeWidthConstraints() { - final constraintsLocal = constraints!; - return constraintsLocal.hasBoundedWidth - ? LayoutProperties.describeAxis( - constraintsLocal.minWidth, - constraintsLocal.maxWidth, - 'w', - ) - : 'w=unconstrained'; - } - - @override - String describeHeightConstraints() { - final constraintsLocal = constraints!; - return constraintsLocal.hasBoundedHeight - ? LayoutProperties.describeAxis( - constraintsLocal.minHeight, - constraintsLocal.maxHeight, - 'h', - ) - : 'h=unconstrained'; - } - - @override - String describeWidth() => 'w=${toStringAsFixed(size.width)}'; - - @override - String describeHeight() => 'h=${toStringAsFixed(size.height)}'; - - @override - String? get description => end.description; - - @override - double dimension(Axis axis) { - return lerpDouble( - begin.dimension(axis), - end.dimension(axis), - animation.value, - )!; - } - - @override - num? get flexFactor => - lerpDouble(begin.flexFactor, end.flexFactor, animation.value); - - @override - bool get hasChildren => children.isNotEmpty; - - @override - double get height => size.height; - - @override - bool get isFlex => begin.isFlex && end.isFlex; - - @override - RemoteDiagnosticsNode get node => end.node; - - @override - Size get size { - return Size.lerp(begin.size, end.size, animation.value)!; - } - - @override - int get totalChildren => end.totalChildren; - - @override - double get width => size.width; - - @override - bool get hasFlexFactor => begin.hasFlexFactor && end.hasFlexFactor; - - @override - LayoutProperties copyWith({ - List? children, - BoxConstraints? constraints, - String? description, - int? flexFactor, - FlexFit? flexFit, - bool? isFlex, - Size? size, - }) { - return LayoutProperties.values( - node: node, - children: children ?? this.children, - constraints: constraints ?? this.constraints, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - flexFit: flexFit ?? this.flexFit, - isFlex: isFlex ?? this.isFlex, - size: size ?? this.size, - ); - } - - @override - bool get isOverflowWidth => end.isOverflowWidth; - - @override - bool get isOverflowHeight => end.isOverflowHeight; - - @override - FlexFit? get flexFit => end.flexFit; - - @override - List get displayChildren => end.displayChildren; -} - -class LayoutExplorerBackground extends StatelessWidget { - const LayoutExplorerBackground({super.key, required this.colorScheme}); - - final ColorScheme colorScheme; - - @override - Widget build(BuildContext context) { - return Positioned.fill( - child: Opacity( - opacity: colorScheme.isLight ? 0.3 : 0.2, - child: Image.asset( - colorScheme.isLight - ? negativeSpaceLightAssetName - : negativeSpaceDarkAssetName, - fit: BoxFit.none, - repeat: ImageRepeat.repeat, - alignment: Alignment.topLeft, - ), - ), - ); - } -} - -/// Builds and positions a label for the [LayoutExplorerBackground] as -/// determined by the widget's padding. -class PositionedBackgroundLabel extends StatelessWidget { - const PositionedBackgroundLabel({ - super.key, - required this.labelText, - required this.labelColor, - required this.hasTopPadding, - required this.hasBottomPadding, - required this.hasLeftPadding, - required this.hasRightPadding, - }); - - final String labelText; - final Color labelColor; - final bool hasTopPadding; - final bool hasBottomPadding; - final bool hasLeftPadding; - final bool hasRightPadding; - - @override - Widget build(BuildContext context) { - return Column( - // Push to the bottom if there is no padding on the top. - mainAxisAlignment: !hasTopPadding && hasBottomPadding - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - Row( - // Push to the right if there is no padding on the left. - mainAxisAlignment: (!hasLeftPadding && hasRightPadding) - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - Flexible( - child: WidgetLabel( - labelColor: labelColor, - labelText: labelText, - positionedAtBottom: !hasTopPadding && hasBottomPadding, - ), - ), - ], - ), - ], - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widget_constraints.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widget_constraints.dart deleted file mode 100644 index 465a2cb6440..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widget_constraints.dart +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2024 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_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/primitives/math_utils.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import 'arrow.dart'; -import 'dimension.dart'; -import 'theme.dart'; -import 'utils.dart'; - -class VisualizeWidthAndHeightWithConstraints extends StatelessWidget { - const VisualizeWidthAndHeightWithConstraints({ - super.key, - required this.properties, - double? arrowHeadSize, - required this.child, - this.warnIfUnconstrained = true, - }) : arrowHeadSize = arrowHeadSize ?? defaultIconSize; - - final Widget child; - final LayoutProperties properties; - final double arrowHeadSize; - final bool warnIfUnconstrained; - - @override - Widget build(BuildContext context) { - final propertiesLocal = properties; - final showChildrenWidthsSum = - propertiesLocal is FlexLayoutProperties && - propertiesLocal.isOverflowWidth; - const bottomHeight = widthAndConstraintIndicatorSize; - const rightWidth = heightAndConstraintIndicatorSize; - final colorScheme = Theme.of(context).colorScheme; - - final showOverflowHeight = - properties is FlexLayoutProperties && propertiesLocal.isOverflowHeight; - final heightDescription = RotatedBox( - quarterTurns: 1, - child: dimensionDescription( - TextSpan( - children: [ - TextSpan(text: propertiesLocal.describeHeight()), - if (propertiesLocal.constraints != null) ...[ - if (!showOverflowHeight) const TextSpan(text: '\n'), - TextSpan( - text: ' (${propertiesLocal.describeHeightConstraints()})', - style: - propertiesLocal.constraints!.hasBoundedHeight || - !warnIfUnconstrained - ? null - : TextStyle(color: colorScheme.unconstrainedColor), - ), - ], - if (showOverflowHeight) - TextSpan( - text: - '\nchildren take: ' - '${toStringAsFixed(sum(propertiesLocal.childrenHeights))}', - ), - ], - ), - propertiesLocal.isOverflowHeight, - colorScheme, - ), - ); - final right = Container( - margin: const EdgeInsets.only( - top: margin, - left: margin, - bottom: bottomHeight, - right: minPadding, // custom margin for not sticking to the corner - ), - child: LayoutBuilder( - builder: (context, constraints) { - final displayHeightOutsideArrow = - constraints.maxHeight < minHeightToDisplayHeightInsideArrow; - return Row( - children: [ - Truncateable( - truncate: !displayHeightOutsideArrow, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - arrowStrokeWidth: arrowStrokeWidth, - arrowHeadSize: arrowHeadSize, - direction: Axis.vertical, - child: displayHeightOutsideArrow ? null : heightDescription, - ), - ), - ), - if (displayHeightOutsideArrow) Flexible(child: heightDescription), - ], - ); - }, - ), - ); - - final widthDescription = dimensionDescription( - TextSpan( - children: [ - TextSpan(text: '${propertiesLocal.describeWidth()}; '), - if (propertiesLocal.constraints != null) ...[ - if (!showChildrenWidthsSum) const TextSpan(text: '\n'), - TextSpan( - text: '(${propertiesLocal.describeWidthConstraints()})', - style: - propertiesLocal.constraints!.hasBoundedWidth || - !warnIfUnconstrained - ? null - : TextStyle(color: colorScheme.unconstrainedColor), - ), - ], - if (showChildrenWidthsSum) - TextSpan( - text: - '\nchildren take ' - '${toStringAsFixed(sum(propertiesLocal.childrenWidths))}', - ), - ], - ), - propertiesLocal.isOverflowWidth, - colorScheme, - ); - final bottom = Container( - margin: const EdgeInsets.only( - top: margin, - left: margin, - right: rightWidth, - bottom: minPadding, - ), - child: LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final displayWidthOutsideArrow = - maxWidth < minWidthToDisplayWidthInsideArrow; - return Column( - children: [ - Truncateable( - truncate: !displayWidthOutsideArrow, - child: Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - arrowHeadSize: arrowHeadSize, - arrowStrokeWidth: arrowStrokeWidth, - direction: Axis.horizontal, - child: displayWidthOutsideArrow ? null : widthDescription, - ), - ), - ), - if (displayWidthOutsideArrow) - Flexible( - child: Container( - padding: const EdgeInsets.only(top: minPadding), - child: widthDescription, - ), - ), - ], - ); - }, - ), - ); - return BorderLayout( - center: child, - right: right, - rightWidth: rightWidth, - bottom: bottom, - bottomHeight: bottomHeight, - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widgets_theme.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widgets_theme.dart deleted file mode 100644 index a492a1cddbb..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/widgets_theme.dart +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2024 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'; - -class WidgetTheme { - const WidgetTheme({this.iconAsset, this.color = otherWidgetColor}); - - final String? iconAsset; - final Color color; - - static WidgetTheme fromName(String? widgetType) { - if (widgetType == null) { - return const WidgetTheme(); - } - - return themeMap[_stripBrackets(widgetType)] ?? const WidgetTheme(); - } - - /// Strips the brackets off the widget name. - /// - /// For example: `AnimatedBuilder` -> `AnimatedBuilder`. - static String _stripBrackets(String widgetType) { - final bracketIndex = widgetType.indexOf('<'); - if (bracketIndex == -1) { - return widgetType; - } - - return widgetType.substring(0, bracketIndex); - } - - static const contentWidgetColor = Color(0xff06AC3B); - static const highLevelWidgetColor = Color(0xffAEAEB1); - static const animationWidgetColor = Color(0xffE09D0E); - static const otherWidgetColor = Color(0xff0EA7E0); - - static const animatedTheme = WidgetTheme( - iconAsset: WidgetIcons.animated, - color: animationWidgetColor, - ); - - static const transitionTheme = WidgetTheme( - iconAsset: WidgetIcons.transition, - color: animationWidgetColor, - ); - - static const textTheme = WidgetTheme( - iconAsset: WidgetIcons.text, - color: contentWidgetColor, - ); - - static const imageTheme = WidgetTheme( - iconAsset: WidgetIcons.image, - color: contentWidgetColor, - ); - - static const tabTheme = WidgetTheme(iconAsset: WidgetIcons.tab); - static const scrollTheme = WidgetTheme(iconAsset: WidgetIcons.scroll); - static const highLevelTheme = WidgetTheme(color: highLevelWidgetColor); - static const listTheme = WidgetTheme(iconAsset: WidgetIcons.listView); - static const expandTheme = WidgetTheme(iconAsset: WidgetIcons.expand); - static const alignTheme = WidgetTheme(iconAsset: WidgetIcons.align); - static const gestureTheme = WidgetTheme(iconAsset: WidgetIcons.gesture); - static const textButtonTheme = WidgetTheme(iconAsset: WidgetIcons.textButton); - static const toggleTheme = WidgetTheme( - iconAsset: WidgetIcons.toggle, - color: contentWidgetColor, - ); - - static const themeMap = { - // High-level - 'RenderObjectToWidgetAdapter': WidgetTheme( - iconAsset: WidgetIcons.root, - color: highLevelWidgetColor, - ), - 'CupertinoApp': highLevelTheme, - 'MaterialApp': WidgetTheme(iconAsset: WidgetIcons.materialApp), - 'WidgetsApp': highLevelTheme, - - // Text - 'DefaultTextStyle': textTheme, - 'RichText': textTheme, - 'SelectableText': textTheme, - 'Text': textTheme, - - // Images - 'Icon': imageTheme, - 'Image': imageTheme, - 'RawImage': imageTheme, - - // Animations - 'AnimatedAlign': animatedTheme, - 'AnimatedBuilder': animatedTheme, - 'AnimatedContainer': animatedTheme, - 'AnimatedCrossFade': animatedTheme, - 'AnimatedDefaultTextStyle': animatedTheme, - 'AnimatedListState': animatedTheme, - 'AnimatedModalBarrier': animatedTheme, - 'AnimatedOpacity': animatedTheme, - 'AnimatedPhysicalModel': animatedTheme, - 'AnimatedPositioned': animatedTheme, - 'AnimatedSize': animatedTheme, - 'AnimatedWidget': animatedTheme, - - // Transitions - 'DecoratedBoxTransition': transitionTheme, - 'FadeTransition': transitionTheme, - 'PositionedTransition': transitionTheme, - 'RotationTransition': transitionTheme, - 'ScaleTransition': transitionTheme, - 'SizeTransition': transitionTheme, - 'SlideTransition': transitionTheme, - 'Hero': WidgetTheme( - iconAsset: WidgetIcons.hero, - color: animationWidgetColor, - ), - - // Scroll - 'CustomScrollView': scrollTheme, - 'DraggableScrollableSheet': scrollTheme, - 'SingleChildScrollView': scrollTheme, - 'Scrollable': scrollTheme, - 'Scrollbar': scrollTheme, - 'ScrollConfiguration': scrollTheme, - 'GridView': WidgetTheme(iconAsset: WidgetIcons.gridView), - 'ListView': listTheme, - 'ReorderableListView': listTheme, - 'NestedScrollView': listTheme, - - // Input - 'Checkbox': WidgetTheme( - iconAsset: WidgetIcons.checkbox, - color: contentWidgetColor, - ), - 'Radio': WidgetTheme( - iconAsset: WidgetIcons.radio, - color: contentWidgetColor, - ), - 'Switch': toggleTheme, - 'CupertinoSwitch': toggleTheme, - - // Layout - 'Container': WidgetTheme(iconAsset: WidgetIcons.container), - 'Center': WidgetTheme(iconAsset: WidgetIcons.center), - 'Row': WidgetTheme(iconAsset: WidgetIcons.row), - 'Column': WidgetTheme(iconAsset: WidgetIcons.column), - 'Padding': WidgetTheme(iconAsset: WidgetIcons.padding), - 'SizedBox': WidgetTheme(iconAsset: WidgetIcons.sizedBox), - 'ConstrainedBox': WidgetTheme(iconAsset: WidgetIcons.constrainedBox), - 'Align': alignTheme, - 'Positioned': alignTheme, - 'Expanded': expandTheme, - 'Flexible': expandTheme, - 'Stack': WidgetTheme(iconAsset: WidgetIcons.stack), - 'Wrap': WidgetTheme(iconAsset: WidgetIcons.wrap), - - // Buttons - 'FloatingActionButton': WidgetTheme( - iconAsset: WidgetIcons.floatingActionButton, - color: contentWidgetColor, - ), - 'InkWell': WidgetTheme(iconAsset: WidgetIcons.inkWell), - 'GestureDetector': gestureTheme, - 'RawGestureDetector': gestureTheme, - 'TextButton': textButtonTheme, - 'CupertinoButton': textButtonTheme, - 'ElevatedButton': textButtonTheme, - 'OutlinedButton': WidgetTheme(iconAsset: WidgetIcons.outlinedButton), - - // Tabs - 'Tab': tabTheme, - 'TabBar': tabTheme, - 'TabBarView': tabTheme, - 'BottomNavigationBar': WidgetTheme( - iconAsset: WidgetIcons.bottomNavigationBar, - ), - 'CupertinoTabScaffold': tabTheme, - 'CupertinoTabView': tabTheme, - - // Other - 'Scaffold': WidgetTheme(iconAsset: WidgetIcons.scaffold), - 'CircularProgressIndicator': WidgetTheme( - iconAsset: WidgetIcons.circularProgress, - ), - 'Card': WidgetTheme(iconAsset: WidgetIcons.card), - 'Divider': WidgetTheme(iconAsset: WidgetIcons.divider), - 'AlertDialog': WidgetTheme(iconAsset: WidgetIcons.alertDialog), - 'CircleAvatar': WidgetTheme(iconAsset: WidgetIcons.circleAvatar), - 'Opacity': WidgetTheme(iconAsset: WidgetIcons.opacity), - 'Drawer': WidgetTheme(iconAsset: WidgetIcons.drawer), - 'PageView': WidgetTheme(iconAsset: WidgetIcons.pageView), - 'Material': WidgetTheme(iconAsset: WidgetIcons.material), - 'AppBar': WidgetTheme(iconAsset: WidgetIcons.appBar), - }; -} - -class WidgetIcons { - static const root = 'icons/inspector/widget_icons/root.png'; - static const text = 'icons/inspector/widget_icons/text.png'; - static const icon = 'icons/inspector/widget_icons/icon.png'; - static const image = 'icons/inspector/widget_icons/image.png'; - static const floatingActionButton = - 'icons/inspector/widget_icons/floatingab.png'; - static const checkbox = 'icons/inspector/widget_icons/checkbox.png'; - static const radio = 'icons/inspector/widget_icons/radio.png'; - static const toggle = 'icons/inspector/widget_icons/toggle.png'; - static const animated = 'icons/inspector/widget_icons/animated.png'; - static const transition = 'icons/inspector/widget_icons/transition.png'; - static const hero = 'icons/inspector/widget_icons/hero.png'; - static const container = 'icons/inspector/widget_icons/container.png'; - static const center = 'icons/inspector/widget_icons/center.png'; - static const row = 'icons/inspector/widget_icons/row.png'; - static const column = 'icons/inspector/widget_icons/column.png'; - static const padding = 'icons/inspector/widget_icons/padding.png'; - static const scaffold = 'icons/inspector/widget_icons/scaffold.png'; - static const sizedBox = 'icons/inspector/widget_icons/sizedbox.png'; - static const align = 'icons/inspector/widget_icons/align.png'; - static const scroll = 'icons/inspector/widget_icons/scroll.png'; - static const stack = 'icons/inspector/widget_icons/stack.png'; - static const inkWell = 'icons/inspector/widget_icons/inkwell.png'; - static const gesture = 'icons/inspector/widget_icons/gesture.png'; - static const textButton = 'icons/inspector/widget_icons/textbutton.png'; - static const outlinedButton = - 'icons/inspector/widget_icons/outlinedbutton.png'; - static const gridView = 'icons/inspector/widget_icons/gridview.png'; - static const listView = 'icons/inspector/widget_icons/listView.png'; - - static const alertDialog = 'icons/inspector/widget_icons/alertdialog.png'; - static const card = 'icons/inspector/widget_icons/card.png'; - static const circleAvatar = 'icons/inspector/widget_icons/circleavatar.png'; - static const circularProgress = - 'icons/inspector/widget_icons/circularprogress.png'; - static const constrainedBox = - 'icons/inspector/widget_icons/constrainedbox.png'; - static const divider = 'icons/inspector/widget_icons/divider.png'; - static const drawer = 'icons/inspector/widget_icons/drawer.png'; - static const expand = 'icons/inspector/widget_icons/expand.png'; - static const material = 'icons/inspector/widget_icons/material.png'; - static const opacity = 'icons/inspector/widget_icons/opacity.png'; - static const tab = 'icons/inspector/widget_icons/tab.png'; - static const wrap = 'icons/inspector/widget_icons/wrap.png'; - static const pageView = 'icons/inspector/widget_icons/pageView.png'; - static const appBar = 'icons/inspector/widget_icons/appbar.png'; - static const materialApp = 'icons/inspector/widget_icons/materialapp.png'; - static const bottomNavigationBar = - 'icons/inspector/widget_icons/bottomnavigationbar.png'; -} diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index 45450f842d9..aaf721b6fbb 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -8,7 +8,6 @@ library; import 'package:devtools_shared/devtools_extensions.dart'; import '../framework/screen.dart'; -import '../preferences/preferences.dart'; part 'constants/_cpu_profiler_constants.dart'; part 'constants/_debugger_constants.dart'; @@ -97,6 +96,9 @@ const onDeviceSelection = 'onDeviceSelection'; const inspectorSettings = 'inspectorSettings'; const loggingSettings = 'loggingSettings'; const refreshPubRoots = 'refreshPubRoots'; + +enum InspectorDetailsViewType { layoutExplorer, widgetDetailsTree } + final defaultDetailsViewToLayoutExplorer = InspectorDetailsViewType.layoutExplorer.name; final defaultDetailsViewToWidgetDetails = diff --git a/packages/devtools_app/lib/src/shared/analytics/metrics.dart b/packages/devtools_app/lib/src/shared/analytics/metrics.dart index fa0195a9021..772c5ba1938 100644 --- a/packages/devtools_app/lib/src/shared/analytics/metrics.dart +++ b/packages/devtools_app/lib/src/shared/analytics/metrics.dart @@ -67,12 +67,6 @@ class ProfilerScreenMetrics extends ScreenAnalyticsMetrics { } class InspectorScreenMetrics extends ScreenAnalyticsMetrics { - InspectorScreenMetrics.legacy({ - this.rootSetCount, - this.rowCount, - this.inspectorTreeControllerId, - }) : isV2 = false; - InspectorScreenMetrics.v2({ this.rootSetCount, this.rowCount, diff --git a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart b/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart deleted file mode 100644 index 1afbf488668..00000000000 --- a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2019 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. - -/// Inspector specific tree rendering support. -/// -/// This library must not have direct dependencies on web-only libraries. -/// -/// This allows tests of the complicated logic in this class to run on the VM. -library; - -import 'package:flutter/foundation.dart'; - -import '../../diagnostics/diagnostics_node.dart'; -import '../../ui/search.dart'; - -/// Split text into two groups, word characters at the start of a string and all -/// other characters. -final treeNodePrimaryDescriptionPattern = RegExp(r'^([\w ]+)(.*)$'); -// TODO(jacobr): temporary workaround for missing structure from assertion thrown building -// widget errors. -final assertionThrownBuildingError = RegExp( - r'^(The following assertion was thrown building [a-zA-Z]+)(\(.*\))(:)$', -); - -typedef TreeEventCallback = void Function(InspectorTreeNode node); - -const iconPadding = 4.0; -const chartLineStrokeWidth = 1.0; -const inspectorColumnWidth = 12.0; -const inspectorRowHeight = 16.0; - -/// This class could be refactored out to be a reasonable generic collapsible -/// tree ui node class but we choose to instead make it widget inspector -/// specific as that is the only case we care about. -// TODO(kenz): extend TreeNode class to share tree logic. -class InspectorTreeNode { - InspectorTreeNode({InspectorTreeNode? parent, bool expandChildren = true}) - : _children = [], - _parent = parent, - _isExpanded = expandChildren; - - bool get showLinesToChildren { - return _children.length > 1 && !_children.last.isProperty; - } - - bool get isDirty => _isDirty; - bool _isDirty = true; - - set isDirty(bool dirty) { - if (dirty) { - _isDirty = true; - _shouldShow = null; - if (_childrenCount == null) { - // Already dirty. - return; - } - _childrenCount = null; - if (parent != null) { - parent!.isDirty = true; - } - } else { - _isDirty = false; - } - } - - /// Returns whether the node is currently visible in the tree. - void updateShouldShow(bool value) { - if (value != _shouldShow) { - _shouldShow = value; - for (final child in children) { - child.updateShouldShow(value); - } - } - } - - bool get shouldShow { - final parentLocal = parent; - _shouldShow ??= - parentLocal == null || parentLocal.isExpanded && parentLocal.shouldShow; - return _shouldShow!; - } - - bool? _shouldShow; - - bool selected = false; - - RemoteDiagnosticsNode? _diagnostic; - final List _children; - - Iterable get children => _children; - - bool get isProperty { - final diagnosticLocal = diagnostic; - return diagnosticLocal == null || diagnosticLocal.isProperty; - } - - bool get isExpanded => _isExpanded; - bool _isExpanded; - - bool allowExpandCollapse = true; - - bool get showExpandCollapse { - return (diagnostic?.hasChildren == true || children.isNotEmpty) && - allowExpandCollapse; - } - - set isExpanded(bool value) { - if (value != _isExpanded) { - _isExpanded = value; - isDirty = true; - if (_shouldShow ?? false) { - for (final child in children) { - child.updateShouldShow(value); - } - } - } - } - - InspectorTreeNode? get parent => _parent; - InspectorTreeNode? _parent; - - set parent(InspectorTreeNode? value) { - _parent = value; - _parent?.isDirty = true; - } - - RemoteDiagnosticsNode? get diagnostic => _diagnostic; - - set diagnostic(RemoteDiagnosticsNode? v) { - final value = v!; - _diagnostic = value; - _isExpanded = value.childrenReady; - isDirty = true; - } - - int get childrenCount { - if (!isExpanded) { - _childrenCount = 0; - } - final childrenCountLocal = _childrenCount; - if (childrenCountLocal != null) { - return childrenCountLocal; - } - int count = 0; - for (final child in _children) { - count += child.subtreeSize; - } - return _childrenCount = count; - } - - bool get hasPlaceholderChildren { - return children.length == 1 && children.first.diagnostic == null; - } - - int? _childrenCount; - - int get subtreeSize => childrenCount + 1; - - // TODO(jacobr): move getRowIndex to the InspectorTree class. - int getRowIndex(InspectorTreeNode node) { - int index = 0; - while (true) { - final parent = node.parent; - if (parent == null) { - break; - } - for (final sibling in parent._children) { - if (sibling == node) { - break; - } - index += sibling.subtreeSize; - } - index += 1; // For parent itself. - node = parent; - } - return index; - } - - // TODO(jacobr): move this method to the InspectorTree class. - // TODO: optimize this method. - InspectorTreeRow? getRow(int index) { - if (subtreeSize <= index) { - return null; - } - - final ticks = []; - InspectorTreeNode node = this; - int current = 0; - int depth = 0; - - // Iterate till getting the result to return. - while (true) { - final style = node.diagnostic?.style; - final indented = - style != DiagnosticsTreeStyle.flat && - style != DiagnosticsTreeStyle.error; - if (current == index) { - return InspectorTreeRow( - node: node, - index: index, - ticks: ticks, - depth: depth, - lineToParent: - !node.isProperty && - index != 0 && - node.parent!.showLinesToChildren, - ); - } - assert(index > current); - current++; - final children = node._children; - int i; - for (i = 0; i < children.length; ++i) { - final child = children[i]; - final subtreeSize = child.subtreeSize; - if (current + subtreeSize > index) { - node = child; - if (children.length > 1 && - i + 1 != children.length && - !children.last.isProperty) { - if (indented) { - ticks.add(depth); - } - } - break; - } - current += subtreeSize; - } - assert(i < children.length); - if (indented) { - depth++; - } - } - } - - void removeChild(InspectorTreeNode child) { - child.parent = null; - final removed = _children.remove(child); - assert(removed); - isDirty = true; - } - - void appendChild(InspectorTreeNode child) { - _children.add(child); - child.parent = this; - isDirty = true; - } - - void clearChildren() { - _children.clear(); - isDirty = true; - } -} - -/// A row in the tree with all information required to render it. -class InspectorTreeRow with SearchableDataMixin { - InspectorTreeRow({ - required this.node, - required this.index, - required this.ticks, - required this.depth, - required this.lineToParent, - }); - - final InspectorTreeNode node; - - /// Column indexes of ticks to draw lines from parents to children. - final List ticks; - final int depth; - final int index; - final bool lineToParent; - - bool get isSelected => node.selected; -} - -/// Callback issued every time a node is added to the tree. -typedef NodeAddedCallback = - void Function( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, - ); - -class InspectorTreeConfig { - InspectorTreeConfig({ - this.onNodeAdded, - this.onClientActiveChange, - this.onSelectionChange, - this.onExpand, - }); - - final NodeAddedCallback? onNodeAdded; - final VoidCallback? onSelectionChange; - final void Function(bool added)? onClientActiveChange; - final TreeEventCallback? onExpand; -} - -enum SearchTargetType { - widget, - // TODO(https://github.com/flutter/devtools/issues/3489) implement other search scopes: details, all etc -} diff --git a/packages/devtools_app/lib/src/shared/console/widgets/description.dart b/packages/devtools_app/lib/src/shared/console/widgets/description.dart index 7ea83892fd9..107cec45a7b 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/description.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/description.dart @@ -17,7 +17,7 @@ import '../../primitives/utils.dart'; import '../../ui/hover.dart'; import '../../ui/icons.dart'; import '../../ui/utils.dart'; -import '../eval/inspector_tree.dart'; +import '../eval/inspector_tree_v2.dart'; import 'expandable_variable.dart'; final _colorIconMaker = ColorIconMaker(); @@ -58,7 +58,7 @@ class DiagnosticsNodeDescription extends StatelessWidget { final TextStyle? style; final TextStyle? nodeDescriptionHighlightStyle; // TODO(https://github.com/flutter/devtools/issues/7860): Remove and default - // to true when turning on inspector V2. This is currently true for the V2 + // to true when turning on inspector. This is currently true for the // inspector and false for the legacy inspector. final bool emphasizeNodesFromLocalProject; final String? actionLabel; diff --git a/packages/devtools_app/lib/src/shared/diagnostics/diagnostics_node.dart b/packages/devtools_app/lib/src/shared/diagnostics/diagnostics_node.dart index b149944769e..9f07dbbdb40 100644 --- a/packages/devtools_app/lib/src/shared/diagnostics/diagnostics_node.dart +++ b/packages/devtools_app/lib/src/shared/diagnostics/diagnostics_node.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:vm_service/vm_service.dart'; -import '../../screens/inspector_v2/inspector_data_models.dart'; +import '../../screens/inspector/inspector_data_models.dart'; import '../primitives/utils.dart'; import '../ui/icons.dart'; import 'object_group_api.dart'; diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index c30e6be29b3..f4ac7be3d01 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -72,8 +72,8 @@ extension FeatureFlags on Never { /// Flag to enable the new Inspector panel. /// /// https://github.com/flutter/devtools/issues/7854 - static final inspectorV2 = BooleanFeatureFlag( - name: 'inspectorV2', + static final inspector = BooleanFeatureFlag( + name: 'inspector', enabled: true, ); @@ -93,7 +93,7 @@ extension FeatureFlags on Never { memorySaveLoad, devToolsExtensions, dapDebugging, - inspectorV2, + inspector, aiAssistant, }; diff --git a/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart b/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart index d3d3f7dc8bc..16950eb15d6 100644 --- a/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart +++ b/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart @@ -11,7 +11,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:vm_service/vm_service.dart'; -import '../../screens/inspector_shared/inspector_screen.dart'; +import '../../screens/inspector/inspector_screen.dart'; import '../../screens/logging/logging_screen.dart'; import '../../screens/network/network_screen.dart'; import '../../screens/performance/performance_screen.dart'; diff --git a/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart b/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart index 440c2cc3d1b..7efdbcbfeb7 100644 --- a/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart @@ -4,25 +4,10 @@ part of 'preferences.dart'; -enum InspectorDetailsViewType { - layoutExplorer(nameOverride: 'Layout Explorer'), - widgetDetailsTree(nameOverride: 'Widget Details Tree'); - - const InspectorDetailsViewType({String? nameOverride}) - : _nameOverride = nameOverride; - - final String? _nameOverride; - - String get key => _nameOverride ?? name; -} - class InspectorPreferencesController extends DisposableController with AutoDisposeControllerMixin { ValueListenable get hoverEvalModeEnabled => _hoverEvalMode; - ValueListenable get legacyInspectorEnabled => _legacyInspectorEnabled; ValueListenable get autoRefreshEnabled => _autoRefreshEnabled; - ValueListenable get defaultDetailsView => - _defaultDetailsView; ListValueNotifier get pubRootDirectories => _pubRootDirectories; ValueListenable get isRefreshingPubRootDirectories => _pubRootDirectoriesAreBusy; @@ -30,21 +15,13 @@ class InspectorPreferencesController extends DisposableController serviceConnection.inspectorService; final _hoverEvalMode = ValueNotifier(false); - final _legacyInspectorEnabled = ValueNotifier(false); final _autoRefreshEnabled = ValueNotifier(true); final _pubRootDirectories = ListValueNotifier([]); final _pubRootDirectoriesAreBusy = ValueNotifier(false); final _busyCounter = ValueNotifier(0); - final _defaultDetailsView = ValueNotifier( - InspectorDetailsViewType.layoutExplorer, - ); static const _hoverEvalModeStorageId = 'inspector.hoverEvalMode'; - static const _legacyInspectorEnabledStorageId = - 'inspector.legacyInspectorEnabled'; static const _autoRefreshEnabledStorageId = 'inspector.autoRefreshEnabled'; - static const _defaultDetailsViewStorageId = - 'inspector.defaultDetailsViewType'; static const _customPubRootDirectoriesStoragePrefix = 'inspector.customPubRootDirectories'; @@ -83,11 +60,8 @@ class InspectorPreferencesController extends DisposableController @override Future init() async { await _initHoverEvalMode(); - await _initLegacyInspectorEnabled(); await _initAutoRefreshEnabled(); - // TODO(jacobr): consider initializing this first as it is not blocking. _initPubRootDirectories(); - await _initDefaultInspectorDetailsView(); } Future _initHoverEvalMode() async { @@ -98,16 +72,6 @@ class InspectorPreferencesController extends DisposableController ); } - Future _initLegacyInspectorEnabled() async { - // TODO(https://github.com/flutter/devtools/issues/8667): Consider removing - // the old 'inspector.inspectorV2Enabled' key if it is set. - await _updateLegacyInspectorEnabled(); - _saveBooleanPreferenceChanges( - preferenceStorageId: _legacyInspectorEnabledStorageId, - preferenceNotifier: _legacyInspectorEnabled, - ); - } - Future _initAutoRefreshEnabled() async { await _updateAutoRefreshEnabled(); _saveBooleanPreferenceChanges( @@ -124,14 +88,6 @@ class InspectorPreferencesController extends DisposableController ); } - Future _updateLegacyInspectorEnabled() async { - await _updateBooleanPreference( - preferenceStorageId: _legacyInspectorEnabledStorageId, - preferenceNotifier: _legacyInspectorEnabled, - defaultValue: false, - ); - } - Future _updateAutoRefreshEnabled() async { await _updateBooleanPreference( preferenceStorageId: _autoRefreshEnabledStorageId, @@ -164,31 +120,6 @@ class InspectorPreferencesController extends DisposableController preferenceNotifier.value = preferenceValue == 'true'; } - Future _initDefaultInspectorDetailsView() async { - await _updateInspectorDetailsViewSelection(); - - addAutoDisposeListener(_defaultDetailsView, () { - safeUnawaited( - storage.setValue( - _defaultDetailsViewStorageId, - _defaultDetailsView.value.name.toString(), - ), - ); - }); - } - - Future _updateInspectorDetailsViewSelection() async { - final inspectorDetailsView = await storage.getValue( - _defaultDetailsViewStorageId, - ); - - if (inspectorDetailsView != null) { - _defaultDetailsView.value = InspectorDetailsViewType.values.firstWhere( - (e) => e.name.toString() == inspectorDetailsView, - ); - } - } - void _initPubRootDirectories() { addAutoDisposeListener( serviceConnection.serviceManager.connectedState, @@ -245,9 +176,7 @@ class InspectorPreferencesController extends DisposableController _checkedFlutterPubRoot = false; await _updateMainScriptRef(); await _updateHoverEvalMode(); - await _updateLegacyInspectorEnabled(); await loadPubRootDirectories(); - await _updateInspectorDetailsViewSelection(); } Future loadPubRootDirectories() async { @@ -473,17 +402,8 @@ class InspectorPreferencesController extends DisposableController _hoverEvalMode.value = enableHoverEvalMode; } - @visibleForTesting - void setLegacyInspectorEnabled(bool legacyInspectorEnabled) { - _legacyInspectorEnabled.value = legacyInspectorEnabled; - } - @visibleForTesting void setAutoRefreshEnabled(bool autoRefreshEnabled) { _autoRefreshEnabled.value = autoRefreshEnabled; } - - void setDefaultInspectorDetailsView(InspectorDetailsViewType value) { - _defaultDetailsView.value = value; - } } diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart index 06ee38c5f8b..4583884eda0 100644 --- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart @@ -107,7 +107,7 @@ class PreferencesController extends DisposableController final _extensions = ExtensionsPreferencesController(); // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up after - // Inspector V2 has been released. + // Inspector has been released. InspectorPreferencesController get inspector => _inspector; final _inspector = InspectorPreferencesController(); diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj index ce7a071de3b..bd41a2cd64a 100644 --- a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */; }; + FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,6 +63,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -77,9 +81,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,6 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,12 +106,22 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2451268784E41F8223F03594 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */, + A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -127,6 +148,8 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, + 59A677840E39A9BEE994C28D /* Pods */, + 2451268784E41F8223F03594 /* Frameworks */, ); sourceTree = ""; }; @@ -175,6 +198,20 @@ path = Runner; sourceTree = ""; }; + 59A677840E39A9BEE994C28D /* Pods */ = { + isa = PBXGroup; + children = ( + D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */, + 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */, + 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */, + 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */, + B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */, + BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -182,6 +219,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -200,6 +238,7 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, @@ -330,6 +369,50 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -381,6 +464,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -395,6 +479,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -409,6 +494,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/packages/devtools_app/test/screens/inspector/diagnostics_test.dart b/packages/devtools_app/test/screens/inspector/diagnostics_test.dart index 1e1bb3e2457..97f24b660f7 100644 --- a/packages/devtools_app/test/screens/inspector/diagnostics_test.dart +++ b/packages/devtools_app/test/screens/inspector/diagnostics_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app/src/shared/ui/utils.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; @@ -46,6 +47,7 @@ void main() { } '''); setUp(() { + setEnableExperiments(); setGlobal( DevToolsEnvironmentParameters, ExternalDevToolsEnvironmentParameters(), diff --git a/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart b/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart index b546faae833..0d339af4902 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart @@ -5,6 +5,8 @@ import 'dart:collection'; import 'package:devtools_app/devtools_app.dart'; + +import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; @@ -14,6 +16,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { setUp(() { + setEnableExperiments(); setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); setGlobal(IdeTheme, IdeTheme()); }); diff --git a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart index 659959add0c..bedc41560de 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart @@ -2,92 +2,128 @@ // 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. +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:collection/collection.dart'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/inspector/layout_explorer/ui/utils.dart'; +import 'package:devtools_app/src/screens/inspector/widget_properties/properties_view.dart'; +import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/helpers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; import '../../test_infra/flutter_test_driver.dart' show FlutterRunConfiguration; import '../../test_infra/flutter_test_environment.dart'; import '../../test_infra/matchers/matchers.dart'; +// Note: This test uses packages/devtools_app/test/test_infra/fixtures/flutter_app +// running on the flutter-tester device. + // This is a bit conservative to ensure we do not get flakes due to // slow interactions with the VM Service. This delay could likely be // reduced to under 1 second without introducing flakes. const inspectorChangeSettleTime = Duration(seconds: 2); void main() { - const windowSize = Size(2600.0, 1200.0); // We need to use real async in this test so we need to use this binding. initializeLiveTestWidgetsFlutterBindingWithAssets(); + const windowSize = Size(2600.0, 1200.0); - late FlutterTestEnvironment env; + final env = FlutterTestEnvironment( + const FlutterRunConfiguration(withDebugger: true), + testAppDirectory: 'test/test_infra/fixtures/inspector_app', + ); - Future resetInspectorSelection() async { + env.afterEverySetup = () async { final service = serviceConnection.inspectorService; + await _resetPubRootDirectories(service as InspectorService); if (env.reuseTestEnvironment) { // Ensure the previous test did not set the selection on the device. // TODO(jacobr): add a proper method to WidgetInspectorService that does // this. setSelection currently ignores null selection requests which is // a misfeature. - await service!.inspectorLibrary.eval( + await service.inspectorLibrary.eval( 'WidgetInspectorService.instance.selection.clear()', isAlive: null, ); } - } + }; setUp(() async { await env.setupEnvironment(); setGlobal(BannerMessagesController, BannerMessagesController()); - // Ensure the legacy inspector is enabled: - preferences.inspector.setLegacyInspectorEnabled(true); }); - group('screenshot tests', () { - setUpAll(() { - env = FlutterTestEnvironment( - const FlutterRunConfiguration(withDebugger: true), - ); - env.afterEverySetup = resetInspectorSelection; - }); + tearDown(() async { + await env.tearDownEnvironment(force: true); + }); - tearDownAll(() async { - await env.tearDownEnvironment(force: true); - }); + tearDownAll(() { + env.finalTeardown(); + }); - testWidgetsWithWindowSize('navigation', windowSize, ( + group('screenshot tests', () { + testWidgetsWithWindowSize('initial load', windowSize, ( WidgetTester tester, ) async { - await env.setupEnvironment(); expect(serviceConnection.serviceManager.service, equals(env.service)); expect(serviceConnection.serviceManager.isolateManager, isNotNull); - final screen = InspectorScreen(); - await tester.pumpWidget( - wrapWithInspectorControllers(Builder(builder: screen.build)), - ); - await tester.pump(const Duration(seconds: 1)); - final InspectorScreenBodyState state = tester.state( - find.byType(InspectorScreenBody), - ); - final controller = state.controller; - while (!controller.flutterAppFrameReady) { - await controller.maybeLoadUI(); - await tester.pumpAndSettle(); - } - // Give time for the initial animation to complete. - await tester.pumpAndSettle(inspectorChangeSettleTime); + await _loadInspectorUI(tester); + await expectLater( find.byType(InspectorScreenBody), matchesDevToolsGolden( '../../test_infra/goldens/integration_inspector_initial_load.png', ), ); + }); + + testWidgetsWithWindowSize( + 'loads after a hot-restart', + windowSize, + (WidgetTester tester) async { + // Load the inspector panel. + await _loadInspectorUI(tester); + + // Expect the Center widget to be visible in the widget tree. + final centerWidgetFinder = find.richText('CustomCenter'); + expect(centerWidgetFinder, findsOneWidget); + + // Trigger a hot-restart and wait for the first Flutter frame. + await env.flutter!.hotRestart(); + await _waitForFlutterFrame(tester, isInitialLoad: false); + + // Wait for the Center widget to be visible again. + final centerWidgetFinderWithRetries = await retryUntilFound( + centerWidgetFinder, + tester: tester, + ); + expect(centerWidgetFinderWithRetries, findsOneWidget); + + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../../test_infra/goldens/integration_inspector_after_hot_restart.png', + ), + ); + }, + skip: true, // https://github.com/flutter/devtools/issues/8179 + ); + + testWidgetsWithWindowSize('widget selection', windowSize, ( + WidgetTester tester, + ) async { + await _loadInspectorUI(tester); - // Click on the Center widget (row index #5) - await tester.tap(find.richText('Center')); + // Select the CustomCenter widget (row index #4) + await tester.tap(find.richText('CustomCenter')); await tester.pumpAndSettle(inspectorChangeSettleTime); await expectLater( find.byType(InspectorScreenBody), @@ -96,349 +132,383 @@ void main() { ), ); - // Select the details tree. - await tester.tap( - find.text(InspectorDetailsViewType.widgetDetailsTree.key), + // Verify the properties are displayed: + verifyPropertyIsVisible( + name: 'alignment', + value: 'Alignment.center', + tester: tester, ); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_select_center_details_tree.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', + verifyPropertyIsVisible( + name: 'dependencies', + value: '[Directionality]', + tester: tester, ); + }); - // Select the RichText row. - await tester.tap(find.richText('RichText')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_richtext_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); + testWidgetsWithWindowSize( + 'expand and collapse implementation widgets', + windowSize, + (WidgetTester tester) async { + await _loadInspectorUI(tester); + + // Toggle implementation widgets on. + await _toggleImplementationWidgets(tester); + + // Before hidden widgets are expanded, confirm the implementing + // Container of CustomContainer is hidden: + final hideableNodeFinder = findNodeMatching('Container'); + expect(hideableNodeFinder, findsNothing); + + // Expand the hidden group that contains the Container: + final moreWidgetsRow = findChildRowOf('CustomContainer'); + final expandButton = findExpandCollapseButtonForRow( + rowFinder: moreWidgetsRow, + isExpand: true, + ); + await tester.tap(expandButton); + await tester.pumpAndSettle(inspectorChangeSettleTime); + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../../test_infra/goldens/integration_inspector_implementation_widgets_expanded.png', + ), + // Implementation widgets from Flutter framework are not guaranteed to + // be stable. + skip: 'https://github.com/flutter/flutter/issues/172037', + ); + + // Confirm the Container is visible, and select it: + expect(hideableNodeFinder, findsOneWidget); + await tester.tap(hideableNodeFinder); + await tester.pumpAndSettle(inspectorChangeSettleTime); + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../../test_infra/goldens/integration_inspector_hideable_widget_selected.png', + ), + // Implementation widgets from Flutter framework are not guaranteed to + // be stable. + skip: 'https://github.com/flutter/flutter/issues/172037', + ); + + // Collapse the hidden group that contains the Container: + final collapsibleRow = findChildRowOf('CustomContainer'); + final collapseButton = findExpandCollapseButtonForRow( + rowFinder: collapsibleRow, + isExpand: false, + ); + await tester.tap(collapseButton); + await tester.pumpAndSettle(inspectorChangeSettleTime); + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../../test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png', + ), + ); + }, + ); + + testWidgetsWithWindowSize('search for implementation widgets', windowSize, ( + WidgetTester tester, + ) async { + await _loadInspectorUI(tester); - // Test hovering over the icon shown when a property has its default - // value. - // TODO(jacobr): support tooltips in the Flutter version of the inspector. - // https://github.com/flutter/devtools/issues/2570. - // For example, verify that the tooltip hovering over the default value - // icons is "Default value". - // Test selecting a widget. - - // Two 'Scaffold's: a breadcrumb and an actual tree item - expect(find.richText('Scaffold'), findsNWidgets(2)); - // select Scaffold widget in summary tree. - await tester.tap(find.richText('Scaffold').last); - await tester.pumpAndSettle(inspectorChangeSettleTime); - // This tree is huge. If there is a change to package:flutter it may - // change. If this happens don't panic and rebaseline the golden. - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_scaffold_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); + // Toggle implementation widgets on. + await _toggleImplementationWidgets(tester); - // The important thing about this is that the details tree should scroll - // instead of re-rooting as the selected row is already visible in the - // details tree. - await tester.tap(find.richText('AnimatedPhysicalModel')); + // Before searching, confirm the implementing DefaultTextStyle of + // CustomApp is hidden: + final hideableNodeFinder = findNodeMatching('DefaultTextStyle'); + expect(hideableNodeFinder, findsNothing); + + // Search for the DefaultTextStyle: + final searchButtonFinder = find.ancestor( + of: find.byIcon(Icons.search), + matching: find.byType(ToolbarAction), + ); + await tester.tap(searchButtonFinder); + await tester.pumpAndSettle(inspectorChangeSettleTime); + await tester.enterText(find.byType(TextField), 'DefaultTextStyle'); await tester.pumpAndSettle(inspectorChangeSettleTime); + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Confirm the DefaultTextStyle is visible and selected: + expect(hideableNodeFinder, findsOneWidget); await expectLater( find.byType(InspectorScreenBody), matchesDevToolsGolden( - '../../test_infra/goldens/integration_animated_physical_model_selected.png', + '../../test_infra/goldens/integration_inspector_hideable_widget_selected_from_search.png', ), // Implementation widgets from Flutter framework are not guaranteed to // be stable. skip: 'https://github.com/flutter/flutter/issues/172037', ); - - await env.tearDownEnvironment(); }); + }); - // TODO(jacobr): convert these tests to screenshot tests like the initial - // state test. - /* - - - // Intentionally trigger multiple quick navigate action to ensure that - // multiple quick navigation commands in a row do not trigger race - // conditions getting out of order updates from the server. - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar <-- selected\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - // Make sure we don't go off the bottom of the tree. - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' [/icons/inspector/textArea.png]Text <-- selected\n', - ), - ); - tree.navigateUp(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar <-- selected\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - tree.navigateLeft(); - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar <-- selected\n', - ), - ); - tree.navigateLeft(); - // First navigate left goes to the parent. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold <-- selected\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); - tree.navigateLeft(); - // Next navigate left closes the parent. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▶[S]Scaffold <-- selected\n', - ), - ); + testWidgetsWithWindowSize('hide all implementation widgets', windowSize, ( + WidgetTester tester, + ) async { + await _loadInspectorUI(tester); + + // Toggle implementation widgets on. + await _toggleImplementationWidgets(tester); + + // Confirm the hidden widgets are visible behind affordances like "X more + // widgets". + expect(find.richTextContaining('more widgets...'), findsWidgets); + + // Toggle implementation widgets off. + await _toggleImplementationWidgets(tester); + + // Confirm that the hidden widgets are no longer visible. + expect(find.richTextContaining('more widgets...'), findsNothing); + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../../test_infra/goldens/integration_inspector_implementation_widgets_hidden.png', + ), + ); + + // Refresh the tree. + final refreshTreeButton = find.descendant( + of: find.byType(ToolbarAction), + matching: find.byIcon(Icons.refresh), + ); + + await tester.tap(refreshTreeButton); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Confirm that the hidden widgets are still not visible. + expect(find.richTextContaining('more widgets...'), findsNothing); + }); - tree.navigateRight(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold <-- selected\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); + // TODO(elliette): Expand into test group for cases when: + // - selected widget is implementation widget and implementation widgets are hidden (this test case) + // - selected widget is implementation widget and implementation widgets are visible + // - selected widget is not implementation widget and implementation widgets are hidden + // - selected widget is not implementation widget and implementation widgets are visible + testWidgetsWithWindowSize('selecting implementation widget', windowSize, ( + WidgetTester tester, + ) async { + // Load the Inspector. + await _loadInspectorUI(tester); + + // Toggle implementation widgets on. + await _toggleImplementationWidgets(tester); + + await tester.pumpAndSettle(inspectorChangeSettleTime); + final state = + tester.state(find.byType(InspectorScreenBody)) + as InspectorScreenBodyState; + + // Find the CustomText diagnostic node. + final diagnostics = state.controller.inspectorTree.rowsInTree.value.map( + (row) => row!.node.diagnostic, + ); + final customTextDiagnostic = diagnostics.firstWhere( + (d) => d?.description == 'CustomText', + )!; + expect(customTextDiagnostic.isCreatedByLocalProject, isTrue); + + // Toggle implementation widgets off. + await _toggleImplementationWidgets(tester); + + // Verify the CustomText diagnostic node is still in the tree. + final diagnosticsNow = state.controller.inspectorTree.rowsInTree.value.map( + (row) => row!.node.diagnostic, + ); + expect( + diagnosticsNow.any((d) => d?.valueRef == customTextDiagnostic.valueRef), + isTrue, + ); + + // Get the implementing Text child of the CustomText diagnostic node. + final service = serviceConnection.inspectorService as InspectorService; + final group = service.createObjectGroup('test-group'); + final customTextSubtree = await group.getDetailsSubtree( + customTextDiagnostic, + ); + final textDiagnostic = (await customTextSubtree!.children)!.firstWhere( + (child) => child.description == 'Text', + ); + + // Verify the Text child is an implementation node that is not in the tree. + expect(textDiagnostic.isCreatedByLocalProject, isFalse); + expect( + diagnosticsNow.any((d) => d?.valueRef == textDiagnostic.valueRef), + isFalse, + ); + + // Mimic selecting the Text diagnostic node with the on-device inspector. + await group.setSelectionInspector(textDiagnostic.valueRef, false); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Verify the CustomText node is now selected. + final selectedNode = state.controller.selectedNode.value; + expect( + selectedNode!.diagnostic!.valueRef, + equals(customTextDiagnostic.valueRef), + ); + + // Verify the notification about selecting an implementation widget is displayed. + expect( + find.text('Selected an implementation widget of CustomText: Text.'), + findsOneWidget, + ); + }); - // Node is already expanded so this is equivalent to navigate down. - tree.navigateRight(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); + testWidgetsWithWindowSize( + 'tree nodes contain only essential information', + windowSize, + (WidgetTester tester) async { + const requiredDetailsForTreeNode = [ + 'description', + 'shouldIndent', + 'valueId', + 'widgetRuntimeType', + ]; + const possibleDetailsForTreeNode = [ + 'textPreview', + 'children', + 'createdByLocalProject', + ]; + const extraneousDetailsForTreeNode = [ + 'creationLocation', + 'type', + 'style', + 'hasChildren', + 'stateful', + ]; + + await _loadInspectorUI(tester); + final state = + tester.state(find.byType(InspectorScreenBody)) + as InspectorScreenBodyState; + final rowsInTree = state.controller.inspectorTree.rowsInTree.value; + + for (final row in rowsInTree) { + final detailKeys = row?.node.diagnostic?.json.keys ?? const []; + expect( + requiredDetailsForTreeNode.every( + (detail) => detailKeys.contains(detail), + ), + isTrue, + ); + expect( + detailKeys.every( + (detail) => + requiredDetailsForTreeNode.contains(detail) || + possibleDetailsForTreeNode.contains(detail), + ), + isTrue, + ); + expect( + detailKeys.none( + (detail) => extraneousDetailsForTreeNode.contains(detail), + ), + isTrue, + ); + } + }, + ); - await detailsTree.nextUiFrame; + group('auto-refresh after code edits', () { + final flutterAppMainPath = p.join(env.testAppDirectory, 'lib', 'main.dart'); + String flutterMainContents = ''; - // Make sure the details and main trees have not gotten out of sync. - expect( - detailsTree.toStringDeep(hidePropertyLines: true), - equalsIgnoringHashCodes('▼[C]Center <-- selected\n' - '└─▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[/icons/inspector/textArea.png]RichText\n'), - ); + setUp(() { + // Save contents of main.dart file. + flutterMainContents = File(flutterAppMainPath).readAsStringSync(); - await env.tearDownEnvironment(); + // Enable auto-refresh. + preferences.inspector.setAutoRefreshEnabled(true); }); - */ - - // TODO(jacobr): uncomment hotReload test once the hot reload test is not - // flaky. https://github.com/flutter/devtools/issues/642 - /* - test('hotReload', () async { - if (flutterVersion == '1.2.1') { - // This test can be flaky in Flutter 1.2.1 because of - // https://github.com/dart-lang/sdk/issues/33838 - // so we just skip it. This block of code can be removed after the next - // stable flutter release. - // TODO(dantup): Remove this. - return; - } - await env.setupEnvironment(); - - await serviceManager.performHotReload(); - // Ensure the inspector does not fall over and die after a hot reload. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text <-- selected\n' - ' └─▼[A]AppBar\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - // TODO(jacobr): would be nice to have some tests that trigger a hot - // reload that actually changes app state in a meaningful way. + tearDown(() { + // Re-set contents of main.dart. + File( + flutterAppMainPath, + ).writeAsStringSync(flutterMainContents, flush: true); - await env.tearDownEnvironment(); + // Re-set changes to auto refresh. + preferences.inspector.setAutoRefreshEnabled(true); }); - */ - // TODO(jacobr): uncomment out the hotRestart tests once - // https://github.com/flutter/devtools/issues/337 is fixed. - /* - test('hotRestart', () async { - await env.setupEnvironment(); - - // The important thing about this is that the details tree should scroll - // instead of re-rooting as the selected row is already visible in the - // details tree. - simulateRowClick(tree, rowIndex: 4); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R]root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), - ); - /// After the hot restart some existing calls to the vm service may - /// timeout and that is ok. - serviceManager.manager.service.doNotWaitForPendingFuturesBeforeExit(); - - await serviceManager.performHotRestart(); - // The isolate starts out paused on a hot restart so we have to resume - // it manually to make the test pass. - - await serviceManager.manager.service - .resume(serviceManager.isolateManager.selectedIsolate.id); - - // First UI transition is to an empty tree. - await detailsTree.nextUiFrame; - expect(tree.toStringDeep(), equalsIgnoringHashCodes('\n')); - - // Notice that the selection has been lost due to the hot restart. - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), + void makeEditToFlutterMain({ + required String toReplace, + required String replaceWith, + }) { + final file = File(flutterAppMainPath); + final fileContents = file.readAsStringSync(); + file.writeAsStringSync( + fileContents.replaceAll(toReplace, replaceWith), + flush: true, ); + } - // Verify that the selection can actually be changed after a restart. - simulateRowClick(tree, rowIndex: 4); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), - ); - await env.tearDownEnvironment(); - }); -*/ + testWidgetsWithWindowSize( + 'changing parent widget of selected', + windowSize, + (WidgetTester tester) async { + await _loadInspectorUI(tester); + + // Toggle implementation widgets on. + await _toggleImplementationWidgets(tester); + + // Give time for the initial animation to complete. + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Verify the CustomButton widget is after the CustomCenter widget. + expect( + _treeRowsAreInOrder( + treeRowDescriptions: ['CustomCenter', 'CustomButton'], + startingAtIndex: 7, + ), + isTrue, + ); + + // Verify the CustomButton widget is not visible in the properties view. + expect(_findWidgetLabelMatching('CustomButton'), findsNothing); + + // Select the CustomButton widget. + await tester.tap(_findTreeRowMatching('CustomButton')); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Verify the CustomButton widget is now visible in the properties view. + expect(_findWidgetLabelMatching('CustomButton'), findsOneWidget); + + // Make edit to main.dart to replace CustomCenter with an Align. + makeEditToFlutterMain(toReplace: 'CustomCenter', replaceWith: 'Align'); + await env.flutter!.hotReload(); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Verify the Align is now in the widget tree instead of Center. + expect( + _treeRowsAreInOrder( + treeRowDescriptions: ['Align', 'CustomButton'], + startingAtIndex: 7, + ), + isTrue, + ); + + // Verify the CustomButton widget is still selected. + expect(_findWidgetLabelMatching('CustomButton'), findsOneWidget); + }, + ); }); group('widget errors', () { - setUpAll(() async { - env = FlutterTestEnvironment( - testAppDirectory: 'test/test_infra/fixtures/inspector_app', - const FlutterRunConfiguration(withDebugger: true), - ); + testWidgetsWithWindowSize('show navigator and error labels', windowSize, ( + WidgetTester tester, + ) async { await env.setupEnvironment( config: const FlutterRunConfiguration( withDebugger: true, entryScript: 'lib/overflow_errors.dart', ), ); - env.afterEverySetup = resetInspectorSelection; - // Enable the legacy inspector. - preferences.inspector.setLegacyInspectorEnabled(true); - }); - - testWidgetsWithWindowSize('show navigator and error labels', windowSize, ( - WidgetTester tester, - ) async { expect(serviceConnection.serviceManager.service, equals(env.service)); expect(serviceConnection.serviceManager.isolateManager, isNotNull); @@ -447,14 +517,8 @@ void main() { wrapWithInspectorControllers(Builder(builder: screen.build)), ); await tester.pumpAndSettle(const Duration(seconds: 1)); - final InspectorScreenBodyState state = tester.state( - find.byType(InspectorScreenBody), - ); - final controller = state.controller; - while (!controller.flutterAppFrameReady) { - await controller.maybeLoadUI(); - await tester.pumpAndSettle(); - } + await _waitForFlutterFrame(tester); + await env.flutter!.hotReload(); // Give time for the initial animation to complete. await tester.pumpAndSettle(inspectorChangeSettleTime); @@ -476,8 +540,168 @@ void main() { '../../test_infra/goldens/integration_inspector_errors_2_error_selected.png', ), ); - - await env.tearDownEnvironment(); }); }); } + +Future _toggleImplementationWidgets(WidgetTester tester) async { + // Tap the "Show Implementation Widgets" button (selected by default). + final showImplementationWidgetsButton = find.descendant( + of: find.byType(DevToolsToggleButton), + matching: find.text('Show Implementation Widgets'), + ); + expect(showImplementationWidgetsButton, findsOneWidget); + await tester.tap(showImplementationWidgetsButton); + await tester.pumpAndSettle(inspectorChangeSettleTime); +} + +Future _loadInspectorUI(WidgetTester tester) async { + final screen = InspectorScreen(); + await tester.pumpWidget( + wrapWithInspectorControllers(Builder(builder: screen.build)), + ); + await tester.pump(const Duration(seconds: 1)); + await _waitForFlutterFrame(tester); + + await tester.pumpAndSettle(inspectorChangeSettleTime); +} + +Future _waitForFlutterFrame( + WidgetTester tester, { + bool isInitialLoad = true, +}) async { + final state = + tester.state(find.byType(InspectorScreenBody)) + as InspectorScreenBodyState; + final controller = state.controller; + while (!controller.flutterAppFrameReady) { + // On the initial load, we might have instantiated the controller after the + // first Flutter frame was sent. In which case, calling `maybeLoadUI` is + // necessary to ensure we detect that the widget tree is ready. + if (isInitialLoad) { + await controller.maybeLoadUI(); + } + await tester.pump(safePumpDuration); + } +} + +Finder findNodeMatching(String text) => find.ancestor( + of: find.richText(text), + matching: find.byType(DescriptionDisplay), +); + +Finder findChildRowOf(String description) { + final parentRowFinder = _findTreeRowMatching(description); + final parentWidget = _getWidgetFromFinder( + parentRowFinder, + ); + final parentIndex = parentWidget.row.index; + + return find.byType(InspectorRowContent).at(parentIndex + 1); +} + +Finder findExpandCollapseButtonForRow({ + required Finder rowFinder, + required bool isExpand, +}) { + final expandCollapseButtonFinder = find.descendant( + of: rowFinder, + matching: find.byType(TextButton), + ); + expect(expandCollapseButtonFinder, findsOneWidget); + + final expandCollapseButtonTextFinder = find.descendant( + of: expandCollapseButtonFinder, + matching: find.text(isExpand ? '(expand)' : '(collapse)'), + ); + expect(expandCollapseButtonTextFinder, findsOneWidget); + + return expandCollapseButtonFinder; +} + +void verifyPropertyIsVisible({ + required String name, + required String value, + required WidgetTester tester, +}) { + // Verify the property name is visible: + final propertyNameFinder = find.descendant( + of: find.byType(PropertyName), + matching: find.text(name), + ); + expect(propertyNameFinder, findsOneWidget); + + // Verify the property value is visible: + final propertyValueFinder = find.descendant( + of: find.byType(PropertyValue), + matching: find.richText(value), + ); + expect(propertyValueFinder, findsOneWidget); + + // Verify the property name and value are aligned: + final propertyNameCenter = tester.getCenter(propertyNameFinder); + final propertyValueCenter = tester.getCenter(propertyValueFinder); + expect(propertyNameCenter.dy, equals(propertyValueCenter.dy)); +} + +bool areHorizontallyAligned( + Finder widgetAFinder, + Finder widgetBFinder, { + required WidgetTester tester, +}) { + final widgetACenter = tester.getCenter(widgetAFinder); + final widgetBCenter = tester.getCenter(widgetBFinder); + + return widgetACenter.dy == widgetBCenter.dy; +} + +bool _treeRowsAreInOrder({ + required List treeRowDescriptions, + required int startingAtIndex, +}) { + final treeRowIndices = []; + + for (final description in treeRowDescriptions) { + final treeRow = _getWidgetFromFinder( + _findTreeRowMatching(description), + ); + treeRowIndices.add(treeRow.row.index); + } + + int indexToCheck = startingAtIndex; + for (final index in treeRowIndices) { + if (index == indexToCheck) { + indexToCheck++; + } else { + return false; + } + } + return true; +} + +Finder _findTreeRowMatching(String description) => find.ancestor( + of: find.richText(description), + matching: find.byType(InspectorRowContent), +); + +Finder _findWidgetLabelMatching(String description) => find.ancestor( + of: find.richText(description), + matching: find.byType(WidgetLabel), +); + +T _getWidgetFromFinder(Finder finder) => + finder.first.evaluate().first.widget as T; + +Future _resetPubRootDirectories(InspectorService inspectorService) async { + final currentPubRootDirectories = await inspectorService + .getPubRootDirectories(); + if (currentPubRootDirectories != null) { + await inspectorService.removePubRootDirectories(currentPubRootDirectories); + } + + final rootLibrary = await serviceConnection.serviceManager + .mainIsolateRootLibraryUriAsString(); + if (rootLibrary != null) { + await inspectorService.addPubRootDirectories([rootLibrary]); + } +} diff --git a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart index 0a09554f33e..2423517c983 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart @@ -8,10 +8,12 @@ import 'dart:convert'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/inspector/inspector_settings_dialog.dart'; import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/screens/inspector/layout_explorer/layout_explorer.dart'; -import 'package:devtools_app/src/screens/inspector_shared/inspector_settings_dialog.dart'; +import 'package:devtools_app/src/screens/inspector/widget_details.dart'; import 'package:devtools_app/src/service/service_extensions.dart' as extensions; +import 'package:devtools_app/src/shared/feature_flags.dart'; +import 'package:devtools_app/src/shared/ui/tab.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; @@ -27,7 +29,7 @@ void main() { late FakeServiceConnectionManager fakeServiceConnection; late FakeServiceExtensionManager fakeExtensionManager; - const windowSize = Size(2600.0, 1200.0); + const windowSize = Size(3500.0, 1200.0); final debuggerController = createMockDebuggerControllerWithDefaults(); @@ -40,6 +42,7 @@ void main() { } setUp(() { + setEnableExperiments(); fakeServiceConnection = FakeServiceConnectionManager(); fakeExtensionManager = fakeServiceConnection.serviceManager.serviceExtensionManager; @@ -57,10 +60,7 @@ void main() { setGlobal(PreferencesController, PreferencesController()); setGlobal(Storage, FlutterTestStorage()); setGlobal(NotificationService, NotificationService()); - setGlobal(BannerMessagesController, BannerMessagesController()); fakeServiceConnection.consoleService.ensureServiceInitialized(); - // Enable the legacy inspector: - preferences.inspector.setLegacyInspectorEnabled(true); }); Future mockExtensions() async { @@ -100,7 +100,6 @@ void main() { WidgetTester tester, ) async { await tester.pumpWidget(buildInspectorScreen()); - await tester.pumpAndSettle(); expect(find.byType(InspectorScreenBody), findsOneWidget); }); @@ -304,12 +303,19 @@ void main() { 'should render StoryOfYourFlexWidget', windowSize, (WidgetTester tester) async { - final controller = TestInspectorController()..setSelectedNode(treeNode); + final controller = TestInspectorController() + ..setSelectedNode(treeNode) + ..setSelectedDiagnostic(diagnostic); await tester.pumpWidget( MaterialApp( - home: Scaffold(body: LayoutExplorerTab(controller: controller)), + home: Scaffold(body: WidgetDetails(controller: controller)), ), ); + + // Navigate to the flex explorer tab. + await tester.tap(_findFlexExplorerTab()); + await tester.pumpAndSettle(); + expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); }, ); @@ -321,11 +327,23 @@ void main() { final controller = TestInspectorController(); await tester.pumpWidget( MaterialApp( - home: Scaffold(body: LayoutExplorerTab(controller: controller)), + home: Scaffold(body: WidgetDetails(controller: controller)), ), ); - expect(find.byType(FlexLayoutExplorerWidget), findsNothing); - controller.setSelectedNode(treeNode); + + // Flex explorer is not available for selected wiget. + expect(_findFlexExplorerTab(), findsNothing); + + // Select a flex widget. + controller + ..setSelectedNode(treeNode) + ..setSelectedDiagnostic(diagnostic); + await tester.pumpAndSettle(); + + // Flex explorer is available for the flex widget. + final flexExplorerTab = _findFlexExplorerTab(); + expect(flexExplorerTab, findsOneWidget); + await tester.tap(flexExplorerTab); await tester.pumpAndSettle(); expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); }, @@ -346,8 +364,12 @@ void main() { await tester.pumpWidget(buildInspectorScreen()); await tester.tap(find.byType(SettingsOutlinedButton)); - await tester.pumpAndSettle(); - expect(find.byType(FlutterInspectorSettingsDialog), findsOneWidget); + + final settingsDialogFinder = await retryUntilFound( + find.byType(FlutterInspectorSettingsDialog), + tester: tester, + ); + expect(settingsDialogFinder, findsOneWidget); final hoverCheckBoxSetting = find.ancestor( of: find.richTextContaining('Enable hover inspection'), @@ -358,7 +380,7 @@ void main() { matching: find.byType(NotifierCheckbox), ); await tester.tap(hoverModeCheckBox); - await tester.pumpAndSettle(); + await tester.pump(safePumpDuration); expect( preferences.inspector.hoverEvalModeEnabled.value, !startingHoverEvalModeValue, @@ -366,9 +388,13 @@ void main() { }, ); }); - // TODO(jacobr): add screenshot tests that connect to a test application // in the same way the inspector_controller test does today and take golden // images. Alternately: support an offline inspector mode and add tests of // that mode which would enable faster tests that run as unittests. } + +Finder _findFlexExplorerTab() => find.descendant( + of: find.byType(DevToolsTab), + matching: find.text('Flex explorer'), +); diff --git a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart index 17d1b1238a3..feda195f583 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart @@ -3,7 +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/screens/inspector/inspector_breadcrumbs.dart'; + import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; @@ -37,7 +37,6 @@ void main() { inspectorController = InspectorController( inspectorTree: InspectorTreeController(), - detailsTree: InspectorTreeController(), treeType: FlutterTreeType.widget, )..firstInspectorTreeLoadCompleted = true; }); @@ -45,24 +44,17 @@ void main() { Future pumpInspectorTree( WidgetTester tester, { required InspectorTreeController treeController, - bool isSummaryTree = false, }) async { final debuggerController = DebuggerController(); - final summaryTreeController = isSummaryTree - ? null - : InspectorTreeController(); await tester.pumpWidget( wrapWithControllers( debugger: debuggerController, InspectorTree( controller: inspectorController, treeController: treeController, - summaryTreeController: summaryTreeController, - isSummaryTree: isSummaryTree, ), ), ); - await tester.pumpAndSettle(); } group('InspectorTreeController', () { @@ -76,21 +68,21 @@ void main() { ); await pumpInspectorTree(tester, treeController: treeController); - expect(treeController.getRow(const Offset(0, -100.0)), isNull); - expect(treeController.getRowOffset(-1), equals(0)); + expect(treeController.rowForOffset(const Offset(0, -100.0)), isNull); + expect(treeController.rowOffset(-1), equals(0)); - expect(treeController.getRow(const Offset(0, 0.0)), isNull); - expect(treeController.getRowOffset(0), equals(0)); + expect(treeController.rowForOffset(const Offset(0, 0.0)), isNull); + expect(treeController.rowOffset(0), equals(0)); treeController.root = InspectorTreeNode() ..appendChild(InspectorTreeNode()); await pumpInspectorTree(tester, treeController: treeController); - expect(treeController.getRow(const Offset(0, -20))!.index, 0); - expect(treeController.getRowOffset(-1), equals(0)); - expect(treeController.getRow(const Offset(0, 0.0)), isNotNull); - expect(treeController.getRowOffset(0), equals(0)); + expect(treeController.rowForOffset(const Offset(0, -20))!.index, 0); + expect(treeController.rowOffset(-1), equals(0)); + expect(treeController.rowForOffset(const Offset(0, 0.0)), isNotNull); + expect(treeController.rowOffset(0), equals(0)); // This operation would previously throw an exception in debug builds // and infinite loop in release builds. @@ -143,33 +135,5 @@ void main() { expect(find.richText('Text: "Multiline text content"'), findsOneWidget); }); - - testWidgets('Shows breadcrumbs in Widget detail tree', (tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Hello'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.byType(InspectorBreadcrumbNavigator), findsOneWidget); - }); - - testWidgets('Shows no breadcrumbs widget in summary tree', (tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Hello'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree( - tester, - treeController: treeController, - isSummaryTree: true, - ); - - expect(find.byType(InspectorBreadcrumbNavigator), findsNothing); - }); }); } diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart index a8b5264ff9c..22099522a70 100644 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart +++ b/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart @@ -5,7 +5,7 @@ import 'dart:convert'; import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree.dart'; +import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; import 'package:devtools_app/src/shared/diagnostics/diagnostics_node.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:devtools_test/helpers.dart'; diff --git a/packages/devtools_app/test/screens/inspector_v2/diagnostics_test.dart b/packages/devtools_app/test/screens/inspector_v2/diagnostics_test.dart deleted file mode 100644 index 97f24b660f7..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/diagnostics_test.dart +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2022 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:convert'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/shared/feature_flags.dart'; -import 'package:devtools_app/src/shared/ui/utils.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'; - -void main() { - group('DiagnosticsNodeDescription', () { - final renderObjectJson = jsonDecode(''' - { - "properties": [ - { - "description": "horizontal", - "name": "direction" - }, - { - "description": "start", - "name": "mainAxisAlignment" - }, - { - "description": "max", - "name": "mainAxisSize" - }, - { - "description": "center", - "name": "crossAxisAlignment" - }, - { - "description": "ltr", - "name": "textDirection" - }, - { - "description": "down", - "name": "verticalDirection" - } - ] - } - '''); - setUp(() { - setEnableExperiments(); - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); - }); - - group('hover eval', () { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - }; - final inspectorService = MockInspectorObjectGroupBase(); - final diagnostic = RemoteDiagnosticsNode( - nodeJson, - inspectorService, - false, - null, - ); - late DiagnosticsNodeDescription diagnosticsNodeDescription; - - setUp(() { - preferences.inspector.setHoverEvalMode(true); - diagnosticsNodeDescription = DiagnosticsNodeDescription(diagnostic); - }); - - testWidgets('can be enabled from preferences', ( - WidgetTester tester, - ) async { - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), true); - }); - - testWidgets('can be disabled from preferences', ( - WidgetTester tester, - ) async { - preferences.inspector.setHoverEvalMode(false); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), false); - }); - - testWidgets('disabled when inspector service not set', ( - WidgetTester tester, - ) async { - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), false); - }); - }); - - group('approximateNodeWidth', () { - const epsilon = 7.0; - testWidgets('property diagnostics node with name and description', ( - WidgetTester tester, - ) async { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - 'description': - 'this is a showname description, which will show up after the name', - 'showName': true, - 'name': 'THE NAME to be shown', - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - true, - null, - ); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // There are many rich texts, containg the name, and description. - final allRichTexts = find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .evaluate() - .map((e) => e.widget as RichText); - final measuredWidthOfAllRichTexts = allRichTexts.fold( - 0, - (previousValue, richText) => - previousValue + calculateTextSpanWidth(richText.text as TextSpan), - ); - expect( - approximatedWidth, - moreOrLessEquals(measuredWidthOfAllRichTexts, epsilon: epsilon), - ); - }); - - testWidgets('diagnostics node with icon and description', ( - WidgetTester tester, - ) async { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'description': 'This is the description', - 'children': [], - 'showName': false, - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedTextWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // The icon is part of the clickable width, so we include it. - final measuredIconWidth = tester - .getSize( - find.descendant( - of: diagnosticsNodeFind, - matching: find.byType(AssetImageIcon), - ), - ) - .width; - - // There is only one rich text widget, containing the description. - final richTextWidget = - find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .first - .evaluate() - .first - .widget - as RichText; - final measuredTextWidth = calculateTextSpanWidth( - richTextWidget.text as TextSpan, - ); - - expect( - approximatedTextWidth, - moreOrLessEquals( - measuredTextWidth + measuredIconWidth, - epsilon: epsilon, - ), - ); - }); - - testWidgets('error node with different fontSize', ( - WidgetTester tester, - ) async { - // Nodes with normal levels default to using the default fontSize, so - // using an error level node allows us to test different font sizes. - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - 'description': - 'this is a showname description, which will show up after the name', - 'showName': true, - 'name': 'THE NAME to be shown', - 'level': 'error', - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - - //Use a textStyle that is much larger than the normal style - const textStyle = TextStyle(fontSize: 24.0, fontFamily: 'Roboto'); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - style: textStyle, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // There are many rich texts, containg the name, and description. - final allRichTexts = find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .evaluate() - .map((e) => e.widget as RichText); - - final measuredWidthOfAllRichTexts = allRichTexts.fold(0, ( - previousValue, - richText, - ) { - final originalTextSpan = richText.text as TextSpan; - - return previousValue + calculateTextSpanWidth(originalTextSpan); - }); - - expect( - approximatedWidth, - moreOrLessEquals(measuredWidthOfAllRichTexts, epsilon: epsilon), - ); - }); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart deleted file mode 100644 index 6db56eb0c62..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2021 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:collection'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/shared/feature_flags.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'; - -void main() { - setUp(() { - setEnableExperiments(); - setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); - setGlobal(IdeTheme, IdeTheme()); - }); - - group('Inspector Error Navigator', () { - Future testNavigate( - WidgetTester tester, { - required IconData tapIcon, - required int errorCount, - int? startIndex, - int? expectedIndex, - }) async { - var index = startIndex; - final navigator = ErrorNavigator( - errorIndex: index, - errors: _generateErrors(errorCount), - onSelectError: (newIndex) => index = newIndex, - ); - - await tester.pumpWidget(wrap(navigator)); - await tester.tap(find.byIcon(tapIcon)); - - expect(index, equals(expectedIndex)); - } - - testWidgets('shows count when no selection', (WidgetTester tester) async { - await tester.pumpWidget( - wrap( - ErrorNavigator( - errorIndex: null, - errors: _generateErrors(10), - onSelectError: (_) {}, - ), - ), - ); - expect(find.text('Errors: 10'), findsOneWidget); - }); - - testWidgets('shows x/y when selected error', (WidgetTester tester) async { - await tester.pumpWidget( - wrap( - ErrorNavigator( - errorIndex: 0, - errors: _generateErrors(10), - onSelectError: (_) {}, - ), - ), - ); - expect(find.text('Error 1/10'), findsOneWidget); - }); - - testWidgets( - 'can navigate forwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_down, - errorCount: 10, - startIndex: 5, - expectedIndex: 6, - ), - ); - - testWidgets( - 'can navigate backwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_up, - errorCount: 10, - startIndex: 5, - expectedIndex: 4, - ), - ); - - testWidgets( - 'wraps forwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_down, - errorCount: 10, - startIndex: 9, - expectedIndex: 0, - ), - ); - - testWidgets( - 'wraps backwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_up, - errorCount: 10, - startIndex: 0, - expectedIndex: 9, - ), - ); - }); -} - -LinkedHashMap _generateErrors(int count) => - LinkedHashMap.fromEntries( - List.generate( - count, - (index) => MapEntry( - 'error-$index', - InspectableWidgetError('Error $index', 'error-$index'), - ), - ), - ); diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart deleted file mode 100644 index c9f902fdb60..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart +++ /dev/null @@ -1,803 +0,0 @@ -// Copyright 2020 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. - -@TestOn('vm') -library; - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:devtools_app/devtools_app.dart' - hide InspectorScreenBodyState, InspectorScreenBody, InspectorRowContent; -import 'package:devtools_app/src/screens/inspector/inspector_screen_body.dart' - as legacy; -import 'package:devtools_app/src/screens/inspector_shared/inspector_controls.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; -import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/ui/utils.dart'; -import 'package:devtools_app/src/screens/inspector_v2/widget_properties/properties_view.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; - -import '../../test_infra/flutter_test_driver.dart' show FlutterRunConfiguration; -import '../../test_infra/flutter_test_environment.dart'; -import '../../test_infra/matchers/matchers.dart'; - -// Note: This test uses packages/devtools_app/test/test_infra/fixtures/flutter_app -// running on the flutter-tester device. - -// This is a bit conservative to ensure we do not get flakes due to -// slow interactions with the VM Service. This delay could likely be -// reduced to under 1 second without introducing flakes. -const inspectorChangeSettleTime = Duration(seconds: 2); - -void main() { - // We need to use real async in this test so we need to use this binding. - initializeLiveTestWidgetsFlutterBindingWithAssets(); - const windowSize = Size(2600.0, 1200.0); - - final env = FlutterTestEnvironment( - const FlutterRunConfiguration(withDebugger: true), - testAppDirectory: 'test/test_infra/fixtures/inspector_app', - ); - - env.afterEverySetup = () async { - final service = serviceConnection.inspectorService; - await _resetPubRootDirectories(service as InspectorService); - if (env.reuseTestEnvironment) { - // Ensure the previous test did not set the selection on the device. - // TODO(jacobr): add a proper method to WidgetInspectorService that does - // this. setSelection currently ignores null selection requests which is - // a misfeature. - await service.inspectorLibrary.eval( - 'WidgetInspectorService.instance.selection.clear()', - isAlive: null, - ); - } - }; - - setUp(() async { - await env.setupEnvironment(); - // Enable the V2 inspector: - preferences.inspector.setLegacyInspectorEnabled(false); - setGlobal(BannerMessagesController, BannerMessagesController()); - }); - - tearDown(() async { - await env.tearDownEnvironment(force: true); - // Re-set changes to preferences: - preferences.inspector.setLegacyInspectorEnabled(true); - }); - - tearDownAll(() { - env.finalTeardown(); - }); - - group('screenshot tests', () { - testWidgetsWithWindowSize('initial load', windowSize, ( - WidgetTester tester, - ) async { - expect(serviceConnection.serviceManager.service, equals(env.service)); - expect(serviceConnection.serviceManager.isolateManager, isNotNull); - - await _loadInspectorUI(tester); - - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_initial_load.png', - ), - ); - }); - - testWidgetsWithWindowSize( - 'loads after a hot-restart', - windowSize, - (WidgetTester tester) async { - // Load the inspector panel. - await _loadInspectorUI(tester); - - // Expect the Center widget to be visible in the widget tree. - final centerWidgetFinder = find.richText('CustomCenter'); - expect(centerWidgetFinder, findsOneWidget); - - // Trigger a hot-restart and wait for the first Flutter frame. - await env.flutter!.hotRestart(); - await _waitForFlutterFrame(tester, isInitialLoad: false); - - // Wait for the Center widget to be visible again. - final centerWidgetFinderWithRetries = await retryUntilFound( - centerWidgetFinder, - tester: tester, - ); - expect(centerWidgetFinderWithRetries, findsOneWidget); - - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_after_hot_restart.png', - ), - ); - }, - skip: true, // https://github.com/flutter/devtools/issues/8179 - ); - - testWidgetsWithWindowSize('widget selection', windowSize, ( - WidgetTester tester, - ) async { - await _loadInspectorUI(tester); - - // Select the CustomCenter widget (row index #4) - await tester.tap(find.richText('CustomCenter')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_select_center.png', - ), - ); - - // Verify the properties are displayed: - verifyPropertyIsVisible( - name: 'alignment', - value: 'Alignment.center', - tester: tester, - ); - verifyPropertyIsVisible( - name: 'dependencies', - value: '[Directionality]', - tester: tester, - ); - }); - - testWidgetsWithWindowSize( - 'expand and collapse implementation widgets', - windowSize, - (WidgetTester tester) async { - await _loadInspectorUI(tester); - - // Toggle implementation widgets on. - await _toggleImplementationWidgets(tester); - - // Before hidden widgets are expanded, confirm the implementing - // Container of CustomContainer is hidden: - final hideableNodeFinder = findNodeMatching('Container'); - expect(hideableNodeFinder, findsNothing); - - // Expand the hidden group that contains the Container: - final moreWidgetsRow = findChildRowOf('CustomContainer'); - final expandButton = findExpandCollapseButtonForRow( - rowFinder: moreWidgetsRow, - isExpand: true, - ); - await tester.tap(expandButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - // Confirm the Container is visible, and select it: - expect(hideableNodeFinder, findsOneWidget); - await tester.tap(hideableNodeFinder); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - // Collapse the hidden group that contains the Container: - final collapsibleRow = findChildRowOf('CustomContainer'); - final collapseButton = findExpandCollapseButtonForRow( - rowFinder: collapsibleRow, - isExpand: false, - ); - await tester.tap(collapseButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png', - ), - ); - }, - ); - - testWidgetsWithWindowSize('search for implementation widgets', windowSize, ( - WidgetTester tester, - ) async { - await _loadInspectorUI(tester); - - // Toggle implementation widgets on. - await _toggleImplementationWidgets(tester); - - // Before searching, confirm the implementing DefaultTextStyle of - // CustomApp is hidden: - final hideableNodeFinder = findNodeMatching('DefaultTextStyle'); - expect(hideableNodeFinder, findsNothing); - - // Search for the DefaultTextStyle: - final searchButtonFinder = find.ancestor( - of: find.byIcon(Icons.search), - matching: find.byType(ToolbarAction), - ); - await tester.tap(searchButtonFinder); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await tester.enterText(find.byType(TextField), 'DefaultTextStyle'); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await tester.tap(find.byIcon(Icons.close)); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Confirm the DefaultTextStyle is visible and selected: - expect(hideableNodeFinder, findsOneWidget); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - }); - }); - - testWidgetsWithWindowSize('hide all implementation widgets', windowSize, ( - WidgetTester tester, - ) async { - await _loadInspectorUI(tester); - - // Toggle implementation widgets on. - await _toggleImplementationWidgets(tester); - - // Confirm the hidden widgets are visible behind affordances like "X more - // widgets". - expect(find.richTextContaining('more widgets...'), findsWidgets); - - // Toggle implementation widgets off. - await _toggleImplementationWidgets(tester); - - // Confirm that the hidden widgets are no longer visible. - expect(find.richTextContaining('more widgets...'), findsNothing); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png', - ), - ); - - // Refresh the tree. - final refreshTreeButton = find.descendant( - of: find.byType(ToolbarAction), - matching: find.byIcon(Icons.refresh), - ); - - await tester.tap(refreshTreeButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Confirm that the hidden widgets are still not visible. - expect(find.richTextContaining('more widgets...'), findsNothing); - }); - - // TODO(elliette): Expand into test group for cases when: - // - selected widget is implementation widget and implementation widgets are hidden (this test case) - // - selected widget is implementation widget and implementation widgets are visible - // - selected widget is not implementation widget and implementation widgets are hidden - // - selected widget is not implementation widget and implementation widgets are visible - testWidgetsWithWindowSize('selecting implementation widget', windowSize, ( - WidgetTester tester, - ) async { - // Load the Inspector. - await _loadInspectorUI(tester); - - // Toggle implementation widgets on. - await _toggleImplementationWidgets(tester); - - await tester.pumpAndSettle(inspectorChangeSettleTime); - final state = - tester.state(find.byType(InspectorScreenBody)) - as InspectorScreenBodyState; - - // Find the CustomText diagnostic node. - final diagnostics = state.controller.inspectorTree.rowsInTree.value.map( - (row) => row!.node.diagnostic, - ); - final customTextDiagnostic = diagnostics.firstWhere( - (d) => d?.description == 'CustomText', - )!; - expect(customTextDiagnostic.isCreatedByLocalProject, isTrue); - - // Toggle implementation widgets off. - await _toggleImplementationWidgets(tester); - - // Verify the CustomText diagnostic node is still in the tree. - final diagnosticsNow = state.controller.inspectorTree.rowsInTree.value.map( - (row) => row!.node.diagnostic, - ); - expect( - diagnosticsNow.any((d) => d?.valueRef == customTextDiagnostic.valueRef), - isTrue, - ); - - // Get the implementing Text child of the CustomText diagnostic node. - final service = serviceConnection.inspectorService as InspectorService; - final group = service.createObjectGroup('test-group'); - final customTextSubtree = await group.getDetailsSubtree( - customTextDiagnostic, - ); - final textDiagnostic = (await customTextSubtree!.children)!.firstWhere( - (child) => child.description == 'Text', - ); - - // Verify the Text child is an implementation node that is not in the tree. - expect(textDiagnostic.isCreatedByLocalProject, isFalse); - expect( - diagnosticsNow.any((d) => d?.valueRef == textDiagnostic.valueRef), - isFalse, - ); - - // Mimic selecting the Text diagnostic node with the on-device inspector. - await group.setSelectionInspector(textDiagnostic.valueRef, false); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the CustomText node is now selected. - final selectedNode = state.controller.selectedNode.value; - expect( - selectedNode!.diagnostic!.valueRef, - equals(customTextDiagnostic.valueRef), - ); - - // Verify the notification about selecting an implementation widget is displayed. - expect( - find.text('Selected an implementation widget of CustomText: Text.'), - findsOneWidget, - ); - }); - - testWidgetsWithWindowSize('can revert to legacy inspector', windowSize, ( - WidgetTester tester, - ) async { - await _loadInspectorUI(tester); - - // Select the CustomCenter widget (row index #4) - await tester.tap(find.richText('CustomCenter')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Disable Inspector V2: - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is visible: - await expectLater( - find.byType(legacy.InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_revert_to_legacy.png', - ), - ); - }); - - // Test to verify https://github.com/flutter/devtools/issues/8487 is fixed. - testWidgetsWithWindowSize( - 'revert to legacy inspector, hot-restart, and back to new inspector', - windowSize, - (WidgetTester tester) async { - await _loadInspectorUI(tester); - - // Disable Inspector V2. - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is visible. - expect(find.richTextContaining('Widget Details Tree'), findsOneWidget); - - // Trigger a hot restart. - await env.flutter!.hotRestart(); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Enable Inspector V2. - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is not visible. - expect(find.richTextContaining('Widget Details Tree'), findsNothing); - - // Wait for the widget tree to load. - final centerWidgetFinder = find.richText('Center'); - final centerWidgetFinderWithRetries = await retryUntilFound( - centerWidgetFinder, - tester: tester, - retries: 10, - ); - expect(centerWidgetFinderWithRetries, findsOneWidget); - }, - skip: true, // https://github.com/flutter/devtools/issues/8490 - ); - - testWidgetsWithWindowSize( - 'tree nodes contain only essential information', - windowSize, - (WidgetTester tester) async { - const requiredDetailsForTreeNode = [ - 'description', - 'shouldIndent', - 'valueId', - 'widgetRuntimeType', - ]; - const possibleDetailsForTreeNode = [ - 'textPreview', - 'children', - 'createdByLocalProject', - ]; - const extraneousDetailsForTreeNode = [ - 'creationLocation', - 'type', - 'style', - 'hasChildren', - 'stateful', - ]; - - await _loadInspectorUI(tester); - final state = - tester.state(find.byType(InspectorScreenBody)) - as InspectorScreenBodyState; - final rowsInTree = state.controller.inspectorTree.rowsInTree.value; - - for (final row in rowsInTree) { - final detailKeys = row?.node.diagnostic?.json.keys ?? const []; - expect( - requiredDetailsForTreeNode.every( - (detail) => detailKeys.contains(detail), - ), - isTrue, - ); - expect( - detailKeys.every( - (detail) => - requiredDetailsForTreeNode.contains(detail) || - possibleDetailsForTreeNode.contains(detail), - ), - isTrue, - ); - expect( - detailKeys.none( - (detail) => extraneousDetailsForTreeNode.contains(detail), - ), - isTrue, - ); - } - }, - ); - - group('auto-refresh after code edits', () { - final flutterAppMainPath = p.join(env.testAppDirectory, 'lib', 'main.dart'); - String flutterMainContents = ''; - - setUp(() { - // Save contents of main.dart file. - flutterMainContents = File(flutterAppMainPath).readAsStringSync(); - - // Enable auto-refresh. - preferences.inspector.setAutoRefreshEnabled(true); - }); - - tearDown(() { - // Re-set contents of main.dart. - File( - flutterAppMainPath, - ).writeAsStringSync(flutterMainContents, flush: true); - - // Re-set changes to auto refresh. - preferences.inspector.setAutoRefreshEnabled(true); - }); - - void makeEditToFlutterMain({ - required String toReplace, - required String replaceWith, - }) { - final file = File(flutterAppMainPath); - final fileContents = file.readAsStringSync(); - file.writeAsStringSync( - fileContents.replaceAll(toReplace, replaceWith), - flush: true, - ); - } - - testWidgetsWithWindowSize( - 'changing parent widget of selected', - windowSize, - (WidgetTester tester) async { - await _loadInspectorUI(tester); - - // Toggle implementation widgets on. - await _toggleImplementationWidgets(tester); - - // Give time for the initial animation to complete. - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the CustomButton widget is after the CustomCenter widget. - expect( - _treeRowsAreInOrder( - treeRowDescriptions: ['CustomCenter', 'CustomButton'], - startingAtIndex: 7, - ), - isTrue, - ); - - // Verify the CustomButton widget is not visible in the properties view. - expect(_findWidgetLabelMatching('CustomButton'), findsNothing); - - // Select the CustomButton widget. - await tester.tap(_findTreeRowMatching('CustomButton')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the CustomButton widget is now visible in the properties view. - expect(_findWidgetLabelMatching('CustomButton'), findsOneWidget); - - // Make edit to main.dart to replace CustomCenter with an Align. - makeEditToFlutterMain(toReplace: 'CustomCenter', replaceWith: 'Align'); - await env.flutter!.hotReload(); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the Align is now in the widget tree instead of Center. - expect( - _treeRowsAreInOrder( - treeRowDescriptions: ['Align', 'CustomButton'], - startingAtIndex: 7, - ), - isTrue, - ); - - // Verify the CustomButton widget is still selected. - expect(_findWidgetLabelMatching('CustomButton'), findsOneWidget); - }, - ); - }); - - group('widget errors', () { - testWidgetsWithWindowSize('show navigator and error labels', windowSize, ( - WidgetTester tester, - ) async { - await env.setupEnvironment( - config: const FlutterRunConfiguration( - withDebugger: true, - entryScript: 'lib/overflow_errors.dart', - ), - ); - expect(serviceConnection.serviceManager.service, equals(env.service)); - expect(serviceConnection.serviceManager.isolateManager, isNotNull); - - final screen = InspectorScreen(); - await tester.pumpWidget( - wrapWithInspectorControllers(Builder(builder: screen.build)), - ); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await _waitForFlutterFrame(tester); - - await env.flutter!.hotReload(); - // Give time for the initial animation to complete. - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png', - ), - ); - - // Navigate so one of the errors is selected. - for (var i = 0; i < 2; i++) { - await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); - await tester.pumpAndSettle(inspectorChangeSettleTime); - } - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png', - ), - ); - }); - }); -} - -Future _toggleImplementationWidgets(WidgetTester tester) async { - // Tap the "Show Implementation Widgets" button (selected by default). - final showImplementationWidgetsButton = find.descendant( - of: find.byType(DevToolsToggleButton), - matching: find.text('Show Implementation Widgets'), - ); - expect(showImplementationWidgetsButton, findsOneWidget); - await tester.tap(showImplementationWidgetsButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); -} - -Future _loadInspectorUI(WidgetTester tester) async { - final screen = InspectorScreen(); - await tester.pumpWidget( - wrapWithInspectorControllers(Builder(builder: screen.build)), - ); - await tester.pump(const Duration(seconds: 1)); - await _waitForFlutterFrame(tester); - - await tester.pumpAndSettle(inspectorChangeSettleTime); -} - -Future _waitForFlutterFrame( - WidgetTester tester, { - bool isInitialLoad = true, -}) async { - final state = - tester.state(find.byType(InspectorScreenBody)) - as InspectorScreenBodyState; - final controller = state.controller; - while (!controller.flutterAppFrameReady) { - // On the initial load, we might have instantiated the controller after the - // first Flutter frame was sent. In which case, calling `maybeLoadUI` is - // necessary to ensure we detect that the widget tree is ready. - if (isInitialLoad) { - await controller.maybeLoadUI(); - } - await tester.pump(safePumpDuration); - } -} - -Finder findNodeMatching(String text) => find.ancestor( - of: find.richText(text), - matching: find.byType(DescriptionDisplay), -); - -Finder findChildRowOf(String description) { - final parentRowFinder = _findTreeRowMatching(description); - final parentWidget = _getWidgetFromFinder( - parentRowFinder, - ); - final parentIndex = parentWidget.row.index; - - return find.byType(InspectorRowContent).at(parentIndex + 1); -} - -Finder findExpandCollapseButtonForRow({ - required Finder rowFinder, - required bool isExpand, -}) { - final expandCollapseButtonFinder = find.descendant( - of: rowFinder, - matching: find.byType(TextButton), - ); - expect(expandCollapseButtonFinder, findsOneWidget); - - final expandCollapseButtonTextFinder = find.descendant( - of: expandCollapseButtonFinder, - matching: find.text(isExpand ? '(expand)' : '(collapse)'), - ); - expect(expandCollapseButtonTextFinder, findsOneWidget); - - return expandCollapseButtonFinder; -} - -Future toggleLegacyInspector(WidgetTester tester) async { - // Open settings dialog. - final inspectorSettingsDialogButton = find.descendant( - of: find.byType(InspectorServiceExtensionButtonGroup), - matching: find.byType(SettingsOutlinedButton), - ); - await tester.tap(inspectorSettingsDialogButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Toggle the "legacy Inspector" checkbox. - final settingsRow = find.ancestor( - of: find.richTextContaining('Use legacy inspector'), - matching: find.byType(Row), - ); - final inspectorCheckbox = find.descendant( - of: settingsRow, - matching: find.byType(NotifierCheckbox), - ); - await tester.tap(inspectorCheckbox); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Close the settings dialog. - final closeButton = find.byType(DialogCloseButton); - await tester.tap(closeButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); -} - -void verifyPropertyIsVisible({ - required String name, - required String value, - required WidgetTester tester, -}) { - // Verify the property name is visible: - final propertyNameFinder = find.descendant( - of: find.byType(PropertyName), - matching: find.text(name), - ); - expect(propertyNameFinder, findsOneWidget); - - // Verify the property value is visible: - final propertyValueFinder = find.descendant( - of: find.byType(PropertyValue), - matching: find.richText(value), - ); - expect(propertyValueFinder, findsOneWidget); - - // Verify the property name and value are aligned: - final propertyNameCenter = tester.getCenter(propertyNameFinder); - final propertyValueCenter = tester.getCenter(propertyValueFinder); - expect(propertyNameCenter.dy, equals(propertyValueCenter.dy)); -} - -bool areHorizontallyAligned( - Finder widgetAFinder, - Finder widgetBFinder, { - required WidgetTester tester, -}) { - final widgetACenter = tester.getCenter(widgetAFinder); - final widgetBCenter = tester.getCenter(widgetBFinder); - - return widgetACenter.dy == widgetBCenter.dy; -} - -bool _treeRowsAreInOrder({ - required List treeRowDescriptions, - required int startingAtIndex, -}) { - final treeRowIndices = []; - - for (final description in treeRowDescriptions) { - final treeRow = _getWidgetFromFinder( - _findTreeRowMatching(description), - ); - treeRowIndices.add(treeRow.row.index); - } - - int indexToCheck = startingAtIndex; - for (final index in treeRowIndices) { - if (index == indexToCheck) { - indexToCheck++; - } else { - return false; - } - } - return true; -} - -Finder _findTreeRowMatching(String description) => find.ancestor( - of: find.richText(description), - matching: find.byType(InspectorRowContent), -); - -Finder _findWidgetLabelMatching(String description) => find.ancestor( - of: find.richText(description), - matching: find.byType(WidgetLabel), -); - -T _getWidgetFromFinder(Finder finder) => - finder.first.evaluate().first.widget as T; - -Future _resetPubRootDirectories(InspectorService inspectorService) async { - final currentPubRootDirectories = await inspectorService - .getPubRootDirectories(); - if (currentPubRootDirectories != null) { - await inspectorService.removePubRootDirectories(currentPubRootDirectories); - } - - final rootLibrary = await serviceConnection.serviceManager - .mainIsolateRootLibraryUriAsString(); - if (rootLibrary != null) { - await inspectorService.addPubRootDirectories([rootLibrary]); - } -} diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart deleted file mode 100644 index 813cc1eb0b9..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2019 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. - -// Fake construction requires number of unawaited calls. -// ignore_for_file: discarded_futures - -import 'dart:convert'; - -import 'package:devtools_app/devtools_app.dart' - hide - InspectorController, - InspectorTreeController, - InspectorScreenBody, - ErrorNavigator, - InspectorTreeNode; -import 'package:devtools_app/src/screens/inspector_shared/inspector_settings_dialog.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; -import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/screens/inspector_v2/widget_details.dart'; -import 'package:devtools_app/src/service/service_extensions.dart' as extensions; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; -import 'package:devtools_app/src/shared/feature_flags.dart'; -import 'package:devtools_app/src/shared/ui/tab.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' hide Fake; -import 'package:mockito/mockito.dart'; - -import '../../test_infra/flutter_test_storage.dart'; - -void main() { - final screen = InspectorScreen(); - - late FakeServiceConnectionManager fakeServiceConnection; - late FakeServiceExtensionManager fakeExtensionManager; - const windowSize = Size(2600.0, 1200.0); - - final debuggerController = createMockDebuggerControllerWithDefaults(); - - Widget buildInspectorScreen() { - return wrapWithControllers( - Builder(builder: screen.build), - debugger: debuggerController, - inspector: InspectorScreenController(), - ); - } - - setUp(() { - setEnableExperiments(); - fakeServiceConnection = FakeServiceConnectionManager(); - fakeExtensionManager = - fakeServiceConnection.serviceManager.serviceExtensionManager; - mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!); - when( - fakeServiceConnection.errorBadgeManager.errorCountNotifier('inspector'), - ).thenReturn(ValueNotifier(0)); - - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(ServiceConnectionManager, fakeServiceConnection); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(Storage, FlutterTestStorage()); - setGlobal(NotificationService, NotificationService()); - fakeServiceConnection.consoleService.ensureServiceInitialized(); - }); - - Future mockExtensions() async { - fakeExtensionManager.extensionValueOnDevice = { - extensions.toggleSelectWidgetMode.extension: true, - extensions.enableOnDeviceInspector.extension: true, - extensions.toggleOnDeviceWidgetInspector.extension: true, - extensions.debugPaint.extension: false, - }; - await fakeExtensionManager.fakeAddServiceExtension( - extensions.toggleOnDeviceWidgetInspector.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.toggleSelectWidgetMode.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.enableOnDeviceInspector.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.debugPaint.extension, - ); - await fakeExtensionManager.fakeFrame(); - } - - void mockNoExtensionsAvailable() { - fakeExtensionManager.extensionValueOnDevice = { - extensions.toggleOnDeviceWidgetInspector.extension: true, - extensions.toggleSelectWidgetMode.extension: false, - extensions.debugPaint.extension: false, - }; - // Don't actually send any events to the client indicating that service - // extensions are avaiable. - fakeExtensionManager.fakeFrame(); - } - - testWidgetsWithWindowSize('builds its tab', windowSize, ( - WidgetTester tester, - ) async { - await tester.pumpWidget(buildInspectorScreen()); - expect(find.byType(InspectorScreenBody), findsOneWidget); - }); - - group('Widget Errors', () { - // Display of error navigator/indicators is tested by a golden in - // inspector_integration_test.dart - - testWidgetsWithWindowSize( - 'does not render error navigator if no errors', - windowSize, - (WidgetTester tester) async { - await tester.pumpWidget(buildInspectorScreen()); - expect(find.byType(ErrorNavigator), findsNothing); - }, - ); - }); - - testWidgetsWithWindowSize('builds with no data', windowSize, ( - WidgetTester tester, - ) async { - // Make sure the window is wide enough to display description text. - - await tester.pumpWidget(buildInspectorScreen()); - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect(find.byTooltip('Refresh Tree'), findsOneWidget); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - // Make sure there is not an overflow if the window is narrow. - // TODO(jacobr): determine why there are overflows in the test environment - // but not on the actual device for this cae. - // await setWindowSize(const Size(1000.0, 1200.0)); - // Verify that description text is no-longer shown. - // expect(find.text(extensions.debugPaint.description), findsOneWidget); - }); - - testWidgetsWithWindowSize( - 'Test toggling service extension buttons', - windowSize, - (WidgetTester tester) async { - await mockExtensions(); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - await tester.pumpWidget(buildInspectorScreen()); - - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isTrue, - ); - - // We need a frame to find out that the service extension state has changed. - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect( - find.text(extensions.toggleSelectWidgetMode.title), - findsOneWidget, - ); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - await tester.pump(); - await tester.tap(find.text(extensions.toggleSelectWidgetMode.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isFalse, - ); - // Verify the other service extension's state hasn't changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - - await tester.tap(find.text(extensions.toggleSelectWidgetMode.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isTrue, - ); - - await tester.tap(find.text(extensions.debugPaint.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isTrue, - ); - }, - ); - - testWidgetsWithWindowSize( - 'Test toggling service extension buttons with no extensions available', - windowSize, - (WidgetTester tester) async { - mockNoExtensionsAvailable(); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - await tester.pumpWidget(buildInspectorScreen()); - await tester.pump(); - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - findsOneWidget, - ); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - await tester.pump(); - - await tester.tap( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - ); - // Verify the service extension state has not changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - await tester.tap( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - ); - // Verify the service extension state has not changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - // TODO(jacobr): also verify that the service extension buttons look - // visually disabled. - }, - ); - - group('LayoutDetailsTab', () { - final renderObjectJson = jsonDecode(''' - { - "properties": [ - { - "description": "horizontal", - "name": "direction" - }, - { - "description": "start", - "name": "mainAxisAlignment" - }, - { - "description": "max", - "name": "mainAxisSize" - }, - { - "description": "center", - "name": "crossAxisAlignment" - }, - { - "description": "ltr", - "name": "textDirection" - }, - { - "description": "down", - "name": "verticalDirection" - } - ] - } - '''); - final diagnostic = RemoteDiagnosticsNode( - { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - }, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - testWidgetsWithWindowSize( - 'should render StoryOfYourFlexWidget', - windowSize, - (WidgetTester tester) async { - final controller = TestInspectorV2Controller() - ..setSelectedNode(treeNode) - ..setSelectedDiagnostic(diagnostic); - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: WidgetDetails(controller: controller)), - ), - ); - - // Navigate to the flex explorer tab. - await tester.tap(_findFlexExplorerTab()); - await tester.pumpAndSettle(); - - expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); - }, - ); - - testWidgetsWithWindowSize( - 'should listen to controller selection event', - windowSize, - (WidgetTester tester) async { - final controller = TestInspectorV2Controller(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: WidgetDetails(controller: controller)), - ), - ); - - // Flex explorer is not available for selected wiget. - expect(_findFlexExplorerTab(), findsNothing); - - // Select a flex widget. - controller - ..setSelectedNode(treeNode) - ..setSelectedDiagnostic(diagnostic); - await tester.pumpAndSettle(); - - // Flex explorer is available for the flex widget. - final flexExplorerTab = _findFlexExplorerTab(); - expect(flexExplorerTab, findsOneWidget); - await tester.tap(flexExplorerTab); - await tester.pumpAndSettle(); - expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); - }, - ); - }); - - group('FlutterInspectorSettingsDialog', () { - const startingHoverEvalModeValue = false; - - setUp(() { - preferences.inspector.setHoverEvalMode(startingHoverEvalModeValue); - }); - - testWidgetsWithWindowSize( - 'can update hover inspection setting', - windowSize, - (WidgetTester tester) async { - await tester.pumpWidget(buildInspectorScreen()); - - await tester.tap(find.byType(SettingsOutlinedButton)); - - final settingsDialogFinder = await retryUntilFound( - find.byType(FlutterInspectorSettingsDialog), - tester: tester, - ); - expect(settingsDialogFinder, findsOneWidget); - - final hoverCheckBoxSetting = find.ancestor( - of: find.richTextContaining('Enable hover inspection'), - matching: find.byType(CheckboxSetting), - ); - final hoverModeCheckBox = find.descendant( - of: hoverCheckBoxSetting, - matching: find.byType(NotifierCheckbox), - ); - await tester.tap(hoverModeCheckBox); - await tester.pump(safePumpDuration); - expect( - preferences.inspector.hoverEvalModeEnabled.value, - !startingHoverEvalModeValue, - ); - }, - ); - }); - // TODO(jacobr): add screenshot tests that connect to a test application - // in the same way the inspector_controller test does today and take golden - // images. Alternately: support an offline inspector mode and add tests of - // that mode which would enable faster tests that run as unittests. -} - -Finder _findFlexExplorerTab() => find.descendant( - of: find.byType(DevToolsTab), - matching: find.text('Flex explorer'), -); diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart deleted file mode 100644 index 5a7c8966850..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2021 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' - hide - InspectorController, - InspectorTreeController, - InspectorTree, - InspectorTreeConfig, - InspectorTreeNode; -import 'package:devtools_app/src/screens/inspector_v2/inspector_controller.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.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' hide Fake; -import 'package:mockito/mockito.dart'; - -import 'utils/inspector_tree.dart'; - -void main() { - late FakeServiceConnectionManager fakeServiceConnection; - late InspectorController inspectorController; - - setUp(() { - fakeServiceConnection = FakeServiceConnectionManager(); - final app = fakeServiceConnection.serviceManager.connectedApp!; - when(app.isFlutterAppNow).thenReturn(true); - when(app.isProfileBuildNow).thenReturn(false); - - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(ServiceConnectionManager, fakeServiceConnection); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(NotificationService, NotificationService()); - setGlobal(BreakpointManager, BreakpointManager()); - mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!); - - inspectorController = InspectorController( - inspectorTree: InspectorTreeController(), - treeType: FlutterTreeType.widget, - )..firstInspectorTreeLoadCompleted = true; - }); - - Future pumpInspectorTree( - WidgetTester tester, { - required InspectorTreeController treeController, - }) async { - final debuggerController = DebuggerController(); - await tester.pumpWidget( - wrapWithControllers( - debugger: debuggerController, - InspectorTree( - controller: inspectorController, - treeController: treeController, - ), - ), - ); - } - - group('InspectorTreeController', () { - testWidgets('Row with negative index regression test', ( - WidgetTester tester, - ) async { - final treeController = InspectorTreeController() - ..config = InspectorTreeConfig( - onNodeAdded: (_, _) {}, - onClientActiveChange: (_) {}, - ); - await pumpInspectorTree(tester, treeController: treeController); - - expect(treeController.rowForOffset(const Offset(0, -100.0)), isNull); - expect(treeController.rowOffset(-1), equals(0)); - - expect(treeController.rowForOffset(const Offset(0, 0.0)), isNull); - expect(treeController.rowOffset(0), equals(0)); - - treeController.root = InspectorTreeNode() - ..appendChild(InspectorTreeNode()); - - await pumpInspectorTree(tester, treeController: treeController); - - expect(treeController.rowForOffset(const Offset(0, -20))!.index, 0); - expect(treeController.rowOffset(-1), equals(0)); - expect(treeController.rowForOffset(const Offset(0, 0.0)), isNotNull); - expect(treeController.rowOffset(0), equals(0)); - - // This operation would previously throw an exception in debug builds - // and infinite loop in release builds. - treeController.scrollToRect(const Rect.fromLTWH(0, -20, 100, 100)); - }); - }); - - group('Inspector tree content preview', () { - testWidgets('Shows simple text preview', (WidgetTester tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Content'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Content"'), findsOneWidget); - }); - - testWidgets('Shows preview from Text.rich', (WidgetTester tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text.rich( - TextSpan( - children: [ - TextSpan(text: 'Rich '), - TextSpan(text: 'text'), - ], - ), - ), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Rich text"'), findsOneWidget); - }); - - testWidgets('Strips new lines from text preview', ( - WidgetTester tester, - ) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Multiline\ntext\n\ncontent'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Multiline text content"'), findsOneWidget); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/arrow_test.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/arrow_test.dart deleted file mode 100644 index 587958b1752..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/arrow_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2019 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/src/screens/inspector_v2/layout_explorer/ui/arrow.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../../../test_infra/matchers/matchers.dart'; - -void main() { - const relativeGoldenPath = - '../../../../test_infra/goldens/inspector_v2/layout_explorer/flex'; - - group('Arrow Golden Tests', () { - group('Unidirectional', () { - Widget buildUnidirectionalArrowWrapper(ArrowType type) => Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 100, - height: 100, - child: ArrowWrapper.unidirectional( - type: type, - arrowColor: Colors.black, - arrowHeadSize: 8.0, - child: Container(width: 10, height: 10, color: Colors.red), - ), - ), - ); - testWidgets('left', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.left); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_left.png', - ), - ); - }, skip: kIsWeb); - testWidgets('up', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.up); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_up.png', - ), - ); - }, skip: kIsWeb); - testWidgets('right', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.right); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_right.png', - ), - ); - }, skip: kIsWeb); - testWidgets('down', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.down); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_down.png', - ), - ); - }, skip: kIsWeb); - }); - - group('Bidirectional', () { - Widget buildBidirectionalArrowWrapper(Axis direction) => Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 100, - height: 100, - child: ArrowWrapper.bidirectional( - direction: direction, - arrowColor: Colors.black, - arrowHeadSize: 8.0, - child: Container(width: 10, height: 10, color: Colors.red), - ), - ), - ); - testWidgets('horizontal', (WidgetTester tester) async { - final widget = buildBidirectionalArrowWrapper(Axis.horizontal); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_bidirectional_horizontal.png', - ), - ); - }, skip: kIsWeb); - testWidgets('vertical', (WidgetTester tester) async { - final widget = buildBidirectionalArrowWrapper(Axis.vertical); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_bidirectional_vertical.png', - ), - ); - }, skip: kIsWeb); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart deleted file mode 100644 index 5f4ea4a2fbf..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2019 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:convert'; - -import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; -import 'package:devtools_app/src/shared/diagnostics/diagnostics_node.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 '../../../../test_infra/matchers/matchers.dart'; - -// TODO(albertusangga): Re-enable tests in this files -// https://github.com/flutter/devtools/issues/1403 -void main() { - const windowSize = Size(1750, 1750); - const relativeGoldenPath = - '../../../../test_infra/goldens/inspector_v2/layout_explorer/flex'; - - Map buildDiagnosticsNodeJson(Axis axis) => jsonDecode(''' - { - "description": "${axis == Axis.horizontal ? 'Row' : 'Column'}", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267513", - "valueId": "inspector-251", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(w=300.0, h=60.0)", - "minWidth": "300.0", - "minHeight": "60.0", - "maxHeight": "60.0", - "maxWidth": "300.0" - }, - "size": { - "width": "300.0", - "height": "60.0" - }, - "isFlex": true, - "children": [ - { - "description": "Container", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267524", - "valueId": "inspector-269", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(0.0<=w<=Infinity, 0.0<=h<=56.0)", - "minWidth": "0.0", - "minHeight": "0.0", - "maxHeight": "56.0", - "maxWidth": "Infinity" - }, - "size": { - "width": "56.0", - "height": "25.0" - }, - "flexFactor": null, - "createdByLocalProject": true, - "children": [], - "widgetRuntimeType": "Container", - "stateful": false - }, - { - "description": "Expanded", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267563", - "valueId": "inspector-332", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(w=40.0, 0.0<=h<=56.0)", - "minWidth": "40.0", - "minHeight": "0.0", - "maxHeight": "56.0", - "maxWidth": "40.0" - }, - "size": { - "width": "40.0", - "height": "31.0" - }, - "flexFactor": 1, - "createdByLocalProject": true, - "children": [], - "widgetRuntimeType": "Expanded" - } - ], - "widgetRuntimeType": "${axis == Axis.horizontal ? 'Row' : 'Column'}", - "renderObject": { - "description": "RenderFlex#6cfb1 relayoutBoundary=up5", - "type": "DiagnosticableTreeNode", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-3758", - "valueId": "inspector-118", - "summaryTree": true, - "properties": [ - { - "description": " (can use size)", - "type": "DiagnosticsProperty", - "name": "parentData", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3759", - "valueId": "inspector-120", - "summaryTree": true, - "properties": [], - "ifNull": "MISSING", - "tooltip": "can use size", - "missingIfNull": true, - "propertyType": "ParentData", - "defaultLevel": "info" - }, - { - "description": "${axis.name}", - "type": "EnumProperty", - "name": "direction", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3762", - "valueId": "inspector-126", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "Axis", - "defaultLevel": "info" - }, - { - "description": "start", - "type": "EnumProperty", - "name": "mainAxisAlignment", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3763", - "valueId": "inspector-128", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "MainAxisAlignment", - "defaultLevel": "info" - }, - { - "description": "max", - "type": "EnumProperty", - "name": "mainAxisSize", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3764", - "valueId": "inspector-130", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "MainAxisSize", - "defaultLevel": "info" - }, - { - "description": "center", - "type": "EnumProperty", - "name": "crossAxisAlignment", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3765", - "valueId": "inspector-132", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "CrossAxisAlignment", - "defaultLevel": "info" - }, - { - "description": "ltr", - "type": "EnumProperty", - "name": "textDirection", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3766", - "valueId": "inspector-83", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "TextDirection", - "defaultLevel": "info" - }, - { - "description": "down", - "type": "EnumProperty", - "name": "verticalDirection", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3767", - "valueId": "inspector-135", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "VerticalDirection", - "defaultLevel": "info" - }, - { - "description": "alphabetic", - "type": "EnumProperty", - "name": "textBaseline", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3767", - "valueId": "inspector-135", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "TextBaseline", - "defaultLevel": "info" - } - ] - } - } - '''); - - Widget wrap(Widget widget) { - return MaterialApp(home: Scaffold(body: widget)); - } - - /// current workaround for flaky image asset testing. - /// https://github.com/flutter/flutter/issues/38997 - Future pump(WidgetTester tester, Widget w) async { - await tester.runAsync(() async { - await tester.pumpWidget(w); - for (final element in find.byType(Image).evaluate()) { - final widget = element.widget as Image; - final image = widget.image; - await precacheImage(image, element); - await tester.pumpAndSettle(); - } - }); - } - - testWidgetsWithWindowSize('Row golden test', windowSize, ( - WidgetTester tester, - ) async { - final rowWidgetJsonNode = buildDiagnosticsNodeJson(Axis.horizontal); - final diagnostic = RemoteDiagnosticsNode( - rowWidgetJsonNode, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorV2Controller()..setSelectedNode(treeNode); - final widget = wrap(FlexLayoutExplorerWidget(controller)); - await pump(tester, widget); - await tester.pumpAndSettle(); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden('$relativeGoldenPath/story_of_row_layout.png'), - ); - }, skip: true); - - testWidgetsWithWindowSize('Column golden test', windowSize, ( - WidgetTester tester, - ) async { - final columnWidgetJsonNode = buildDiagnosticsNodeJson(Axis.vertical); - final diagnostic = RemoteDiagnosticsNode( - columnWidgetJsonNode, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorV2Controller()..setSelectedNode(treeNode); - final widget = wrap(FlexLayoutExplorerWidget(controller)); - await pump(tester, widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden('$relativeGoldenPath/story_of_column_layout.png'), - ); - }, skip: true); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/inspector_data_models_test.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/inspector_data_models_test.dart deleted file mode 100644 index 6c8302d73df..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/inspector_data_models_test.dart +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright 2019 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/src/screens/inspector_v2/inspector_data_models.dart'; -import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/ui/theme.dart'; -import 'package:devtools_app/src/shared/primitives/math_utils.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'layout_explorer_test_utils.dart'; - -void main() { - setGlobal(IdeTheme, IdeTheme()); - - group('FlexLayoutProperties tests', () { - Future toFlexLayoutProperties( - Flex flex, { - required WidgetTester tester, - int subtreeDepth = 2, - double? width, - double? height, - }) async { - final wrappedWidget = SizedBox(width: width, height: height, child: flex); - final rootNodeDiagnostics = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: wrappedWidget, - tester: tester, - subtreeDepth: subtreeDepth, - ); - final flexDiagnostics = rootNodeDiagnostics.childrenNow.first; - return FlexLayoutProperties.fromDiagnostics(flexDiagnostics); - } - - testWidgets( - 'FlexLayoutProperties.fromJson creates correct value from enum', - (tester) async { - const widget = Row( - textDirection: TextDirection.ltr, - children: [SizedBox()], - ); - final flexProperties = await toFlexLayoutProperties( - widget, - tester: tester, - ); - expect(flexProperties.direction, Axis.horizontal); - expect(flexProperties.mainAxisAlignment, MainAxisAlignment.start); - expect(flexProperties.mainAxisSize, MainAxisSize.max); - expect(flexProperties.crossAxisAlignment, CrossAxisAlignment.center); - expect(flexProperties.textDirection, TextDirection.ltr); - expect(flexProperties.verticalDirection, VerticalDirection.down); - expect(flexProperties.textBaseline, null); - }, - ); - - testWidgets('startIsTopLeft should return false', (tester) async { - const columnWidget = Column( - verticalDirection: VerticalDirection.up, - children: [SizedBox()], - ); - final columnProperties = await toFlexLayoutProperties( - columnWidget, - tester: tester, - ); - expect(columnProperties.startIsTopLeft, false); - - const rowWidget = Row( - textDirection: TextDirection.rtl, - children: [SizedBox()], - ); - final rowProperties = await toFlexLayoutProperties( - rowWidget, - tester: tester, - ); - expect(rowProperties.startIsTopLeft, false); - }); - - testWidgets( - 'displayChildren is the same as children when start is top left', - (tester) async { - final widget = Column(children: [const SizedBox(), Container()]); - final properties = await toFlexLayoutProperties(widget, tester: tester); - expect(properties.startIsTopLeft, true); - expect(properties.displayChildren[0].description, 'SizedBox'); - expect(properties.displayChildren[1].description, 'Container'); - }, - ); - - testWidgets( - 'displayChildren is a reversed children when start is not top left', - (tester) async { - final widget = Column( - verticalDirection: VerticalDirection.up, - children: [const SizedBox(), Container()], - ); - final properties = await toFlexLayoutProperties(widget, tester: tester); - expect(properties.startIsTopLeft, false); - expect(properties.displayChildren[0].description, 'Container'); - expect(properties.displayChildren[1].description, 'SizedBox'); - }, - ); - - group('childrenRenderProperties tests', () { - const maxMainAxisDimension = 500.0; - - double maxSizeAvailable(Axis _) => maxMainAxisDimension; - - List childrenRenderProperties( - FlexLayoutProperties properties, - ) => properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - - final childrenWidgets = [ - const SizedBox(width: 50.0), - const SizedBox(width: 75.0, height: 25.0), - ]; - - testWidgets( - 'returns correct RenderProperties with main axis not flipped when start is top left', - (tester) async { - final widget = Row(children: childrenWidgets); - final properties = await toFlexLayoutProperties( - widget, - width: maxMainAxisDimension, - tester: tester, - subtreeDepth: 3, - ); - final renderProps = properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - expect(renderProps.length, 3); - expect(renderProps, [ - RenderProperties( - axis: Axis.horizontal, - size: const Size(250, 250), - realSize: const Size(50.0, 0.0), - offset: const Offset(0.0, 125.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(261.5, 500), - realSize: const Size(75.0, 25.0), - offset: const Offset(250.0, 0.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(400, 500), - realSize: const Size(375.0, 25.0), - offset: const Offset(511.5, 0.0), - isFreeSpace: true, - layoutProperties: properties, - ), - ]); - }, - ); - - testWidgets( - 'returns correct RenderProperties with main axis flipped when start is not top left', - (tester) async { - final widget = Row( - textDirection: TextDirection.rtl, - children: childrenWidgets, - ); - final properties = await toFlexLayoutProperties( - widget, - tester: tester, - width: maxMainAxisDimension, - subtreeDepth: 3, - ); - final renderProps = properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - expect(renderProps.length, 3); - expect(renderProps, [ - RenderProperties( - axis: Axis.horizontal, - size: const Size(261.5, 500.0), - realSize: const Size(75.0, 25.0), - offset: const Offset(400.0, 0.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(250.0, 250.0), - realSize: const Size(50.0, 0.0), - offset: const Offset(661.5, 125.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(400, 500), - realSize: const Size(375.0, 25.0), - offset: const Offset(0.0, 0.0), - isFreeSpace: true, - layoutProperties: properties, - ), - ]); - }, - ); - - testWidgets( - 'when the start is not top left, render properties should be equals to its mirrored version', - (tester) async { - Row buildWidget({ - required bool flipMainAxis, - required MainAxisAlignment mainAxisAlignment, - }) => Row( - textDirection: flipMainAxis ? TextDirection.rtl : TextDirection.ltr, - mainAxisAlignment: flipMainAxis - ? mainAxisAlignment.reversed - : mainAxisAlignment, - children: flipMainAxis - ? childrenWidgets.reversed.toList() - : childrenWidgets, - ); - for (final mainAxisAlignment in MainAxisAlignment.values) { - final originalWidgetRenderProperties = childrenRenderProperties( - await toFlexLayoutProperties( - buildWidget( - flipMainAxis: false, - mainAxisAlignment: mainAxisAlignment, - ), - tester: tester, - ), - ); - final mirroredWidgetRenderProperties = childrenRenderProperties( - await toFlexLayoutProperties( - buildWidget( - flipMainAxis: true, - mainAxisAlignment: mainAxisAlignment, - ), - tester: tester, - ), - ); - expect( - originalWidgetRenderProperties, - mirroredWidgetRenderProperties, - ); - } - }, - ); - }); - }); - - group('LayoutProperties tests', () { - testWidgets('deserializes RemoteDiagnosticsNode correctly', (tester) async { - const constraints = BoxConstraints( - minWidth: 432.0, - maxWidth: 432.0, - minHeight: 56.0, - maxHeight: 56.0, - ); - const size = Size(432.0, 56.0); - final widget = Container( - width: size.width, - height: size.height, - constraints: constraints, - child: const Row(children: [SizedBox()]), - ); - final diagnosticsNode = await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - subtreeDepth: 2, - ); - final rowDiagnosticsNode = diagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(rowDiagnosticsNode); - - expect(layoutProperties.size, size); - expect(layoutProperties.constraints, constraints); - expect(layoutProperties.totalChildren, 1); - }); - - group('describeWidthConstraints and describeHeightConstraints', () { - testWidgets('single value', (tester) async { - const width = 25.0; - const height = 56.0; - const constraints = BoxConstraints.tightFor( - width: width, - height: height, - ); - final widget = ConstrainedBox( - constraints: constraints, - child: const SizedBox(), - ); - final constrainedBoxDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final sizedBoxDiagnosticsNode = - constrainedBoxDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(sizedBoxDiagnosticsNode); - expect(layoutProperties.describeHeightConstraints(), 'h=$height'); - expect(layoutProperties.describeWidthConstraints(), 'w=$width'); - }); - - testWidgets('range value', (tester) async { - const minWidth = 25.0, maxWidth = 50.0; - const minHeight = 75.0, maxHeight = 100.0; - const constraints = BoxConstraints( - minWidth: minWidth, - maxWidth: maxWidth, - minHeight: minHeight, - maxHeight: maxHeight, - ); - final widget = ConstrainedBox( - constraints: constraints, - child: const SizedBox(), - ); - final constrainedBoxDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final sizedBoxDiagnosticsNode = - constrainedBoxDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(sizedBoxDiagnosticsNode); - expect( - layoutProperties.describeHeightConstraints(), - '$minHeight<=h<=$maxHeight', - ); - expect( - layoutProperties.describeWidthConstraints(), - '$minWidth<=w<=$maxWidth', - ); - }); - - testWidgets('unconstrained width', (tester) async { - final widget = Row(children: [Container()]); - final rowDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerDiagnosticsNode = rowDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerDiagnosticsNode); - expect( - layoutProperties.describeWidthConstraints(), - 'width is unconstrained', - ); - }); - - testWidgets('unconstrained height', (tester) async { - final widget = Column(children: [Container()]); - final columnDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerDiagnosticsNode = - columnDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerDiagnosticsNode); - expect( - layoutProperties.describeHeightConstraints(), - 'height is unconstrained', - ); - }); - }); - - testWidgets('describeWidth and describeHeight', (tester) async { - const width = 432.5, height = 56.0; - final widget = SizedBox(width: width, height: height, child: Container()); - final sizedBoxNode = await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerNode = sizedBoxNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerNode); - expect(layoutProperties.describeHeight(), 'h=$height'); - expect(layoutProperties.describeWidth(), 'w=$width'); - }); - }); - - group('computeRenderSizes', () { - test( - 'scale sizes so the largestSize maps to largestRenderSize with forceToOccupyMaxSize=false', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 200.0, - largestRenderSize: 600.0, - maxSizeAvailable: 2000, - useMaxSizeAvailable: false, - ); - expect(renderSizes, [200.0, 400.0, 600.0]); - expect(sum(renderSizes), lessThan(2000)); - }, - ); - - test( - 'scale sizes so the items fit maxSizeAvailable with forceToOccupyMaxSize=true', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 200.0, - largestRenderSize: 600.0, - maxSizeAvailable: 2000, - ); - expect(renderSizes, [200.0, 666.6666666666667, 1133.3333333333335]); - expect(sum(renderSizes) - 2000.0, lessThan(0.01)); - }, - ); - - test( - 'scale sizes when the items exceeds maxSizeAvailable with forceToOccupyMaxSize=true should not change any behavior', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 300.0, - largestRenderSize: 900.0, - maxSizeAvailable: 250.0, - ); - expect(renderSizes, [300.0, 600.0, 900.0]); - expect(sum(renderSizes), greaterThan(250.0)); - }, - ); - }); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_serialization_delegate.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_serialization_delegate.dart deleted file mode 100644 index 3f1b6091ea3..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_serialization_delegate.dart +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2020 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/rendering.dart'; -import 'package:flutter/widgets.dart'; - -/// This class is merely a duplicate of the InspectorSerializationDelegate inside the `assets/scripts/inspector_polyfill_script.dart`. -/// This delegate is used for testing Widget Diagnostics Node so that we don't have to create manual JSON each time we want to create new test cases. -/// TODO(adalberht): Ask Jacob on what's the better solution on code reuse between the Layout Explorer polyfillscripts and the code inside test package? -class LayoutExplorerSerializationDelegate - extends InspectorSerializationDelegate { - LayoutExplorerSerializationDelegate({ - String super.groupName = '', - super.subtreeDepth, - required super.service, - }) : super( - summaryTree: true, - addAdditionalPropertiesCallback: (node, delegate) { - final additionalJson = {}; - final value = node.value; - if (value is Element) { - final renderObject = value.renderObject!; - additionalJson['renderObject'] = renderObject - .toDiagnosticsNode() - .toJsonMap( - delegate.copyWith(subtreeDepth: 0, includeProperties: true), - ); - // Required for test. - // ignore: invalid_use_of_protected_member - final constraints = renderObject.constraints; - - final constraintsProperty = { - 'type': constraints.runtimeType.toString(), - 'description': constraints.toString(), - }; - if (constraints is BoxConstraints) { - constraintsProperty.addAll({ - 'minWidth': constraints.minWidth.toString(), - 'minHeight': constraints.minHeight.toString(), - 'maxWidth': constraints.maxWidth.toString(), - 'maxHeight': constraints.maxHeight.toString(), - }); - } - additionalJson['constraints'] = constraintsProperty; - - if (renderObject is RenderBox) { - additionalJson['size'] = { - 'width': renderObject.size.width.toString(), - 'height': renderObject.size.height.toString(), - }; - - final parentData = renderObject.parentData; - if (parentData is FlexParentData) { - additionalJson['flexFactor'] = parentData.flex ?? 0; - additionalJson['flexFit'] = - (parentData.fit ?? FlexFit.tight).name; - } - } - } - return additionalJson; - }, - ); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_test_utils.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_test_utils.dart deleted file mode 100644 index c2b1f449af6..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/layout_explorer_test_utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2020 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/src/shared/diagnostics/diagnostics_node.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'layout_explorer_serialization_delegate.dart'; - -Future widgetToLayoutExplorerRemoteDiagnosticsNode({ - required Widget widget, - required WidgetTester tester, - int subtreeDepth = 1, -}) async { - await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); - final element = find.byWidget(widget).evaluate().first; - final nodeJson = element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.dense) - .toJsonMap( - LayoutExplorerSerializationDelegate( - subtreeDepth: subtreeDepth, - service: WidgetInspectorService.instance, - ), - ); - return RemoteDiagnosticsNode(nodeJson, null, false, null); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/widget_theme_test.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/widget_theme_test.dart deleted file mode 100644 index 7e3bd1a671f..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/widget_theme_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 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/src/screens/inspector_v2/layout_explorer/ui/widgets_theme.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Test WidgetTheme', () { - test('Correct asset from widget with a type', () { - const widgetName = 'AnimatedBuilder'; - final theme = WidgetTheme.fromName(widgetName); - expect(theme.iconAsset, 'icons/inspector/widget_icons/animated.png'); - }); - - test('Has default theme for custom widget', () { - const widgetName = 'CustomWidget'; - final theme = WidgetTheme.fromName(widgetName); - expect(theme.color, WidgetTheme.otherWidgetColor); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart b/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart deleted file mode 100644 index d2aac2767d3..00000000000 --- a/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2021 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' - hide InspectorTreeController, InspectorTreeConfig, InspectorTreeNode; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; - -/// Create an `InspectorTreeControllerFlutter` from a single `RemoteDiagnosticsNode` -InspectorTreeController inspectorTreeControllerFromNode( - RemoteDiagnosticsNode node, -) { - final controller = InspectorTreeController() - ..config = InspectorTreeConfig( - onNodeAdded: (_, _) {}, - onClientActiveChange: (_) {}, - ); - - controller.root = InspectorTreeNode() - ..appendChild(InspectorTreeNode()..diagnostic = node); - - return controller; -} - -/// Replicates the functionality of `getRootWidgetSummaryTreeWithPreviews` from -/// inspector_polyfill_script.dart -Future widgetToInspectorTreeDiagnosticsNode({ - required Widget widget, - required WidgetTester tester, -}) async { - await tester.pumpWidget(wrap(widget)); - final element = find.byWidget(widget).evaluate().first; - final nodeJson = element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.dense) - .toJsonMap( - InspectorSerializationDelegate( - service: WidgetInspectorService.instance, - subtreeDepth: 1000000, - summaryTree: true, - addAdditionalPropertiesCallback: (node, delegate) { - final additionalJson = {}; - - final value = node.value; - if (value is Element) { - final renderObject = value.renderObject; - if (renderObject is RenderParagraph) { - additionalJson['textPreview'] = renderObject.text.toPlainText(); - } - } - - return additionalJson; - }, - ), - ); - - return RemoteDiagnosticsNode(nodeJson, null, false, null); -} diff --git a/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart b/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart index 69b74f5b25a..e5f17d19665 100644 --- a/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart +++ b/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart @@ -4,7 +4,7 @@ import 'package:devtools_app/src/screens/app_size/app_size_screen.dart'; import 'package:devtools_app/src/screens/debugger/debugger_screen.dart'; -import 'package:devtools_app/src/screens/inspector_shared/inspector_screen.dart'; +import 'package:devtools_app/src/screens/inspector/inspector_screen.dart'; import 'package:devtools_app/src/screens/logging/logging_screen.dart'; import 'package:devtools_app/src/screens/memory/framework/memory_screen.dart'; import 'package:devtools_app/src/screens/network/network_screen.dart'; 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 762e03cb9db..9bebbbe7105 100644 --- a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart +++ b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart @@ -19,7 +19,7 @@ void main() { expect(FeatureFlags.memorySaveLoad.isEnabled, false); expect(FeatureFlags.devToolsExtensions.isEnabled, isExternalBuild); expect(FeatureFlags.dapDebugging.isEnabled, false); - expect(FeatureFlags.inspectorV2.isEnabled, true); + expect(FeatureFlags.inspector.isEnabled, true); expect(FeatureFlags.aiAssistant.isEnabled, false); }); diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_horizontal.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_horizontal.png deleted file mode 100644 index d4db9f0be33..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_horizontal.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_vertical.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_vertical.png deleted file mode 100644 index dd855d25754..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_bidirectional_vertical.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_down.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_down.png deleted file mode 100644 index 6e864b5a7e8..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_down.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_left.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_left.png deleted file mode 100644 index ad28c36593f..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_left.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_right.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_right.png deleted file mode 100644 index aca93792545..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_right.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_up.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_up.png deleted file mode 100644 index d3f2615bbf7..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/arrow_unidirectional_up.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_column_layout.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_column_layout.png deleted file mode 100644 index b9008504226..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_column_layout.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_row_layout.png b/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_row_layout.png deleted file mode 100644 index a2ac4318469..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/inspector_v2/layout_explorer/flex/story_of_row_layout.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_after_hot_restart.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_after_hot_restart.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_after_hot_restart.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_after_hot_restart.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_animated_physical_model_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_animated_physical_model_selected.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_animated_physical_model_selected.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_animated_physical_model_selected.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png index 3c50814ca9c..5904b379b54 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png index 80f0bf9c286..d64b611d80f 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_hideable_widget_selected.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_hideable_widget_selected.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_hideable_widget_selected_from_search.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_hideable_widget_selected_from_search.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_expanded.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_expanded.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png index fc7ac52325f..26d0044cf97 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_revert_to_legacy.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_revert_to_legacy.png similarity index 100% rename from packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_revert_to_legacy.png rename to packages/devtools_app/test/test_infra/goldens/integration_inspector_revert_to_legacy.png diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png index 18394b8db9b..d2d00d92589 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png deleted file mode 100644 index 5904b379b54..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png deleted file mode 100644 index d64b611d80f..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png deleted file mode 100644 index 26d0044cf97..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png and /dev/null differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png deleted file mode 100644 index d2d00d92589..00000000000 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png and /dev/null differ diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index fee470e121e..d4fd88878fb 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -7,12 +7,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:devtools_app/devtools_app.dart'; -// ignore: implementation_imports, required to separate V2 inspector imports. -import 'package:devtools_app/src/screens/inspector_v2/inspector_controller.dart' - as inspector_v2; -// ignore: implementation_imports, required to separate V2 inspector imports. -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart' - as inspector_v2; import 'package:devtools_app_shared/service.dart'; import 'package:devtools_shared/devtools_shared.dart'; import 'package:flutter/foundation.dart'; @@ -77,39 +71,20 @@ class TestInspectorController extends Fake implements InspectorController { ValueListenable get selectedNode => _selectedNode; final _selectedNode = ValueNotifier(null); - @override - void setSelectedNode(InspectorTreeNode? newSelection) { - _selectedNode.value = newSelection; - } - - @override - InspectorService get inspectorService => service; -} - -class TestInspectorV2Controller extends Fake - implements inspector_v2.InspectorController { - InspectorService service = FakeInspectorService(); - - @override - ValueListenable get selectedNode => - _selectedNode; - final _selectedNode = ValueNotifier(null); - @override RemoteDiagnosticsNode? get selectedDiagnostic => _selectedDiagnostic; RemoteDiagnosticsNode? _selectedDiagnostic; @override - ValueListenable - get selectedNodeProperties => - ValueNotifier(( + ValueListenable get selectedNodeProperties => + ValueNotifier(( widgetProperties: [], renderProperties: [], layoutProperties: null, )); @override - void setSelectedNode(inspector_v2.InspectorTreeNode? newSelection) { + void setSelectedNode(InspectorTreeNode? newSelection) { _selectedNode.value = newSelection; } diff --git a/tool/flutter_customer_tests/test.sh b/tool/flutter_customer_tests/test.sh index 3ab28912ab4..b8b1080dcc0 100755 --- a/tool/flutter_customer_tests/test.sh +++ b/tool/flutter_customer_tests/test.sh @@ -22,5 +22,4 @@ cd ../devtools_app flutter pub get flutter test --tags=include-for-flutter-customer-tests test/ flutter test --exclude-tags=skip-for-flutter-customer-tests test/screens/inspector/ -flutter test --exclude-tags=skip-for-flutter-customer-tests test/screens/inspector_v2/ flutter test --exclude-tags=skip-for-flutter-customer-tests test/shared/