diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 6afe24e3fac..06ce004cf1a 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -2293,6 +2293,9 @@ class _WidgetInspectorState extends State @override Widget build(BuildContext context) { + // Be careful changing this build method. The _InspectorOverlayLayer + // assumes the root RenderObject for the WidgetInspector will be + // a RenderStack with a _RenderInspectorOverlay as the last child. return Stack(children: [ GestureDetector( onTap: _handleTap, @@ -2441,15 +2444,16 @@ class _RenderInspectorOverlay extends RenderBox { context.addLayer(_InspectorOverlayLayer( overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height), selection: selection, + rootRenderObject: parent is RenderObject ? parent as RenderObject : null, )); } } @immutable class _TransformedRect { - _TransformedRect(RenderObject object) + _TransformedRect(RenderObject object, RenderObject ancestor) : rect = object.semanticBounds, - transform = object.getTransformTo(null); + transform = object.getTransformTo(ancestor); final Rect rect; final Matrix4 transform; @@ -2517,6 +2521,7 @@ class _InspectorOverlayLayer extends Layer { _InspectorOverlayLayer({ @required this.overlayRect, @required this.selection, + @required this.rootRenderObject, }) : assert(overlayRect != null), assert(selection != null) { bool inDebugMode = false; @@ -2543,6 +2548,10 @@ class _InspectorOverlayLayer extends Layer { /// (as described at [Layer]). final Rect overlayRect; + /// Widget inspector root render object. The selection overlay will be painted + /// with transforms relative to this render object. + final RenderObject rootRenderObject; + _InspectorOverlayRenderState _lastState; /// Picture generated from _lastState. @@ -2557,16 +2566,21 @@ class _InspectorOverlayLayer extends Layer { return; final RenderObject selected = selection.current; + + if (!_isInInspectorRenderObjectTree(selected)) + return; + final List<_TransformedRect> candidates = <_TransformedRect>[]; for (final RenderObject candidate in selection.candidates) { - if (candidate == selected || !candidate.attached) + if (candidate == selected || !candidate.attached + || !_isInInspectorRenderObjectTree(candidate)) continue; - candidates.add(_TransformedRect(candidate)); + candidates.add(_TransformedRect(candidate, rootRenderObject)); } final _InspectorOverlayRenderState state = _InspectorOverlayRenderState( overlayRect: overlayRect, - selected: _TransformedRect(selected), + selected: _TransformedRect(selected, rootRenderObject), tooltip: selection.currentElement.toStringShort(), textDirection: TextDirection.ltr, candidates: candidates, @@ -2583,6 +2597,9 @@ class _InspectorOverlayLayer extends Layer { final ui.PictureRecorder recorder = ui.PictureRecorder(); final Canvas canvas = Canvas(recorder, state.overlayRect); final Size size = state.overlayRect.size; + // The overlay rect could have an offset if the widget inspector does + // not take all the screen. + canvas.translate(state.overlayRect.left, state.overlayRect.top); final Paint fillPaint = Paint() ..style = PaintingStyle.fill @@ -2695,6 +2712,24 @@ class _InspectorOverlayLayer extends Layer { }) { return false; } + + /// Return whether or not a render object belongs to this inspector widget + /// tree. + /// The inspector selection is static, so if there are multiple inspector + /// overlays in the same app (i.e. an storyboard), a selected or candidate + /// render object may not belong to this tree. + bool _isInInspectorRenderObjectTree(RenderObject child) { + RenderObject current = child.parent as RenderObject; + while (current != null) { + // We found the widget inspector render object. + if (current is RenderStack + && current.lastChild is _RenderInspectorOverlay) { + return rootRenderObject == current; + } + current = current.parent as RenderObject; + } + return false; + } } const double _kScreenEdgeMargin = 10.0; diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 957c3b40876..a0ae9e16ca6 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -512,6 +512,123 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2)); }); + testWidgets('WidgetInspector with Transform above', (WidgetTester tester) async { + final GlobalKey childKey = GlobalKey(); + final GlobalKey repaintBoundaryKey = GlobalKey(); + + final Matrix4 mainTransform = Matrix4.identity() + ..translate(50.0, 30.0) + ..scale(0.8, 0.8) + ..translate(100.0, 50.0); + + await tester.pumpWidget( + RepaintBoundary( + key: repaintBoundaryKey, + child: Container( + color: Colors.grey, + child: Transform( + transform: mainTransform, + child: Directionality( + textDirection: TextDirection.ltr, + child: WidgetInspector( + selectButtonBuilder: null, + child: Container( + color: Colors.white, + child: Center( + child: Container( + key: childKey, + height: 100.0, + width: 50.0, + color: Colors.red, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(childKey)); + await tester.pump(); + + await expectLater( + find.byKey(repaintBoundaryKey), + matchesGoldenFile('inspector.overlay_positioning_with_transform.png'), + ); + }); + + testWidgets('Multiple widget inspectors', (WidgetTester tester) async { + // This test verifies that interacting with different inspectors + // works correctly. This use case may be an app that displays multiple + // apps inside (i.e. a storyboard). + final GlobalKey selectButton1Key = GlobalKey(); + final GlobalKey selectButton2Key = GlobalKey(); + + final GlobalKey inspector1Key = GlobalKey(); + final GlobalKey inspector2Key = GlobalKey(); + + final GlobalKey child1Key = GlobalKey(); + final GlobalKey child2Key = GlobalKey(); + + InspectorSelectButtonBuilder selectButtonBuilder(Key key) { + return (BuildContext context, VoidCallback onPressed) { + return Material(child: RaisedButton(onPressed: onPressed, key: key)); + }; + } + + // State type is private, hence using dynamic. + // The inspector state is static, so it's enough with reading one of them. + dynamic getInspectorState() => inspector1Key.currentState; + String paragraphText(RenderParagraph paragraph) { + final TextSpan textSpan = paragraph.text as TextSpan; + return textSpan.text; + } + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: [ + Flexible( + child: WidgetInspector( + key: inspector1Key, + selectButtonBuilder: selectButtonBuilder(selectButton1Key), + child: Container( + key: child1Key, + child: const Text('Child 1'), + ), + ), + ), + Flexible( + child: WidgetInspector( + key: inspector2Key, + selectButtonBuilder: selectButtonBuilder(selectButton2Key), + child: Container( + key: child2Key, + child: const Text('Child 2'), + ), + ), + ), + ], + ), + ), + ); + + final InspectorSelection selection = getInspectorState().selection as InspectorSelection; + // The selection is static, so it may be initialized from previous tests. + selection?.clear(); + + await tester.tap(find.text('Child 1')); + await tester.pump(); + expect(paragraphText(selection.current as RenderParagraph), equals('Child 1')); + + await tester.tap(find.text('Child 2')); + await tester.pump(); + expect(paragraphText(selection.current as RenderParagraph), equals('Child 2')); + }); + test('WidgetInspectorService null id', () { service.disposeAllGroups(); expect(service.toObject(null), isNull);