Repository navigation
Add service extension getSemanticsTree - #189635
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds a new service extension, getSemanticsTree, to the WidgetInspectorService to retrieve and serialize the semantics tree as a JSON map. Feedback suggests enhancing the serialization by including additional properties (such as tooltips, actions, and transform matrices), prioritizing the non-deprecated rootPipelineOwner in pipeline owner lookup, and utilizing ensureVisualUpdate instead of scheduleWarmUpFrame for safer frame scheduling.
|
Thanks for taking this on, I'm very excited for the accessibility panel in DevTools! I think my main concern is I'm not sure that adding these new service extensions to the existing |
|
Yeah adding new AccessibilityInspector should be easier for future maintenance. |
Completely agree with @elliette. |
| Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async { | ||
| _semanticsHandle ??= SemanticsBinding.instance.ensureSemantics(); | ||
|
|
||
| PipelineOwner? findPipelineOwner() { |
There was a problem hiding this comment.
Is there a reason for having this declared as function inside _getSemanticsTree vs as a separate private method?
There was a problem hiding this comment.
No particular reasons, I've moved it out as a private method for readability
| }; | ||
| } | ||
|
|
||
| final SemanticsOwner semanticsOwner = pipelineOwner.semanticsOwner!; |
There was a problem hiding this comment.
Should we guard against the case where semanticsOwner is null here? (Maybe by returning error response like above?)
There was a problem hiding this comment.
added the error response
| return <String, dynamic>{'error': 'rootSemanticsNode is null', 'needsFrame': true}; | ||
| } | ||
|
|
||
| Map<String, dynamic> toJsonMap(SemanticsNode node) { |
There was a problem hiding this comment.
Consider implementing toJsonMap on SemanticsNode instead
There was a problem hiding this comment.
moved to SemanticsNode
|
|
||
| PipelineOwner? findPipelineOwner() { | ||
| for (final RenderView renderView in RendererBinding.instance.renderViews) { | ||
| if (renderView.owner?.semanticsOwner != null) { |
There was a problem hiding this comment.
Looks like this is returning the first semanticsOwner of any renderView. Could there be multiple such renderViews? Maybe with multi-view?
There was a problem hiding this comment.
Actually I haven't really thought about how multi-view semantics tree UI should look like in the dev tool tab. I will support single view for now, and add a todo about multi view here.
| } | ||
|
|
||
| Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async { | ||
| _semanticsHandle ??= SemanticsBinding.instance.ensureSemantics(); |
There was a problem hiding this comment.
Could this be initialized but undisposed? It is disposed only in resetAllState, which is test-only.
There was a problem hiding this comment.
Good catch!
Added disposeSemantics similar to widget inspector's disposeGroup call
62ac6f8 to
14c3c1f
Compare
| } | ||
|
|
||
| Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async { | ||
| _semanticsHandle ??= SemanticsBinding.instance.ensureSemantics(); |
There was a problem hiding this comment.
we should probably separate out the ensuresemantics to its own service extension incase we have have other use case we want to enable semantics but not get the semantics tree.
There was a problem hiding this comment.
since now enablesemantics is a separate endpoint, we should just return something like semantics not enabled
| Map<String, dynamic> toJsonMap({ | ||
| DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder, | ||
| }) { | ||
| final SemanticsData data = getSemanticsData(); |
There was a problem hiding this comment.
consider implement toJson method in SemanticsData directly so it will be more discoverable when adding new property.
also vmservice will call toJson on complex object directly if you throw it into the response IIRC. so there is really to have to call toJsonMap directly
| } | ||
| } | ||
| final children = <Map<String, dynamic>>[ | ||
| for (final SemanticsNode child in debugListChildrenInOrder(childOrder)) |
There was a problem hiding this comment.
this is in traversal order, should also find a way to return hittest order or both at the same time.
can also consider return a flat map where key is id, and the children list will just be a list of id instead of recursive object.
also the transform here is hittest transform, traversal transform is computed before addtoupdate. if we want to include both, we probably need to refactor something out
| return <String, dynamic>{'error': 'rootSemanticsNode is null', 'needsFrame': true}; | ||
| } | ||
|
|
||
| return root.toJsonMap(); |
There was a problem hiding this comment.
should put this in a more standard format like
{'data': root} incase we want to send more thing besides the tree in the future
| } | ||
| final PipelineOwner rootOwner = RendererBinding.instance.rootPipelineOwner; | ||
| if (rootOwner.semanticsOwner != null) { | ||
| return rootOwner; |
There was a problem hiding this comment.
root is always empty i think
| 'width': rect.width, | ||
| 'height': rect.height, | ||
| }, | ||
| if (transform != null) 'transform': transform!.storage.toList(), |
There was a problem hiding this comment.
not something need to be address, just note that this is hittest transform.
| } | ||
|
|
||
| Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async { | ||
| _semanticsHandle ??= SemanticsBinding.instance.ensureSemantics(); |
There was a problem hiding this comment.
since now enablesemantics is a separate endpoint, we should just return something like semantics not enabled
|
|
||
| export 'foundation.dart' show Brightness, UniqueKey; | ||
| export 'rendering.dart' show TextSelectionHandleType; | ||
| export 'src/widgets/accessibility_inspector.dart'; |
There was a problem hiding this comment.
Why is this being exported? I'm not sure it needs to be public.
There was a problem hiding this comment.
I was following the practice of widget_inspector.dart.
But yeah accessibility_inspector.dart doesn't really need to be exported, it doesn't define public widgets or anything, i will revert the export here
chunhtai
left a comment
There was a problem hiding this comment.
mostly lgtm, just some question about error handling
| final PipelineOwner? pipelineOwner = _findPipelineOwner(); | ||
| final SemanticsOwner? semanticsOwner = pipelineOwner?.semanticsOwner; | ||
| if (semanticsOwner == null) { | ||
| return <String, dynamic>{'error': 'No PipelineOwner with SemanticsOwner found'}; |
There was a problem hiding this comment.
Hi @bkonyi, when we want to return error, should we throw directly? looking at the registerServiceExtension, looks like it will convert error throw into a error response to the call
There was a problem hiding this comment.
It really depends. If you throw, it's going to be reported as a generic extension error and also go through FlutterError.reportError. BindingBase.registerServiceExtension's implementation unfortunately takes away a lot of control over error responses.
FWIW, I haven't found other service extensions in the framework that throw or explicitly return error state. You could always consider just returning empty responses and/or add a "success" property to indicate whether or not the request was successful.
There was a problem hiding this comment.
I will just keep the current approach and not throw then.
| final PipelineOwner? pipelineOwner = _findPipelineOwner(); | ||
| final SemanticsOwner? semanticsOwner = pipelineOwner?.semanticsOwner; | ||
| if (semanticsOwner == null) { | ||
| return <String, dynamic>{'error': 'No PipelineOwner with SemanticsOwner found'}; |
There was a problem hiding this comment.
It really depends. If you throw, it's going to be reported as a generic extension error and also go through FlutterError.reportError. BindingBase.registerServiceExtension's implementation unfortunately takes away a lot of control over error responses.
FWIW, I haven't found other service extensions in the framework that throw or explicitly return error state. You could always consider just returning empty responses and/or add a "success" property to indicate whether or not the request was successful.
63cabe7 to
095dc5c
Compare
elliette
left a comment
There was a problem hiding this comment.
LGTM with one note about the formatting change
| typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function( | ||
| List<SemanticsConfiguration>, | ||
| ); | ||
| typedef ChildSemanticsConfigurationsDelegate = |
There was a problem hiding this comment.
I think we are trying to avoid formatting changes until #187204 is resolved so that we can re-format as part of the Dart bump
3a7e481 to
d6c7ba9
Compare
tracking issue: flutter/devtools#9893
Add a service extension ,it will be used to visualize semantics tree in devtool.
Pre-launch Checklist
tracking issue: flutter/devtools#9893
It will used to visualize semantics tree in devtool
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.