1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6library;
7
8import 'dart:async';
9import 'dart:collection' show HashMap;
10import 'dart:convert';
11import 'dart:developer' as developer;
12import 'dart:math' as math;
13import 'dart:ui' as ui
14 show
15 ClipOp,
16 FlutterView,
17 Image,
18 ImageByteFormat,
19 Paragraph,
20 Picture,
21 PictureRecorder,
22 PointMode,
23 SceneBuilder,
24 Vertices;
25
26import 'package:flutter/foundation.dart';
27import 'package:flutter/rendering.dart';
28import 'package:flutter/scheduler.dart';
29import 'package:meta/meta_meta.dart';
30
31import 'basic.dart';
32import 'binding.dart';
33import 'debug.dart';
34import 'framework.dart';
35import 'gesture_detector.dart';
36import 'service_extensions.dart';
37import 'view.dart';
38
39/// Signature for the builder callback used by
40/// [WidgetInspector.selectButtonBuilder].
41typedef InspectorSelectButtonBuilder = Widget Function(BuildContext context, VoidCallback onPressed);
42
43/// Signature for a method that registers the service extension `callback` with
44/// the given `name`.
45///
46/// Used as argument to [WidgetInspectorService.initServiceExtensions]. The
47/// [BindingBase.registerServiceExtension] implements this signature.
48typedef RegisterServiceExtensionCallback = void Function({
49 required String name,
50 required ServiceExtensionCallback callback,
51});
52
53/// A layer that mimics the behavior of another layer.
54///
55/// A proxy layer is used for cases where a layer needs to be placed into
56/// multiple trees of layers.
57class _ProxyLayer extends Layer {
58 _ProxyLayer(this._layer);
59
60 final Layer _layer;
61
62 @override
63 void addToScene(ui.SceneBuilder builder) {
64 _layer.addToScene(builder);
65 }
66
67 @override
68 @protected
69 bool findAnnotations<S extends Object>(
70 AnnotationResult<S> result,
71 Offset localPosition, {
72 required bool onlyFirst,
73 }) {
74 return _layer.findAnnotations(result, localPosition, onlyFirst: onlyFirst);
75 }
76}
77
78/// A [Canvas] that multicasts all method calls to a main canvas and a
79/// secondary screenshot canvas so that a screenshot can be recorded at the same
80/// time as performing a normal paint.
81class _MulticastCanvas implements Canvas {
82 _MulticastCanvas({
83 required Canvas main,
84 required Canvas screenshot,
85 }) : _main = main,
86 _screenshot = screenshot;
87
88 final Canvas _main;
89 final Canvas _screenshot;
90
91 @override
92 void clipPath(Path path, { bool doAntiAlias = true }) {
93 _main.clipPath(path, doAntiAlias: doAntiAlias);
94 _screenshot.clipPath(path, doAntiAlias: doAntiAlias);
95 }
96
97 @override
98 void clipRRect(RRect rrect, { bool doAntiAlias = true }) {
99 _main.clipRRect(rrect, doAntiAlias: doAntiAlias);
100 _screenshot.clipRRect(rrect, doAntiAlias: doAntiAlias);
101 }
102
103 @override
104 void clipRect(Rect rect, { ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true }) {
105 _main.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
106 _screenshot.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
107 }
108
109 @override
110 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) {
111 _main.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
112 _screenshot.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
113 }
114
115 @override
116 void drawAtlas(ui.Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color>? colors, BlendMode? blendMode, Rect? cullRect, Paint paint) {
117 _main.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
118 _screenshot.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
119 }
120
121 @override
122 void drawCircle(Offset c, double radius, Paint paint) {
123 _main.drawCircle(c, radius, paint);
124 _screenshot.drawCircle(c, radius, paint);
125 }
126
127 @override
128 void drawColor(Color color, BlendMode blendMode) {
129 _main.drawColor(color, blendMode);
130 _screenshot.drawColor(color, blendMode);
131 }
132
133 @override
134 void drawDRRect(RRect outer, RRect inner, Paint paint) {
135 _main.drawDRRect(outer, inner, paint);
136 _screenshot.drawDRRect(outer, inner, paint);
137 }
138
139 @override
140 void drawImage(ui.Image image, Offset p, Paint paint) {
141 _main.drawImage(image, p, paint);
142 _screenshot.drawImage(image, p, paint);
143 }
144
145 @override
146 void drawImageNine(ui.Image image, Rect center, Rect dst, Paint paint) {
147 _main.drawImageNine(image, center, dst, paint);
148 _screenshot.drawImageNine(image, center, dst, paint);
149 }
150
151 @override
152 void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) {
153 _main.drawImageRect(image, src, dst, paint);
154 _screenshot.drawImageRect(image, src, dst, paint);
155 }
156
157 @override
158 void drawLine(Offset p1, Offset p2, Paint paint) {
159 _main.drawLine(p1, p2, paint);
160 _screenshot.drawLine(p1, p2, paint);
161 }
162
163 @override
164 void drawOval(Rect rect, Paint paint) {
165 _main.drawOval(rect, paint);
166 _screenshot.drawOval(rect, paint);
167 }
168
169 @override
170 void drawPaint(Paint paint) {
171 _main.drawPaint(paint);
172 _screenshot.drawPaint(paint);
173 }
174
175 @override
176 void drawParagraph(ui.Paragraph paragraph, Offset offset) {
177 _main.drawParagraph(paragraph, offset);
178 _screenshot.drawParagraph(paragraph, offset);
179 }
180
181 @override
182 void drawPath(Path path, Paint paint) {
183 _main.drawPath(path, paint);
184 _screenshot.drawPath(path, paint);
185 }
186
187 @override
188 void drawPicture(ui.Picture picture) {
189 _main.drawPicture(picture);
190 _screenshot.drawPicture(picture);
191 }
192
193 @override
194 void drawPoints(ui.PointMode pointMode, List<Offset> points, Paint paint) {
195 _main.drawPoints(pointMode, points, paint);
196 _screenshot.drawPoints(pointMode, points, paint);
197 }
198
199 @override
200 void drawRRect(RRect rrect, Paint paint) {
201 _main.drawRRect(rrect, paint);
202 _screenshot.drawRRect(rrect, paint);
203 }
204
205 @override
206 void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List? colors, BlendMode? blendMode, Rect? cullRect, Paint paint) {
207 _main.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
208 _screenshot.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
209 }
210
211 @override
212 void drawRawPoints(ui.PointMode pointMode, Float32List points, Paint paint) {
213 _main.drawRawPoints(pointMode, points, paint);
214 _screenshot.drawRawPoints(pointMode, points, paint);
215 }
216
217 @override
218 void drawRect(Rect rect, Paint paint) {
219 _main.drawRect(rect, paint);
220 _screenshot.drawRect(rect, paint);
221 }
222
223 @override
224 void drawShadow(Path path, Color color, double elevation, bool transparentOccluder) {
225 _main.drawShadow(path, color, elevation, transparentOccluder);
226 _screenshot.drawShadow(path, color, elevation, transparentOccluder);
227 }
228
229 @override
230 void drawVertices(ui.Vertices vertices, BlendMode blendMode, Paint paint) {
231 _main.drawVertices(vertices, blendMode, paint);
232 _screenshot.drawVertices(vertices, blendMode, paint);
233 }
234
235 @override
236 int getSaveCount() {
237 // The main canvas is used instead of the screenshot canvas as the main
238 // canvas is guaranteed to be consistent with the canvas expected by the
239 // normal paint pipeline so any logic depending on getSaveCount() will
240 // behave the same as for the regular paint pipeline.
241 return _main.getSaveCount();
242 }
243
244 @override
245 void restore() {
246 _main.restore();
247 _screenshot.restore();
248 }
249
250 @override
251 void rotate(double radians) {
252 _main.rotate(radians);
253 _screenshot.rotate(radians);
254 }
255
256 @override
257 void save() {
258 _main.save();
259 _screenshot.save();
260 }
261
262 @override
263 void saveLayer(Rect? bounds, Paint paint) {
264 _main.saveLayer(bounds, paint);
265 _screenshot.saveLayer(bounds, paint);
266 }
267
268 @override
269 void scale(double sx, [ double? sy ]) {
270 _main.scale(sx, sy);
271 _screenshot.scale(sx, sy);
272 }
273
274 @override
275 void skew(double sx, double sy) {
276 _main.skew(sx, sy);
277 _screenshot.skew(sx, sy);
278 }
279
280 @override
281 void transform(Float64List matrix4) {
282 _main.transform(matrix4);
283 _screenshot.transform(matrix4);
284 }
285
286 @override
287 void translate(double dx, double dy) {
288 _main.translate(dx, dy);
289 _screenshot.translate(dx, dy);
290 }
291
292 @override
293 dynamic noSuchMethod(Invocation invocation) {
294 super.noSuchMethod(invocation);
295 }
296}
297
298Rect _calculateSubtreeBoundsHelper(RenderObject object, Matrix4 transform) {
299 Rect bounds = MatrixUtils.transformRect(transform, object.semanticBounds);
300
301 object.visitChildren((RenderObject child) {
302 final Matrix4 childTransform = transform.clone();
303 object.applyPaintTransform(child, childTransform);
304 Rect childBounds = _calculateSubtreeBoundsHelper(child, childTransform);
305 final Rect? paintClip = object.describeApproximatePaintClip(child);
306 if (paintClip != null) {
307 final Rect transformedPaintClip = MatrixUtils.transformRect(
308 transform,
309 paintClip,
310 );
311 childBounds = childBounds.intersect(transformedPaintClip);
312 }
313
314 if (childBounds.isFinite && !childBounds.isEmpty) {
315 bounds = bounds.isEmpty ? childBounds : bounds.expandToInclude(childBounds);
316 }
317 });
318
319 return bounds;
320}
321
322/// Calculate bounds for a render object and all of its descendants.
323Rect _calculateSubtreeBounds(RenderObject object) {
324 return _calculateSubtreeBoundsHelper(object, Matrix4.identity());
325}
326
327/// A layer that omits its own offset when adding children to the scene so that
328/// screenshots render to the scene in the local coordinate system of the layer.
329class _ScreenshotContainerLayer extends OffsetLayer {
330 @override
331 void addToScene(ui.SceneBuilder builder) {
332 addChildrenToScene(builder);
333 }
334}
335
336/// Data shared between nested [_ScreenshotPaintingContext] objects recording
337/// a screenshot.
338class _ScreenshotData {
339 _ScreenshotData({
340 required this.target,
341 }) : containerLayer = _ScreenshotContainerLayer() {
342 // TODO(polina-c): stop duplicating code across disposables
343 // https://github.com/flutter/flutter/issues/137435
344 if (kFlutterMemoryAllocationsEnabled) {
345 FlutterMemoryAllocations.instance.dispatchObjectCreated(
346 library: 'package:flutter/widgets.dart',
347 className: '$_ScreenshotData',
348 object: this,
349 );
350 }
351 }
352
353 /// Target to take a screenshot of.
354 final RenderObject target;
355
356 /// Root of the layer tree containing the screenshot.
357 final OffsetLayer containerLayer;
358
359 /// Whether the screenshot target has already been found in the render tree.
360 bool foundTarget = false;
361
362 /// Whether paint operations should record to the screenshot.
363 ///
364 /// At least one of [includeInScreenshot] and [includeInRegularContext] must
365 /// be true.
366 bool includeInScreenshot = false;
367
368 /// Whether paint operations should record to the regular context.
369 ///
370 /// This should only be set to false before paint operations that should only
371 /// apply to the screenshot such rendering debug information about the
372 /// [target].
373 ///
374 /// At least one of [includeInScreenshot] and [includeInRegularContext] must
375 /// be true.
376 bool includeInRegularContext = true;
377
378 /// Offset of the screenshot corresponding to the offset [target] was given as
379 /// part of the regular paint.
380 Offset get screenshotOffset {
381 assert(foundTarget);
382 return containerLayer.offset;
383 }
384 set screenshotOffset(Offset offset) {
385 containerLayer.offset = offset;
386 }
387
388 /// Releases allocated resources.
389 @mustCallSuper
390 void dispose() {
391 if (kFlutterMemoryAllocationsEnabled) {
392 FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
393 }
394 containerLayer.dispose();
395 }
396}
397
398/// A place to paint to build screenshots of [RenderObject]s.
399///
400/// Requires that the render objects have already painted successfully as part
401/// of the regular rendering pipeline.
402/// This painting context behaves the same as standard [PaintingContext] with
403/// instrumentation added to compute a screenshot of a specified [RenderObject]
404/// added. To correctly mimic the behavior of the regular rendering pipeline, the
405/// full subtree of the first [RepaintBoundary] ancestor of the specified
406/// [RenderObject] will also be rendered rather than just the subtree of the
407/// render object.
408class _ScreenshotPaintingContext extends PaintingContext {
409 _ScreenshotPaintingContext({
410 required ContainerLayer containerLayer,
411 required Rect estimatedBounds,
412 required _ScreenshotData screenshotData,
413 }) : _data = screenshotData,
414 super(containerLayer, estimatedBounds);
415
416 final _ScreenshotData _data;
417
418 // Recording state
419 PictureLayer? _screenshotCurrentLayer;
420 ui.PictureRecorder? _screenshotRecorder;
421 Canvas? _screenshotCanvas;
422 _MulticastCanvas? _multicastCanvas;
423
424 @override
425 Canvas get canvas {
426 if (_data.includeInScreenshot) {
427 if (_screenshotCanvas == null) {
428 _startRecordingScreenshot();
429 }
430 assert(_screenshotCanvas != null);
431 return _data.includeInRegularContext ? _multicastCanvas! : _screenshotCanvas!;
432 } else {
433 assert(_data.includeInRegularContext);
434 return super.canvas;
435 }
436 }
437
438 bool get _isScreenshotRecording {
439 final bool hasScreenshotCanvas = _screenshotCanvas != null;
440 assert(() {
441 if (hasScreenshotCanvas) {
442 assert(_screenshotCurrentLayer != null);
443 assert(_screenshotRecorder != null);
444 assert(_screenshotCanvas != null);
445 } else {
446 assert(_screenshotCurrentLayer == null);
447 assert(_screenshotRecorder == null);
448 assert(_screenshotCanvas == null);
449 }
450 return true;
451 }());
452 return hasScreenshotCanvas;
453 }
454
455 void _startRecordingScreenshot() {
456 assert(_data.includeInScreenshot);
457 assert(!_isScreenshotRecording);
458 _screenshotCurrentLayer = PictureLayer(estimatedBounds);
459 _screenshotRecorder = ui.PictureRecorder();
460 _screenshotCanvas = Canvas(_screenshotRecorder!);
461 _data.containerLayer.append(_screenshotCurrentLayer!);
462 if (_data.includeInRegularContext) {
463 _multicastCanvas = _MulticastCanvas(
464 main: super.canvas,
465 screenshot: _screenshotCanvas!,
466 );
467 } else {
468 _multicastCanvas = null;
469 }
470 }
471
472 @override
473 void stopRecordingIfNeeded() {
474 super.stopRecordingIfNeeded();
475 _stopRecordingScreenshotIfNeeded();
476 }
477
478 void _stopRecordingScreenshotIfNeeded() {
479 if (!_isScreenshotRecording) {
480 return;
481 }
482 // There is no need to ever draw repaint rainbows as part of the screenshot.
483 _screenshotCurrentLayer!.picture = _screenshotRecorder!.endRecording();
484 _screenshotCurrentLayer = null;
485 _screenshotRecorder = null;
486 _multicastCanvas = null;
487 _screenshotCanvas = null;
488 }
489
490 @override
491 void appendLayer(Layer layer) {
492 if (_data.includeInRegularContext) {
493 super.appendLayer(layer);
494 if (_data.includeInScreenshot) {
495 assert(!_isScreenshotRecording);
496 // We must use a proxy layer here as the layer is already attached to
497 // the regular layer tree.
498 _data.containerLayer.append(_ProxyLayer(layer));
499 }
500 } else {
501 // Only record to the screenshot.
502 assert(!_isScreenshotRecording);
503 assert(_data.includeInScreenshot);
504 layer.remove();
505 _data.containerLayer.append(layer);
506 return;
507 }
508 }
509
510 @override
511 PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
512 if (_data.foundTarget) {
513 // We have already found the screenshotTarget in the layer tree
514 // so we can optimize and use a standard PaintingContext.
515 return super.createChildContext(childLayer, bounds);
516 } else {
517 return _ScreenshotPaintingContext(
518 containerLayer: childLayer,
519 estimatedBounds: bounds,
520 screenshotData: _data,
521 );
522 }
523 }
524
525 @override
526 void paintChild(RenderObject child, Offset offset) {
527 final bool isScreenshotTarget = identical(child, _data.target);
528 if (isScreenshotTarget) {
529 assert(!_data.includeInScreenshot);
530 assert(!_data.foundTarget);
531 _data.foundTarget = true;
532 _data.screenshotOffset = offset;
533 _data.includeInScreenshot = true;
534 }
535 super.paintChild(child, offset);
536 if (isScreenshotTarget) {
537 _stopRecordingScreenshotIfNeeded();
538 _data.includeInScreenshot = false;
539 }
540 }
541
542 /// Captures an image of the current state of [renderObject] and its children.
543 ///
544 /// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
545 /// by the top-left corner of [renderBounds], and have dimensions equal to the
546 /// size of [renderBounds] multiplied by [pixelRatio].
547 ///
548 /// To use [toImage], the render object must have gone through the paint phase
549 /// (i.e. [RenderObject.debugNeedsPaint] must be false).
550 ///
551 /// The [pixelRatio] describes the scale between the logical pixels and the
552 /// size of the output image. It is independent of the
553 /// [FlutterView.devicePixelRatio] for the device, so specifying 1.0 (the default)
554 /// will give you a 1:1 mapping between logical pixels and the output pixels
555 /// in the image.
556 ///
557 /// The [debugPaint] argument specifies whether the image should include the
558 /// output of [RenderObject.debugPaint] for [renderObject] with
559 /// [debugPaintSizeEnabled] set to true. Debug paint information is not
560 /// included for the children of [renderObject] so that it is clear precisely
561 /// which object the debug paint information references.
562 ///
563 /// See also:
564 ///
565 /// * [RenderRepaintBoundary.toImage] for a similar API for [RenderObject]s
566 /// that are repaint boundaries that can be used outside of the inspector.
567 /// * [OffsetLayer.toImage] for a similar API at the layer level.
568 /// * [dart:ui.Scene.toImage] for more information about the image returned.
569 static Future<ui.Image> toImage(
570 RenderObject renderObject,
571 Rect renderBounds, {
572 double pixelRatio = 1.0,
573 bool debugPaint = false,
574 }) async {
575 RenderObject repaintBoundary = renderObject;
576 while (!repaintBoundary.isRepaintBoundary) {
577 repaintBoundary = repaintBoundary.parent!;
578 }
579 final _ScreenshotData data = _ScreenshotData(target: renderObject);
580 final _ScreenshotPaintingContext context = _ScreenshotPaintingContext(
581 containerLayer: repaintBoundary.debugLayer!,
582 estimatedBounds: repaintBoundary.paintBounds,
583 screenshotData: data,
584 );
585
586 if (identical(renderObject, repaintBoundary)) {
587 // Painting the existing repaint boundary to the screenshot is sufficient.
588 // We don't just take a direct screenshot of the repaint boundary as we
589 // want to capture debugPaint information as well.
590 data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer!));
591 data.foundTarget = true;
592 final OffsetLayer offsetLayer = repaintBoundary.debugLayer! as OffsetLayer;
593 data.screenshotOffset = offsetLayer.offset;
594 } else {
595 // Repaint everything under the repaint boundary.
596 // We call debugInstrumentRepaintCompositedChild instead of paintChild as
597 // we need to force everything under the repaint boundary to repaint.
598 PaintingContext.debugInstrumentRepaintCompositedChild(
599 repaintBoundary,
600 customContext: context,
601 );
602 }
603
604 // The check that debugPaintSizeEnabled is false exists to ensure we only
605 // call debugPaint when it wasn't already called.
606 if (debugPaint && !debugPaintSizeEnabled) {
607 data.includeInRegularContext = false;
608 // Existing recording may be to a canvas that draws to both the normal and
609 // screenshot canvases.
610 context.stopRecordingIfNeeded();
611 assert(data.foundTarget);
612 data.includeInScreenshot = true;
613
614 debugPaintSizeEnabled = true;
615 try {
616 renderObject.debugPaint(context, data.screenshotOffset);
617 } finally {
618 debugPaintSizeEnabled = false;
619 context.stopRecordingIfNeeded();
620 }
621 }
622
623 // We must build the regular scene before we can build the screenshot
624 // scene as building the screenshot scene assumes addToScene has already
625 // been called successfully for all layers in the regular scene.
626 repaintBoundary.debugLayer!.buildScene(ui.SceneBuilder());
627
628 final ui.Image image;
629
630 try {
631 image = await data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
632 } finally {
633 data.dispose();
634 }
635
636 return image;
637 }
638}
639
640/// A class describing a step along a path through a tree of [DiagnosticsNode]
641/// objects.
642///
643/// This class is used to bundle all data required to display the tree with just
644/// the nodes along a path expanded into a single JSON payload.
645class _DiagnosticsPathNode {
646 /// Creates a full description of a step in a path through a tree of
647 /// [DiagnosticsNode] objects.
648 _DiagnosticsPathNode({
649 required this.node,
650 required this.children,
651 this.childIndex,
652 });
653
654 /// Node at the point in the path this [_DiagnosticsPathNode] is describing.
655 final DiagnosticsNode node;
656
657 /// Children of the [node] being described.
658 ///
659 /// This value is cached instead of relying on `node.getChildren()` as that
660 /// method call might create new [DiagnosticsNode] objects for each child
661 /// and we would prefer to use the identical [DiagnosticsNode] for each time
662 /// a node exists in the path.
663 final List<DiagnosticsNode> children;
664
665 /// Index of the child that the path continues on.
666 ///
667 /// Equal to null if the path does not continue.
668 final int? childIndex;
669}
670
671List<_DiagnosticsPathNode>? _followDiagnosticableChain(
672 List<Diagnosticable> chain, {
673 String? name,
674 DiagnosticsTreeStyle? style,
675}) {
676 final List<_DiagnosticsPathNode> path = <_DiagnosticsPathNode>[];
677 if (chain.isEmpty) {
678 return path;
679 }
680 DiagnosticsNode diagnostic = chain.first.toDiagnosticsNode(name: name, style: style);
681 for (int i = 1; i < chain.length; i += 1) {
682 final Diagnosticable target = chain[i];
683 bool foundMatch = false;
684 final List<DiagnosticsNode> children = diagnostic.getChildren();
685 for (int j = 0; j < children.length; j += 1) {
686 final DiagnosticsNode child = children[j];
687 if (child.value == target) {
688 foundMatch = true;
689 path.add(_DiagnosticsPathNode(
690 node: diagnostic,
691 children: children,
692 childIndex: j,
693 ));
694 diagnostic = child;
695 break;
696 }
697 }
698 assert(foundMatch);
699 }
700 path.add(_DiagnosticsPathNode(node: diagnostic, children: diagnostic.getChildren()));
701 return path;
702}
703
704/// Signature for the selection change callback used by
705/// [WidgetInspectorService.selectionChangedCallback].
706typedef InspectorSelectionChangedCallback = void Function();
707
708/// Structure to help reference count Dart objects referenced by a GUI tool
709/// using [WidgetInspectorService].
710///
711/// Does not hold the object from garbage collection.
712@visibleForTesting
713class InspectorReferenceData {
714 /// Creates an instance of [InspectorReferenceData].
715 InspectorReferenceData(Object object, this.id) {
716 // These types are not supported by [WeakReference].
717 // See https://api.dart.dev/stable/3.0.2/dart-core/WeakReference-class.html
718 if (object is String || object is num || object is bool) {
719 _value = object;
720 return;
721 }
722
723 _ref = WeakReference<Object>(object);
724 }
725
726 WeakReference<Object>? _ref;
727
728 Object? _value;
729
730 /// The id of the object in the widget inspector records.
731 final String id;
732
733 /// The number of times the object has been referenced.
734 int count = 1;
735
736 /// The value.
737 Object? get value => _ref?.target ?? _value;
738}
739
740// Production implementation of [WidgetInspectorService].
741class _WidgetInspectorService with WidgetInspectorService {
742 _WidgetInspectorService() {
743 selection.addListener(() => selectionChangedCallback?.call());
744 }
745}
746
747/// Service used by GUI tools to interact with the [WidgetInspector].
748///
749/// Calls to this object are typically made from GUI tools such as the [Flutter
750/// IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
751/// using the [Dart VM Service](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md).
752/// This class uses its own object id and manages object lifecycles itself
753/// instead of depending on the [object ids](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#getobject)
754/// specified by the VM Service Protocol because the VM Service Protocol ids
755/// expire unpredictably. Object references are tracked in groups so that tools
756/// that clients can use dereference all objects in a group with a single
757/// operation making it easier to avoid memory leaks.
758///
759/// All methods in this class are appropriate to invoke from debugging tools
760/// using the VM service protocol to evaluate Dart expressions of the
761/// form `WidgetInspectorService.instance.methodName(arg1, arg2, ...)`. If you
762/// make changes to any instance method of this class you need to verify that
763/// the [Flutter IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
764/// widget inspector support still works with the changes.
765///
766/// All methods returning String values return JSON.
767mixin WidgetInspectorService {
768 /// Ring of cached JSON values to prevent JSON from being garbage
769 /// collected before it can be requested over the VM service protocol.
770 final List<String?> _serializeRing = List<String?>.filled(20, null);
771 int _serializeRingIndex = 0;
772
773 /// The current [WidgetInspectorService].
774 static WidgetInspectorService get instance => _instance;
775 static WidgetInspectorService _instance = _WidgetInspectorService();
776
777 /// Whether the inspector is in select mode.
778 ///
779 /// In select mode, pointer interactions trigger widget selection instead of
780 /// normal interactions. Otherwise the previously selected widget is
781 /// highlighted but the application can be interacted with normally.
782 @visibleForTesting
783 final ValueNotifier<bool> isSelectMode = ValueNotifier<bool>(true);
784
785 @protected
786 static set instance(WidgetInspectorService instance) {
787 _instance = instance;
788 }
789
790 static bool _debugServiceExtensionsRegistered = false;
791
792 /// Ground truth tracking what object(s) are currently selected used by both
793 /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector]
794 /// displayed on the device.
795 final InspectorSelection selection = InspectorSelection();
796
797 /// Callback typically registered by the [WidgetInspector] to receive
798 /// notifications when [selection] changes.
799 ///
800 /// The Flutter IntelliJ Plugin does not need to listen for this event as it
801 /// instead listens for `dart:developer` `inspect` events which also trigger
802 /// when the inspection target changes on device.
803 InspectorSelectionChangedCallback? selectionChangedCallback;
804
805 /// The VM service protocol does not keep alive object references so this
806 /// class needs to manually manage groups of objects that should be kept
807 /// alive.
808 final Map<String, Set<InspectorReferenceData>> _groups = <String, Set<InspectorReferenceData>>{};
809 final Map<String, InspectorReferenceData> _idToReferenceData = <String, InspectorReferenceData>{};
810 final WeakMap<Object, String> _objectToId = WeakMap<Object, String>();
811 int _nextId = 0;
812
813 /// The pubRootDirectories that are currently configured for the widget inspector.
814 List<String>? _pubRootDirectories;
815
816 /// Memoization for [_isLocalCreationLocation].
817 final HashMap<String, bool> _isLocalCreationCache = HashMap<String, bool>();
818
819 bool _trackRebuildDirtyWidgets = false;
820 bool _trackRepaintWidgets = false;
821
822 /// Registers a service extension method with the given name (full
823 /// name "ext.flutter.inspector.name").
824 ///
825 /// The given callback is called when the extension method is called. The
826 /// callback must return a value that can be converted to JSON using
827 /// `json.encode()` (see [JsonEncoder]). The return value is stored as a
828 /// property named `result` in the JSON. In case of failure, the failure is
829 /// reported to the remote caller and is dumped to the logs.
830 @protected
831 void registerServiceExtension({
832 required String name,
833 required ServiceExtensionCallback callback,
834 required RegisterServiceExtensionCallback registerExtension,
835 }) {
836 registerExtension(
837 name: 'inspector.$name',
838 callback: callback,
839 );
840 }
841
842 /// Registers a service extension method with the given name (full
843 /// name "ext.flutter.inspector.name"), which takes no arguments.
844 void _registerSignalServiceExtension({
845 required String name,
846 required FutureOr<Object?> Function() callback,
847 required RegisterServiceExtensionCallback registerExtension,
848 }) {
849 registerServiceExtension(
850 name: name,
851 callback: (Map<String, String> parameters) async {
852 return <String, Object?>{'result': await callback()};
853 },
854 registerExtension: registerExtension,
855 );
856 }
857
858 /// Registers a service extension method with the given name (full
859 /// name "ext.flutter.inspector.name"), which takes a single optional argument
860 /// "objectGroup" specifying what group is used to manage lifetimes of
861 /// object references in the returned JSON (see [disposeGroup]).
862 /// If "objectGroup" is omitted, the returned JSON will not include any object
863 /// references to avoid leaking memory.
864 void _registerObjectGroupServiceExtension({
865 required String name,
866 required FutureOr<Object?> Function(String objectGroup) callback,
867 required RegisterServiceExtensionCallback registerExtension,
868 }) {
869 registerServiceExtension(
870 name: name,
871 callback: (Map<String, String> parameters) async {
872 return <String, Object?>{'result': await callback(parameters['objectGroup']!)};
873 },
874 registerExtension: registerExtension,
875 );
876 }
877
878 /// Registers a service extension method with the given name (full
879 /// name "ext.flutter.inspector.name"), which takes a single argument
880 /// "enabled" which can have the value "true" or the value "false"
881 /// or can be omitted to read the current value. (Any value other
882 /// than "true" is considered equivalent to "false". Other arguments
883 /// are ignored.)
884 ///
885 /// Calls the `getter` callback to obtain the value when
886 /// responding to the service extension method being called.
887 ///
888 /// Calls the `setter` callback with the new value when the
889 /// service extension method is called with a new value.
890 void _registerBoolServiceExtension({
891 required String name,
892 required AsyncValueGetter<bool> getter,
893 required AsyncValueSetter<bool> setter,
894 required RegisterServiceExtensionCallback registerExtension,
895 }) {
896 registerServiceExtension(
897 name: name,
898 callback: (Map<String, String> parameters) async {
899 if (parameters.containsKey('enabled')) {
900 final bool value = parameters['enabled'] == 'true';
901 await setter(value);
902 _postExtensionStateChangedEvent(name, value);
903 }
904 return <String, dynamic>{'enabled': await getter() ? 'true' : 'false'};
905 },
906 registerExtension: registerExtension,
907 );
908 }
909
910 /// Sends an event when a service extension's state is changed.
911 ///
912 /// Clients should listen for this event to stay aware of the current service
913 /// extension state. Any service extension that manages a state should call
914 /// this method on state change.
915 ///
916 /// `value` reflects the newly updated service extension value.
917 ///
918 /// This will be called automatically for service extensions registered via
919 /// [BindingBase.registerBoolServiceExtension].
920 void _postExtensionStateChangedEvent(String name, Object? value) {
921 postEvent(
922 'Flutter.ServiceExtensionStateChanged',
923 <String, Object?>{
924 'extension': 'ext.flutter.inspector.$name',
925 'value': value,
926 },
927 );
928 }
929
930 /// Registers a service extension method with the given name (full
931 /// name "ext.flutter.inspector.name") which takes an optional parameter named
932 /// "arg" and a required parameter named "objectGroup" used to control the
933 /// lifetimes of object references in the returned JSON (see [disposeGroup]).
934 void _registerServiceExtensionWithArg({
935 required String name,
936 required FutureOr<Object?> Function(String? objectId, String objectGroup) callback,
937 required RegisterServiceExtensionCallback registerExtension,
938 }) {
939 registerServiceExtension(
940 name: name,
941 callback: (Map<String, String> parameters) async {
942 assert(parameters.containsKey('objectGroup'));
943 return <String, Object?>{
944 'result': await callback(parameters['arg'], parameters['objectGroup']!),
945 };
946 },
947 registerExtension: registerExtension,
948 );
949 }
950
951 /// Registers a service extension method with the given name (full
952 /// name "ext.flutter.inspector.name"), that takes arguments
953 /// "arg0", "arg1", "arg2", ..., "argn".
954 void _registerServiceExtensionVarArgs({
955 required String name,
956 required FutureOr<Object?> Function(List<String> args) callback,
957 required RegisterServiceExtensionCallback registerExtension,
958 }) {
959 registerServiceExtension(
960 name: name,
961 callback: (Map<String, String> parameters) async {
962 int index;
963 final List<String> args = <String>[
964 for (index = 0; parameters['arg$index'] != null; index++)
965 parameters['arg$index']!,
966 ];
967 // Verify that the only arguments other than perhaps 'isolateId' are
968 // arguments we have already handled.
969 assert(index == parameters.length || (index == parameters.length - 1 && parameters.containsKey('isolateId')));
970 return <String, Object?>{'result': await callback(args)};
971 },
972 registerExtension: registerExtension,
973 );
974 }
975
976 /// Cause the entire tree to be rebuilt. This is used by development tools
977 /// when the application code has changed and is being hot-reloaded, to cause
978 /// the widget tree to pick up any changed implementations.
979 ///
980 /// This is expensive and should not be called except during development.
981 @protected
982 Future<void> forceRebuild() {
983 final WidgetsBinding binding = WidgetsBinding.instance;
984 if (binding.rootElement != null) {
985 binding.buildOwner!.reassemble(binding.rootElement!);
986 return binding.endOfFrame;
987 }
988 return Future<void>.value();
989 }
990
991 static const String _consoleObjectGroup = 'console-group';
992
993 int _errorsSinceReload = 0;
994
995 void _reportStructuredError(FlutterErrorDetails details) {
996 final Map<String, Object?> errorJson = _nodeToJson(
997 details.toDiagnosticsNode(),
998 InspectorSerializationDelegate(
999 groupName: _consoleObjectGroup,
1000 subtreeDepth: 5,
1001 includeProperties: true,
1002 maxDescendantsTruncatableNode: 5,
1003 service: this,
1004 ),
1005 )!;
1006
1007 errorJson['errorsSinceReload'] = _errorsSinceReload;
1008 if (_errorsSinceReload == 0) {
1009 errorJson['renderedErrorText'] = TextTreeRenderer(
1010 wrapWidthProperties: FlutterError.wrapWidth,
1011 maxDescendentsTruncatableNode: 5,
1012 ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight();
1013 } else {
1014 errorJson['renderedErrorText'] = 'Another exception was thrown: ${details.summary}';
1015 }
1016
1017 _errorsSinceReload += 1;
1018 postEvent('Flutter.Error', errorJson);
1019 }
1020
1021 /// Resets the count of errors since the last hot reload.
1022 ///
1023 /// This data is sent to clients as part of the 'Flutter.Error' service
1024 /// protocol event. Clients may choose to display errors received after the
1025 /// first error differently.
1026 void _resetErrorCount() {
1027 _errorsSinceReload = 0;
1028 }
1029
1030 /// Whether structured errors are enabled.
1031 ///
1032 /// Structured errors provide semantic information that can be used by IDEs
1033 /// to enhance the display of errors with rich formatting.
1034 bool isStructuredErrorsEnabled() {
1035 // This is a debug mode only feature and will default to false for
1036 // profile mode.
1037 bool enabled = false;
1038 assert(() {
1039 // TODO(kenz): add support for structured errors on the web.
1040 enabled = const bool.fromEnvironment('flutter.inspector.structuredErrors', defaultValue: !kIsWeb);
1041 return true;
1042 }());
1043 return enabled;
1044 }
1045
1046 /// Called to register service extensions.
1047 ///
1048 /// See also:
1049 ///
1050 /// * <https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#rpcs-requests-and-responses>
1051 /// * [BindingBase.initServiceExtensions], which explains when service
1052 /// extensions can be used.
1053 void initServiceExtensions(RegisterServiceExtensionCallback registerExtension) {
1054 final FlutterExceptionHandler defaultExceptionHandler = FlutterError.presentError;
1055
1056 if (isStructuredErrorsEnabled()) {
1057 FlutterError.presentError = _reportStructuredError;
1058 }
1059 assert(!_debugServiceExtensionsRegistered);
1060 assert(() {
1061 _debugServiceExtensionsRegistered = true;
1062 return true;
1063 }());
1064
1065 SchedulerBinding.instance.addPersistentFrameCallback(_onFrameStart);
1066
1067 _registerBoolServiceExtension(
1068 name: WidgetInspectorServiceExtensions.structuredErrors.name,
1069 getter: () async => FlutterError.presentError == _reportStructuredError,
1070 setter: (bool value) {
1071 FlutterError.presentError = value ? _reportStructuredError : defaultExceptionHandler;
1072 return Future<void>.value();
1073 },
1074 registerExtension: registerExtension,
1075 );
1076
1077 _registerBoolServiceExtension(
1078 name: WidgetInspectorServiceExtensions.show.name,
1079 getter: () async => WidgetsBinding.instance.debugShowWidgetInspectorOverride,
1080 setter: (bool value) {
1081 if (WidgetsBinding.instance.debugShowWidgetInspectorOverride != value) {
1082 WidgetsBinding.instance.debugShowWidgetInspectorOverride = value;
1083 }
1084 return Future<void>.value();
1085 },
1086 registerExtension: registerExtension,
1087 );
1088
1089 if (isWidgetCreationTracked()) {
1090 // Service extensions that are only supported if widget creation locations
1091 // are tracked.
1092 _registerBoolServiceExtension(
1093 name: WidgetInspectorServiceExtensions.trackRebuildDirtyWidgets.name,
1094 getter: () async => _trackRebuildDirtyWidgets,
1095 setter: (bool value) async {
1096 if (value == _trackRebuildDirtyWidgets) {
1097 return;
1098 }
1099 _rebuildStats.resetCounts();
1100 _trackRebuildDirtyWidgets = value;
1101 if (value) {
1102 assert(debugOnRebuildDirtyWidget == null);
1103 debugOnRebuildDirtyWidget = _onRebuildWidget;
1104 // Trigger a rebuild so there are baseline stats for rebuilds
1105 // performed by the app.
1106 await forceRebuild();
1107 return;
1108 } else {
1109 debugOnRebuildDirtyWidget = null;
1110 return;
1111 }
1112 },
1113 registerExtension: registerExtension,
1114 );
1115
1116 _registerSignalServiceExtension(
1117 name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
1118 callback: () {
1119 return _locationIdMapToJson();
1120 },
1121 registerExtension: registerExtension,
1122 );
1123
1124 _registerBoolServiceExtension(
1125 name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
1126 getter: () async => _trackRepaintWidgets,
1127 setter: (bool value) async {
1128 if (value == _trackRepaintWidgets) {
1129 return;
1130 }
1131 _repaintStats.resetCounts();
1132 _trackRepaintWidgets = value;
1133 if (value) {
1134 assert(debugOnProfilePaint == null);
1135 debugOnProfilePaint = _onPaint;
1136 // Trigger an immediate paint so the user has some baseline painting
1137 // stats to view.
1138 void markTreeNeedsPaint(RenderObject renderObject) {
1139 renderObject.markNeedsPaint();
1140 renderObject.visitChildren(markTreeNeedsPaint);
1141 }
1142 RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint);
1143 } else {
1144 debugOnProfilePaint = null;
1145 }
1146 },
1147 registerExtension: registerExtension,
1148 );
1149 }
1150
1151 _registerSignalServiceExtension(
1152 name: WidgetInspectorServiceExtensions.disposeAllGroups.name,
1153 callback: () async {
1154 disposeAllGroups();
1155 return null;
1156 },
1157 registerExtension: registerExtension,
1158 );
1159 _registerObjectGroupServiceExtension(
1160 name: WidgetInspectorServiceExtensions.disposeGroup.name,
1161 callback: (String name) async {
1162 disposeGroup(name);
1163 return null;
1164 },
1165 registerExtension: registerExtension,
1166 );
1167 _registerSignalServiceExtension(
1168 name: WidgetInspectorServiceExtensions.isWidgetTreeReady.name,
1169 callback: isWidgetTreeReady,
1170 registerExtension: registerExtension,
1171 );
1172 _registerServiceExtensionWithArg(
1173 name: WidgetInspectorServiceExtensions.disposeId.name,
1174 callback: (String? objectId, String objectGroup) async {
1175 disposeId(objectId, objectGroup);
1176 return null;
1177 },
1178 registerExtension: registerExtension,
1179 );
1180 _registerServiceExtensionVarArgs(
1181 name: WidgetInspectorServiceExtensions.setPubRootDirectories.name,
1182 callback: (List<String> args) async {
1183 setPubRootDirectories(args);
1184 return null;
1185 },
1186 registerExtension: registerExtension,
1187 );
1188 _registerServiceExtensionVarArgs(
1189 name: WidgetInspectorServiceExtensions.addPubRootDirectories.name,
1190 callback: (List<String> args) async {
1191 addPubRootDirectories(args);
1192 return null;
1193 },
1194 registerExtension: registerExtension,
1195 );
1196 _registerServiceExtensionVarArgs(
1197 name: WidgetInspectorServiceExtensions.removePubRootDirectories.name,
1198 callback: (List<String> args) async {
1199 removePubRootDirectories(args);
1200 return null;
1201 },
1202 registerExtension: registerExtension,
1203 );
1204 registerServiceExtension(
1205 name: WidgetInspectorServiceExtensions.getPubRootDirectories.name,
1206 callback: pubRootDirectories,
1207 registerExtension: registerExtension,
1208 );
1209 _registerServiceExtensionWithArg(
1210 name: WidgetInspectorServiceExtensions.setSelectionById.name,
1211 callback: setSelectionById,
1212 registerExtension: registerExtension,
1213 );
1214 _registerServiceExtensionWithArg(
1215 name: WidgetInspectorServiceExtensions.getParentChain.name,
1216 callback: _getParentChain,
1217 registerExtension: registerExtension,
1218 );
1219 _registerServiceExtensionWithArg(
1220 name: WidgetInspectorServiceExtensions.getProperties.name,
1221 callback: _getProperties,
1222 registerExtension: registerExtension,
1223 );
1224 _registerServiceExtensionWithArg(
1225 name: WidgetInspectorServiceExtensions.getChildren.name,
1226 callback: _getChildren,
1227 registerExtension: registerExtension,
1228 );
1229
1230 _registerServiceExtensionWithArg(
1231 name: WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
1232 callback: _getChildrenSummaryTree,
1233 registerExtension: registerExtension,
1234 );
1235
1236 _registerServiceExtensionWithArg(
1237 name: WidgetInspectorServiceExtensions.getChildrenDetailsSubtree.name,
1238 callback: _getChildrenDetailsSubtree,
1239 registerExtension: registerExtension,
1240 );
1241
1242 _registerObjectGroupServiceExtension(
1243 name: WidgetInspectorServiceExtensions.getRootWidget.name,
1244 callback: _getRootWidget,
1245 registerExtension: registerExtension,
1246 );
1247 _registerObjectGroupServiceExtension(
1248 name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
1249 callback: _getRootWidgetSummaryTree,
1250 registerExtension: registerExtension,
1251 );
1252 registerServiceExtension(
1253 name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name,
1254 callback: _getRootWidgetSummaryTreeWithPreviews,
1255 registerExtension: registerExtension,
1256 );
1257 registerServiceExtension(
1258 name: WidgetInspectorServiceExtensions.getRootWidgetTree.name,
1259 callback: _getRootWidgetTree,
1260 registerExtension: registerExtension,
1261 );
1262 registerServiceExtension(
1263 name: WidgetInspectorServiceExtensions.getDetailsSubtree.name,
1264 callback: (Map<String, String> parameters) async {
1265 assert(parameters.containsKey('objectGroup'));
1266 final String? subtreeDepth = parameters['subtreeDepth'];
1267 return <String, Object?>{
1268 'result': _getDetailsSubtree(
1269 parameters['arg'],
1270 parameters['objectGroup'],
1271 subtreeDepth != null ? int.parse(subtreeDepth) : 2,
1272 ),
1273 };
1274 },
1275 registerExtension: registerExtension,
1276 );
1277 _registerServiceExtensionWithArg(
1278 name: WidgetInspectorServiceExtensions.getSelectedWidget.name,
1279 callback: _getSelectedWidget,
1280 registerExtension: registerExtension,
1281 );
1282 _registerServiceExtensionWithArg(
1283 name: WidgetInspectorServiceExtensions.getSelectedSummaryWidget.name,
1284 callback: _getSelectedSummaryWidget,
1285 registerExtension: registerExtension,
1286 );
1287
1288 _registerSignalServiceExtension(
1289 name: WidgetInspectorServiceExtensions.isWidgetCreationTracked.name,
1290 callback: isWidgetCreationTracked,
1291 registerExtension: registerExtension,
1292 );
1293 registerServiceExtension(
1294 name: WidgetInspectorServiceExtensions.screenshot.name,
1295 callback: (Map<String, String> parameters) async {
1296 assert(parameters.containsKey('id'));
1297 assert(parameters.containsKey('width'));
1298 assert(parameters.containsKey('height'));
1299
1300 final ui.Image? image = await screenshot(
1301 toObject(parameters['id']),
1302 width: double.parse(parameters['width']!),
1303 height: double.parse(parameters['height']!),
1304 margin: parameters.containsKey('margin') ?
1305 double.parse(parameters['margin']!) : 0.0,
1306 maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
1307 double.parse(parameters['maxPixelRatio']!) : 1.0,
1308 debugPaint: parameters['debugPaint'] == 'true',
1309 );
1310 if (image == null) {
1311 return <String, Object?>{'result': null};
1312 }
1313 final ByteData? byteData = await image.toByteData(format:ui.ImageByteFormat.png);
1314 image.dispose();
1315
1316 return <String, Object>{
1317 'result': base64.encoder.convert(Uint8List.view(byteData!.buffer)),
1318 };
1319 },
1320 registerExtension: registerExtension,
1321 );
1322 registerServiceExtension(
1323 name: WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
1324 callback: _getLayoutExplorerNode,
1325 registerExtension: registerExtension,
1326 );
1327 registerServiceExtension(
1328 name: WidgetInspectorServiceExtensions.setFlexFit.name,
1329 callback: _setFlexFit,
1330 registerExtension: registerExtension,
1331 );
1332 registerServiceExtension(
1333 name: WidgetInspectorServiceExtensions.setFlexFactor.name,
1334 callback: _setFlexFactor,
1335 registerExtension: registerExtension,
1336 );
1337 registerServiceExtension(
1338 name: WidgetInspectorServiceExtensions.setFlexProperties.name,
1339 callback: _setFlexProperties,
1340 registerExtension: registerExtension,
1341 );
1342 }
1343
1344 void _clearStats() {
1345 _rebuildStats.resetCounts();
1346 _repaintStats.resetCounts();
1347 }
1348
1349 /// Clear all InspectorService object references.
1350 ///
1351 /// Use this method only for testing to ensure that object references from one
1352 /// test case do not impact other test cases.
1353 @visibleForTesting
1354 @protected
1355 void disposeAllGroups() {
1356 _groups.clear();
1357 _idToReferenceData.clear();
1358 _objectToId.clear();
1359 _nextId = 0;
1360 }
1361
1362 /// Reset all InspectorService state.
1363 ///
1364 /// Use this method only for testing to write hermetic tests for
1365 /// WidgetInspectorService.
1366 @visibleForTesting
1367 @protected
1368 @mustCallSuper
1369 void resetAllState() {
1370 disposeAllGroups();
1371 selection.clear();
1372 resetPubRootDirectories();
1373 }
1374
1375 /// Free all references to objects in a group.
1376 ///
1377 /// Objects and their associated ids in the group may be kept alive by
1378 /// references from a different group.
1379 @protected
1380 void disposeGroup(String name) {
1381 final Set<InspectorReferenceData>? references = _groups.remove(name);
1382 if (references == null) {
1383 return;
1384 }
1385 references.forEach(_decrementReferenceCount);
1386 }
1387
1388 void _decrementReferenceCount(InspectorReferenceData reference) {
1389 reference.count -= 1;
1390 assert(reference.count >= 0);
1391 if (reference.count == 0) {
1392 final Object? value = reference.value;
1393 if (value != null) {
1394 _objectToId.remove(value);
1395 }
1396 _idToReferenceData.remove(reference.id);
1397 }
1398 }
1399
1400 /// Returns a unique id for [object] that will remain live at least until
1401 /// [disposeGroup] is called on [groupName].
1402 @protected
1403 String? toId(Object? object, String groupName) {
1404 if (object == null) {
1405 return null;
1406 }
1407
1408 final Set<InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<InspectorReferenceData>.identity());
1409 String? id = _objectToId[object];
1410 InspectorReferenceData referenceData;
1411 if (id == null) {
1412 // TODO(polina-c): comment here why we increase memory footprint by the prefix 'inspector-'.
1413 // https://github.com/flutter/devtools/issues/5995
1414 id = 'inspector-$_nextId';
1415 _nextId += 1;
1416 _objectToId[object] = id;
1417 referenceData = InspectorReferenceData(object, id);
1418 _idToReferenceData[id] = referenceData;
1419 group.add(referenceData);
1420 } else {
1421 referenceData = _idToReferenceData[id]!;
1422 if (group.add(referenceData)) {
1423 referenceData.count += 1;
1424 }
1425 }
1426 return id;
1427 }
1428
1429 /// Returns whether the application has rendered its first frame and it is
1430 /// appropriate to display the Widget tree in the inspector.
1431 @protected
1432 bool isWidgetTreeReady([ String? groupName ]) {
1433 return WidgetsBinding.instance.debugDidSendFirstFrameEvent;
1434 }
1435
1436 /// Returns the Dart object associated with a reference id.
1437 ///
1438 /// The `groupName` parameter is not required by is added to regularize the
1439 /// API surface of the methods in this class called from the Flutter IntelliJ
1440 /// Plugin.
1441 @protected
1442 Object? toObject(String? id, [ String? groupName ]) {
1443 if (id == null) {
1444 return null;
1445 }
1446
1447 final InspectorReferenceData? data = _idToReferenceData[id];
1448 if (data == null) {
1449 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]);
1450 }
1451 return data.value;
1452 }
1453
1454 /// Returns the object to introspect to determine the source location of an
1455 /// object's class.
1456 ///
1457 /// The Dart object for the id is returned for all cases but [Element] objects
1458 /// where the [Widget] configuring the [Element] is returned instead as the
1459 /// class of the [Widget] is more relevant than the class of the [Element].
1460 ///
1461 /// The `groupName` parameter is not required by is added to regularize the
1462 /// API surface of methods called from the Flutter IntelliJ Plugin.
1463 @protected
1464 Object? toObjectForSourceLocation(String id, [ String? groupName ]) {
1465 final Object? object = toObject(id);
1466 if (object is Element) {
1467 return object.widget;
1468 }
1469 return object;
1470 }
1471
1472 /// Remove the object with the specified `id` from the specified object
1473 /// group.
1474 ///
1475 /// If the object exists in other groups it will remain alive and the object
1476 /// id will remain valid.
1477 @protected
1478 void disposeId(String? id, String groupName) {
1479 if (id == null) {
1480 return;
1481 }
1482
1483 final InspectorReferenceData? referenceData = _idToReferenceData[id];
1484 if (referenceData == null) {
1485 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]);
1486 }
1487 if (_groups[groupName]?.remove(referenceData) != true) {
1488 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id is not in group')]);
1489 }
1490 _decrementReferenceCount(referenceData);
1491 }
1492
1493 /// Set the list of directories that should be considered part of the local
1494 /// project.
1495 ///
1496 /// The local project directories are used to distinguish widgets created by
1497 /// the local project from widgets created from inside the framework
1498 /// or other packages.
1499 @protected
1500 @Deprecated(
1501 'Use addPubRootDirectories instead. '
1502 'This feature was deprecated after v3.18.0-2.0.pre.',
1503 )
1504 void setPubRootDirectories(List<String> pubRootDirectories) {
1505 addPubRootDirectories(pubRootDirectories);
1506 }
1507
1508 /// Resets the list of directories, that should be considered part of the
1509 /// local project, to the value passed in [pubRootDirectories].
1510 ///
1511 /// The local project directories are used to distinguish widgets created by
1512 /// the local project from widgets created from inside the framework
1513 /// or other packages.
1514 @visibleForTesting
1515 @protected
1516 void resetPubRootDirectories() {
1517 _pubRootDirectories = <String>[];
1518 _isLocalCreationCache.clear();
1519 }
1520
1521 /// Add a list of directories that should be considered part of the local
1522 /// project.
1523 ///
1524 /// The local project directories are used to distinguish widgets created by
1525 /// the local project from widgets created from inside the framework
1526 /// or other packages.
1527 @protected
1528 void addPubRootDirectories(List<String> pubRootDirectories) {
1529 pubRootDirectories = pubRootDirectories.map<String>((String directory) => Uri.parse(directory).path).toList();
1530
1531 final Set<String> directorySet = Set<String>.from(pubRootDirectories);
1532 if (_pubRootDirectories != null) {
1533 directorySet.addAll(_pubRootDirectories!);
1534 }
1535
1536 _pubRootDirectories = directorySet.toList();
1537 _isLocalCreationCache.clear();
1538 }
1539
1540 /// Remove a list of directories that should no longer be considered part
1541 /// of the local project.
1542 ///
1543 /// The local project directories are used to distinguish widgets created by
1544 /// the local project from widgets created from inside the framework
1545 /// or other packages.
1546 @protected
1547 void removePubRootDirectories(List<String> pubRootDirectories) {
1548 if (_pubRootDirectories == null) {
1549 return;
1550 }
1551 pubRootDirectories = pubRootDirectories.map<String>((String directory) => Uri.parse(directory).path).toList();
1552
1553 final Set<String> directorySet = Set<String>.from(_pubRootDirectories!);
1554 directorySet.removeAll(pubRootDirectories);
1555
1556 _pubRootDirectories = directorySet.toList();
1557 _isLocalCreationCache.clear();
1558 }
1559
1560 /// Returns the list of directories that should be considered part of the
1561 /// local project.
1562 @protected
1563 @visibleForTesting
1564 Future<Map<String, dynamic>> pubRootDirectories(
1565 Map<String, String> parameters,
1566 ) {
1567 return Future<Map<String, Object>>.value(<String, Object>{
1568 'result': _pubRootDirectories ?? <String>[],
1569 });
1570 }
1571
1572 /// Set the [WidgetInspector] selection to the object matching the specified
1573 /// id if the object is valid object to set as the inspector selection.
1574 ///
1575 /// Returns true if the selection was changed.
1576 ///
1577 /// The `groupName` parameter is not required by is added to regularize the
1578 /// API surface of methods called from the Flutter IntelliJ Plugin.
1579 @protected
1580 bool setSelectionById(String? id, [ String? groupName ]) {
1581 return setSelection(toObject(id), groupName);
1582 }
1583
1584 /// Set the [WidgetInspector] selection to the specified `object` if it is
1585 /// a valid object to set as the inspector selection.
1586 ///
1587 /// Returns true if the selection was changed.
1588 ///
1589 /// The `groupName` parameter is not needed but is specified to regularize the
1590 /// API surface of methods called from the Flutter IntelliJ Plugin.
1591 @protected
1592 bool setSelection(Object? object, [ String? groupName ]) {
1593 switch (object) {
1594 case Element() when object != selection.currentElement:
1595 selection.currentElement = object;
1596 _sendInspectEvent(selection.currentElement);
1597 return true;
1598 case RenderObject() when object != selection.current:
1599 selection.current = object;
1600 _sendInspectEvent(selection.current);
1601 return true;
1602 }
1603 return false;
1604 }
1605
1606 /// Notify attached tools to navigate to an object's source location.
1607 void _sendInspectEvent(Object? object){
1608 inspect(object);
1609
1610 final _Location? location = _getSelectedSummaryWidgetLocation(null);
1611 if (location != null) {
1612 postEvent(
1613 'navigate',
1614 <String, Object>{
1615 'fileUri': location.file, // URI file path of the location.
1616 'line': location.line, // 1-based line number.
1617 'column': location.column, // 1-based column number.
1618 'source': 'flutter.inspector',
1619 },
1620 stream: 'ToolEvent',
1621 );
1622 }
1623 }
1624
1625 /// Returns a DevTools uri linking to a specific element on the inspector page.
1626 String? _devToolsInspectorUriForElement(Element element) {
1627 if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
1628 final String? inspectorRef = toId(element, _consoleObjectGroup);
1629 if (inspectorRef != null) {
1630 return devToolsInspectorUri(inspectorRef);
1631 }
1632 }
1633 return null;
1634 }
1635
1636 /// Returns the DevTools inspector uri for the given vm service connection and
1637 /// inspector reference.
1638 @visibleForTesting
1639 String devToolsInspectorUri(String inspectorRef) {
1640 assert(activeDevToolsServerAddress != null);
1641 assert(connectedVmServiceUri != null);
1642
1643 final Uri uri = Uri.parse(activeDevToolsServerAddress!).replace(
1644 queryParameters: <String, dynamic>{
1645 'uri': connectedVmServiceUri,
1646 'inspectorRef': inspectorRef,
1647 },
1648 );
1649
1650 // We cannot add the '/#/inspector' path by means of
1651 // [Uri.replace(path: '/#/inspector')] because the '#' character will be
1652 // encoded when we try to print the url as a string. DevTools will not
1653 // load properly if this character is encoded in the url.
1654 // Related: https://github.com/flutter/devtools/issues/2475.
1655 final String devToolsInspectorUri = uri.toString();
1656 final int startQueryParamIndex = devToolsInspectorUri.indexOf('?');
1657 // The query parameter character '?' should be present because we manually
1658 // added query parameters above.
1659 assert(startQueryParamIndex != -1);
1660 return '${devToolsInspectorUri.substring(0, startQueryParamIndex)}'
1661 '/#/inspector'
1662 '${devToolsInspectorUri.substring(startQueryParamIndex)}';
1663 }
1664
1665 /// Returns JSON representing the chain of [DiagnosticsNode] instances from
1666 /// root of the tree to the [Element] or [RenderObject] matching `id`.
1667 ///
1668 /// The JSON contains all information required to display a tree view with
1669 /// all nodes other than nodes along the path collapsed.
1670 @protected
1671 String getParentChain(String id, String groupName) {
1672 return _safeJsonEncode(_getParentChain(id, groupName));
1673 }
1674
1675 List<Object?> _getParentChain(String? id, String groupName) {
1676 final Object? value = toObject(id);
1677 final List<_DiagnosticsPathNode> path = switch (value) {
1678 RenderObject() => _getRenderObjectParentChain(value, groupName)!,
1679 Element() => _getElementParentChain(value, groupName),
1680 _ => throw FlutterError.fromParts(<DiagnosticsNode>[
1681 ErrorSummary('Cannot get parent chain for node of type ${value.runtimeType}'),
1682 ]),
1683 };
1684
1685 InspectorSerializationDelegate createDelegate() =>
1686 InspectorSerializationDelegate(groupName: groupName, service: this);
1687
1688 return <Object?>[
1689 for (final _DiagnosticsPathNode pathNode in path)
1690 if (createDelegate() case final InspectorSerializationDelegate delegate)
1691 <String, Object?>{
1692 'node': _nodeToJson(pathNode.node, delegate),
1693 'children': _nodesToJson(pathNode.children, delegate, parent: pathNode.node),
1694 'childIndex': pathNode.childIndex,
1695 },
1696 ];
1697 }
1698
1699 List<Element> _getRawElementParentChain(Element element, { required int? numLocalParents }) {
1700 List<Element> elements = element.debugGetDiagnosticChain();
1701 if (numLocalParents != null) {
1702 for (int i = 0; i < elements.length; i += 1) {
1703 if (_isValueCreatedByLocalProject(elements[i])) {
1704 numLocalParents = numLocalParents! - 1;
1705 if (numLocalParents <= 0) {
1706 elements = elements.take(i + 1).toList();
1707 break;
1708 }
1709 }
1710 }
1711 }
1712 return elements.reversed.toList();
1713 }
1714
1715 List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName, { int? numLocalParents }) {
1716 return _followDiagnosticableChain(
1717 _getRawElementParentChain(element, numLocalParents: numLocalParents),
1718 ) ?? const <_DiagnosticsPathNode>[];
1719 }
1720
1721 List<_DiagnosticsPathNode>? _getRenderObjectParentChain(RenderObject? renderObject, String groupName) {
1722 final List<RenderObject> chain = <RenderObject>[];
1723 while (renderObject != null) {
1724 chain.add(renderObject);
1725 renderObject = renderObject.parent;
1726 }
1727 return _followDiagnosticableChain(chain.reversed.toList());
1728 }
1729
1730 Map<String, Object?>? _nodeToJson(
1731 DiagnosticsNode? node,
1732 InspectorSerializationDelegate delegate,
1733 ) {
1734 return node?.toJsonMap(delegate);
1735 }
1736
1737 bool _isValueCreatedByLocalProject(Object? value) {
1738 final _Location? creationLocation = _getCreationLocation(value);
1739 if (creationLocation == null) {
1740 return false;
1741 }
1742 return _isLocalCreationLocation(creationLocation.file);
1743 }
1744
1745 bool _isLocalCreationLocationImpl(String locationUri) {
1746 final String file = Uri.parse(locationUri).path;
1747
1748 // By default check whether the creation location was within package:flutter.
1749 if (_pubRootDirectories == null) {
1750 // TODO(chunhtai): Make it more robust once
1751 // https://github.com/flutter/flutter/issues/32660 is fixed.
1752 return !file.contains('packages/flutter/');
1753 }
1754 for (final String directory in _pubRootDirectories!) {
1755 if (file.startsWith(directory)) {
1756 return true;
1757 }
1758 }
1759 return false;
1760 }
1761
1762 /// Memoized version of [_isLocalCreationLocationImpl].
1763 bool _isLocalCreationLocation(String locationUri) {
1764 final bool? cachedValue = _isLocalCreationCache[locationUri];
1765 if (cachedValue != null) {
1766 return cachedValue;
1767 }
1768 final bool result = _isLocalCreationLocationImpl(locationUri);
1769 _isLocalCreationCache[locationUri] = result;
1770 return result;
1771 }
1772
1773 /// Wrapper around `json.encode` that uses a ring of cached values to prevent
1774 /// the Dart garbage collector from collecting objects between when
1775 /// the value is returned over the VM service protocol and when the
1776 /// separate observatory protocol command has to be used to retrieve its full
1777 /// contents.
1778 //
1779 // TODO(jacobr): Replace this with a better solution once
1780 // https://github.com/dart-lang/sdk/issues/32919 is fixed.
1781 String _safeJsonEncode(Object? object) {
1782 final String jsonString = json.encode(object);
1783 _serializeRing[_serializeRingIndex] = jsonString;
1784 _serializeRingIndex = (_serializeRingIndex + 1) % _serializeRing.length;
1785 return jsonString;
1786 }
1787
1788 List<DiagnosticsNode> _truncateNodes(Iterable<DiagnosticsNode> nodes, int maxDescendentsTruncatableNode) {
1789 if (nodes.every((DiagnosticsNode node) => node.value is Element) && isWidgetCreationTracked()) {
1790 final List<DiagnosticsNode> localNodes = nodes
1791 .where((DiagnosticsNode node) => _isValueCreatedByLocalProject(node.value))
1792 .toList();
1793 if (localNodes.isNotEmpty) {
1794 return localNodes;
1795 }
1796 }
1797 return nodes.take(maxDescendentsTruncatableNode).toList();
1798 }
1799
1800 List<Map<String, Object?>> _nodesToJson(
1801 List<DiagnosticsNode> nodes,
1802 InspectorSerializationDelegate delegate, {
1803 required DiagnosticsNode? parent,
1804 }) {
1805 return DiagnosticsNode.toJsonList(nodes, parent, delegate);
1806 }
1807
1808 /// Returns a JSON representation of the properties of the [DiagnosticsNode]
1809 /// object that `diagnosticsNodeId` references.
1810 @protected
1811 String getProperties(String diagnosticsNodeId, String groupName) {
1812 return _safeJsonEncode(_getProperties(diagnosticsNodeId, groupName));
1813 }
1814
1815 List<Object> _getProperties(String? diagnosticableId, String groupName) {
1816 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1817 if (node == null) {
1818 return const <Object>[];
1819 }
1820 return _nodesToJson(node.getProperties(), InspectorSerializationDelegate(groupName: groupName, service: this), parent: node);
1821 }
1822
1823 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1824 /// object that `diagnosticsNodeId` references.
1825 String getChildren(String diagnosticsNodeId, String groupName) {
1826 return _safeJsonEncode(_getChildren(diagnosticsNodeId, groupName));
1827 }
1828
1829 List<Object> _getChildren(String? diagnosticsNodeId, String groupName) {
1830 final DiagnosticsNode? node = toObject(diagnosticsNodeId) as DiagnosticsNode?;
1831 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, service: this);
1832 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node);
1833 }
1834
1835 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1836 /// object that `diagnosticsNodeId` references only including children that
1837 /// were created directly by user code.
1838 ///
1839 /// {@template flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
1840 /// Requires [Widget] creation locations which are only available for debug
1841 /// mode builds when the `--track-widget-creation` flag is enabled on the call
1842 /// to the `flutter` tool. This flag is enabled by default in debug builds.
1843 /// {@endtemplate}
1844 ///
1845 /// See also:
1846 ///
1847 /// * [isWidgetCreationTracked] which indicates whether this method can be
1848 /// used.
1849 String getChildrenSummaryTree(String diagnosticsNodeId, String groupName) {
1850 return _safeJsonEncode(_getChildrenSummaryTree(diagnosticsNodeId, groupName));
1851 }
1852
1853 DiagnosticsNode? _idToDiagnosticsNode(String? diagnosticableId) {
1854 final Object? object = toObject(diagnosticableId);
1855 return objectToDiagnosticsNode(object);
1856 }
1857
1858 /// If possible, returns [DiagnosticsNode] for the object.
1859 @visibleForTesting
1860 static DiagnosticsNode? objectToDiagnosticsNode(Object? object) {
1861 if (object is Diagnosticable) {
1862 return object.toDiagnosticsNode();
1863 }
1864 return null;
1865 }
1866
1867 List<Object> _getChildrenSummaryTree(String? diagnosticableId, String groupName) {
1868 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1869 if (node == null) {
1870 return <Object>[];
1871 }
1872
1873 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, summaryTree: true, service: this);
1874 return _nodesToJson(_getChildrenFiltered(node, delegate), delegate, parent: node);
1875 }
1876
1877 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1878 /// object that [diagnosticableId] references providing information needed
1879 /// for the details subtree view.
1880 ///
1881 /// The details subtree shows properties inline and includes all children
1882 /// rather than a filtered set of important children.
1883 String getChildrenDetailsSubtree(String diagnosticableId, String groupName) {
1884 return _safeJsonEncode(_getChildrenDetailsSubtree(diagnosticableId, groupName));
1885 }
1886
1887 List<Object> _getChildrenDetailsSubtree(String? diagnosticableId, String groupName) {
1888 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1889 // With this value of minDepth we only expand one extra level of important nodes.
1890 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, includeProperties: true, service: this);
1891 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node);
1892 }
1893
1894 bool _shouldShowInSummaryTree(DiagnosticsNode node) {
1895 if (node.level == DiagnosticLevel.error) {
1896 return true;
1897 }
1898 final Object? value = node.value;
1899 if (value is! Diagnosticable) {
1900 return true;
1901 }
1902 if (value is! Element || !isWidgetCreationTracked()) {
1903 // Creation locations are not available so include all nodes in the
1904 // summary tree.
1905 return true;
1906 }
1907 return _isValueCreatedByLocalProject(value);
1908 }
1909
1910 List<DiagnosticsNode> _getChildrenFiltered(
1911 DiagnosticsNode node,
1912 InspectorSerializationDelegate delegate,
1913 ) {
1914 return _filterChildren(node.getChildren(), delegate);
1915 }
1916
1917 List<DiagnosticsNode> _filterChildren(
1918 List<DiagnosticsNode> nodes,
1919 InspectorSerializationDelegate delegate,
1920 ) {
1921 final List<DiagnosticsNode> children = <DiagnosticsNode>[
1922 for (final DiagnosticsNode child in nodes)
1923 if (!delegate.summaryTree || _shouldShowInSummaryTree(child))
1924 child
1925 else
1926 ..._getChildrenFiltered(child, delegate),
1927 ];
1928 return children;
1929 }
1930
1931 /// Returns a JSON representation of the [DiagnosticsNode] for the root
1932 /// [Element].
1933 String getRootWidget(String groupName) {
1934 return _safeJsonEncode(_getRootWidget(groupName));
1935 }
1936
1937 Map<String, Object?>? _getRootWidget(String groupName) {
1938 return _nodeToJson(WidgetsBinding.instance.rootElement?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
1939 }
1940
1941 /// Returns a JSON representation of the [DiagnosticsNode] for the root
1942 /// [Element] showing only nodes that should be included in a summary tree.
1943 String getRootWidgetSummaryTree(String groupName) {
1944 return _safeJsonEncode(_getRootWidgetSummaryTree(groupName));
1945 }
1946
1947 Map<String, Object?>? _getRootWidgetSummaryTree(
1948 String groupName, {
1949 Map<String, Object>? Function(DiagnosticsNode, InspectorSerializationDelegate)? addAdditionalPropertiesCallback,
1950 }) {
1951 return _getRootWidgetTreeImpl(
1952 groupName: groupName,
1953 isSummaryTree: true,
1954 withPreviews: false,
1955 addAdditionalPropertiesCallback: addAdditionalPropertiesCallback,
1956 );
1957 }
1958
1959 Future<Map<String, Object?>> _getRootWidgetSummaryTreeWithPreviews(
1960 Map<String, String> parameters,
1961 ) {
1962 final String groupName = parameters['groupName']!;
1963 final Map<String, Object?>? result = _getRootWidgetTreeImpl(
1964 groupName: groupName,
1965 isSummaryTree: true,
1966 withPreviews: true,
1967 );
1968 return Future<Map<String, dynamic>>.value(<String, dynamic>{
1969 'result': result,
1970 });
1971 }
1972
1973 Future<Map<String, Object?>> _getRootWidgetTree(
1974 Map<String, String> parameters,
1975 ) {
1976 final String groupName = parameters['groupName']!;
1977 final bool isSummaryTree = parameters['isSummaryTree'] == 'true';
1978 final bool withPreviews = parameters['withPreviews'] == 'true';
1979
1980 final Map<String, Object?>? result = _getRootWidgetTreeImpl(
1981 groupName: groupName,
1982 isSummaryTree: isSummaryTree,
1983 withPreviews: withPreviews,
1984 );
1985
1986 return Future<Map<String, dynamic>>.value(<String, dynamic>{
1987 'result': result,
1988 });
1989 }
1990
1991 Map<String, Object?>? _getRootWidgetTreeImpl({
1992 required String groupName,
1993 required bool isSummaryTree,
1994 required bool withPreviews,
1995 Map<String, Object>? Function(
1996 DiagnosticsNode, InspectorSerializationDelegate)?
1997 addAdditionalPropertiesCallback,
1998 }) {
1999 final bool shouldAddAdditionalProperties =
2000 addAdditionalPropertiesCallback != null || withPreviews;
2001
2002 // Combine the given addAdditionalPropertiesCallback with logic to add text
2003 // previews as well (if withPreviews is true):
2004 Map<String, Object>? combinedAddAdditionalPropertiesCallback(
2005 DiagnosticsNode node,
2006 InspectorSerializationDelegate delegate,
2007 ) {
2008 final Map<String, Object> additionalPropertiesJson =
2009 addAdditionalPropertiesCallback?.call(node, delegate) ??
2010 <String, Object>{};
2011 if (!withPreviews) {
2012 return additionalPropertiesJson;
2013 }
2014 final Object? value = node.value;
2015 if (value is Element) {
2016 final RenderObject? renderObject = value.renderObject;
2017 if (renderObject is RenderParagraph) {
2018 additionalPropertiesJson['textPreview'] =
2019 renderObject.text.toPlainText();
2020 }
2021 }
2022 return additionalPropertiesJson;
2023 }
2024 return _nodeToJson(
2025 WidgetsBinding.instance.rootElement?.toDiagnosticsNode(),
2026 InspectorSerializationDelegate(
2027 groupName: groupName,
2028 subtreeDepth: 1000000,
2029 summaryTree: isSummaryTree,
2030 service: this,
2031 addAdditionalPropertiesCallback: shouldAddAdditionalProperties
2032 ? combinedAddAdditionalPropertiesCallback
2033 : null,
2034 ),
2035 );
2036 }
2037
2038 /// Returns a JSON representation of the subtree rooted at the
2039 /// [DiagnosticsNode] object that `diagnosticsNodeId` references providing
2040 /// information needed for the details subtree view.
2041 ///
2042 /// The number of levels of the subtree that should be returned is specified
2043 /// by the [subtreeDepth] parameter. This value defaults to 2 for backwards
2044 /// compatibility.
2045 ///
2046 /// See also:
2047 ///
2048 /// * [getChildrenDetailsSubtree], a method to get children of a node
2049 /// in the details subtree.
2050 String getDetailsSubtree(
2051 String diagnosticableId,
2052 String groupName, {
2053 int subtreeDepth = 2,
2054 }) {
2055 return _safeJsonEncode(_getDetailsSubtree(diagnosticableId, groupName, subtreeDepth));
2056 }
2057
2058 Map<String, Object?>? _getDetailsSubtree(
2059 String? diagnosticableId,
2060 String? groupName,
2061 int subtreeDepth,
2062 ) {
2063 final DiagnosticsNode? root = _idToDiagnosticsNode(diagnosticableId);
2064 if (root == null) {
2065 return null;
2066 }
2067 return _nodeToJson(
2068 root,
2069 InspectorSerializationDelegate(
2070 groupName: groupName,
2071 subtreeDepth: subtreeDepth,
2072 includeProperties: true,
2073 service: this,
2074 ),
2075 );
2076 }
2077
2078 /// Returns a [DiagnosticsNode] representing the currently selected [Element].
2079 @protected
2080 String getSelectedWidget(String? previousSelectionId, String groupName) {
2081 if (previousSelectionId != null) {
2082 debugPrint('previousSelectionId is deprecated in API');
2083 }
2084 return _safeJsonEncode(_getSelectedWidget(null, groupName));
2085 }
2086
2087 /// Captures an image of the current state of an [object] that is a
2088 /// [RenderObject] or [Element].
2089 ///
2090 /// The returned [ui.Image] has uncompressed raw RGBA bytes and will be scaled
2091 /// to be at most [width] pixels wide and [height] pixels tall. The returned
2092 /// image will never have a scale between logical pixels and the
2093 /// size of the output image larger than maxPixelRatio.
2094 /// [margin] indicates the number of pixels relative to the un-scaled size of
2095 /// the [object] to include as a margin to include around the bounds of the
2096 /// [object] in the screenshot. Including a margin can be useful to capture
2097 /// areas that are slightly outside of the normal bounds of an object such as
2098 /// some debug paint information.
2099 @protected
2100 Future<ui.Image?> screenshot(
2101 Object? object, {
2102 required double width,
2103 required double height,
2104 double margin = 0.0,
2105 double maxPixelRatio = 1.0,
2106 bool debugPaint = false,
2107 }) async {
2108 if (object is! Element && object is! RenderObject) {
2109 return null;
2110 }
2111 final RenderObject? renderObject = object is Element ? object.renderObject : (object as RenderObject?);
2112 if (renderObject == null || !renderObject.attached) {
2113 return null;
2114 }
2115
2116 if (renderObject.debugNeedsLayout) {
2117 final PipelineOwner owner = renderObject.owner!;
2118 assert(!owner.debugDoingLayout);
2119 owner
2120 ..flushLayout()
2121 ..flushCompositingBits()
2122 ..flushPaint();
2123
2124 // If we still need layout, then that means that renderObject was skipped
2125 // in the layout phase and therefore can't be painted. It is clearer to
2126 // return null indicating that a screenshot is unavailable than to return
2127 // an empty image.
2128 if (renderObject.debugNeedsLayout) {
2129 return null;
2130 }
2131 }
2132
2133 Rect renderBounds = _calculateSubtreeBounds(renderObject);
2134 if (margin != 0.0) {
2135 renderBounds = renderBounds.inflate(margin);
2136 }
2137 if (renderBounds.isEmpty) {
2138 return null;
2139 }
2140
2141 final double pixelRatio = math.min(
2142 maxPixelRatio,
2143 math.min(
2144 width / renderBounds.width,
2145 height / renderBounds.height,
2146 ),
2147 );
2148
2149 return _ScreenshotPaintingContext.toImage(
2150 renderObject,
2151 renderBounds,
2152 pixelRatio: pixelRatio,
2153 debugPaint: debugPaint,
2154 );
2155 }
2156
2157 Future<Map<String, Object?>> _getLayoutExplorerNode(
2158 Map<String, String> parameters,
2159 ) {
2160 final String? diagnosticableId = parameters['id'];
2161 final int subtreeDepth = int.parse(parameters['subtreeDepth']!);
2162 final String? groupName = parameters['groupName'];
2163 Map<String, dynamic>? result = <String, dynamic>{};
2164 final DiagnosticsNode? root = _idToDiagnosticsNode(diagnosticableId);
2165 if (root == null) {
2166 return Future<Map<String, dynamic>>.value(<String, dynamic>{
2167 'result': result,
2168 });
2169 }
2170 result = _nodeToJson(
2171 root,
2172 InspectorSerializationDelegate(
2173 groupName: groupName,
2174 summaryTree: true,
2175 subtreeDepth: subtreeDepth,
2176 service: this,
2177 addAdditionalPropertiesCallback:
2178 (DiagnosticsNode node, InspectorSerializationDelegate delegate) {
2179 final Object? value = node.value;
2180 final RenderObject? renderObject =
2181 value is Element ? value.renderObject : null;
2182 if (renderObject == null) {
2183 return const <String, Object>{};
2184 }
2185
2186 final DiagnosticsSerializationDelegate
2187 renderObjectSerializationDelegate = delegate.copyWith(
2188 subtreeDepth: 0,
2189 includeProperties: true,
2190 expandPropertyValues: false,
2191 );
2192 final Map<String, Object> additionalJson = <String, Object>{
2193 // Only include renderObject properties separately if this value is not already the renderObject.
2194 // Only include if we are expanding property values to mitigate the risk of infinite loops if
2195 // RenderObjects have properties that are Element objects.
2196 if (value is! RenderObject && delegate.expandPropertyValues)
2197 'renderObject': renderObject
2198 .toDiagnosticsNode()
2199 .toJsonMap(renderObjectSerializationDelegate),
2200 };
2201
2202 final RenderObject? renderParent = renderObject.parent;
2203 if (renderParent != null &&
2204 delegate.subtreeDepth > 0 &&
2205 delegate.expandPropertyValues) {
2206 final Object? parentCreator = renderParent.debugCreator;
2207 if (parentCreator is DebugCreator) {
2208 additionalJson['parentRenderElement'] =
2209 parentCreator.element.toDiagnosticsNode().toJsonMap(
2210 delegate.copyWith(
2211 subtreeDepth: 0,
2212 includeProperties: true,
2213 ),
2214 );
2215 // TODO(jacobr): also describe the path back up the tree to
2216 // the RenderParentElement from the current element. It
2217 // could be a surprising distance up the tree if a lot of
2218 // elements don't have their own RenderObjects.
2219 }
2220 }
2221
2222 try {
2223 if (!renderObject.debugNeedsLayout) {
2224 // ignore: invalid_use_of_protected_member
2225 final Constraints constraints = renderObject.constraints;
2226 final Map<String, Object> constraintsProperty = <String, Object>{
2227 'type': constraints.runtimeType.toString(),
2228 'description': constraints.toString(),
2229 };
2230 if (constraints is BoxConstraints) {
2231 constraintsProperty.addAll(<String, Object>{
2232 'minWidth': constraints.minWidth.toString(),
2233 'minHeight': constraints.minHeight.toString(),
2234 'maxWidth': constraints.maxWidth.toString(),
2235 'maxHeight': constraints.maxHeight.toString(),
2236 });
2237 }
2238 additionalJson['constraints'] = constraintsProperty;
2239 }
2240 } catch (e) {
2241 // Constraints are sometimes unavailable even though
2242 // debugNeedsLayout is false.
2243 }
2244
2245 try {
2246 if (renderObject is RenderBox) {
2247 additionalJson['isBox'] = true;
2248 additionalJson['size'] = <String, Object>{
2249 'width': renderObject.size.width.toString(),
2250 'height': renderObject.size.height.toString(),
2251 };
2252
2253 final ParentData? parentData = renderObject.parentData;
2254 if (parentData is FlexParentData) {
2255 additionalJson['flexFactor'] = parentData.flex!;
2256 additionalJson['flexFit'] =
2257 (parentData.fit ?? FlexFit.tight).name;
2258 } else if (parentData is BoxParentData) {
2259 final Offset offset = parentData.offset;
2260 additionalJson['parentData'] = <String, Object>{
2261 'offsetX': offset.dx.toString(),
2262 'offsetY': offset.dy.toString(),
2263 };
2264 }
2265 } else if (renderObject is RenderView) {
2266 additionalJson['size'] = <String, Object>{
2267 'width': renderObject.size.width.toString(),
2268 'height': renderObject.size.height.toString(),
2269 };
2270 }
2271 } catch (e) {
2272 // Not laid out yet.
2273 }
2274 return additionalJson;
2275 },
2276 ),
2277 );
2278 return Future<Map<String, dynamic>>.value(<String, dynamic>{
2279 'result': result,
2280 });
2281 }
2282
2283 Future<Map<String, dynamic>> _setFlexFit(Map<String, String> parameters) {
2284 final String? id = parameters['id'];
2285 final String parameter = parameters['flexFit']!;
2286 final FlexFit flexFit = _toEnumEntry<FlexFit>(FlexFit.values, parameter);
2287 final Object? object = toObject(id);
2288 bool succeed = false;
2289 if (object != null && object is Element) {
2290 final RenderObject? render = object.renderObject;
2291 final ParentData? parentData = render?.parentData;
2292 if (parentData is FlexParentData) {
2293 parentData.fit = flexFit;
2294 render!.markNeedsLayout();
2295 succeed = true;
2296 }
2297 }
2298 return Future<Map<String, Object>>.value(<String, Object>{
2299 'result': succeed,
2300 });
2301 }
2302
2303 Future<Map<String, dynamic>> _setFlexFactor(Map<String, String> parameters) {
2304 final String? id = parameters['id'];
2305 final String flexFactor = parameters['flexFactor']!;
2306 final int? factor = flexFactor == 'null' ? null : int.parse(flexFactor);
2307 final dynamic object = toObject(id);
2308 bool succeed = false;
2309 if (object != null && object is Element) {
2310 final RenderObject? render = object.renderObject;
2311 final ParentData? parentData = render?.parentData;
2312 if (parentData is FlexParentData) {
2313 parentData.flex = factor;
2314 render!.markNeedsLayout();
2315 succeed = true;
2316 }
2317 }
2318 return Future<Map<String, Object>>.value(<String, Object>{
2319 'result': succeed
2320 });
2321 }
2322
2323 Future<Map<String, dynamic>> _setFlexProperties(
2324 Map<String, String> parameters,
2325 ) {
2326 final String? id = parameters['id'];
2327 final MainAxisAlignment mainAxisAlignment = _toEnumEntry<MainAxisAlignment>(
2328 MainAxisAlignment.values,
2329 parameters['mainAxisAlignment']!,
2330 );
2331 final CrossAxisAlignment crossAxisAlignment =
2332 _toEnumEntry<CrossAxisAlignment>(
2333 CrossAxisAlignment.values,
2334 parameters['crossAxisAlignment']!,
2335 );
2336 final Object? object = toObject(id);
2337 bool succeed = false;
2338 if (object != null && object is Element) {
2339 final RenderObject? render = object.renderObject;
2340 if (render is RenderFlex) {
2341 render.mainAxisAlignment = mainAxisAlignment;
2342 render.crossAxisAlignment = crossAxisAlignment;
2343 render.markNeedsLayout();
2344 render.markNeedsPaint();
2345 succeed = true;
2346 }
2347 }
2348 return Future<Map<String, Object>>.value(<String, Object>{
2349 'result': succeed
2350 });
2351 }
2352
2353 T _toEnumEntry<T>(List<T> enumEntries, String name) {
2354 for (final T entry in enumEntries) {
2355 if (entry.toString() == name) {
2356 return entry;
2357 }
2358 }
2359 throw Exception('Enum value $name not found');
2360 }
2361
2362 Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) {
2363 return _nodeToJson(
2364 _getSelectedWidgetDiagnosticsNode(previousSelectionId),
2365 InspectorSerializationDelegate(groupName: groupName, service: this),
2366 );
2367 }
2368
2369 DiagnosticsNode? _getSelectedWidgetDiagnosticsNode(String? previousSelectionId) {
2370 final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
2371 final Element? current = selection.currentElement;
2372 return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
2373 }
2374
2375 /// Returns a [DiagnosticsNode] representing the currently selected [Element]
2376 /// if the selected [Element] should be shown in the summary tree otherwise
2377 /// returns the first ancestor of the selected [Element] shown in the summary
2378 /// tree.
2379 String getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2380 if (previousSelectionId != null) {
2381 debugPrint('previousSelectionId is deprecated in API');
2382 }
2383 return _safeJsonEncode(_getSelectedSummaryWidget(null, groupName));
2384 }
2385
2386 _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) {
2387 return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value);
2388 }
2389
2390 DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) {
2391 if (!isWidgetCreationTracked()) {
2392 return _getSelectedWidgetDiagnosticsNode(previousSelectionId);
2393 }
2394 final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
2395 Element? current = selection.currentElement;
2396 if (current != null && !_isValueCreatedByLocalProject(current)) {
2397 Element? firstLocal;
2398 for (final Element candidate in current.debugGetDiagnosticChain()) {
2399 if (_isValueCreatedByLocalProject(candidate)) {
2400 firstLocal = candidate;
2401 break;
2402 }
2403 }
2404 current = firstLocal;
2405 }
2406 return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
2407 }
2408
2409 Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2410 return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this));
2411 }
2412
2413 /// Returns whether [Widget] creation locations are available.
2414 ///
2415 /// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
2416 bool isWidgetCreationTracked() {
2417 _widgetCreationTracked ??= const _WidgetForTypeTests() is _HasCreationLocation;
2418 return _widgetCreationTracked!;
2419 }
2420
2421 bool? _widgetCreationTracked;
2422
2423 late Duration _frameStart;
2424 late int _frameNumber;
2425
2426 void _onFrameStart(Duration timeStamp) {
2427 _frameStart = timeStamp;
2428 _frameNumber = PlatformDispatcher.instance.frameData.frameNumber;
2429 SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart');
2430 }
2431
2432 void _onFrameEnd(Duration timeStamp) {
2433 if (_trackRebuildDirtyWidgets) {
2434 _postStatsEvent('Flutter.RebuiltWidgets', _rebuildStats);
2435 }
2436 if (_trackRepaintWidgets) {
2437 _postStatsEvent('Flutter.RepaintWidgets', _repaintStats);
2438 }
2439 }
2440
2441 void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
2442 postEvent(
2443 eventName,
2444 stats.exportToJson(
2445 _frameStart,
2446 frameNumber: _frameNumber,
2447 ),
2448 );
2449 }
2450
2451 /// All events dispatched by a [WidgetInspectorService] use this method
2452 /// instead of calling [developer.postEvent] directly.
2453 ///
2454 /// This allows tests for [WidgetInspectorService] to track which events were
2455 /// dispatched by overriding this method.
2456 @protected
2457 void postEvent(
2458 String eventKind,
2459 Map<Object, Object?> eventData, {
2460 String stream = 'Extension',
2461 }) {
2462 developer.postEvent(eventKind, eventData, stream: stream);
2463 }
2464
2465 /// All events dispatched by a [WidgetInspectorService] use this method
2466 /// instead of calling [developer.inspect].
2467 ///
2468 /// This allows tests for [WidgetInspectorService] to track which events were
2469 /// dispatched by overriding this method.
2470 @protected
2471 void inspect(Object? object) {
2472 developer.inspect(object);
2473 }
2474
2475 final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
2476 final _ElementLocationStatsTracker _repaintStats = _ElementLocationStatsTracker();
2477
2478 void _onRebuildWidget(Element element, bool builtOnce) {
2479 _rebuildStats.add(element);
2480 }
2481
2482 void _onPaint(RenderObject renderObject) {
2483 try {
2484 final Element? element = (renderObject.debugCreator as DebugCreator?)?.element;
2485 if (element is! RenderObjectElement) {
2486 // This branch should not hit as long as all RenderObjects were created
2487 // by Widgets. It is possible there might be some render objects
2488 // created directly without using the Widget layer so we add this check
2489 // to improve robustness.
2490 return;
2491 }
2492 _repaintStats.add(element);
2493
2494 // Give all ancestor elements credit for repainting as long as they do
2495 // not have their own associated RenderObject.
2496 element.visitAncestorElements((Element ancestor) {
2497 if (ancestor is RenderObjectElement) {
2498 // This ancestor has its own RenderObject so we can precisely track
2499 // when it repaints.
2500 return false;
2501 }
2502 _repaintStats.add(ancestor);
2503 return true;
2504 });
2505 }
2506 catch (exception, stack) {
2507 FlutterError.reportError(
2508 FlutterErrorDetails(
2509 exception: exception,
2510 stack: stack,
2511 library: 'widget inspector library',
2512 context: ErrorDescription('while tracking widget repaints'),
2513 ),
2514 );
2515 }
2516 }
2517
2518 /// This method is called by [WidgetsBinding.performReassemble] to flush caches
2519 /// of obsolete values after a hot reload.
2520 ///
2521 /// Do not call this method directly. Instead, use
2522 /// [BindingBase.reassembleApplication].
2523 void performReassemble() {
2524 _clearStats();
2525 _resetErrorCount();
2526 }
2527}
2528
2529/// Accumulator for a count associated with a specific source location.
2530///
2531/// The accumulator stores whether the source location is [local] and what its
2532/// [id] for efficiency encoding terse JSON payloads describing counts.
2533class _LocationCount {
2534 _LocationCount({
2535 required this.location,
2536 required this.id,
2537 required this.local,
2538 });
2539
2540 /// Location id.
2541 final int id;
2542
2543 /// Whether the location is local to the current project.
2544 final bool local;
2545
2546 final _Location location;
2547
2548 int get count => _count;
2549 int _count = 0;
2550
2551 /// Reset the count.
2552 void reset() {
2553 _count = 0;
2554 }
2555
2556 /// Increment the count.
2557 void increment() {
2558 _count++;
2559 }
2560}
2561
2562/// A stat tracker that aggregates a performance metric for [Element] objects at
2563/// the granularity of creation locations in source code.
2564///
2565/// This class is optimized to minimize the size of the JSON payloads describing
2566/// the aggregate statistics, for stable memory usage, and low CPU usage at the
2567/// expense of somewhat higher overall memory usage. Stable memory usage is more
2568/// important than peak memory usage to avoid the false impression that the
2569/// user's app is leaking memory each frame.
2570///
2571/// The number of unique widget creation locations tends to be at most in the
2572/// low thousands for regular flutter apps so the peak memory usage for this
2573/// class is not an issue.
2574class _ElementLocationStatsTracker {
2575 // All known creation location tracked.
2576 //
2577 // This could also be stored as a `Map` but this
2578 // representation is more efficient as all location ids from 0 to n are
2579 // typically present.
2580 //
2581 // All logic in this class assumes that if `_stats[i]` is not null
2582 // `_stats[i].id` equals `i`.
2583 final List<_LocationCount?> _stats = <_LocationCount?>[];
2584
2585 /// Locations with a non-zero count.
2586 final List<_LocationCount> active = <_LocationCount>[];
2587
2588 /// Locations that were added since stats were last exported.
2589 ///
2590 /// Only locations local to the current project are included as a performance
2591 /// optimization.
2592 final List<_LocationCount> newLocations = <_LocationCount>[];
2593
2594 /// Increments the count associated with the creation location of [element] if
2595 /// the creation location is local to the current project.
2596 void add(Element element) {
2597 final Object widget = element.widget;
2598 if (widget is! _HasCreationLocation) {
2599 return;
2600 }
2601 final _HasCreationLocation creationLocationSource = widget;
2602 final _Location? location = creationLocationSource._location;
2603 if (location == null) {
2604 return;
2605 }
2606 final int id = _toLocationId(location);
2607
2608 _LocationCount entry;
2609 if (id >= _stats.length || _stats[id] == null) {
2610 // After the first frame, almost all creation ids will already be in
2611 // _stats so this slow path will rarely be hit.
2612 while (id >= _stats.length) {
2613 _stats.add(null);
2614 }
2615 entry = _LocationCount(
2616 location: location,
2617 id: id,
2618 local: WidgetInspectorService.instance._isLocalCreationLocation(location.file),
2619 );
2620 if (entry.local) {
2621 newLocations.add(entry);
2622 }
2623 _stats[id] = entry;
2624 } else {
2625 entry = _stats[id]!;
2626 }
2627
2628 // We could in the future add an option to track stats for all widgets but
2629 // that would significantly increase the size of the events posted using
2630 // [developer.postEvent] and current use cases for this feature focus on
2631 // helping users find problems with their widgets not the platform
2632 // widgets.
2633 if (entry.local) {
2634 if (entry.count == 0) {
2635 active.add(entry);
2636 }
2637 entry.increment();
2638 }
2639 }
2640
2641 /// Clear all aggregated statistics.
2642 void resetCounts() {
2643 // We chose to only reset the active counts instead of clearing all data
2644 // to reduce the number memory allocations performed after the first frame.
2645 // Once an app has warmed up, location stats tracking should not
2646 // trigger significant additional memory allocations. Avoiding memory
2647 // allocations is important to minimize the impact this class has on cpu
2648 // and memory performance of the running app.
2649 for (final _LocationCount entry in active) {
2650 entry.reset();
2651 }
2652 active.clear();
2653 }
2654
2655 /// Exports the current counts and then resets the stats to prepare to track
2656 /// the next frame of data.
2657 Map<String, dynamic> exportToJson(Duration startTime, {required int frameNumber}) {
2658 final List<int> events = List<int>.filled(active.length * 2, 0);
2659 int j = 0;
2660 for (final _LocationCount stat in active) {
2661 events[j++] = stat.id;
2662 events[j++] = stat.count;
2663 }
2664
2665 final Map<String, dynamic> json = <String, dynamic>{
2666 'startTime': startTime.inMicroseconds,
2667 'frameNumber': frameNumber,
2668 'events': events,
2669 };
2670
2671 // Encode the new locations using the older encoding.
2672 if (newLocations.isNotEmpty) {
2673 // Add all newly used location ids to the JSON.
2674 final Map<String, List<int>> locationsJson = <String, List<int>>{};
2675 for (final _LocationCount entry in newLocations) {
2676 final _Location location = entry.location;
2677 final List<int> jsonForFile = locationsJson.putIfAbsent(
2678 location.file,
2679 () => <int>[],
2680 );
2681 jsonForFile..add(entry.id)..add(location.line)..add(location.column);
2682 }
2683 json['newLocations'] = locationsJson;
2684 }
2685
2686 // Encode the new locations using the newer encoding (as of v2.4.0).
2687 if (newLocations.isNotEmpty) {
2688 final Map<String, Map<String, List<Object?>>> fileLocationsMap = <String, Map<String, List<Object?>>>{};
2689 for (final _LocationCount entry in newLocations) {
2690 final _Location location = entry.location;
2691 final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
2692 location.file, () => <String, List<Object?>>{
2693 'ids': <int>[],
2694 'lines': <int>[],
2695 'columns': <int>[],
2696 'names': <String?>[],
2697 },
2698 );
2699
2700 locations['ids']!.add(entry.id);
2701 locations['lines']!.add(location.line);
2702 locations['columns']!.add(location.column);
2703 locations['names']!.add(location.name);
2704 }
2705 json['locations'] = fileLocationsMap;
2706 }
2707
2708 resetCounts();
2709 newLocations.clear();
2710 return json;
2711 }
2712}
2713
2714class _WidgetForTypeTests extends Widget {
2715 const _WidgetForTypeTests();
2716
2717 @override
2718 Element createElement() => throw UnimplementedError();
2719}
2720
2721/// A widget that enables inspecting the child widget's structure.
2722///
2723/// Select a location on your device or emulator and view what widgets and
2724/// render object that best matches the location. An outline of the selected
2725/// widget and terse summary information is shown on device with detailed
2726/// information is shown in the observatory or in IntelliJ when using the
2727/// Flutter Plugin.
2728///
2729/// The inspector has a select mode and a view mode.
2730///
2731/// In the select mode, tapping the device selects the widget that best matches
2732/// the location of the touch and switches to view mode. Dragging a finger on
2733/// the device selects the widget under the drag location but does not switch
2734/// modes. Touching the very edge of the bounding box of a widget triggers
2735/// selecting the widget even if another widget that also overlaps that
2736/// location would otherwise have priority.
2737///
2738/// In the view mode, the previously selected widget is outlined, however,
2739/// touching the device has the same effect it would have if the inspector
2740/// wasn't present. This allows interacting with the application and viewing how
2741/// the selected widget changes position. Clicking on the select icon in the
2742/// bottom left corner of the application switches back to select mode.
2743class WidgetInspector extends StatefulWidget {
2744 /// Creates a widget that enables inspection for the child.
2745 const WidgetInspector({
2746 super.key,
2747 required this.child,
2748 required this.selectButtonBuilder,
2749 });
2750
2751 /// The widget that is being inspected.
2752 final Widget child;
2753
2754 /// A builder that is called to create the select button.
2755 ///
2756 /// The `onPressed` callback passed as an argument to the builder should be
2757 /// hooked up to the returned widget.
2758 final InspectorSelectButtonBuilder? selectButtonBuilder;
2759
2760 @override
2761 State<WidgetInspector> createState() => _WidgetInspectorState();
2762}
2763
2764class _WidgetInspectorState extends State<WidgetInspector>
2765 with WidgetsBindingObserver {
2766
2767 _WidgetInspectorState();
2768
2769 Offset? _lastPointerLocation;
2770
2771 late InspectorSelection selection;
2772
2773 late bool isSelectMode;
2774
2775 final GlobalKey _ignorePointerKey = GlobalKey();
2776
2777 /// Distance from the edge of the bounding box for an element to consider
2778 /// as selecting the edge of the bounding box.
2779 static const double _edgeHitMargin = 2.0;
2780
2781 @override
2782 void initState() {
2783 super.initState();
2784
2785 WidgetInspectorService.instance.selection
2786 .addListener(_selectionInformationChanged);
2787 WidgetInspectorService.instance.isSelectMode
2788 .addListener(_selectionInformationChanged);
2789 selection = WidgetInspectorService.instance.selection;
2790 isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
2791 }
2792
2793 @override
2794 void dispose() {
2795 WidgetInspectorService.instance.selection
2796 .removeListener(_selectionInformationChanged);
2797 WidgetInspectorService.instance.isSelectMode
2798 .removeListener(_selectionInformationChanged);
2799 super.dispose();
2800 }
2801
2802 void _selectionInformationChanged() => setState((){
2803 selection = WidgetInspectorService.instance.selection;
2804 isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
2805 });
2806
2807 bool _hitTestHelper(
2808 List<RenderObject> hits,
2809 List<RenderObject> edgeHits,
2810 Offset position,
2811 RenderObject object,
2812 Matrix4 transform,
2813 ) {
2814 bool hit = false;
2815 final Matrix4? inverse = Matrix4.tryInvert(transform);
2816 if (inverse == null) {
2817 // We cannot invert the transform. That means the object doesn't appear on
2818 // screen and cannot be hit.
2819 return false;
2820 }
2821 final Offset localPosition = MatrixUtils.transformPoint(inverse, position);
2822
2823 final List<DiagnosticsNode> children = object.debugDescribeChildren();
2824 for (int i = children.length - 1; i >= 0; i -= 1) {
2825 final DiagnosticsNode diagnostics = children[i];
2826 if (diagnostics.style == DiagnosticsTreeStyle.offstage ||
2827 diagnostics.value is! RenderObject) {
2828 continue;
2829 }
2830 final RenderObject child = diagnostics.value! as RenderObject;
2831 final Rect? paintClip = object.describeApproximatePaintClip(child);
2832 if (paintClip != null && !paintClip.contains(localPosition)) {
2833 continue;
2834 }
2835
2836 final Matrix4 childTransform = transform.clone();
2837 object.applyPaintTransform(child, childTransform);
2838 if (_hitTestHelper(hits, edgeHits, position, child, childTransform)) {
2839 hit = true;
2840 }
2841 }
2842
2843 final Rect bounds = object.semanticBounds;
2844 if (bounds.contains(localPosition)) {
2845 hit = true;
2846 // Hits that occur on the edge of the bounding box of an object are
2847 // given priority to provide a way to select objects that would
2848 // otherwise be hard to select.
2849 if (!bounds.deflate(_edgeHitMargin).contains(localPosition)) {
2850 edgeHits.add(object);
2851 }
2852 }
2853 if (hit) {
2854 hits.add(object);
2855 }
2856 return hit;
2857 }
2858
2859 /// Returns the list of render objects located at the given position ordered
2860 /// by priority.
2861 ///
2862 /// All render objects that are not offstage that match the location are
2863 /// included in the list of matches. Priority is given to matches that occur
2864 /// on the edge of a render object's bounding box and to matches found by
2865 /// [RenderBox.hitTest].
2866 List<RenderObject> hitTest(Offset position, RenderObject root) {
2867 final List<RenderObject> regularHits = <RenderObject>[];
2868 final List<RenderObject> edgeHits = <RenderObject>[];
2869
2870 _hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null));
2871 // Order matches by the size of the hit area.
2872 double area(RenderObject object) {
2873 final Size size = object.semanticBounds.size;
2874 return size.width * size.height;
2875 }
2876 regularHits.sort((RenderObject a, RenderObject b) => area(a).compareTo(area(b)));
2877 final Set<RenderObject> hits = <RenderObject>{
2878 ...edgeHits,
2879 ...regularHits,
2880 };
2881 return hits.toList();
2882 }
2883
2884 void _inspectAt(Offset position) {
2885 if (!isSelectMode) {
2886 return;
2887 }
2888
2889 final RenderIgnorePointer ignorePointer = _ignorePointerKey.currentContext!.findRenderObject()! as RenderIgnorePointer;
2890 final RenderObject userRender = ignorePointer.child!;
2891 final List<RenderObject> selected = hitTest(position, userRender);
2892
2893 selection.candidates = selected;
2894 }
2895
2896 void _handlePanDown(DragDownDetails event) {
2897 _lastPointerLocation = event.globalPosition;
2898 _inspectAt(event.globalPosition);
2899 }
2900
2901 void _handlePanUpdate(DragUpdateDetails event) {
2902 _lastPointerLocation = event.globalPosition;
2903 _inspectAt(event.globalPosition);
2904 }
2905
2906 void _handlePanEnd(DragEndDetails details) {
2907 // If the pan ends on the edge of the window assume that it indicates the
2908 // pointer is being dragged off the edge of the display not a regular touch
2909 // on the edge of the display. If the pointer is being dragged off the edge
2910 // of the display we do not want to select anything. A user can still select
2911 // a widget that is only at the exact screen margin by tapping.
2912 final ui.FlutterView view = View.of(context);
2913 final Rect bounds = (Offset.zero & (view.physicalSize / view.devicePixelRatio)).deflate(_kOffScreenMargin);
2914 if (!bounds.contains(_lastPointerLocation!)) {
2915 selection.clear();
2916 }
2917 }
2918
2919 void _handleTap() {
2920 if (!isSelectMode) {
2921 return;
2922 }
2923 if (_lastPointerLocation != null) {
2924 _inspectAt(_lastPointerLocation!);
2925 WidgetInspectorService.instance._sendInspectEvent(selection.current);
2926 }
2927
2928 // Only exit select mode if there is a button to return to select mode.
2929 if (widget.selectButtonBuilder != null) {
2930 WidgetInspectorService.instance.isSelectMode.value = false;
2931 }
2932 }
2933
2934 void _handleEnableSelect() {
2935 WidgetInspectorService.instance.isSelectMode.value = true;
2936 }
2937
2938 @override
2939 Widget build(BuildContext context) {
2940 // Be careful changing this build method. The _InspectorOverlayLayer
2941 // assumes the root RenderObject for the WidgetInspector will be
2942 // a RenderStack with a _RenderInspectorOverlay as the last child.
2943 return Stack(children: <Widget>[
2944 GestureDetector(
2945 onTap: _handleTap,
2946 onPanDown: _handlePanDown,
2947 onPanEnd: _handlePanEnd,
2948 onPanUpdate: _handlePanUpdate,
2949 behavior: HitTestBehavior.opaque,
2950 excludeFromSemantics: true,
2951 child: IgnorePointer(
2952 ignoring: isSelectMode,
2953 key: _ignorePointerKey,
2954 child: widget.child,
2955 ),
2956 ),
2957 if (!isSelectMode && widget.selectButtonBuilder != null)
2958 Positioned(
2959 left: _kInspectButtonMargin,
2960 bottom: _kInspectButtonMargin,
2961 child: widget.selectButtonBuilder!(context, _handleEnableSelect),
2962 ),
2963 _InspectorOverlay(selection: selection),
2964 ]);
2965 }
2966}
2967
2968/// Mutable selection state of the inspector.
2969class InspectorSelection with ChangeNotifier {
2970 /// Creates an instance of [InspectorSelection].
2971 InspectorSelection() {
2972 if (kFlutterMemoryAllocationsEnabled) {
2973 ChangeNotifier.maybeDispatchObjectCreation(this);
2974 }
2975 }
2976
2977 /// Render objects that are candidates to be selected.
2978 ///
2979 /// Tools may wish to iterate through the list of candidates.
2980 List<RenderObject> get candidates => _candidates;
2981 List<RenderObject> _candidates = <RenderObject>[];
2982 set candidates(List<RenderObject> value) {
2983 _candidates = value;
2984 _index = 0;
2985 _computeCurrent();
2986 }
2987
2988 /// Index within the list of candidates that is currently selected.
2989 int get index => _index;
2990 int _index = 0;
2991 set index(int value) {
2992 _index = value;
2993 _computeCurrent();
2994 }
2995
2996 /// Set the selection to empty.
2997 void clear() {
2998 _candidates = <RenderObject>[];
2999 _index = 0;
3000 _computeCurrent();
3001 }
3002
3003 /// Selected render object typically from the [candidates] list.
3004 ///
3005 /// Setting [candidates] or calling [clear] resets the selection.
3006 ///
3007 /// Returns null if the selection is invalid.
3008 RenderObject? get current => active ? _current : null;
3009
3010 RenderObject? _current;
3011 set current(RenderObject? value) {
3012 if (_current != value) {
3013 _current = value;
3014 _currentElement = (value?.debugCreator as DebugCreator?)?.element;
3015 notifyListeners();
3016 }
3017 }
3018
3019 /// Selected [Element] consistent with the [current] selected [RenderObject].
3020 ///
3021 /// Setting [candidates] or calling [clear] resets the selection.
3022 ///
3023 /// Returns null if the selection is invalid.
3024 Element? get currentElement {
3025 return _currentElement?.debugIsDefunct ?? true ? null : _currentElement;
3026 }
3027
3028 Element? _currentElement;
3029 set currentElement(Element? element) {
3030 if (element?.debugIsDefunct ?? false) {
3031 _currentElement = null;
3032 _current = null;
3033 notifyListeners();
3034 return;
3035 }
3036 if (currentElement != element) {
3037 _currentElement = element;
3038 _current = element!.findRenderObject();
3039 notifyListeners();
3040 }
3041 }
3042
3043 void _computeCurrent() {
3044 if (_index < candidates.length) {
3045 _current = candidates[index];
3046 _currentElement = (_current?.debugCreator as DebugCreator?)?.element;
3047 notifyListeners();
3048 } else {
3049 _current = null;
3050 _currentElement = null;
3051 notifyListeners();
3052 }
3053 }
3054
3055 /// Whether the selected render object is attached to the tree or has gone
3056 /// out of scope.
3057 bool get active => _current != null && _current!.attached;
3058}
3059
3060class _InspectorOverlay extends LeafRenderObjectWidget {
3061 const _InspectorOverlay({
3062 required this.selection,
3063 });
3064
3065 final InspectorSelection selection;
3066
3067 @override
3068 _RenderInspectorOverlay createRenderObject(BuildContext context) {
3069 return _RenderInspectorOverlay(selection: selection);
3070 }
3071
3072 @override
3073 void updateRenderObject(BuildContext context, _RenderInspectorOverlay renderObject) {
3074 renderObject.selection = selection;
3075 }
3076}
3077
3078class _RenderInspectorOverlay extends RenderBox {
3079 _RenderInspectorOverlay({ required InspectorSelection selection })
3080 : _selection = selection;
3081
3082 InspectorSelection get selection => _selection;
3083 InspectorSelection _selection;
3084 set selection(InspectorSelection value) {
3085 if (value != _selection) {
3086 _selection = value;
3087 }
3088 markNeedsPaint();
3089 }
3090
3091 @override
3092 bool get sizedByParent => true;
3093
3094 @override
3095 bool get alwaysNeedsCompositing => true;
3096
3097 @override
3098 Size computeDryLayout(BoxConstraints constraints) {
3099 return constraints.constrain(Size.infinite);
3100 }
3101
3102 @override
3103 void paint(PaintingContext context, Offset offset) {
3104 assert(needsCompositing);
3105 context.addLayer(_InspectorOverlayLayer(
3106 overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
3107 selection: selection,
3108 rootRenderObject: parent is RenderObject ? parent! : null,
3109 ));
3110 }
3111}
3112
3113@immutable
3114class _TransformedRect {
3115 _TransformedRect(RenderObject object, RenderObject? ancestor)
3116 : rect = object.semanticBounds,
3117 transform = object.getTransformTo(ancestor);
3118
3119 final Rect rect;
3120 final Matrix4 transform;
3121
3122 @override
3123 bool operator ==(Object other) {
3124 if (other.runtimeType != runtimeType) {
3125 return false;
3126 }
3127 return other is _TransformedRect
3128 && other.rect == rect
3129 && other.transform == transform;
3130 }
3131
3132 @override
3133 int get hashCode => Object.hash(rect, transform);
3134}
3135
3136/// State describing how the inspector overlay should be rendered.
3137///
3138/// The equality operator can be used to determine whether the overlay needs to
3139/// be rendered again.
3140@immutable
3141class _InspectorOverlayRenderState {
3142 const _InspectorOverlayRenderState({
3143 required this.overlayRect,
3144 required this.selected,
3145 required this.candidates,
3146 required this.tooltip,
3147 required this.textDirection,
3148 });
3149
3150 final Rect overlayRect;
3151 final _TransformedRect selected;
3152 final List<_TransformedRect> candidates;
3153 final String tooltip;
3154 final TextDirection textDirection;
3155
3156 @override
3157 bool operator ==(Object other) {
3158 if (other.runtimeType != runtimeType) {
3159 return false;
3160 }
3161 return other is _InspectorOverlayRenderState
3162 && other.overlayRect == overlayRect
3163 && other.selected == selected
3164 && listEquals<_TransformedRect>(other.candidates, candidates)
3165 && other.tooltip == tooltip;
3166 }
3167
3168 @override
3169 int get hashCode => Object.hash(overlayRect, selected, Object.hashAll(candidates), tooltip);
3170}
3171
3172const int _kMaxTooltipLines = 5;
3173const Color _kTooltipBackgroundColor = Color.fromARGB(230, 60, 60, 60);
3174const Color _kHighlightedRenderObjectFillColor = Color.fromARGB(128, 128, 128, 255);
3175const Color _kHighlightedRenderObjectBorderColor = Color.fromARGB(128, 64, 64, 128);
3176
3177/// A layer that outlines the selected [RenderObject] and candidate render
3178/// objects that also match the last pointer location.
3179///
3180/// This approach is horrific for performance and is only used here because this
3181/// is limited to debug mode. Do not duplicate the logic in production code.
3182class _InspectorOverlayLayer extends Layer {
3183 /// Creates a layer that displays the inspector overlay.
3184 _InspectorOverlayLayer({
3185 required this.overlayRect,
3186 required this.selection,
3187 required this.rootRenderObject,
3188 }) {
3189 bool inDebugMode = false;
3190 assert(() {
3191 inDebugMode = true;
3192 return true;
3193 }());
3194 if (!inDebugMode) {
3195 throw FlutterError.fromParts(<DiagnosticsNode>[
3196 ErrorSummary(
3197 'The inspector should never be used in production mode due to the '
3198 'negative performance impact.',
3199 ),
3200 ]);
3201 }
3202 }
3203
3204 InspectorSelection selection;
3205
3206 /// The rectangle in this layer's coordinate system that the overlay should
3207 /// occupy.
3208 ///
3209 /// The scene must be explicitly recomposited after this property is changed
3210 /// (as described at [Layer]).
3211 final Rect overlayRect;
3212
3213 /// Widget inspector root render object. The selection overlay will be painted
3214 /// with transforms relative to this render object.
3215 final RenderObject? rootRenderObject;
3216
3217 _InspectorOverlayRenderState? _lastState;
3218
3219 /// Picture generated from _lastState.
3220 ui.Picture? _picture;
3221
3222 TextPainter? _textPainter;
3223 double? _textPainterMaxWidth;
3224
3225 @override
3226 void dispose() {
3227 _textPainter?.dispose();
3228 _textPainter = null;
3229 _picture?.dispose();
3230 super.dispose();
3231 }
3232
3233 @override
3234 void addToScene(ui.SceneBuilder builder) {
3235 if (!selection.active) {
3236 return;
3237 }
3238
3239 final RenderObject selected = selection.current!;
3240
3241 if (!_isInInspectorRenderObjectTree(selected)) {
3242 return;
3243 }
3244
3245 final List<_TransformedRect> candidates = <_TransformedRect>[];
3246 for (final RenderObject candidate in selection.candidates) {
3247 if (candidate == selected || !candidate.attached
3248 || !_isInInspectorRenderObjectTree(candidate)) {
3249 continue;
3250 }
3251 candidates.add(_TransformedRect(candidate, rootRenderObject));
3252 }
3253 final _TransformedRect selectedRect = _TransformedRect(selected, rootRenderObject);
3254 final String widgetName = selection.currentElement!.toStringShort();
3255 final String width = selectedRect.rect.width.toStringAsFixed(1);
3256 final String height = selectedRect.rect.height.toStringAsFixed(1);
3257
3258 final _InspectorOverlayRenderState state = _InspectorOverlayRenderState(
3259 overlayRect: overlayRect,
3260 selected: selectedRect,
3261 tooltip: '$widgetName ($width x $height)',
3262 textDirection: TextDirection.ltr,
3263 candidates: candidates,
3264 );
3265
3266 if (state != _lastState) {
3267 _lastState = state;
3268 _picture?.dispose();
3269 _picture = _buildPicture(state);
3270 }
3271 builder.addPicture(Offset.zero, _picture!);
3272 }
3273
3274 ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
3275 final ui.PictureRecorder recorder = ui.PictureRecorder();
3276 final Canvas canvas = Canvas(recorder, state.overlayRect);
3277 final Size size = state.overlayRect.size;
3278 // The overlay rect could have an offset if the widget inspector does
3279 // not take all the screen.
3280 canvas.translate(state.overlayRect.left, state.overlayRect.top);
3281
3282 final Paint fillPaint = Paint()
3283 ..style = PaintingStyle.fill
3284 ..color = _kHighlightedRenderObjectFillColor;
3285
3286 final Paint borderPaint = Paint()
3287 ..style = PaintingStyle.stroke
3288 ..strokeWidth = 1.0
3289 ..color = _kHighlightedRenderObjectBorderColor;
3290
3291 // Highlight the selected renderObject.
3292 final Rect selectedPaintRect = state.selected.rect.deflate(0.5);
3293 canvas
3294 ..save()
3295 ..transform(state.selected.transform.storage)
3296 ..drawRect(selectedPaintRect, fillPaint)
3297 ..drawRect(selectedPaintRect, borderPaint)
3298 ..restore();
3299
3300 // Show all other candidate possibly selected elements. This helps selecting
3301 // render objects by selecting the edge of the bounding box shows all
3302 // elements the user could toggle the selection between.
3303 for (final _TransformedRect transformedRect in state.candidates) {
3304 canvas
3305 ..save()
3306 ..transform(transformedRect.transform.storage)
3307 ..drawRect(transformedRect.rect.deflate(0.5), borderPaint)
3308 ..restore();
3309 }
3310
3311 final Rect targetRect = MatrixUtils.transformRect(
3312 state.selected.transform, state.selected.rect,
3313 );
3314 if (!targetRect.hasNaN) {
3315 final Offset target = Offset(targetRect.left, targetRect.center.dy);
3316 const double offsetFromWidget = 9.0;
3317 final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3318
3319 _paintDescription(
3320 canvas,
3321 state.tooltip,
3322 state.textDirection,
3323 target,
3324 verticalOffset,
3325 size,
3326 targetRect,
3327 );
3328 }
3329 // TODO(jacobr): provide an option to perform a debug paint of just the
3330 // selected widget.
3331 return recorder.endRecording();
3332 }
3333
3334 void _paintDescription(
3335 Canvas canvas,
3336 String message,
3337 TextDirection textDirection,
3338 Offset target,
3339 double verticalOffset,
3340 Size size,
3341 Rect targetRect,
3342 ) {
3343 canvas.save();
3344 final double maxWidth = math.max(
3345 size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding),
3346 0,
3347 );
3348 final TextSpan? textSpan = _textPainter?.text as TextSpan?;
3349 if (_textPainter == null || textSpan!.text != message || _textPainterMaxWidth != maxWidth) {
3350 _textPainterMaxWidth = maxWidth;
3351 _textPainter?.dispose();
3352 _textPainter = TextPainter()
3353 ..maxLines = _kMaxTooltipLines
3354 ..ellipsis = '...'
3355 ..text = TextSpan(style: _messageStyle, text: message)
3356 ..textDirection = textDirection
3357 ..layout(maxWidth: maxWidth);
3358 }
3359
3360 final Size tooltipSize = _textPainter!.size + const Offset(_kTooltipPadding * 2, _kTooltipPadding * 2);
3361 final Offset tipOffset = positionDependentBox(
3362 size: size,
3363 childSize: tooltipSize,
3364 target: target,
3365 verticalOffset: verticalOffset,
3366 preferBelow: false,
3367 );
3368
3369 final Paint tooltipBackground = Paint()
3370 ..style = PaintingStyle.fill
3371 ..color = _kTooltipBackgroundColor;
3372 canvas.drawRect(
3373 Rect.fromPoints(
3374 tipOffset,
3375 tipOffset.translate(tooltipSize.width, tooltipSize.height),
3376 ),
3377 tooltipBackground,
3378 );
3379
3380 double wedgeY = tipOffset.dy;
3381 final bool tooltipBelow = tipOffset.dy > target.dy;
3382 if (!tooltipBelow) {
3383 wedgeY += tooltipSize.height;
3384 }
3385
3386 const double wedgeSize = _kTooltipPadding * 2;
3387 double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2;
3388 wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2);
3389 final List<Offset> wedge = <Offset>[
3390 Offset(wedgeX - wedgeSize, wedgeY),
3391 Offset(wedgeX + wedgeSize, wedgeY),
3392 Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)),
3393 ];
3394 canvas.drawPath(Path()..addPolygon(wedge, true), tooltipBackground);
3395 _textPainter!.paint(canvas, tipOffset + const Offset(_kTooltipPadding, _kTooltipPadding));
3396 canvas.restore();
3397 }
3398
3399 @override
3400 @protected
3401 bool findAnnotations<S extends Object>(
3402 AnnotationResult<S> result,
3403 Offset localPosition, {
3404 bool? onlyFirst,
3405 }) {
3406 return false;
3407 }
3408
3409 /// Return whether or not a render object belongs to this inspector widget
3410 /// tree.
3411 /// The inspector selection is static, so if there are multiple inspector
3412 /// overlays in the same app (i.e. an storyboard), a selected or candidate
3413 /// render object may not belong to this tree.
3414 bool _isInInspectorRenderObjectTree(RenderObject child) {
3415 RenderObject? current = child.parent;
3416 while (current != null) {
3417 // We found the widget inspector render object.
3418 if (current is RenderStack
3419 && current.lastChild is _RenderInspectorOverlay) {
3420 return rootRenderObject == current;
3421 }
3422 current = current.parent;
3423 }
3424 return false;
3425 }
3426}
3427
3428const double _kScreenEdgeMargin = 10.0;
3429const double _kTooltipPadding = 5.0;
3430const double _kInspectButtonMargin = 10.0;
3431
3432/// Interpret pointer up events within with this margin as indicating the
3433/// pointer is moving off the device.
3434const double _kOffScreenMargin = 1.0;
3435
3436const TextStyle _messageStyle = TextStyle(
3437 color: Color(0xFFFFFFFF),
3438 fontSize: 10.0,
3439 height: 1.2,
3440);
3441
3442/// Interface for classes that track the source code location the their
3443/// constructor was called from.
3444///
3445/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3446// ignore: unused_element
3447abstract class _HasCreationLocation {
3448 _Location? get _location;
3449}
3450
3451/// A tuple with file, line, and column number, for displaying human-readable
3452/// file locations.
3453class _Location {
3454 const _Location({
3455 required this.file,
3456 required this.line,
3457 required this.column,
3458 // TODO(srawlins): `unused_element_parameter` is being separated from
3459 // `unused_element`. Ignore both names until the separation is complete.
3460 // ignore: unused_element, unused_element_parameter
3461 this.name,
3462 });
3463
3464 /// File path of the location.
3465 final String file;
3466
3467 /// 1-based line number.
3468 final int line;
3469
3470 /// 1-based column number.
3471 final int column;
3472
3473 /// Optional name of the parameter or function at this location.
3474 final String? name;
3475
3476 Map<String, Object?> toJsonMap() {
3477 return <String, Object?>{
3478 'file': file,
3479 'line': line,
3480 'column': column,
3481 if (name != null) 'name': name,
3482 };
3483 }
3484
3485 @override
3486 String toString() => <String>[if (name != null) name!, file, '$line', '$column'].join(':');
3487}
3488
3489bool _isDebugCreator(DiagnosticsNode node) => node is DiagnosticsDebugCreator;
3490
3491/// Transformer to parse and gather information about [DiagnosticsDebugCreator].
3492///
3493/// This function will be registered to [FlutterErrorDetails.propertiesTransformers]
3494/// in [WidgetsBinding.initInstances].
3495///
3496/// This is meant to be called only in debug mode. In other modes, it yields an empty list.
3497Iterable<DiagnosticsNode> debugTransformDebugCreator(Iterable<DiagnosticsNode> properties) {
3498 if (!kDebugMode) {
3499 return <DiagnosticsNode>[];
3500 }
3501 final List<DiagnosticsNode> pending = <DiagnosticsNode>[];
3502 ErrorSummary? errorSummary;
3503 for (final DiagnosticsNode node in properties) {
3504 if (node is ErrorSummary) {
3505 errorSummary = node;
3506 break;
3507 }
3508 }
3509 bool foundStackTrace = false;
3510 final List<DiagnosticsNode> result = <DiagnosticsNode>[];
3511 for (final DiagnosticsNode node in properties) {
3512 if (!foundStackTrace && node is DiagnosticsStackTrace) {
3513 foundStackTrace = true;
3514 }
3515 if (_isDebugCreator(node)) {
3516 result.addAll(_parseDiagnosticsNode(node, errorSummary));
3517 } else {
3518 if (foundStackTrace) {
3519 pending.add(node);
3520 } else {
3521 result.add(node);
3522 }
3523 }
3524 }
3525 result.addAll(pending);
3526 return result;
3527}
3528
3529/// Transform the input [DiagnosticsNode].
3530///
3531/// Return null if input [DiagnosticsNode] is not applicable.
3532Iterable<DiagnosticsNode> _parseDiagnosticsNode(
3533 DiagnosticsNode node,
3534 ErrorSummary? errorSummary,
3535) {
3536 assert(_isDebugCreator(node));
3537 try {
3538 final DebugCreator debugCreator = node.value! as DebugCreator;
3539 final Element element = debugCreator.element;
3540 return _describeRelevantUserCode(element, errorSummary);
3541 } catch (error, stack) {
3542 scheduleMicrotask(() {
3543 FlutterError.reportError(FlutterErrorDetails(
3544 exception: error,
3545 stack: stack,
3546 library: 'widget inspector',
3547 informationCollector: () => <DiagnosticsNode>[
3548 DiagnosticsNode.message('This exception was caught while trying to describe the user-relevant code of another error.'),
3549 ],
3550 ));
3551 });
3552 return <DiagnosticsNode>[];
3553 }
3554}
3555
3556Iterable<DiagnosticsNode> _describeRelevantUserCode(
3557 Element element,
3558 ErrorSummary? errorSummary,
3559) {
3560 if (!WidgetInspectorService.instance.isWidgetCreationTracked()) {
3561 return <DiagnosticsNode>[
3562 ErrorDescription(
3563 'Widget creation tracking is currently disabled. Enabling '
3564 'it enables improved error messages. It can be enabled by passing '
3565 '`--track-widget-creation` to `flutter run` or `flutter test`.',
3566 ),
3567 ErrorSpacer(),
3568 ];
3569 }
3570
3571 bool isOverflowError() {
3572 if (errorSummary != null && errorSummary.value.isNotEmpty) {
3573 final Object summary = errorSummary.value.first;
3574 if (summary is String && summary.startsWith('A RenderFlex overflowed by')) {
3575 return true;
3576 }
3577 }
3578 return false;
3579 }
3580
3581 final List<DiagnosticsNode> nodes = <DiagnosticsNode>[];
3582 bool processElement(Element target) {
3583 // TODO(chunhtai): should print out all the widgets that are about to cross
3584 // package boundaries.
3585 if (debugIsLocalCreationLocation(target)) {
3586 DiagnosticsNode? devToolsDiagnostic;
3587
3588 // TODO(kenz): once the inspector is better at dealing with broken trees,
3589 // we can enable deep links for more errors than just RenderFlex overflow
3590 // errors. See https://github.com/flutter/flutter/issues/74918.
3591 if (isOverflowError()) {
3592 final String? devToolsInspectorUri =
3593 WidgetInspectorService.instance._devToolsInspectorUriForElement(target);
3594 if (devToolsInspectorUri != null) {
3595 devToolsDiagnostic = DevToolsDeepLinkProperty(
3596 'To inspect this widget in Flutter DevTools, visit: $devToolsInspectorUri',
3597 devToolsInspectorUri,
3598 );
3599 }
3600 }
3601
3602 nodes.addAll(<DiagnosticsNode>[
3603 DiagnosticsBlock(
3604 name: 'The relevant error-causing widget was',
3605 children: <DiagnosticsNode>[
3606 ErrorDescription('${target.widget.toStringShort()} ${_describeCreationLocation(target)}'),
3607 ],
3608 ),
3609 ErrorSpacer(),
3610 if (devToolsDiagnostic != null) ...<DiagnosticsNode>[devToolsDiagnostic, ErrorSpacer()],
3611 ]);
3612 return false;
3613 }
3614 return true;
3615 }
3616 if (processElement(element)) {
3617 element.visitAncestorElements(processElement);
3618 }
3619 return nodes;
3620}
3621
3622/// Debugging message for DevTools deep links.
3623///
3624/// The [value] for this property is a string representation of the Flutter
3625/// DevTools url.
3626class DevToolsDeepLinkProperty extends DiagnosticsProperty<String> {
3627 /// Creates a diagnostics property that displays a deep link to Flutter DevTools.
3628 ///
3629 /// The [value] of this property will return a map of data for the Flutter
3630 /// DevTools deep link, including the full `url`, the Flutter DevTools `screenId`,
3631 /// and the `objectId` in Flutter DevTools that this diagnostic references.
3632 DevToolsDeepLinkProperty(String description, String url)
3633 : super('', url, description: description, level: DiagnosticLevel.info);
3634}
3635
3636/// Returns if an object is user created.
3637///
3638/// This always returns false if it is not called in debug mode.
3639///
3640/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3641///
3642/// Currently is local creation locations are only available for
3643/// [Widget] and [Element].
3644bool debugIsLocalCreationLocation(Object object) {
3645 bool isLocal = false;
3646 assert(() {
3647 final _Location? location = _getCreationLocation(object);
3648 if (location != null) {
3649 isLocal = WidgetInspectorService.instance._isLocalCreationLocation(location.file);
3650 }
3651 return true;
3652 }());
3653 return isLocal;
3654}
3655
3656/// Returns true if a [Widget] is user created.
3657///
3658/// This is a faster variant of `debugIsLocalCreationLocation` that is available
3659/// in debug and profile builds but only works for [Widget].
3660bool debugIsWidgetLocalCreation(Widget widget) {
3661 final _Location? location = _getObjectCreationLocation(widget);
3662 return location != null &&
3663 WidgetInspectorService.instance._isLocalCreationLocation(location.file);
3664}
3665
3666/// Returns the creation location of an object in String format if one is available.
3667///
3668/// ex: "file:///path/to/main.dart:4:3"
3669///
3670/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3671///
3672/// Currently creation locations are only available for [Widget] and [Element].
3673String? _describeCreationLocation(Object object) {
3674 final _Location? location = _getCreationLocation(object);
3675 return location?.toString();
3676}
3677
3678_Location? _getObjectCreationLocation(Object object) {
3679 return object is _HasCreationLocation ? object._location : null;
3680}
3681
3682/// Returns the creation location of an object if one is available.
3683///
3684/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3685///
3686/// Currently creation locations are only available for [Widget] and [Element].
3687_Location? _getCreationLocation(Object? object) {
3688 final Object? candidate = object is Element && !object.debugIsDefunct ? object.widget : object;
3689 return candidate == null ? null : _getObjectCreationLocation(candidate);
3690}
3691
3692// _Location objects are always const so we don't need to worry about the GC
3693// issues that are a concern for other object ids tracked by
3694// [WidgetInspectorService].
3695final Map<_Location, int> _locationToId = <_Location, int>{};
3696final List<_Location> _locations = <_Location>[];
3697
3698int _toLocationId(_Location location) {
3699 int? id = _locationToId[location];
3700 if (id != null) {
3701 return id;
3702 }
3703 id = _locations.length;
3704 _locations.add(location);
3705 _locationToId[location] = id;
3706 return id;
3707}
3708
3709Map<String, dynamic> _locationIdMapToJson() {
3710 const String idsKey = 'ids';
3711 const String linesKey = 'lines';
3712 const String columnsKey = 'columns';
3713 const String namesKey = 'names';
3714
3715 final Map<String, Map<String, List<Object?>>> fileLocationsMap =
3716 <String, Map<String, List<Object?>>>{};
3717 for (final MapEntry<_Location, int> entry in _locationToId.entries) {
3718 final _Location location = entry.key;
3719 final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
3720 location.file,
3721 () => <String, List<Object?>>{
3722 idsKey: <int>[],
3723 linesKey: <int>[],
3724 columnsKey: <int>[],
3725 namesKey: <String?>[],
3726 },
3727 );
3728
3729 locations[idsKey]!.add(entry.value);
3730 locations[linesKey]!.add(location.line);
3731 locations[columnsKey]!.add(location.column);
3732 locations[namesKey]!.add(location.name);
3733 }
3734 return fileLocationsMap;
3735}
3736
3737/// A delegate that configures how a hierarchy of [DiagnosticsNode]s are
3738/// serialized by the Flutter Inspector.
3739@visibleForTesting
3740class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate {
3741 /// Creates an [InspectorSerializationDelegate] that serialize [DiagnosticsNode]
3742 /// for Flutter Inspector service.
3743 InspectorSerializationDelegate({
3744 this.groupName,
3745 this.summaryTree = false,
3746 this.maxDescendantsTruncatableNode = -1,
3747 this.expandPropertyValues = true,
3748 this.subtreeDepth = 1,
3749 this.includeProperties = false,
3750 required this.service,
3751 this.addAdditionalPropertiesCallback,
3752 });
3753
3754 /// Service used by GUI tools to interact with the [WidgetInspector].
3755 final WidgetInspectorService service;
3756
3757 /// Optional [groupName] parameter which indicates that the json should
3758 /// contain live object ids.
3759 ///
3760 /// Object ids returned as part of the json will remain live at least until
3761 /// [WidgetInspectorService.disposeGroup()] is called on [groupName].
3762 final String? groupName;
3763
3764 /// Whether the tree should only include nodes created by the local project.
3765 final bool summaryTree;
3766
3767 /// Maximum descendants of [DiagnosticsNode] before truncating.
3768 final int maxDescendantsTruncatableNode;
3769
3770 @override
3771 final bool includeProperties;
3772
3773 @override
3774 final int subtreeDepth;
3775
3776 @override
3777 final bool expandPropertyValues;
3778
3779 /// Callback to add additional experimental serialization properties.
3780 ///
3781 /// This callback can be used to customize the serialization of DiagnosticsNode
3782 /// objects for experimental features in widget inspector clients such as
3783 /// [Dart DevTools](https://github.com/flutter/devtools).
3784 final Map<String, Object>? Function(DiagnosticsNode, InspectorSerializationDelegate)? addAdditionalPropertiesCallback;
3785
3786 final List<DiagnosticsNode> _nodesCreatedByLocalProject = <DiagnosticsNode>[];
3787
3788 bool get _interactive => groupName != null;
3789
3790 @override
3791 Map<String, Object?> additionalNodeProperties(DiagnosticsNode node) {
3792 final Map<String, Object?> result = <String, Object?>{};
3793 final Object? value = node.value;
3794 if (_interactive) {
3795 result['valueId'] = service.toId(value, groupName!);
3796 }
3797 if (summaryTree) {
3798 result['summaryTree'] = true;
3799 }
3800 final _Location? creationLocation = _getCreationLocation(value);
3801 if (creationLocation != null) {
3802 result['locationId'] = _toLocationId(creationLocation);
3803 result['creationLocation'] = creationLocation.toJsonMap();
3804 if (service._isLocalCreationLocation(creationLocation.file)) {
3805 _nodesCreatedByLocalProject.add(node);
3806 result['createdByLocalProject'] = true;
3807 }
3808 }
3809 if (addAdditionalPropertiesCallback != null) {
3810 result.addAll(addAdditionalPropertiesCallback!(node, this) ?? <String, Object>{});
3811 }
3812 return result;
3813 }
3814
3815 @override
3816 DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) {
3817 // The tricky special case here is that when in the detailsTree,
3818 // we keep subtreeDepth from going down to zero until we reach nodes
3819 // that also exist in the summary tree. This ensures that every time
3820 // you expand a node in the details tree, you expand the entire subtree
3821 // up until you reach the next nodes shared with the summary tree.
3822 return summaryTree || subtreeDepth > 1 || service._shouldShowInSummaryTree(node)
3823 ? copyWith(subtreeDepth: subtreeDepth - 1)
3824 : this;
3825 }
3826
3827 @override
3828 List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
3829 return service._filterChildren(nodes, this);
3830 }
3831
3832 @override
3833 List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
3834 final bool createdByLocalProject = _nodesCreatedByLocalProject.contains(owner);
3835 return nodes.where((DiagnosticsNode node) {
3836 return !node.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info);
3837 }).toList();
3838 }
3839
3840 @override
3841 List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
3842 if (maxDescendantsTruncatableNode >= 0 &&
3843 owner!.allowTruncate &&
3844 nodes.length > maxDescendantsTruncatableNode) {
3845 nodes = service._truncateNodes(nodes, maxDescendantsTruncatableNode);
3846 }
3847 return nodes;
3848 }
3849
3850 @override
3851 DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties, bool? expandPropertyValues}) {
3852 return InspectorSerializationDelegate(
3853 groupName: groupName,
3854 summaryTree: summaryTree,
3855 maxDescendantsTruncatableNode: maxDescendantsTruncatableNode,
3856 expandPropertyValues: expandPropertyValues ?? this.expandPropertyValues,
3857 subtreeDepth: subtreeDepth ?? this.subtreeDepth,
3858 includeProperties: includeProperties ?? this.includeProperties,
3859 service: service,
3860 addAdditionalPropertiesCallback: addAdditionalPropertiesCallback,
3861 );
3862 }
3863}
3864
3865@Target(<TargetKind>{TargetKind.method})
3866class _WidgetFactory {
3867 const _WidgetFactory();
3868}
3869
3870/// Annotation which marks a function as a widget factory for the purpose of
3871/// widget creation tracking.
3872///
3873/// When widget creation tracking is enabled, the framework tracks the source
3874/// code location of the constructor call for each widget instance. This
3875/// information is used by the DevTools to provide an improved developer
3876/// experience. For example, it allows the Flutter inspector to present the
3877/// widget tree in a manner similar to how the UI was defined in your source
3878/// code.
3879///
3880/// [Widget] constructors are automatically instrumented to track the source
3881/// code location of constructor calls. However, there are cases where
3882/// a function acts as a sort of a constructor for a widget and a call to such
3883/// a function should be considered as the creation location for the returned
3884/// widget instance.
3885///
3886/// Annotating a function with this annotation marks the function as a widget
3887/// factory. The framework will then instrument that function in the same way
3888/// as it does for [Widget] constructors.
3889///
3890/// Tracking will not work correctly if the function has optional positional
3891/// parameters.
3892///
3893/// Currently this annotation is only supported on extension methods.
3894///
3895/// {@tool snippet}
3896///
3897/// This example shows how to use the [widgetFactory] annotation to mark an
3898/// extension method as a widget factory:
3899///
3900/// ```dart
3901/// extension PaddingModifier on Widget {
3902/// @widgetFactory
3903/// Widget padding(EdgeInsetsGeometry padding) {
3904/// return Padding(padding: padding, child: this);
3905/// }
3906/// }
3907/// ```
3908///
3909/// When using the above extension method, the framework will track the
3910/// creation location of the [Padding] widget instance as the source code
3911/// location where the `padding` extension method was called:
3912///
3913/// ```dart
3914/// // continuing from previous example...
3915/// const Text('Hello World!')
3916/// .padding(const EdgeInsets.all(8));
3917/// ```
3918///
3919/// {@end-tool}
3920///
3921/// See also:
3922///
3923/// * the documentation for [Track widget creation](https://flutter.dev/to/track-widget-creation).
3924// The below ignore is needed because the static type of the annotation is used
3925// by the CFE kernel transformer that implements the instrumentation to
3926// recognize the annotation.
3927// ignore: library_private_types_in_public_api
3928const _WidgetFactory widgetFactory = _WidgetFactory();
3929
3930/// Does not hold keys from garbage collection.
3931@visibleForTesting
3932class WeakMap<K, V> {
3933 Expando<Object> _objects = Expando<Object>();
3934
3935 /// Strings, numbers, booleans.
3936 final Map<K, V> _primitives = <K, V>{};
3937
3938 bool _isPrimitive(Object? key) {
3939 return key == null || key is String || key is num || key is bool;
3940 }
3941
3942 /// Returns the value for the given [key] or null if [key] is not in the map
3943 /// or garbage collected.
3944 ///
3945 /// Does not support records to act as keys.
3946 V? operator [](K key){
3947 if (_isPrimitive(key)) {
3948 return _primitives[key];
3949 } else {
3950 return _objects[key!] as V?;
3951 }
3952 }
3953
3954 /// Associates the [key] with the given [value].
3955 void operator []=(K key, V value){
3956 if (_isPrimitive(key)) {
3957 _primitives[key] = value;
3958 } else {
3959 _objects[key!] = value;
3960 }
3961 }
3962
3963 /// Removes the value for the given [key] from the map.
3964 V? remove(K key) {
3965 if (_isPrimitive(key)) {
3966 return _primitives.remove(key);
3967 } else {
3968 final V? result = _objects[key!] as V?;
3969 _objects[key] = null;
3970 return result;
3971 }
3972 }
3973
3974 /// Removes all pairs from the map.
3975 void clear() {
3976 _objects = Expando<Object>();
3977 _primitives.clear();
3978 }
3979}
3980