[Inspector V2] Toggle button hides implementation widgets in the widget tree (#8143)

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
index 6cc0fb4..3689324 100644
--- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart
@@ -216,6 +216,11 @@
     (widgetProperties: [], renderProperties: []),
   );
 
+  /// Whether the implementation widgets are hidden in the widget tree.
+  ValueListenable<bool> get implementationWidgetsHidden =>
+      _implementationWidgetsHidden;
+  final _implementationWidgetsHidden = ValueNotifier<bool>(false);
+
   InspectorTreeNode? lastExpanded;
 
   bool isActive = false;
@@ -403,7 +408,10 @@
     }
   }
 
-  Future<void> _recomputeTreeRoot(RemoteDiagnosticsNode? newSelection) async {
+  Future<void> _recomputeTreeRoot(
+    RemoteDiagnosticsNode? newSelection, {
+    bool hideImplementationWidgets = false,
+  }) async {
     assert(!_disposed);
     final treeGroups = _treeGroups;
     if (_disposed || treeGroups == null) {
@@ -413,7 +421,10 @@
     treeGroups.cancelNext();
     try {
       final group = treeGroups.next;
-      final node = await group.getRoot(treeType);
+      final node = await group.getRoot(
+        treeType,
+        isSummaryTree: hideImplementationWidgets,
+      );
       if (node == null || group.disposed || _disposed) {
         return;
       }
@@ -431,6 +442,7 @@
       inspectorTree.root = rootNode;
 
       refreshSelection(newSelection);
+      _implementationWidgetsHidden.value = hideImplementationWidgets;
     } catch (error, st) {
       _log.shout(error, error, st);
       treeGroups.cancelNext();
@@ -438,6 +450,19 @@
     }
   }
 
+  Future<void> 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);
+    }
+  }
+
   void _clearValueToInspectorTreeNodeMapping() {
     valueToInspectorTreeNode.clear();
   }
@@ -473,10 +498,13 @@
 
   void refreshSelection(RemoteDiagnosticsNode? newSelection) {
     newSelection ??= selectedDiagnostic;
-    setSelectedNode(findMatchingInspectorTreeNode(newSelection));
-    syncSelectionHelper(selection: newSelection);
+    final matchingNode = findMatchingInspectorTreeNode(newSelection);
+    if (matchingNode != null) {
+      setSelectedNode(matchingNode);
+      syncSelectionHelper(selection: newSelection);
 
-    syncTreeSelection();
+      syncTreeSelection();
+    }
   }
 
   void syncTreeSelection() {
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart
new file mode 100644
index 0000000..b115722
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart
@@ -0,0 +1,128 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+import '../../service/service_extension_widgets.dart';
+import '../../service/service_extensions.dart' as extensions;
+import '../../shared/analytics/constants.dart' as gac;
+import '../../shared/common_widgets.dart';
+import '../../shared/globals.dart';
+import 'inspector_controller.dart';
+import 'inspector_screen.dart';
+
+/// Control buttons for the inspector panel.
+class InspectorControls extends StatelessWidget {
+  const InspectorControls({super.key, required this.controller});
+
+  final InspectorController controller;
+
+  static const serviceExtensionButtonsIncludeTextWidth = 1200.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        ValueListenableBuilder<bool>(
+          valueListenable: serviceConnection
+              .serviceManager.serviceExtensionManager
+              .hasServiceExtension(
+            extensions.toggleSelectWidgetMode.extension,
+          ),
+          builder: (_, selectModeSupported, __) {
+            return ServiceExtensionButtonGroup(
+              extensions: [
+                selectModeSupported
+                    ? extensions.toggleSelectWidgetMode
+                    : extensions.toggleOnDeviceWidgetInspector,
+              ],
+              minScreenWidthForTextBeforeScaling:
+                  InspectorScreenBodyState.minScreenWidthForTextBeforeScaling,
+            );
+          },
+        ),
+        const SizedBox(width: defaultSpacing),
+        ShowImplementationWidgetsButton(controller: controller),
+        const Spacer(),
+        const SizedBox(width: defaultSpacing),
+        const InspectorServiceExtensionButtonGroup(),
+      ],
+    );
+  }
+}
+
+/// Group of service extension buttons for the inspector panel that control the
+/// overlays painted on the connected app.
+class InspectorServiceExtensionButtonGroup extends StatelessWidget {
+  const InspectorServiceExtensionButtonGroup({super.key});
+
+  static const serviceExtensionButtonsIncludeTextWidth = 1200.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        ServiceExtensionButtonGroup(
+          minScreenWidthForTextBeforeScaling:
+              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(),
+              ),
+            );
+          },
+        ),
+      ],
+    );
+  }
+}
+
+/// Toggle button that allows showing/hiding the implementation widgets in the
+/// widget tree.
+class ShowImplementationWidgetsButton extends StatelessWidget {
+  const ShowImplementationWidgetsButton({
+    super.key,
+    required this.controller,
+  });
+
+  final InspectorController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueListenableBuilder(
+      valueListenable: controller.implementationWidgetsHidden,
+      builder: (context, isHidden, _) {
+        return DevToolsToggleButton(
+          isSelected: !isHidden,
+          message:
+              'Show widgets created by the Flutter framework or other packages.',
+          label: 'Show Implementation Widgets',
+          onPressed: controller.toggleImplementationWidgetsVisibility,
+          icon: Icons.code,
+          minScreenWidthForTextBeforeScaling:
+              InspectorScreenBodyState.minScreenWidthForTextBeforeScaling,
+        );
+      },
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
index 4468685..38141ec 100644
--- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
@@ -11,8 +11,6 @@
 import 'package:flutter/material.dart';
 import 'package:vm_service/vm_service.dart' hide Stack;
 
-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/common_widgets.dart';
@@ -26,6 +24,7 @@
 import '../../shared/ui/search.dart';
 import '../../shared/utils.dart';
 import 'inspector_controller.dart';
+import 'inspector_controls.dart';
 import 'inspector_tree_controller.dart';
 import 'widget_details.dart';
 
@@ -76,7 +75,6 @@
 
   static const inspectorTreeKey = Key('Inspector Tree');
   static const minScreenWidthForTextBeforeScaling = 900.0;
-  static const serviceExtensionButtonsIncludeTextWidth = 1200.0;
 
   @override
   void dispose() {
@@ -149,31 +147,7 @@
     );
     return Column(
       children: <Widget>[
-        Row(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            ValueListenableBuilder<bool>(
-              valueListenable: serviceConnection
-                  .serviceManager.serviceExtensionManager
-                  .hasServiceExtension(
-                extensions.toggleSelectWidgetMode.extension,
-              ),
-              builder: (_, selectModeSupported, __) {
-                return ServiceExtensionButtonGroup(
-                  extensions: [
-                    selectModeSupported
-                        ? extensions.toggleSelectWidgetMode
-                        : extensions.toggleOnDeviceWidgetInspector,
-                  ],
-                  minScreenWidthForTextBeforeScaling:
-                      minScreenWidthForTextBeforeScaling,
-                );
-              },
-            ),
-            const Spacer(),
-            Row(children: getServiceExtensionWidgets()),
-          ],
-        ),
+        InspectorControls(controller: controller),
         const SizedBox(height: intermediateSpacing),
         Expanded(
           child: widgetTrees,
@@ -252,38 +226,6 @@
     _inspectorTreeController.resetSearch();
   }
 
-  List<Widget> getServiceExtensionWidgets() {
-    return [
-      ServiceExtensionButtonGroup(
-        minScreenWidthForTextBeforeScaling:
-            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
-    ];
-  }
-
   void _refreshInspector() {
     ga.select(gac.inspector, gac.refresh);
     unawaited(
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 4fd282a..0a415b5 100644
--- a/packages/devtools_app/lib/src/shared/console/widgets/description.dart
+++ b/packages/devtools_app/lib/src/shared/console/widgets/description.dart
@@ -414,7 +414,8 @@
       // Grey out nodes that were not created by the local project to emphasize
       // those that were:
       if (emphasizeNodesFromLocalProject &&
-          !diagnosticLocal.isCreatedByLocalProject) {
+          !diagnosticLocal.isCreatedByLocalProject &&
+          diagnosticLocal.description != '[root]') {
         textStyle = textStyle.merge(theme.subtleTextStyle);
       }
 
diff --git a/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart b/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart
index 7168608..c6add98 100644
--- a/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart
+++ b/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart
@@ -6,6 +6,7 @@
     hide InspectorScreen, InspectorScreenBodyState, InspectorScreenBody;
 import 'package:devtools_app/src/screens/inspector_v2/inspector_screen.dart';
 import 'package:devtools_app/src/screens/inspector_v2/widget_properties/properties_view.dart';
+import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_test/helpers.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -49,6 +50,10 @@
     await env.setupEnvironment();
   });
 
+  tearDown(() async {
+    await env.tearDownEnvironment();
+  });
+
   tearDownAll(() async {
     await env.tearDownEnvironment(force: true);
   });
@@ -71,8 +76,6 @@
             '../test_infra/goldens/integration_inspector_v2_initial_load.png',
           ),
         );
-
-        await env.tearDownEnvironment();
       },
     );
 
@@ -111,8 +114,6 @@
           value: '[Directionality]',
           tester: tester,
         );
-
-        await env.tearDownEnvironment();
       },
     );
 
@@ -168,8 +169,6 @@
             '../test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png',
           ),
         );
-
-        await env.tearDownEnvironment();
       },
     );
 
@@ -206,12 +205,46 @@
             '../test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png',
           ),
         );
-
-        await env.tearDownEnvironment();
       },
     );
   });
 
+  testWidgetsWithWindowSize(
+    'hide all implementation widgets',
+    windowSize,
+    (WidgetTester tester) async {
+      await _loadInspectorUI(tester);
+
+      // Give time for the initial animation to complete.
+      await tester.pumpAndSettle(inspectorChangeSettleTime);
+
+      // Confirm the hidden widgets are visible behind affordances like "X more
+      // widgets".
+      expect(
+        find.richTextContaining('more widgets...'),
+        findsWidgets,
+      );
+
+      // 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);
+
+      // 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',
+        ),
+      );
+    },
+  );
+
   group('widget errors', () {
     testWidgetsWithWindowSize(
       'show navigator and error labels',
@@ -262,8 +295,6 @@
             '../test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png',
           ),
         );
-
-        await env.tearDownEnvironment();
       },
     );
   });
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
index b43556e..061da41 100644
--- 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
Binary files 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
index c0ecbe1..c14c312 100644
--- 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
Binary files 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_v2_hideable_widget_selected.png
index 20fa6d5..038ff2e 100644
--- 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_v2_hideable_widget_selected.png
Binary files differ
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_v2_hideable_widget_selected_from_search.png
index 20fa6d5..038ff2e 100644
--- 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_v2_hideable_widget_selected_from_search.png
Binary files differ
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_v2_implementation_widgets_collapsed.png
index 01eed6c..ec04838 100644
--- 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_v2_implementation_widgets_collapsed.png
Binary files differ
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_v2_implementation_widgets_expanded.png
index fdbb441..13c4e2b 100644
--- 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_v2_implementation_widgets_expanded.png
Binary files differ
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_v2_implementation_widgets_hidden.png
new file mode 100644
index 0000000..a1d0c15
--- /dev/null
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png
Binary files 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
index 4cf41d5..fde40a7 100644
--- 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
Binary files 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
index 115415b..d832c94 100644
--- 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
Binary files differ
diff --git a/packages/devtools_app_shared/lib/src/ui/buttons.dart b/packages/devtools_app_shared/lib/src/ui/buttons.dart
index 12e3072..d991589 100644
--- a/packages/devtools_app_shared/lib/src/ui/buttons.dart
+++ b/packages/devtools_app_shared/lib/src/ui/buttons.dart
@@ -213,6 +213,7 @@
     this.outlined = true,
     this.label,
     this.shape,
+    this.minScreenWidthForTextBeforeScaling,
   });
 
   final String message;
@@ -229,6 +230,8 @@
 
   final bool outlined;
 
+  final double? minScreenWidthForTextBeforeScaling;
+
   @override
   Widget build(BuildContext context) {
     return DevToolsToggleButtonGroup(
@@ -245,6 +248,8 @@
             child: MaterialIconLabel(
               iconData: icon,
               label: label,
+              minScreenWidthForTextBeforeScaling:
+                  minScreenWidthForTextBeforeScaling,
             ),
           ),
         ),