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

Commit 4f3f7bb

Browse files
authored
[web] Fix event offset for transformed widgets (and text input nodes). (#41870)
Text inputs have moved outside of the shadowDOM and are now using the pointer event offset calculation algorithm that platform views use. However, transforms (e.g. scaling) applied to the input element aren't currently accounted for, which leads to incorrect offsets and clicks being registered inaccurately. This PR attempts to transform those offset coordinates using the transform matrix data that is included in the geometry information sent over to `text_editing.dart` from the framework. ## Issues * Fixes flutter/flutter#125948 (text editing) * Fixes flutter/flutter#126661 (platform view scaling) * Fixes flutter/flutter#126754 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent f3902ae commit 4f3f7bb

File tree

2 files changed

+155
-33
lines changed

2 files changed

+155
-33
lines changed

lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:typed_data';
6+
7+
import 'package:ui/src/engine/embedder.dart';
8+
import 'package:ui/src/engine/text_editing/text_editing.dart';
9+
import 'package:ui/src/engine/vector_math.dart';
510
import 'package:ui/ui.dart' as ui show Offset;
611

712
import '../dom.dart';
@@ -24,48 +29,50 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarge
2429
return _computeOffsetForTalkbackEvent(event, actualTarget);
2530
}
2631

32+
// On one of our text-editing nodes
33+
final bool isInput = flutterViewEmbedder.textEditingHostNode.contains(event.target! as DomNode);
34+
if (isInput) {
35+
final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry;
36+
if (inputGeometry != null) {
37+
return _computeOffsetForInputs(event, inputGeometry);
38+
}
39+
}
40+
41+
// On another DOM Element (normally a platform view)
2742
final bool isTargetOutsideOfShadowDOM = event.target != actualTarget;
2843
if (isTargetOutsideOfShadowDOM) {
29-
return _computeOffsetRelativeToActualTarget(event, actualTarget);
44+
final DomRect origin = actualTarget.getBoundingClientRect();
45+
// event.clientX/Y and origin.x/y are relative **to the viewport**.
46+
// (This doesn't work with 3D translations of the parent element.)
47+
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
48+
return ui.Offset(event.clientX - origin.x, event.clientY - origin.y);
3049
}
50+
3151
// Return the offsetX/Y in the normal case.
3252
// (This works with 3D translations of the parent element.)
3353
return ui.Offset(event.offsetX, event.offsetY);
3454
}
3555

36-
/// Computes the event offset when hovering over any nodes that don't exist in
37-
/// the shadowDOM such as platform views or text editing nodes.
38-
///
39-
/// This still uses offsetX/Y, but adds the offset from the top/left corner of the
40-
/// platform view to the Flutter View (`actualTarget`).
56+
/// Computes the offsets for input nodes, which live outside of the shadowDOM.
57+
/// Since inputs can be transformed (scaled, translated, etc), we can't rely on
58+
/// `_computeOffsetRelativeToActualTarget` to calculate accurate coordinates, as
59+
/// it only handles the case where inputs are translated, but will have issues
60+
/// for scaled inputs (see: https://github.com/flutter/flutter/issues/125948).
4161
///
42-
/// ×--FlutterView(actualTarget)--------------+
43-
/// |\ |
44-
/// | x1,y1 |
45-
/// | |
46-
/// | |
47-
/// | ×-PlatformView(target)---------+ |
48-
/// | |\ | |
49-
/// | | x2,y2 | |
50-
/// | | | |
51-
/// | | × (event) | |
52-
/// | | \ | |
53-
/// | | offsetX, offsetY | |
54-
/// | | (Relative to PlatformView) | |
55-
/// | +------------------------------+ |
56-
/// +-----------------------------------------+
57-
///
58-
/// Offset between PlatformView and FlutterView (xP, yP) = (x2 - x1, y2 - y1)
59-
///
60-
/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP)
61-
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
62-
ui.Offset _computeOffsetRelativeToActualTarget(DomMouseEvent event, DomElement actualTarget) {
63-
final DomElement target = event.target! as DomElement;
64-
final DomRect targetRect = target.getBoundingClientRect();
65-
final DomRect actualTargetRect = actualTarget.getBoundingClientRect();
66-
final double offsetTop = targetRect.y - actualTargetRect.y;
67-
final double offsetLeft = targetRect.x - actualTargetRect.x;
68-
return ui.Offset(event.offsetX + offsetLeft, event.offsetY + offsetTop);
62+
/// We compute the offsets here by using the text input geometry data that is
63+
/// sent from the framework, which includes information on how to transform the
64+
/// underlying input element. We transform the `event.offset` points we receive
65+
/// using the values from the input's transform matrix.
66+
ui.Offset _computeOffsetForInputs(DomMouseEvent event, EditableTextGeometry inputGeometry) {
67+
final DomElement targetElement = event.target! as DomHTMLElement;
68+
final DomHTMLElement domElement = textEditing.strategy.activeDomElement;
69+
assert(targetElement == domElement, 'The targeted input element must be the active input element');
70+
final Float32List transformValues = inputGeometry.globalTransform;
71+
assert(transformValues.length == 16);
72+
final Matrix4 transform = Matrix4.fromFloat32List(transformValues);
73+
final Vector3 transformedPoint = transform.perspectiveTransform(x: event.offsetX, y: event.offsetY, z: 0);
74+
75+
return ui.Offset(transformedPoint.x, transformedPoint.y);
6976
}
7077

7178
/// Computes the event offset when TalkBack is firing the event.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2013 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+
@TestOn('browser')
6+
library;
7+
8+
import 'dart:async';
9+
10+
import 'package:test/bootstrap/browser.dart';
11+
import 'package:test/test.dart';
12+
import 'package:ui/src/engine/dom.dart';
13+
import 'package:ui/src/engine/embedder.dart';
14+
import 'package:ui/src/engine/pointer_binding/event_position_helper.dart';
15+
import 'package:ui/ui.dart' as ui show Offset;
16+
17+
void main() {
18+
internalBootstrapBrowserTest(() => doTests);
19+
}
20+
21+
void doTests() {
22+
ensureFlutterViewEmbedderInitialized();
23+
24+
late DomElement target;
25+
late DomElement eventSource;
26+
final StreamController<DomEvent> events = StreamController<DomEvent>.broadcast();
27+
28+
/// Dispatches an event `e` on `target`, and returns it after it's gone through the browser.
29+
Future<DomPointerEvent> dispatchAndCatch(DomElement target, DomPointerEvent e) async {
30+
final Future<DomEvent> nextEvent = events.stream.first;
31+
target.dispatchEvent(e);
32+
return (await nextEvent) as DomPointerEvent;
33+
}
34+
35+
group('computeEventOffsetToTarget', () {
36+
setUp(() {
37+
target = createDomElement('div-target');
38+
eventSource = createDomElement('div-event-source');
39+
target.append(eventSource);
40+
domDocument.body!.append(target);
41+
42+
// make containers known fixed sizes, absolutely positioned elements, so
43+
// we can reason about screen coordinates relatively easily later!
44+
target.style
45+
..position = 'absolute'
46+
..width = '320px'
47+
..height = '240px'
48+
..top = '0px'
49+
..left = '0px';
50+
51+
eventSource.style
52+
..position = 'absolute'
53+
..width = '100px'
54+
..height = '80px'
55+
..top = '100px'
56+
..left = '120px';
57+
58+
target.addEventListener('click', createDomEventListener((DomEvent e) {
59+
events.add(e);
60+
}));
61+
});
62+
63+
tearDown(() {
64+
target.remove();
65+
});
66+
67+
test('Event dispatched by target returns offsetX, offsetY', () async {
68+
// Fire an event contained within target...
69+
final DomMouseEvent event = await dispatchAndCatch(target, createDomPointerEvent(
70+
'click',
71+
<String, Object>{
72+
'bubbles': true,
73+
'clientX': 10,
74+
'clientY': 20,
75+
}
76+
));
77+
78+
expect(event.offsetX, 10);
79+
expect(event.offsetY, 20);
80+
81+
final ui.Offset offset = computeEventOffsetToTarget(event, target);
82+
83+
expect(offset.dx, event.offsetX);
84+
expect(offset.dy, event.offsetY);
85+
});
86+
87+
test('Event dispatched on child re-computes offset (offsetX/Y invalid)', () async {
88+
// Fire an event contained within target...
89+
final DomMouseEvent event = await dispatchAndCatch(eventSource, createDomPointerEvent(
90+
'click',
91+
<String, Object>{
92+
'bubbles': true, // So it can be caught in `target`
93+
'clientX': 140, // x = 20px into `eventSource`.
94+
'clientY': 110, // y = 10px into `eventSource`.
95+
}
96+
));
97+
98+
expect(event.offsetX, 20);
99+
expect(event.offsetY, 10);
100+
101+
final ui.Offset offset = computeEventOffsetToTarget(event, target);
102+
103+
expect(offset.dx, 140);
104+
expect(offset.dy, 110);
105+
});
106+
107+
test('Event dispatched by TalkBack gets a computed offset', () async {
108+
// Fill this in to test _computeOffsetForTalkbackEvent
109+
}, skip: 'To be implemented!');
110+
111+
test('Event dispatched on text editing node computes offset with framework geometry', () async {
112+
// Fill this in to test _computeOffsetForInputs
113+
}, skip: 'To be implemented!');
114+
});
115+
}

0 commit comments

Comments
 (0)