[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'); });