@@ -20,11 +20,13 @@ class LayoutSnapshotCaptureResult {
2020 final LayoutSnapshot snapshot;
2121 final Element ? detectedElement;
2222 final String ? detectedElementType;
23+ final String ? detectedElementLabel;
2324
2425 LayoutSnapshotCaptureResult ({
2526 required this .snapshot,
2627 this .detectedElement,
2728 this .detectedElementType,
29+ this .detectedElementLabel,
2830 });
2931}
3032
@@ -39,6 +41,7 @@ class _CaptureState {
3941 // Results accumulated during traversal
4042 Element ? detectedElement;
4143 String ? detectedElementType;
44+ String ? detectedElementLabel;
4245
4346 _CaptureState ({
4447 required this .rootElement,
@@ -81,8 +84,7 @@ class LayoutSnapshotCapture {
8184 if (rootElement == null ) return null ;
8285
8386 // Step 2: Find starting point (scaffold or root)
84- final scaffoldElement =
85- developer.Timeline .timeSync ('findTopmostScaffold' , () {
87+ final scaffoldElement = developer.Timeline .timeSync ('findTopmostScaffold' , () {
8688 return _findTopmostScaffold (rootElement);
8789 });
8890 final startElement = scaffoldElement ?? rootElement;
@@ -118,16 +120,15 @@ class LayoutSnapshotCapture {
118120 snapshot: tree,
119121 detectedElement: state.detectedElement,
120122 detectedElementType: state.detectedElementType,
123+ detectedElementLabel: state.detectedElementLabel,
121124 );
122125 });
123126 }
124127
125128 /// Checks if an element has a valid RenderBox with size
126129 static bool _hasValidRenderBox (Element element) {
127130 final renderObject = element.renderObject;
128- return renderObject != null &&
129- renderObject is RenderBox &&
130- renderObject.hasSize;
131+ return renderObject != null && renderObject is RenderBox && renderObject.hasSize;
131132 }
132133
133134 /// Checks if an element should be skipped during traversal
@@ -138,8 +139,7 @@ class LayoutSnapshotCapture {
138139
139140 /// Gets the node type for a widget based on the capture state
140141 static String ? _getNodeType (Widget widget, _CaptureState state) {
141- if (state.providedWidgetsTypes != null &&
142- state.providedWidgetsTypes! .isNotEmpty) {
142+ if (state.providedWidgetsTypes != null && state.providedWidgetsTypes! .isNotEmpty) {
143143 return _getUserProvidedWidget (widget, state.providedWidgetsTypes);
144144 }
145145 return _getFrameworkWidget (widget);
@@ -177,9 +177,7 @@ class LayoutSnapshotCapture {
177177 /// Updates the state if a matching element is found
178178 static void _tryDetectElement (Element element, _CaptureState state) {
179179 // Skip if already detected or detection not enabled
180- if (state.detectedElement != null ||
181- state.detectionPosition == null ||
182- state.detectionMode == null ) {
180+ if (state.detectedElement != null || state.detectionPosition == null || state.detectionMode == null ) {
183181 return ;
184182 }
185183
@@ -197,10 +195,14 @@ class LayoutSnapshotCapture {
197195 }
198196
199197 // If it matches and is at the position, record it
200- if (detectedType != null &&
201- _hitTest (element, state.detectionPosition! , state.rootElement)) {
198+ if (detectedType != null && _hitTest (element, state.detectionPosition! , state.rootElement)) {
202199 state.detectedElement = element;
203200 state.detectedElementType = detectedType;
201+
202+ // Extract label for click detection mode
203+ if (state.detectionMode == DetectionMode .click) {
204+ state.detectedElementLabel = _findFirstTextInSubtree (element);
205+ }
204206 }
205207 }
206208
@@ -217,9 +219,7 @@ class LayoutSnapshotCapture {
217219 // Determine the node type
218220 final nodeType = _getNodeType (widget, state);
219221 final isDetected = (state.detectedElement == element);
220- final effectiveNodeType = isDetected && nodeType == null
221- ? state.detectedElementType
222- : nodeType;
222+ final effectiveNodeType = isDetected && nodeType == null ? state.detectedElementType : nodeType;
223223
224224 // Determine highlighting (for click detection)
225225 final isHighlighted = state.detectionMode == DetectionMode .click && isDetected;
@@ -356,8 +356,7 @@ class LayoutSnapshotCapture {
356356 return ;
357357 }
358358
359- if (widget.runtimeType == Scaffold ||
360- widget.runtimeType == CupertinoPageScaffold ) {
359+ if (widget.runtimeType == Scaffold || widget.runtimeType == CupertinoPageScaffold ) {
361360 topmostScaffold = element;
362361 }
363362
@@ -372,8 +371,7 @@ class LayoutSnapshotCapture {
372371 if (rootElement == null ) return false ;
373372
374373 final renderObject = element.renderObject;
375- if (renderObject == null ||
376- (renderObject is RenderBox && ! renderObject.hasSize)) {
374+ if (renderObject == null || (renderObject is RenderBox && ! renderObject.hasSize)) {
377375 return false ;
378376 }
379377
@@ -387,8 +385,7 @@ class LayoutSnapshotCapture {
387385
388386 // Check bounds
389387 final transform = renderObject.getTransformTo (rootElement.renderObject);
390- final paintBounds =
391- MatrixUtils .transformRect (transform, renderObject.paintBounds);
388+ final paintBounds = MatrixUtils .transformRect (transform, renderObject.paintBounds);
392389 return paintBounds.contains (position);
393390 }
394391
@@ -428,8 +425,7 @@ class LayoutSnapshotCapture {
428425 return type;
429426 }
430427
431- static String ? _getUserProvidedWidget (
432- Widget widget, Map <Type , String >? providedWidgets) {
428+ static String ? _getUserProvidedWidget (Widget widget, Map <Type , String >? providedWidgets) {
433429 final type = widget.runtimeType;
434430 if (providedWidgets? .keys.contains (type) == true ) {
435431 return providedWidgets? [type];
@@ -457,11 +453,7 @@ class LayoutSnapshotCapture {
457453 ExpansionTile _ => 'ExpansionTile' ,
458454 Card _ => 'Card' ,
459455 InkWell w when w.onTap != null => 'InkWell' ,
460- GestureDetector w
461- when w.onTap != null ||
462- w.onDoubleTap != null ||
463- w.onLongPress != null =>
464- 'GestureDetector' ,
456+ GestureDetector w when w.onTap != null || w.onDoubleTap != null || w.onLongPress != null => 'GestureDetector' ,
465457 InkResponse w when w.onTap != null => 'InkResponse' ,
466458 InputChip w when w.onPressed != null => 'InputChip' ,
467459 ActionChip w when w.onPressed != null => 'ActionChip' ,
@@ -494,4 +486,42 @@ class LayoutSnapshotCapture {
494486 _ => null ,
495487 };
496488 }
489+
490+ /// Extracts text or semantic label from a widget
491+ /// Returns null if the widget has no text content
492+ /// [maxLength] limits the returned text length (default: 32)
493+ static String ? _extractTextFromWidget (Widget widget, {int maxLength = 32 }) {
494+ final text = switch (widget) {
495+ Text w => w.data ?? w.textSpan? .toPlainText (),
496+ RichText w => w.text.toPlainText (),
497+ Icon w => w.semanticLabel,
498+ _ => null ,
499+ };
500+ return text != null && text.length > maxLength ? text.substring (0 , maxLength) : text;
501+ }
502+
503+ /// Finds the first text or semantic label in the subtree of an element
504+ /// Returns null if no text is found
505+ static String ? _findFirstTextInSubtree (Element element) {
506+ // Try to extract text from this element first
507+ final text = _extractTextFromWidget (element.widget);
508+ if (text != null && text.isNotEmpty) {
509+ return text;
510+ }
511+
512+ // Traverse children to find text
513+ String ? foundText;
514+ element.visitChildElements ((child) {
515+ // Skip if we already found text
516+ if (foundText != null ) return ;
517+
518+ // Skip offstage widgets
519+ if (_shouldSkipElement (child)) return ;
520+
521+ // Recursively search in child
522+ foundText = _findFirstTextInSubtree (child);
523+ });
524+
525+ return foundText;
526+ }
497527}
0 commit comments