• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5part of engine;
6
7/// Set this flag to true to see all the fired events in the console.
8const bool _debugLogPointerEvents = false;
9
10/// The signature of a callback that handles pointer events.
11typedef PointerDataCallback = void Function(List<ui.PointerData>);
12
13class PointerBinding {
14  /// The singleton instance of this object.
15  static PointerBinding get instance => _instance;
16  static PointerBinding _instance;
17
18  PointerBinding(this.domRenderer) {
19    if (_instance == null) {
20      _instance = this;
21      _detector = const PointerSupportDetector();
22      _adapter = _createAdapter();
23    }
24    assert(() {
25      registerHotRestartListener(() {
26        _adapter?.clearListeners();
27      });
28      return true;
29    }());
30  }
31
32  final DomRenderer domRenderer;
33  PointerSupportDetector _detector;
34  BaseAdapter _adapter;
35
36  /// Should be used in tests to define custom detection of pointer support.
37  ///
38  /// ```dart
39  /// // Forces PointerBinding to use mouse events.
40  /// class MyTestDetector extends PointerSupportDetector {
41  ///   @override
42  ///   final bool hasPointerEvents = false;
43  ///
44  ///   @override
45  ///   final bool hasTouchEvents = false;
46  ///
47  ///   @override
48  ///   final bool hasMouseEvents = true;
49  /// }
50  ///
51  /// PointerBinding.instance.debugOverrideDetector(MyTestDetector());
52  /// ```
53  void debugOverrideDetector(PointerSupportDetector newDetector) {
54    newDetector ??= const PointerSupportDetector();
55    // When changing the detector, we need to swap the adapter.
56    if (newDetector != _detector) {
57      _detector = newDetector;
58      _adapter?.clearListeners();
59      _adapter = _createAdapter();
60    }
61  }
62
63  BaseAdapter _createAdapter() {
64    if (_detector.hasPointerEvents) {
65      return PointerAdapter(_onPointerData, domRenderer);
66    }
67    if (_detector.hasTouchEvents) {
68      return TouchAdapter(_onPointerData, domRenderer);
69    }
70    if (_detector.hasMouseEvents) {
71      return MouseAdapter(_onPointerData, domRenderer);
72    }
73    return null;
74  }
75
76  void _onPointerData(List<ui.PointerData> data) {
77    final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data);
78    ui.window.onPointerDataPacket(packet);
79  }
80}
81
82class PointerSupportDetector {
83  const PointerSupportDetector();
84
85  bool get hasPointerEvents => js_util.hasProperty(html.window, 'PointerEvent');
86  bool get hasTouchEvents => js_util.hasProperty(html.window, 'TouchEvent');
87  bool get hasMouseEvents => js_util.hasProperty(html.window, 'MouseEvent');
88
89  @override
90  String toString() =>
91      'pointers:$hasPointerEvents, touch:$hasTouchEvents, mouse:$hasMouseEvents';
92}
93
94/// Common functionality that's shared among adapters.
95abstract class BaseAdapter {
96  static final Map<String, html.EventListener> _listeners =
97      <String, html.EventListener>{};
98
99  final DomRenderer domRenderer;
100  PointerDataCallback _callback;
101  Map<int, bool> _isDownMap = <int, bool>{};
102  bool _isButtonDown(int button) {
103    return _isDownMap[button] == true;
104  }
105
106  void _updateButtonDownState(int button, bool value) {
107    _isDownMap[button] = value;
108  }
109
110  BaseAdapter(this._callback, this.domRenderer) {
111    _setup();
112  }
113
114  /// Each subclass is expected to override this method to attach its own event
115  /// listeners and convert events into pointer events.
116  void _setup();
117
118  /// Remove all active event listeners.
119  void clearListeners() {
120    final html.Element glassPane = domRenderer.glassPaneElement;
121    _listeners.forEach((String eventName, html.EventListener listener) {
122      glassPane.removeEventListener(eventName, listener);
123    });
124    _listeners.clear();
125  }
126
127  void _addEventListener(String eventName, html.EventListener handler) {
128    final html.EventListener loggedHandler = (html.Event event) {
129      if (_debugLogPointerEvents) {
130        print(event.type);
131      }
132      // Report the event to semantics. This information is used to debounce
133      // browser gestures. Semantics tells us whether it is safe to forward
134      // the event to the framework.
135      if (EngineSemanticsOwner.instance.receiveGlobalEvent(event)) {
136        handler(event);
137      }
138    };
139    _listeners[eventName] = loggedHandler;
140    domRenderer.glassPaneElement
141        .addEventListener(eventName, loggedHandler, true);
142  }
143}
144
145const int _kPrimaryMouseButton = 0x1;
146const int _kSecondaryMouseButton = 0x2;
147
148int _pointerButtonFromHtmlEvent(html.Event event) {
149  if (event is html.PointerEvent) {
150    final html.PointerEvent pointerEvent = event;
151    return pointerEvent.button == 2
152        ? _kSecondaryMouseButton
153        : _kPrimaryMouseButton;
154  } else if (event is html.MouseEvent) {
155    final html.MouseEvent mouseEvent = event;
156    return mouseEvent.button == 2
157        ? _kSecondaryMouseButton
158        : _kPrimaryMouseButton;
159  }
160  return _kPrimaryMouseButton;
161}
162
163/// Adapter class to be used with browsers that support native pointer events.
164class PointerAdapter extends BaseAdapter {
165  PointerAdapter(PointerDataCallback callback, DomRenderer domRenderer)
166      : super(callback, domRenderer);
167
168  @override
169  void _setup() {
170    _addEventListener('pointerdown', (html.Event event) {
171      final int pointerButton = _pointerButtonFromHtmlEvent(event);
172      if (_isButtonDown(pointerButton)) {
173        // TODO(flutter_web): Remove this temporary fix for right click
174        // on web platform once context guesture is implemented.
175        _callback(_convertEventToPointerData(ui.PointerChange.up, event));
176      }
177      _updateButtonDownState(pointerButton, true);
178      _callback(_convertEventToPointerData(ui.PointerChange.down, event));
179    });
180
181    _addEventListener('pointermove', (html.Event event) {
182      // TODO(flutter_web): During a drag operation pointermove will set
183      // button to -1 as opposed to mouse move which sets it to 2.
184      // This check is currently defaulting to primary button for now.
185      // Change this when context gesture is implemented in flutter framework.
186      if (!_isButtonDown(_pointerButtonFromHtmlEvent(event))) {
187        return;
188      }
189      _callback(_convertEventToPointerData(ui.PointerChange.move, event));
190    });
191
192    _addEventListener('pointerup', (html.Event event) {
193      // The pointer could have been released by a `pointerout` event, in which
194      // case `pointerup` should have no effect.
195      final int pointerButton = _pointerButtonFromHtmlEvent(event);
196      if (!_isButtonDown(pointerButton)) {
197        return;
198      }
199      _updateButtonDownState(pointerButton, false);
200      _callback(_convertEventToPointerData(ui.PointerChange.up, event));
201    });
202
203    // A browser fires cancel event if it concludes the pointer will no longer
204    // be able to generate events (example: device is deactivated)
205    _addEventListener('pointercancel', (html.Event event) {
206      _callback(_convertEventToPointerData(ui.PointerChange.cancel, event));
207    });
208
209    _addWheelEventListener((html.WheelEvent event) {
210      if (_debugLogPointerEvents) {
211        print(event.type);
212      }
213      _callback(_convertWheelEventToPointerData(event));
214      // Prevent default so mouse wheel event doesn't get converted to
215      // a scroll event that semantic nodes would process.
216      event.preventDefault();
217    });
218  }
219
220  List<ui.PointerData> _convertEventToPointerData(
221    ui.PointerChange change,
222    html.PointerEvent evt,
223  ) {
224    final List<html.PointerEvent> allEvents = _expandEvents(evt);
225    final List<ui.PointerData> data = List<ui.PointerData>(allEvents.length);
226    for (int i = 0; i < allEvents.length; i++) {
227      final html.PointerEvent event = allEvents[i];
228      data[i] = ui.PointerData(
229        change: change,
230        timeStamp: _eventTimeStampToDuration(event.timeStamp),
231        kind: _pointerTypeToDeviceKind(event.pointerType),
232        device: event.pointerId,
233        physicalX: event.client.x,
234        physicalY: event.client.y,
235        buttons: event.buttons,
236        pressure: event.pressure,
237        pressureMin: 0.0,
238        pressureMax: 1.0,
239        tilt: _computeHighestTilt(event),
240      );
241    }
242    return data;
243  }
244
245  List<html.PointerEvent> _expandEvents(html.PointerEvent event) {
246    // For browsers that don't support `getCoalescedEvents`, we fallback to
247    // using the original event.
248    if (js_util.hasProperty(event, 'getCoalescedEvents')) {
249      final List<html.PointerEvent> coalescedEvents =
250          event.getCoalescedEvents();
251      // Some events don't perform coalescing, so they return an empty list. In
252      // that case, we also fallback to using the original event.
253      if (coalescedEvents.isNotEmpty) {
254        return coalescedEvents;
255      }
256    }
257    return <html.PointerEvent>[event];
258  }
259
260  ui.PointerDeviceKind _pointerTypeToDeviceKind(String pointerType) {
261    switch (pointerType) {
262      case 'mouse':
263        return ui.PointerDeviceKind.mouse;
264      case 'pen':
265        return ui.PointerDeviceKind.stylus;
266      case 'touch':
267        return ui.PointerDeviceKind.touch;
268      default:
269        return ui.PointerDeviceKind.unknown;
270    }
271  }
272
273  /// Tilt angle is -90 to + 90. Take maximum deflection and convert to radians.
274  double _computeHighestTilt(html.PointerEvent e) =>
275      (e.tiltX.abs() > e.tiltY.abs() ? e.tiltX : e.tiltY).toDouble() /
276      180.0 *
277      math.pi;
278}
279
280/// Adapter to be used with browsers that support touch events.
281class TouchAdapter extends BaseAdapter {
282  TouchAdapter(PointerDataCallback callback, DomRenderer domRenderer)
283      : super(callback, domRenderer);
284
285  @override
286  void _setup() {
287    _addEventListener('touchstart', (html.Event event) {
288      _updateButtonDownState(_kPrimaryMouseButton, true);
289      _callback(_convertEventToPointerData(ui.PointerChange.down, event));
290    });
291
292    _addEventListener('touchmove', (html.Event event) {
293      event.preventDefault(); // Prevents standard overscroll on iOS/Webkit.
294      if (!_isButtonDown(_kPrimaryMouseButton)) {
295        return;
296      }
297      _callback(_convertEventToPointerData(ui.PointerChange.move, event));
298    });
299
300    _addEventListener('touchend', (html.Event event) {
301      _updateButtonDownState(_kPrimaryMouseButton, false);
302      _callback(_convertEventToPointerData(ui.PointerChange.up, event));
303    });
304
305    _addEventListener('touchcancel', (html.Event event) {
306      _callback(_convertEventToPointerData(ui.PointerChange.cancel, event));
307    });
308  }
309
310  List<ui.PointerData> _convertEventToPointerData(
311    ui.PointerChange change,
312    html.TouchEvent event,
313  ) {
314    final html.TouchList touches = event.changedTouches;
315    final List<ui.PointerData> data = List<ui.PointerData>(touches.length);
316    final int len = touches.length;
317    for (int i = 0; i < len; i++) {
318      final html.Touch touch = touches[i];
319      data[i] = ui.PointerData(
320        change: change,
321        timeStamp: _eventTimeStampToDuration(event.timeStamp),
322        kind: ui.PointerDeviceKind.touch,
323        signalKind: ui.PointerSignalKind.none,
324        device: touch.identifier,
325        physicalX: touch.client.x,
326        physicalY: touch.client.y,
327        pressure: 1.0,
328        pressureMin: 0.0,
329        pressureMax: 1.0,
330      );
331    }
332
333    return data;
334  }
335}
336
337/// Intentionally set to -1 so it doesn't conflict with other device IDs.
338const int _mouseDeviceId = -1;
339
340/// Adapter to be used with browsers that support mouse events.
341class MouseAdapter extends BaseAdapter {
342  MouseAdapter(PointerDataCallback callback, DomRenderer domRenderer)
343      : super(callback, domRenderer);
344
345  @override
346  void _setup() {
347    _addEventListener('mousedown', (html.Event event) {
348      final int pointerButton = _pointerButtonFromHtmlEvent(event);
349      if (_isButtonDown(pointerButton)) {
350        // TODO(flutter_web): Remove this temporary fix for right click
351        // on web platform once context guesture is implemented.
352        _callback(_convertEventToPointerData(ui.PointerChange.up, event));
353      }
354      _updateButtonDownState(pointerButton, true);
355      _callback(_convertEventToPointerData(ui.PointerChange.down, event));
356    });
357
358    _addEventListener('mousemove', (html.Event event) {
359      if (!_isButtonDown(_pointerButtonFromHtmlEvent(event))) {
360        return;
361      }
362      _callback(_convertEventToPointerData(ui.PointerChange.move, event));
363    });
364
365    _addEventListener('mouseup', (html.Event event) {
366      _updateButtonDownState(_pointerButtonFromHtmlEvent(event), false);
367      _callback(_convertEventToPointerData(ui.PointerChange.up, event));
368    });
369
370    _addWheelEventListener((html.WheelEvent event) {
371      if (_debugLogPointerEvents) {
372        print(event.type);
373      }
374      _callback(_convertWheelEventToPointerData(event));
375      event.preventDefault();
376    });
377  }
378
379  List<ui.PointerData> _convertEventToPointerData(
380    ui.PointerChange change,
381    html.MouseEvent event,
382  ) {
383    return <ui.PointerData>[
384      ui.PointerData(
385        change: change,
386        timeStamp: _eventTimeStampToDuration(event.timeStamp),
387        kind: ui.PointerDeviceKind.mouse,
388        signalKind: ui.PointerSignalKind.none,
389        device: _mouseDeviceId,
390        physicalX: event.client.x,
391        physicalY: event.client.y,
392        buttons: event.buttons,
393        pressure: 1.0,
394        pressureMin: 0.0,
395        pressureMax: 1.0,
396      )
397    ];
398  }
399}
400
401/// Convert a floating number timestamp (in milliseconds) to a [Duration] by
402/// splitting it into two integer components: milliseconds + microseconds.
403Duration _eventTimeStampToDuration(num milliseconds) {
404  final int ms = milliseconds.toInt();
405  final int micro =
406      ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt();
407  return Duration(milliseconds: ms, microseconds: micro);
408}
409
410bool _isWheelDeviceAdded = false;
411
412List<ui.PointerData> _convertWheelEventToPointerData(
413  html.WheelEvent event,
414) {
415  const int domDeltaPixel = 0x00;
416  const int domDeltaLine = 0x01;
417  const int domDeltaPage = 0x02;
418
419  // Flutter only supports pixel scroll delta. Convert deltaMode values
420  // to pixels.
421  double deltaX = event.deltaX;
422  double deltaY = event.deltaY;
423  switch (event.deltaMode) {
424    case domDeltaLine:
425      deltaX *= 32.0;
426      deltaY *= 32.0;
427      break;
428    case domDeltaPage:
429      deltaX *= ui.window.physicalSize.width;
430      deltaY *= ui.window.physicalSize.height;
431      break;
432    case domDeltaPixel:
433    default:
434      break;
435  }
436
437  final List<ui.PointerData> data = <ui.PointerData>[];
438  // Only send [PointerChange.add] the first time.
439  if (!_isWheelDeviceAdded) {
440    _isWheelDeviceAdded = true;
441    data.add(ui.PointerData(
442      change: ui.PointerChange.add,
443      timeStamp: _eventTimeStampToDuration(event.timeStamp),
444      kind: ui.PointerDeviceKind.mouse,
445      // In order for Flutter to actually add this pointer, we need to set the
446      // signal to none.
447      signalKind: ui.PointerSignalKind.none,
448      device: _mouseDeviceId,
449      physicalX: event.client.x,
450      physicalY: event.client.y,
451      buttons: event.buttons,
452      pressure: 1.0,
453      pressureMin: 0.0,
454      pressureMax: 1.0,
455      scrollDeltaX: deltaX,
456      scrollDeltaY: deltaY,
457    ));
458  }
459  data.add(ui.PointerData(
460    change: ui.PointerChange.hover,
461    timeStamp: _eventTimeStampToDuration(event.timeStamp),
462    kind: ui.PointerDeviceKind.mouse,
463    signalKind: ui.PointerSignalKind.scroll,
464    device: _mouseDeviceId,
465    physicalX: event.client.x,
466    physicalY: event.client.y,
467    buttons: event.buttons,
468    pressure: 1.0,
469    pressureMin: 0.0,
470    pressureMax: 1.0,
471    scrollDeltaX: deltaX,
472    scrollDeltaY: deltaY,
473  ));
474  return data;
475}
476
477void _addWheelEventListener(void listener(html.WheelEvent e)) {
478  final dynamic eventOptions = js_util.newObject();
479  js_util.setProperty(eventOptions, 'passive', false);
480  js_util.callMethod(PointerBinding.instance.domRenderer.glassPaneElement,
481      'addEventListener', <dynamic>[
482    'wheel',
483    js.allowInterop((html.WheelEvent event) => listener(event)),
484    eventOptions
485  ]);
486}
487