[Story of Layout] Support visualizing flex child and make it clickable (#1373)

* Support visualizing flex child and make it clickable

* Show main/cross axis changes in real devices

* Remove test failures
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
index 67f5f99..1ccec32 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
@@ -10,6 +10,7 @@
 import '../../utils.dart';
 import '../diagnostics_node.dart';
 import '../enum_utils.dart';
+import '../inspector_tree.dart';
 import 'story_of_your_layout/utils.dart';
 
 const Type boxConstraintsType = BoxConstraints;
@@ -93,19 +94,20 @@
 
 // TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed
 class LayoutProperties {
-  LayoutProperties(RemoteDiagnosticsNode node, {int copyLevel = 1})
-      : description = node?.description,
-        size = deserializeSize(node?.size),
-        constraints = deserializeConstraints(node?.constraints),
-        isFlex = node?.isFlex,
-        flexFactor = node?.flexFactor,
+  LayoutProperties(this.node, {int copyLevel = 1})
+      : description = node.diagnostic?.description,
+        size = deserializeSize(node.diagnostic?.size),
+        constraints = deserializeConstraints(node.diagnostic?.constraints),
+        isFlex = node.diagnostic?.isFlex,
+        flexFactor = node.diagnostic?.flexFactor,
         children = copyLevel == 0
             ? []
-            : node?.childrenNow
+            : node.children
                 ?.map((child) =>
                     LayoutProperties(child, copyLevel: copyLevel - 1))
                 ?.toList(growable: false);
 
+  final InspectorTreeNode node;
   final List<LayoutProperties> children;
   final BoxConstraints constraints;
   final String description;
@@ -174,7 +176,7 @@
 /// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed
 class FlexLayoutProperties extends LayoutProperties {
   FlexLayoutProperties._(
-    RemoteDiagnosticsNode node, {
+    InspectorTreeNode node, {
     this.direction,
     this.mainAxisAlignment,
     this.mainAxisSize,
@@ -184,8 +186,9 @@
     this.textBaseline,
   }) : super(node);
 
-  factory FlexLayoutProperties.fromDiagnostics(RemoteDiagnosticsNode node) {
-    final Map<String, Object> renderObjectJson = node.json['renderObject'];
+  factory FlexLayoutProperties.fromNode(InspectorTreeNode node) {
+    final Map<String, Object> renderObjectJson =
+        node.diagnostic.json['renderObject'];
     final List<dynamic> properties = renderObjectJson['properties'];
     final Map<String, Object> data = Map<String, Object>.fromIterable(
       properties,
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_screen_details_tab.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_screen_details_tab.dart
index ed4bbb1..391762e 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/inspector_screen_details_tab.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_screen_details_tab.dart
@@ -6,8 +6,8 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
-import '../diagnostics_node.dart';
 import '../inspector_controller.dart';
+import '../inspector_tree.dart';
 import 'inspector_data_models.dart';
 import 'story_of_your_layout/flex.dart';
 
@@ -101,7 +101,7 @@
     with AutomaticKeepAliveClientMixin<LayoutDetailsTab> {
   InspectorController get controller => widget.controller;
 
-  RemoteDiagnosticsNode get selected => controller?.selectedNode?.diagnostic;
+  InspectorTreeNode get selected => controller?.selectedNode;
 
   void onSelectionChanged() {
     setState(() {});
@@ -122,18 +122,21 @@
   @override
   Widget build(BuildContext context) {
     super.build(context);
-    if (selected == null) return const SizedBox();
-    final properties = LayoutProperties(selected);
-    if (!properties.isFlex)
-      // TODO(albertusangga): Visualize non-flex widget constraint model
-      return Container(
-        child: const Text(
-          'TODOs for Non Flex widget',
-        ),
-      );
+    final diagnostic = selected?.diagnostic;
+    // TODO(albertusangga): Visualize non-flex widget constraint model
+    if (diagnostic == null ||
+        (!diagnostic.isFlex && !(diagnostic.parent?.isFlex ?? false)))
+      return const SizedBox();
+    final flexLayoutProperties = FlexLayoutProperties.fromNode(
+        diagnostic.isFlex ? selected : selected.parent);
+    final highlightChild = diagnostic.isFlex
+        ? null
+        : diagnostic.parent.childrenNow.indexOf(diagnostic);
     return StoryOfYourFlexWidget(
       // TODO(albertusangga): Cache this instead of recomputing every build,
-      FlexLayoutProperties.fromDiagnostics(selected),
+      flexLayoutProperties,
+      highlightChild: highlightChild,
+      inspectorController: controller,
     );
   }
 
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_service_flutter_extension.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_service_flutter_extension.dart
new file mode 100644
index 0000000..ecb51ac
--- /dev/null
+++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_service_flutter_extension.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/rendering.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../inspector_service.dart';
+
+extension InspectorFlutterService on ObjectGroup {
+  Future<InstanceRef> invokeTweakFlexProperties(
+    InspectorInstanceRef ref,
+    MainAxisAlignment mainAxisAlignment,
+    CrossAxisAlignment crossAxisAlignment,
+  ) async {
+    final command = '((){'
+        '  dynamic object = WidgetInspectorService.instance.toObject("${ref?.id}");'
+        '  final render = object.renderObject;'
+        '  render.mainAxisAlignment = $mainAxisAlignment;'
+        '  render.crossAxisAlignment = $crossAxisAlignment;'
+        '})()';
+    final val = await inspectorLibrary.eval(
+      command,
+      isAlive: this,
+    );
+    return val;
+  }
+}
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_tree_flutter.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_tree_flutter.dart
index 6e3ee8d..7eb8ec5 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/inspector_tree_flutter.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_tree_flutter.dart
@@ -569,7 +569,7 @@
                   constraintDisplayController != null)
                 ConstraintsDescription(
                   listenable: constraintDisplayController,
-                  properties: LayoutProperties(node.diagnostic),
+                  properties: LayoutProperties(node),
                 ),
             ],
           ),
diff --git a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
index cc699fd..cd1da7a 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
@@ -10,7 +10,9 @@
 import '../../../ui/colors.dart';
 import '../../../ui/theme.dart';
 import '../../../utils.dart';
+import '../../inspector_controller.dart';
 import '../inspector_data_models.dart';
+import '../inspector_service_flutter_extension.dart';
 import 'arrow.dart';
 import 'utils.dart';
 
@@ -48,12 +50,19 @@
 class StoryOfYourFlexWidget extends StatefulWidget {
   const StoryOfYourFlexWidget(
     this.properties, {
+    this.highlightChild,
+    this.inspectorController,
     Key key,
   })  : assert(properties != null),
         super(key: key);
 
   final FlexLayoutProperties properties;
 
+  // index of child to be highlighted
+  final int highlightChild;
+
+  final InspectorController inspectorController;
+
   @override
   _StoryOfYourFlexWidgetState createState() => _StoryOfYourFlexWidgetState();
 }
@@ -150,6 +159,7 @@
 
   Widget _visualizeChild({
     LayoutProperties childProperties,
+    Color backgroundColor,
     Color borderColor,
     Color textColor,
     Size renderSize,
@@ -159,51 +169,64 @@
     return Positioned(
       top: renderOffset.dy,
       left: renderOffset.dx,
-      child: Container(
-        width: renderSize.width,
-        height: renderSize.height,
-        child: WidgetVisualizer(
-          title: childProperties.description,
-          borderColor: borderColor,
-          textColor: textColor,
-          child: _visualizeWidthAndHeightWithConstraints(
-            arrowHeadSize: arrowHeadSize,
-            widget: Align(
-              alignment: Alignment.topRight,
-              child: Container(
-                margin: const EdgeInsets.only(
-                  top: margin,
-                  left: margin,
-                ),
-                child: Column(
-                  mainAxisAlignment: MainAxisAlignment.start,
-                  crossAxisAlignment: CrossAxisAlignment.end,
-                  children: <Widget>[
-                    Text(
-                      'flex: $flexFactor',
-                      style: TextStyle(fontWeight: FontWeight.bold),
-                    ),
-                    if (flexFactor == 0 || flexFactor == null)
+      child: InkWell(
+        onTap: () async {
+          final controller = widget.inspectorController;
+          final diagnostic = childProperties.node.diagnostic;
+          // TODO(albertusangga) fix/investigate why calling setSelectedNode is not sufficient
+          controller.refreshSelection(diagnostic, diagnostic, false);
+          controller.setSelectedNode(childProperties.node);
+          final inspectorService = await diagnostic.inspectorService;
+          await inspectorService.setSelectionInspector(
+              diagnostic.valueRef, true);
+        },
+        child: Container(
+          width: renderSize.width,
+          height: renderSize.height,
+          child: WidgetVisualizer(
+            backgroundColor: backgroundColor,
+            title: childProperties.description,
+            borderColor: borderColor,
+            textColor: textColor,
+            child: _visualizeWidthAndHeightWithConstraints(
+              arrowHeadSize: arrowHeadSize,
+              widget: Align(
+                alignment: Alignment.topRight,
+                child: Container(
+                  margin: const EdgeInsets.only(
+                    top: margin,
+                    left: margin,
+                  ),
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.start,
+                    crossAxisAlignment: CrossAxisAlignment.end,
+                    children: <Widget>[
                       Text(
-                        'unconstrained ${isRow ? 'horizontal' : 'vertical'}',
-                        style: TextStyle(
-                          color: ThemedColor(
-                            const Color(0xFFD08A29),
-                            Colors.orange.shade700,
-                          ),
-                          fontStyle: FontStyle.italic,
-                        ),
-                        maxLines: 2,
-                        softWrap: true,
-                        overflow: TextOverflow.ellipsis,
-                        textScaleFactor: smallTextScaleFactor,
-                        textAlign: TextAlign.right,
+                        'flex: $flexFactor',
+                        style: TextStyle(fontWeight: FontWeight.bold),
                       ),
-                  ],
+                      if (flexFactor == 0 || flexFactor == null)
+                        Text(
+                          'unconstrained ${isRow ? 'horizontal' : 'vertical'}',
+                          style: TextStyle(
+                            color: ThemedColor(
+                              const Color(0xFFD08A29),
+                              Colors.orange.shade700,
+                            ),
+                            fontStyle: FontStyle.italic,
+                          ),
+                          maxLines: 2,
+                          softWrap: true,
+                          overflow: TextOverflow.ellipsis,
+                          textScaleFactor: smallTextScaleFactor,
+                          textAlign: TextAlign.right,
+                        ),
+                    ],
+                  ),
                 ),
               ),
+              properties: childProperties,
             ),
-            properties: childProperties,
           ),
         ),
       ),
@@ -234,6 +257,10 @@
           final widgetChildren = <Widget>[
             for (var i = 0; i < children.length; i++)
               _visualizeChild(
+                backgroundColor:
+                    widget.highlightChild != null && i == widget.highlightChild
+                        ? theme.backgroundColor
+                        : theme.cardColor,
                 childProperties: children[i],
                 borderColor: i.isOdd ? mainAxisColor : crossAxisColor,
                 textColor: i.isOdd ? null : const Color(0xFF303030),
@@ -358,14 +385,21 @@
                     ),
                   )
               ],
-              onChanged: (Object newSelection) {
-                setState(() {
-                  if (axis == direction) {
-                    properties.mainAxisAlignment = newSelection;
-                  } else {
-                    properties.crossAxisAlignment = newSelection;
-                  }
-                });
+              onChanged: (Object newSelection) async {
+                if (axis == direction) {
+                  properties.mainAxisAlignment = newSelection;
+                } else {
+                  properties.crossAxisAlignment = newSelection;
+                }
+                final service =
+                    await properties.node.diagnostic.inspectorService;
+                final arg = properties.node.diagnostic.valueRef;
+                await service.invokeTweakFlexProperties(
+                  arg,
+                  properties.mainAxisAlignment,
+                  properties.crossAxisAlignment,
+                );
+                setState(() {});
               },
             ),
           )
@@ -505,6 +539,7 @@
     Key key,
     @required this.title,
     this.hint,
+    this.backgroundColor,
     @required this.borderColor,
     this.textColor,
     this.child,
@@ -518,6 +553,7 @@
 
   final Color borderColor;
   final Color textColor;
+  final Color backgroundColor;
 
   @override
   Widget build(BuildContext context) {
@@ -567,6 +603,7 @@
         border: Border.all(
           color: borderColor,
         ),
+        color: backgroundColor,
       ),
       margin: const EdgeInsets.all(1.0),
     );
diff --git a/packages/devtools_app/test/flutter/inspector_screen_test.dart b/packages/devtools_app/test/flutter/inspector_screen_test.dart
index 2529a5d..cead3a5 100644
--- a/packages/devtools_app/test/flutter/inspector_screen_test.dart
+++ b/packages/devtools_app/test/flutter/inspector_screen_test.dart
@@ -226,7 +226,8 @@
         vsync: const TestVSync(),
         duration: const Duration(milliseconds: 1),
       );
-      final node = RemoteDiagnosticsNode(jsonNode, null, false, null);
+      final diagnostic = RemoteDiagnosticsNode(jsonNode, null, false, null);
+      final node = InspectorTreeNode()..diagnostic = diagnostic;
       await tester.pumpWidget(
         MaterialApp(
           home: ConstraintsDescription(
diff --git a/packages/devtools_app/test/flutter/story_of_layout/flex_test.dart b/packages/devtools_app/test/flutter/story_of_layout/flex_test.dart
index 575ebf8..b1bf7cc 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/flex_test.dart
+++ b/packages/devtools_app/test/flutter/story_of_layout/flex_test.dart
@@ -8,6 +8,7 @@
 import 'package:devtools_app/src/inspector/diagnostics_node.dart';
 import 'package:devtools_app/src/inspector/flutter/inspector_data_models.dart';
 import 'package:devtools_app/src/inspector/flutter/story_of_your_layout/flex.dart';
+import 'package:devtools_app/src/inspector/inspector_tree.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -265,10 +266,15 @@
 
   testWidgets('Row golden test', (WidgetTester tester) async {
     final rowWidgetJsonNode = buildDiagnosticsNodeJson(Axis.horizontal);
-    final node = RemoteDiagnosticsNode(rowWidgetJsonNode, null, false, null);
+    final diagnostic =
+        RemoteDiagnosticsNode(rowWidgetJsonNode, null, false, null);
+    final node = InspectorTreeNode()..diagnostic = diagnostic;
+    for (var child in diagnostic.childrenNow) {
+      node.appendChild(InspectorTreeNode()..diagnostic = child);
+    }
     await setWindowSize(windowSize);
     final widget =
-        wrap(StoryOfYourFlexWidget(FlexLayoutProperties.fromDiagnostics(node)));
+        wrap(StoryOfYourFlexWidget(FlexLayoutProperties.fromNode(node)));
     await pump(tester, widget);
     await expectLater(
       find.byWidget(widget),
@@ -278,10 +284,15 @@
 
   testWidgets('Column golden test', (WidgetTester tester) async {
     final columnWidgetJsonNode = buildDiagnosticsNodeJson(Axis.vertical);
-    final node = RemoteDiagnosticsNode(columnWidgetJsonNode, null, false, null);
+    final diagnostic =
+        RemoteDiagnosticsNode(columnWidgetJsonNode, null, false, null);
+    final node = InspectorTreeNode()..diagnostic = diagnostic;
+    for (var child in diagnostic.childrenNow) {
+      node.appendChild(InspectorTreeNode()..diagnostic = child);
+    }
     await setWindowSize(windowSize);
     final widget =
-        wrap(StoryOfYourFlexWidget(FlexLayoutProperties.fromDiagnostics(node)));
+        wrap(StoryOfYourFlexWidget(FlexLayoutProperties.fromNode(node)));
     await pump(tester, widget);
     await expectLater(
       find.byWidget(widget),
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
index 7e1b6ab..e3bbe8d 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
+++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
Binary files differ
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
index 64dcca8..144b144 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
+++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
Binary files differ
diff --git a/packages/devtools_app/test/flutter/story_of_layout/inspector_data_models_test.dart b/packages/devtools_app/test/flutter/story_of_layout/inspector_data_models_test.dart
index a5ceca5..b088614 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/inspector_data_models_test.dart
+++ b/packages/devtools_app/test/flutter/story_of_layout/inspector_data_models_test.dart
@@ -7,6 +7,7 @@
 import 'package:devtools_app/src/inspector/diagnostics_node.dart';
 import 'package:devtools_app/src/inspector/flutter/inspector_data_models.dart';
 import 'package:devtools_app/src/inspector/flutter/story_of_your_layout/utils.dart';
+import 'package:devtools_app/src/inspector/inspector_tree.dart';
 import 'package:flutter/widgets.dart';
 import 'package:test/test.dart';
 
@@ -171,10 +172,11 @@
         ]
       }
     ''');
-    final diagnostics =
+    final diagnostic =
         RemoteDiagnosticsNode({'renderObject': flexJson}, null, null, null);
+    final node = InspectorTreeNode()..diagnostic = diagnostic;
     final FlexLayoutProperties flexProperties =
-        FlexLayoutProperties.fromDiagnostics(diagnostics);
+        FlexLayoutProperties.fromNode(node);
     expect(flexProperties.direction, Axis.horizontal);
     expect(flexProperties.mainAxisAlignment, MainAxisAlignment.start);
     expect(flexProperties.mainAxisSize, MainAxisSize.max);
@@ -300,8 +302,9 @@
         "widgetRuntimeType": "Row"
     }
     ''');
-      final node = RemoteDiagnosticsNode(json, null, false, null);
-      final layoutProperties = LayoutProperties(node);
+      final diagnostic = RemoteDiagnosticsNode(json, null, false, null);
+      final layoutProperties =
+          LayoutProperties(InspectorTreeNode()..diagnostic = diagnostic);
 
       expect(layoutProperties.size, const Size(432.0, 56.0));
       expect(
@@ -331,8 +334,8 @@
               }
             }
           ''');
-        final layoutProperties =
-            LayoutProperties(RemoteDiagnosticsNode(json, null, false, null));
+        final layoutProperties = LayoutProperties(InspectorTreeNode()
+          ..diagnostic = RemoteDiagnosticsNode(json, null, false, null));
         expect(layoutProperties.describeHeightConstraints(), 'h=56.0');
         expect(layoutProperties.describeWidthConstraints(), 'w=25.0');
       });
@@ -352,8 +355,8 @@
               }
             }
           ''');
-        final layoutProperties =
-            LayoutProperties(RemoteDiagnosticsNode(json, null, false, null));
+        final layoutProperties = LayoutProperties(InspectorTreeNode()
+          ..diagnostic = RemoteDiagnosticsNode(json, null, false, null));
         expect(layoutProperties.describeHeightConstraints(), '75.0<=h<=100.0');
         expect(layoutProperties.describeWidthConstraints(), '25.0<=w<=50.0');
       });
@@ -371,8 +374,8 @@
               }
             }
           ''');
-        final layoutProperties =
-            LayoutProperties(RemoteDiagnosticsNode(json, null, false, null));
+        final layoutProperties = LayoutProperties(InspectorTreeNode()
+          ..diagnostic = RemoteDiagnosticsNode(json, null, false, null));
         expect(layoutProperties.describeHeightConstraints(), 'h=unconstrained');
         expect(layoutProperties.describeWidthConstraints(), 'w=unconstrained');
       });
@@ -389,8 +392,8 @@
               }
             }
           ''');
-      final layoutProperties =
-          LayoutProperties(RemoteDiagnosticsNode(json, null, false, null));
+      final layoutProperties = LayoutProperties(InspectorTreeNode()
+        ..diagnostic = RemoteDiagnosticsNode(json, null, false, null));
       expect(layoutProperties.describeHeight(), 'h=56.0');
       expect(layoutProperties.describeWidth(), 'w=432.6');
     });