Skip to content

Commit e693192

Browse files
author
Harry Terkelsen
authored
[web_ui] Check if a pointer is already down for the specific device (flutter#12470)
* WIP on fixing pointer event clicks * Don't set canvaskit as enabled by default * Use a Set of pressed buttons rather than a Map of button status to avoid memory leaks * Add test for pointer binding Fix bug where event listeners weren't properly removed
1 parent 7b0c82b commit e693192

File tree

2 files changed

+159
-19
lines changed

2 files changed

+159
-19
lines changed

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

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,45 @@ class PointerSupportDetector {
101101
'pointers:$hasPointerEvents, touch:$hasTouchEvents, mouse:$hasMouseEvents';
102102
}
103103

104+
class _PressedButton {
105+
const _PressedButton(this.deviceId, this.button);
106+
107+
// The id of the device pressing the button.
108+
final int deviceId;
109+
110+
// The id of the button being pressed.
111+
final int button;
112+
113+
bool operator ==(other) {
114+
if (other is! _PressedButton) return false;
115+
final _PressedButton otherButton = other;
116+
return deviceId == otherButton.deviceId && button == otherButton.button;
117+
}
118+
119+
int get hashCode => ((13801 + deviceId) * 37) + button;
120+
}
121+
104122
/// Common functionality that's shared among adapters.
105123
abstract class BaseAdapter {
106124
static final Map<String, html.EventListener> _listeners =
107125
<String, html.EventListener>{};
108126

109127
final DomRenderer domRenderer;
110128
PointerDataCallback _callback;
111-
Map<int, bool> _isDownMap = <int, bool>{};
112-
bool _isButtonDown(int button) {
113-
return _isDownMap[button] == true;
129+
130+
// A set of the buttons that are currently being pressed.
131+
Set<_PressedButton> _pressedButtons = Set<_PressedButton>();
132+
133+
bool _isButtonDown(int device, int button) {
134+
return _pressedButtons.contains(_PressedButton(device, button));
114135
}
115136

116-
void _updateButtonDownState(int button, bool value) {
117-
_isDownMap[button] = value;
137+
void _updateButtonDownState(int device, int button, bool value) {
138+
if (value) {
139+
_pressedButtons.add(_PressedButton(device, button));
140+
} else {
141+
_pressedButtons.remove(_PressedButton(device, button));
142+
}
118143
}
119144

120145
BaseAdapter(this._callback, this.domRenderer) {
@@ -129,7 +154,7 @@ abstract class BaseAdapter {
129154
void clearListeners() {
130155
final html.Element glassPane = domRenderer.glassPaneElement;
131156
_listeners.forEach((String eventName, html.EventListener listener) {
132-
glassPane.removeEventListener(eventName, listener);
157+
glassPane.removeEventListener(eventName, listener, true);
133158
});
134159
_listeners.clear();
135160
}
@@ -170,6 +195,14 @@ int _pointerButtonFromHtmlEvent(html.Event event) {
170195
return _kPrimaryMouseButton;
171196
}
172197

198+
int _deviceFromHtmlEvent(event) {
199+
if (event is html.PointerEvent) {
200+
final html.PointerEvent pointerEvent = event;
201+
return pointerEvent.pointerId;
202+
}
203+
return _mouseDeviceId;
204+
}
205+
173206
/// Adapter class to be used with browsers that support native pointer events.
174207
class PointerAdapter extends BaseAdapter {
175208
PointerAdapter(PointerDataCallback callback, DomRenderer domRenderer)
@@ -179,12 +212,13 @@ class PointerAdapter extends BaseAdapter {
179212
void _setup() {
180213
_addEventListener('pointerdown', (html.Event event) {
181214
final int pointerButton = _pointerButtonFromHtmlEvent(event);
182-
if (_isButtonDown(pointerButton)) {
215+
final int device = _deviceFromHtmlEvent(event);
216+
if (_isButtonDown(device, pointerButton)) {
183217
// TODO(flutter_web): Remove this temporary fix for right click
184218
// on web platform once context guesture is implemented.
185219
_callback(_convertEventToPointerData(ui.PointerChange.up, event));
186220
}
187-
_updateButtonDownState(pointerButton, true);
221+
_updateButtonDownState(device, pointerButton, true);
188222
_callback(_convertEventToPointerData(ui.PointerChange.down, event));
189223
});
190224

@@ -195,8 +229,9 @@ class PointerAdapter extends BaseAdapter {
195229
// Change this when context gesture is implemented in flutter framework.
196230
final html.PointerEvent pointerEvent = event;
197231
final int pointerButton = _pointerButtonFromHtmlEvent(pointerEvent);
232+
final int device = _deviceFromHtmlEvent(event);
198233
final List<ui.PointerData> data = _convertEventToPointerData(
199-
_isButtonDown(pointerButton)
234+
_isButtonDown(device, pointerButton)
200235
? ui.PointerChange.move
201236
: ui.PointerChange.hover,
202237
pointerEvent);
@@ -214,18 +249,20 @@ class PointerAdapter extends BaseAdapter {
214249
// The pointer could have been released by a `pointerout` event, in which
215250
// case `pointerup` should have no effect.
216251
final int pointerButton = _pointerButtonFromHtmlEvent(event);
217-
if (!_isButtonDown(pointerButton)) {
252+
final int device = _deviceFromHtmlEvent(event);
253+
if (!_isButtonDown(device, pointerButton)) {
218254
return;
219255
}
220-
_updateButtonDownState(pointerButton, false);
256+
_updateButtonDownState(device, pointerButton, false);
221257
_callback(_convertEventToPointerData(ui.PointerChange.up, event));
222258
});
223259

224260
// A browser fires cancel event if it concludes the pointer will no longer
225261
// be able to generate events (example: device is deactivated)
226262
_addEventListener('pointercancel', (html.Event event) {
227263
final int pointerButton = _pointerButtonFromHtmlEvent(event);
228-
_updateButtonDownState(pointerButton, false);
264+
final int device = _deviceFromHtmlEvent(event);
265+
_updateButtonDownState(pointerButton, device, false);
229266
_callback(_convertEventToPointerData(ui.PointerChange.cancel, event));
230267
});
231268

@@ -308,13 +345,14 @@ class TouchAdapter extends BaseAdapter {
308345
@override
309346
void _setup() {
310347
_addEventListener('touchstart', (html.Event event) {
311-
_updateButtonDownState(_kPrimaryMouseButton, true);
348+
_updateButtonDownState(
349+
_deviceFromHtmlEvent(event), _kPrimaryMouseButton, true);
312350
_callback(_convertEventToPointerData(ui.PointerChange.down, event));
313351
});
314352

315353
_addEventListener('touchmove', (html.Event event) {
316354
event.preventDefault(); // Prevents standard overscroll on iOS/Webkit.
317-
if (!_isButtonDown(_kPrimaryMouseButton)) {
355+
if (!_isButtonDown(_deviceFromHtmlEvent(event), _kPrimaryMouseButton)) {
318356
return;
319357
}
320358
_callback(_convertEventToPointerData(ui.PointerChange.move, event));
@@ -324,7 +362,8 @@ class TouchAdapter extends BaseAdapter {
324362
// On Safari Mobile, the keyboard does not show unless this line is
325363
// added.
326364
event.preventDefault();
327-
_updateButtonDownState(_kPrimaryMouseButton, false);
365+
_updateButtonDownState(
366+
_deviceFromHtmlEvent(event), _kPrimaryMouseButton, false);
328367
_callback(_convertEventToPointerData(ui.PointerChange.up, event));
329368
});
330369

@@ -372,27 +411,30 @@ class MouseAdapter extends BaseAdapter {
372411
void _setup() {
373412
_addEventListener('mousedown', (html.Event event) {
374413
final int pointerButton = _pointerButtonFromHtmlEvent(event);
375-
if (_isButtonDown(pointerButton)) {
414+
final int device = _deviceFromHtmlEvent(event);
415+
if (_isButtonDown(device, pointerButton)) {
376416
// TODO(flutter_web): Remove this temporary fix for right click
377417
// on web platform once context guesture is implemented.
378418
_callback(_convertEventToPointerData(ui.PointerChange.up, event));
379419
}
380-
_updateButtonDownState(pointerButton, true);
420+
_updateButtonDownState(device, pointerButton, true);
381421
_callback(_convertEventToPointerData(ui.PointerChange.down, event));
382422
});
383423

384424
_addEventListener('mousemove', (html.Event event) {
385425
final int pointerButton = _pointerButtonFromHtmlEvent(event);
426+
final int device = _deviceFromHtmlEvent(event);
386427
final List<ui.PointerData> data = _convertEventToPointerData(
387-
_isButtonDown(pointerButton)
428+
_isButtonDown(device, pointerButton)
388429
? ui.PointerChange.move
389430
: ui.PointerChange.hover,
390431
event);
391432
_callback(data);
392433
});
393434

394435
_addEventListener('mouseup', (html.Event event) {
395-
_updateButtonDownState(_pointerButtonFromHtmlEvent(event), false);
436+
final int device = _deviceFromHtmlEvent(event);
437+
_updateButtonDownState(device, _pointerButtonFromHtmlEvent(event), false);
396438
_callback(_convertEventToPointerData(ui.PointerChange.up, event));
397439
});
398440

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
import 'dart:html' as html;
6+
import 'dart:typed_data';
7+
8+
import 'package:ui/src/engine.dart';
9+
import 'package:ui/ui.dart' as ui;
10+
11+
import 'package:test/test.dart';
12+
13+
void main() {
14+
group('Pointer Binding', () {
15+
html.Element glassPane = domRenderer.glassPaneElement;
16+
17+
setUp(() {
18+
// Touching domRenderer creates PointerBinding.instance.
19+
domRenderer;
20+
21+
// Set a new detector to reset the state of the listeners.
22+
PointerBinding.instance.debugOverrideDetector(TestPointerDetector());
23+
24+
ui.window.onPointerDataPacket = null;
25+
});
26+
27+
test('can receive pointer events on the glass pane', () {
28+
ui.PointerDataPacket receivedPacket;
29+
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
30+
receivedPacket = packet;
31+
};
32+
33+
glassPane.dispatchEvent(html.PointerEvent('pointerdown', {
34+
'pointerId': 1,
35+
'button': 1,
36+
}));
37+
38+
expect(receivedPacket, isNotNull);
39+
expect(receivedPacket.data[0].device, equals(1));
40+
});
41+
42+
test('synthesizes a pointerup event on two pointerdowns in a row', () {
43+
List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
44+
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
45+
packets.add(packet);
46+
};
47+
48+
glassPane.dispatchEvent(html.PointerEvent('pointerdown', {
49+
'pointerId': 1,
50+
'button': 1,
51+
}));
52+
53+
glassPane.dispatchEvent(html.PointerEvent('pointerdown', {
54+
'pointerId': 1,
55+
'button': 1,
56+
}));
57+
58+
expect(packets, hasLength(3));
59+
expect(packets[0].data[0].change, equals(ui.PointerChange.down));
60+
expect(packets[1].data[0].change, equals(ui.PointerChange.up));
61+
expect(packets[2].data[0].change, equals(ui.PointerChange.down));
62+
});
63+
64+
test('does not synthesize pointer up if from different device', () {
65+
List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
66+
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
67+
packets.add(packet);
68+
};
69+
70+
glassPane.dispatchEvent(html.PointerEvent('pointerdown', {
71+
'pointerId': 1,
72+
'button': 1,
73+
}));
74+
75+
glassPane.dispatchEvent(html.PointerEvent('pointerdown', {
76+
'pointerId': 2,
77+
'button': 1,
78+
}));
79+
80+
expect(packets, hasLength(2));
81+
expect(packets[0].data[0].change, equals(ui.PointerChange.down));
82+
expect(packets[0].data[0].device, equals(1));
83+
expect(packets[1].data[0].change, equals(ui.PointerChange.down));
84+
expect(packets[1].data[0].device, equals(2));
85+
});
86+
});
87+
}
88+
89+
class TestPointerDetector extends PointerSupportDetector {
90+
@override
91+
final bool hasPointerEvents = true;
92+
93+
@override
94+
final bool hasTouchEvents = false;
95+
96+
@override
97+
final bool hasMouseEvents = false;
98+
}

0 commit comments

Comments
 (0)