diff --git a/packages/flutter/lib/src/widgets/service_extensions.dart b/packages/flutter/lib/src/widgets/service_extensions.dart index caeb190758ac0..54971b59c3864 100644 --- a/packages/flutter/lib/src/widgets/service_extensions.dart +++ b/packages/flutter/lib/src/widgets/service_extensions.dart @@ -144,7 +144,22 @@ enum WidgetInspectorServiceExtensions { /// extension is registered. trackRebuildDirtyWidgets, + /// Name of service extension that, when called, returns the mapping of + /// widget locations to ids. + /// + /// This service extension is only supported if + /// [WidgetInspectorService._widgetCreationTracked] is true. + /// + /// See also: + /// + /// * [trackRebuildDirtyWidgets], which toggles dispatching events that use + /// these ids to efficiently indicate the locations of widgets. + /// * [WidgetInspectorService.initServiceExtensions], where the service + /// extension is registered. + widgetLocationIdMap, + /// Name of service extension that, when called, determines whether + /// [WidgetInspectorService._trackRepaintWidgets], which determines whether /// a callback is invoked for every [RenderObject] painted each frame. /// /// See also: diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index d578e594cb858..9ebcf22297469 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -1120,6 +1120,14 @@ mixin WidgetInspectorService { registerExtension: registerExtension, ); + _registerSignalServiceExtension( + name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + callback: () { + return _locationIdMapToJson(); + }, + registerExtension: registerExtension, + ); + _registerBoolServiceExtension( name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name, getter: () async => _trackRepaintWidgets, @@ -2375,9 +2383,11 @@ mixin WidgetInspectorService { bool? _widgetCreationTracked; late Duration _frameStart; + late int _frameNumber; void _onFrameStart(Duration timeStamp) { _frameStart = timeStamp; + _frameNumber = PlatformDispatcher.instance.frameData.frameNumber; SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart'); } @@ -2391,7 +2401,13 @@ mixin WidgetInspectorService { } void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) { - postEvent(eventName, stats.exportToJson(_frameStart)); + postEvent( + eventName, + stats.exportToJson( + _frameStart, + frameNumber: _frameNumber, + ), + ); } /// All events dispatched by a [WidgetInspectorService] use this method @@ -2600,7 +2616,7 @@ class _ElementLocationStatsTracker { /// Exports the current counts and then resets the stats to prepare to track /// the next frame of data. - Map exportToJson(Duration startTime) { + Map exportToJson(Duration startTime, {required int frameNumber}) { final List events = List.filled(active.length * 2, 0); int j = 0; for (final _LocationCount stat in active) { @@ -2610,6 +2626,7 @@ class _ElementLocationStatsTracker { final Map json = { 'startTime': startTime.inMicroseconds, + 'frameNumber': frameNumber, 'events': events, }; @@ -3256,12 +3273,21 @@ class _InspectorOverlayLayer extends Layer { final Rect targetRect = MatrixUtils.transformRect( state.selected.transform, state.selected.rect, ); - final Offset target = Offset(targetRect.left, targetRect.center.dy); - const double offsetFromWidget = 9.0; - final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; - - _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect); - + if (!targetRect.hasNaN) { + final Offset target = Offset(targetRect.left, targetRect.center.dy); + const double offsetFromWidget = 9.0; + final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; + + _paintDescription( + canvas, + state.tooltip, + state.textDirection, + target, + verticalOffset, + size, + targetRect, + ); + } // TODO(jacobr): provide an option to perform a debug paint of just the // selected widget. return recorder.endRecording(); @@ -3651,6 +3677,34 @@ int _toLocationId(_Location location) { return id; } +Map _locationIdMapToJson() { + const String idsKey = 'ids'; + const String linesKey = 'lines'; + const String columnsKey = 'columns'; + const String namesKey = 'names'; + + final Map>> fileLocationsMap = + >>{}; + for (final MapEntry<_Location, int> entry in _locationToId.entries) { + final _Location location = entry.key; + final Map> locations = fileLocationsMap.putIfAbsent( + location.file, + () => >{ + idsKey: [], + linesKey: [], + columnsKey: [], + namesKey: [], + }, + ); + + locations[idsKey]!.add(entry.value); + locations[linesKey]!.add(location.line); + locations[columnsKey]!.add(location.column); + locations[namesKey]!.add(location.name); + } + return fileLocationsMap; +} + /// A delegate that configures how a hierarchy of [DiagnosticsNode]s are /// serialized by the Flutter Inspector. @visibleForTesting diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 14db86f397fa8..b0c3ba6541e45 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -170,7 +170,7 @@ void main() { if (WidgetInspectorService.instance.isWidgetCreationTracked()) { // Some inspector extensions are only exposed if widget creation locations // are tracked. - widgetInspectorExtensionCount += 2; + widgetInspectorExtensionCount += 3; } expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount)); diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index b40f8ed905e5a..32808e30343a1 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -3765,6 +3765,49 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. ); + testWidgets('ext.flutter.inspector.widgetLocationIdMap', + (WidgetTester tester) async { + service.rebuildCount = 0; + + await tester.pumpWidget(const ClockDemo()); + + final Element clockDemoElement = find.byType(ClockDemo).evaluate().first; + + service.setSelection(clockDemoElement, 'my-group'); + final Map jsonObject = (await service.testExtension( + WidgetInspectorServiceExtensions.getSelectedWidget.name, + {'objectGroup': 'my-group'}, + ))! as Map; + final Map creationLocation = + jsonObject['creationLocation']! as Map; + final String file = creationLocation['file']! as String; + expect(file, endsWith('widget_inspector_test.dart')); + + final Map locationMapJson = (await service.testExtension( + WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + {}, + ))! as Map; + + final Map widgetTestLocations = + locationMapJson[file]! as Map; + expect(widgetTestLocations, isNotNull); + + final List ids = widgetTestLocations['ids']! as List; + expect(ids.length, greaterThan(0)); + final List lines = + widgetTestLocations['lines']! as List; + expect(lines.length, equals(ids.length)); + final List columns = + widgetTestLocations['columns']! as List; + expect(columns.length, equals(ids.length)); + final List names = + widgetTestLocations['names']! as List; + expect(names.length, equals(ids.length)); + expect(names, contains('ClockDemo')); + expect(names, contains('Directionality')); + expect(names, contains('ClockText')); + }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. + testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async { service.rebuildCount = 0; @@ -3951,6 +3994,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(rebuildEvents.length, equals(1)); event = removeLastEvent(rebuildEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; newLocations = event['newLocations']! as Map>; fileLocationsMap = event['locations']! as Map>>; @@ -4080,6 +4124,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(repaintEvents.length, equals(1)); event = removeLastEvent(repaintEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; // No new locations were rebuilt. expect(event, isNot(contains('newLocations')));