Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 867d4f2

Browse files
authored
[web:semantics] fix node positioning; expose debug tree (#24903)
1 parent 3de175d commit 867d4f2

File tree

5 files changed

+132
-166
lines changed

5 files changed

+132
-166
lines changed

lib/web_ui/lib/src/engine/dom_renderer.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,27 @@ class DomRenderer {
5454
/// This element is created and inserted in the HTML DOM once. It is never
5555
/// removed or moved. However the [sceneElement] may be replaced inside it.
5656
///
57-
/// This element precedes the [glassPaneElement] so that it never receives
58-
/// input events. All input events are processed by [glassPaneElement] and the
59-
/// semantics tree.
57+
/// This element is inserted after the [semanticsHostElement] so that
58+
/// platform views take precedence in DOM event handling.
6059
html.Element? get sceneHostElement => _sceneHostElement;
6160
html.Element? _sceneHostElement;
6261

62+
/// The element that contains the semantics tree.
63+
///
64+
/// This element is created and inserted in the HTML DOM once. It is never
65+
/// removed or moved.
66+
///
67+
/// We render semantics inside the glasspane for proper focus and event
68+
/// handling. If semantics is behind the glasspane, the phone will disable
69+
/// focusing by touch, only by tabbing around the UI. If semantics is in
70+
/// front of glasspane, then DOM event won't bubble up to the glasspane so
71+
/// it can forward events to the framework.
72+
///
73+
/// This element is inserted before the [semanticsHostElement] so that
74+
/// platform views take precedence in DOM event handling.
75+
html.Element? get semanticsHostElement => _semanticsHostElement;
76+
html.Element? _semanticsHostElement;
77+
6378
/// The last scene element rendered by the [render] method.
6479
html.Element? get sceneElement => _sceneElement;
6580
html.Element? _sceneElement;
@@ -427,6 +442,14 @@ flt-glass-pane * {
427442

428443
_sceneHostElement = createElement('flt-scene-host');
429444

445+
final html.Element semanticsHostElement = createElement('flt-semantics-host');
446+
semanticsHostElement.style
447+
..position = 'absolute'
448+
..transformOrigin = '0 0 0';
449+
_semanticsHostElement = semanticsHostElement;
450+
updateSemanticsScreenProperties();
451+
glassPaneElement.append(semanticsHostElement);
452+
430453
// Don't allow the scene to receive pointer events.
431454
_sceneHostElement!.style.pointerEvents = 'none';
432455

@@ -443,6 +466,12 @@ flt-glass-pane * {
443466
// by the platform view.
444467
glassPaneElement.insertBefore(_accesibilityPlaceholder, _sceneHostElement);
445468

469+
// When debugging semantics, make the scene semi-transparent so that the
470+
// semantics tree is visible.
471+
if (_debugShowSemanticsNodes) {
472+
_sceneHostElement!.style.opacity = '0.3';
473+
}
474+
446475
PointerBinding.initInstance(glassPaneElement);
447476
KeyboardBinding.initInstance(glassPaneElement);
448477

@@ -559,6 +588,13 @@ flt-glass-pane * {
559588
EnginePlatformDispatcher.instance._updateLocales();
560589
}
561590

591+
/// The framework specifies semantics in physical pixels, but CSS uses
592+
/// logical pixels. To compensate, we inject an inverse scale at the root
593+
/// level.
594+
void updateSemanticsScreenProperties() {
595+
_semanticsHostElement!.style.transform = 'scale(${1 / html.window.devicePixelRatio})';
596+
}
597+
562598
/// Called immediately after browser window metrics change.
563599
///
564600
/// When there is a text editing going on in mobile devices, do not change
@@ -569,6 +605,7 @@ flt-glass-pane * {
569605
/// Note: always check for rotations for a mobile device. Update the physical
570606
/// size if the change is caused by a rotation.
571607
void _metricsDidChange(html.Event? event) {
608+
updateSemanticsScreenProperties();
572609
if (isMobile && !window.isRotation() && textEditing.isEditing) {
573610
window.computeOnScreenKeyboardInsets();
574611
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();

lib/web_ui/lib/src/engine/semantics/label_and_value.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ class LabelAndValue extends RoleManager {
9696
..width = '${semanticsObject.rect!.width}px'
9797
..height = '${semanticsObject.rect!.height}px';
9898
}
99-
_auxiliaryValueElement!.style.fontSize = '6px';
99+
100+
// Normally use a small font size so that text doesn't leave the scope
101+
// of the semantics node. When debugging semantics, use a font size
102+
// that's reasonably visible.
103+
_auxiliaryValueElement!.style.fontSize = _debugShowSemanticsNodes ? '12px' : '6px';
100104
semanticsObject.element.append(_auxiliaryValueElement!);
101105
}
102106
_auxiliaryValueElement!.text = combinedValue.toString();

lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 55 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@
66
part of engine;
77

88
/// Set this flag to `true` to cause the engine to visualize the semantics tree
9-
/// on the screen.
9+
/// on the screen for debugging.
1010
///
11-
/// This is useful for debugging.
12-
const bool _debugShowSemanticsNodes = false;
11+
/// This only works in profile and release modes. Debug mode does not support
12+
/// passing compile-time constants.
13+
///
14+
/// Example:
15+
///
16+
/// ```
17+
/// flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true
18+
/// ```
19+
const bool _debugShowSemanticsNodes = bool.fromEnvironment(
20+
'FLUTTER_WEB_DEBUG_SHOW_SEMANTICS',
21+
defaultValue: false,
22+
);
1323

1424
/// Contains updates for the semantics tree.
1525
///
@@ -233,27 +243,29 @@ class SemanticsObject {
233243
/// Creates a semantics tree node with the given [id] and [owner].
234244
SemanticsObject(this.id, this.owner) {
235245
// DOM nodes created for semantics objects are positioned absolutely using
236-
// transforms. We use a transparent color instead of "visibility:hidden" or
237-
// "display:none" so that a screen reader does not ignore these elements.
246+
// transforms.
238247
element.style.position = 'absolute';
239248

240249
// The root node has some properties that other nodes do not.
241-
if (id == 0) {
250+
if (id == 0 && !_debugShowSemanticsNodes) {
242251
// Make all semantics transparent. We use `filter` instead of `opacity`
243252
// attribute because `filter` is stronger. `opacity` does not apply to
244253
// some elements, particularly on iOS, such as the slider thumb and track.
254+
//
255+
// We use transparency instead of "visibility:hidden" or "display:none"
256+
// so that a screen reader does not ignore these elements.
245257
element.style.filter = 'opacity(0%)';
246258

247259
// Make text explicitly transparent to signal to the browser that no
248260
// rasterization needs to be done.
249261
element.style.color = 'rgba(0,0,0,0)';
250262
}
251263

264+
// Make semantic elements visible for debugging by outlining them using a
265+
// green border. We do not use `border` attribute because it affects layout
266+
// (`outline` does not).
252267
if (_debugShowSemanticsNodes) {
253-
element.style
254-
..filter = 'opacity(90%)'
255-
..outline = '1px solid green'
256-
..color = 'purple';
268+
element.style.outline = '1px solid green';
257269
}
258270
}
259271

@@ -853,9 +865,9 @@ class SemanticsObject {
853865
hasIdentityTransform &&
854866
verticalContainerAdjustment == 0.0 &&
855867
horizontalContainerAdjustment == 0.0) {
856-
_resetElementOffsets(element);
868+
_clearSemanticElementTransform(element);
857869
if (containerElement != null) {
858-
_resetElementOffsets(containerElement);
870+
_clearSemanticElementTransform(containerElement);
859871
}
860872
return;
861873
}
@@ -879,81 +891,48 @@ class SemanticsObject {
879891
effectiveTransformIsIdentity = false;
880892
}
881893

882-
if (!effectiveTransformIsIdentity || isMacOrIOS) {
883-
if (effectiveTransformIsIdentity) {
884-
effectiveTransform = Matrix4.identity();
885-
}
886-
if (isDesktop) {
887-
element.style
888-
..transformOrigin = '0 0 0'
889-
..transform = (effectiveTransformIsIdentity ? 'translate(0px 0px 0px)'
890-
: matrix4ToCssTransform(effectiveTransform));
891-
} else {
892-
// Mobile screen readers observed to have errors while calculating the
893-
// semantics focus borders if css `transform` properties are used.
894-
// See: https://github.com/flutter/flutter/issues/68225
895-
// Therefore we are calculating a bounding rectangle for the
896-
// effective transform and use that rectangle to set TLWH css style
897-
// properties.
898-
// Note: Identity matrix is not using this code path.
899-
final ui.Rect rect =
900-
computeBoundingRectangleFromMatrix(effectiveTransform, _rect!);
901-
element.style
902-
..top = '${rect.top}px'
903-
..left = '${rect.left}px'
904-
..width = '${rect.width}px'
905-
..height = '${rect.height}px';
906-
}
894+
if (!effectiveTransformIsIdentity) {
895+
element.style
896+
..transformOrigin = '0 0 0'
897+
..transform = matrix4ToCssTransform(effectiveTransform);
907898
} else {
908-
_resetElementOffsets(element);
909-
// TODO: https://github.com/flutter/flutter/issues/73347
899+
_clearSemanticElementTransform(element);
910900
}
911901

912902
if (containerElement != null) {
913903
if (!hasZeroRectOffset ||
914-
isMacOrIOS ||
915904
verticalContainerAdjustment != 0.0 ||
916905
horizontalContainerAdjustment != 0.0) {
917906
final double translateX = -_rect!.left + horizontalContainerAdjustment;
918907
final double translateY = -_rect!.top + verticalContainerAdjustment;
919-
if (isDesktop) {
920-
containerElement.style
921-
..transformOrigin = '0 0 0'
922-
..transform = 'translate(${translateX}px, ${translateY}px)';
923-
} else {
924-
containerElement.style
925-
..top = '${translateY}px'
926-
..left = '${translateX}px';
927-
}
908+
containerElement.style
909+
..top = '${translateY}px'
910+
..left = '${translateX}px';
928911
} else {
929-
_resetElementOffsets(containerElement);
912+
_clearSemanticElementTransform(containerElement);
930913
}
931914
}
932915
}
933916

934-
// On Mac OS and iOS, VoiceOver requires left=0 top=0 value to correctly
935-
// handle order. See https://github.com/flutter/flutter/issues/73347.
936-
static void _resetElementOffsets(html.Element element) {
917+
/// Clears the transform on a semantic element as if an identity transform is
918+
/// applied.
919+
///
920+
/// On macOS and iOS, VoiceOver requires `left=0; top=0` value to correctly
921+
/// handle traversal order.
922+
///
923+
/// See https://github.com/flutter/flutter/issues/73347.
924+
static void _clearSemanticElementTransform(html.Element element) {
925+
element.style
926+
..removeProperty('transform-origin')
927+
..removeProperty('transform');
937928
if (isMacOrIOS) {
938-
if (isDesktop) {
939-
element.style
940-
..transformOrigin = '0 0 0'
941-
..transform = 'translate(0px, 0px)';
942-
} else {
943-
element.style
944-
..top = '0px'
945-
..left = '0px';
946-
}
929+
element.style
930+
..top = '0px'
931+
..left = '0px';
947932
} else {
948-
if (isDesktop) {
949-
element.style
950-
..removeProperty('transform-origin')
951-
..removeProperty('transform');
952-
} else {
953-
element.style
954-
..removeProperty('top')
955-
..removeProperty('left');
956-
}
933+
element.style
934+
..removeProperty('top')
935+
..removeProperty('left');
957936
}
958937
}
959938

@@ -1493,7 +1472,10 @@ class EngineSemanticsOwner {
14931472
/// Updates the semantics tree from data in the [uiUpdate].
14941473
void updateSemantics(ui.SemanticsUpdate uiUpdate) {
14951474
if (!_semanticsEnabled) {
1496-
return;
1475+
// If we're receiving a semantics update from the framework, it means the
1476+
// developer enabled it programmatically, so we enable it in the engine
1477+
// too.
1478+
semanticsEnabled = true;
14971479
}
14981480

14991481
final SemanticsUpdate update = uiUpdate as SemanticsUpdate;
@@ -1505,19 +1487,7 @@ class EngineSemanticsOwner {
15051487
if (_rootSemanticsElement == null) {
15061488
final SemanticsObject root = _semanticsTree[0]!;
15071489
_rootSemanticsElement = root.element;
1508-
// We render semantics inside the glasspane for proper focus and event
1509-
// handling. If semantics is behind the glasspane, the phone will disable
1510-
// focusing by touch, only by tabbing around the UI. If semantics is in
1511-
// front of glasspane, then DOM event won't bubble up to the glasspane so
1512-
// it can forward events to the framework.
1513-
//
1514-
// We insert the semantics root before the scene host. For all widgets
1515-
// in the scene, except for platform widgets, the scene host will pass the
1516-
// pointer events through to the semantics tree. However, for platform
1517-
// views, the pointer events will not pass through, and will be handled
1518-
// by the platform view.
1519-
domRenderer.glassPaneElement!
1520-
.insertBefore(_rootSemanticsElement!, domRenderer.sceneHostElement);
1490+
domRenderer.semanticsHostElement!.append(root.element);
15211491
}
15221492

15231493
_finalizeTree();

lib/web_ui/lib/src/engine/util.dart

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -595,47 +595,3 @@ int clampInt(int value, int min, int max) {
595595
return value;
596596
}
597597
}
598-
599-
ui.Rect computeBoundingRectangleFromMatrix(Matrix4 transform, ui.Rect rect) {
600-
final Float32List m = transform.storage;
601-
// Apply perspective transform to all 4 corners. Can't use left,top, bottom,
602-
// right since for example rotating 45 degrees would yield inaccurate size.
603-
double x = rect.left;
604-
double y = rect.top;
605-
double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
606-
double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
607-
double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
608-
double minX = xp, maxX = xp;
609-
double minY = yp, maxY = yp;
610-
x = rect.right;
611-
y = rect.bottom;
612-
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
613-
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
614-
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
615-
616-
minX = math.min(minX, xp);
617-
maxX = math.max(maxX, xp);
618-
minY = math.min(minY, yp);
619-
maxY = math.max(maxY, yp);
620-
621-
x = rect.left;
622-
y = rect.bottom;
623-
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
624-
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
625-
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
626-
minX = math.min(minX, xp);
627-
maxX = math.max(maxX, xp);
628-
minY = math.min(minY, yp);
629-
maxY = math.max(maxY, yp);
630-
631-
x = rect.right;
632-
y = rect.top;
633-
wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]);
634-
xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp;
635-
yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp;
636-
minX = math.min(minX, xp);
637-
maxX = math.max(maxX, xp);
638-
minY = math.min(minY, yp);
639-
maxY = math.max(maxY, yp);
640-
return ui.Rect.fromLTWH(minX, minY, maxX - minX, maxY - minY);
641-
}

0 commit comments

Comments
 (0)