blob: 39280b37515f641aa42f960b684707f55bb50feb [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:fidl_fuchsia_ui_input/fidl_async.dart';
import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:flutter/scheduler.dart';
import 'pointer_data_resampler.dart';
/// The default sampling offset.
///
/// Sampling offset is relative to presentation time. If we produce frames
/// 16.667 ms before presentation and input rate is ~60hz, worst case latency
/// is 33.334 ms. This however assumes zero latency from the input driver.
/// 4.666 ms margin is added for this.
const _defaultSamplingOffset = Duration(milliseconds: -38);
/// Listens for pointer events through the updated API and injects them into Flutter input pipeline.
class PointerEventsListener2 extends PointerCaptureListener {
// Holds the fidl binding to receive pointer events.
final PointerCaptureListenerBinding _pointerCaptureListenerBinding =
PointerCaptureListenerBinding();
PointerEventsListener _pointerEventsListener;
PointerEventsListener2({
SchedulerBinding scheduler,
ui.PointerDataPacketCallback callback,
Duration samplingOffset,
}) {
_pointerEventsListener = PointerEventsListener(
scheduler: scheduler,
callback: callback,
samplingOffset: samplingOffset);
}
/// Starts listening to pointer events. Also overrides the original
/// [ui.window.onPointerDataPacket] callback to a NOP since we
/// inject the pointer events received from the [Scenic] service.
void listen(PointerCaptureListenerRegistryProxy registry, ViewRef viewRef) {
_pointerEventsListener.setCallbackState();
registry.registerListener(
_pointerCaptureListenerBinding.wrap(this), viewRef);
}
/// |PointerCaptureListener|.
@override
Future<void> onPointerEvent(PointerEvent event) async {
await _pointerEventsListener.onPointerEvent(event);
}
void stop() {
_pointerEventsListener.stop();
_pointerCaptureListenerBinding.close();
}
}
/// [DEPRECATED] Use PointerEventsListener2 instead.
/// Listens for pointer events and injects them into Flutter input pipeline.
class PointerEventsListener extends PointerCaptureListenerHack {
// Holds the fidl binding to receive pointer events.
final PointerCaptureListenerHackBinding _pointerCaptureListenerBinding =
PointerCaptureListenerHackBinding();
// Holds the last [PointerEvent] mapped to its pointer id. This is used to
// determine the correct [PointerDataPacket] to generate at boundary condition
// of the screen rect.
final _lastPointerEvent = <int, PointerEvent>{};
// Flag to remember that a down event was seen before a move event.
// TODO(sanjayc): Should really convert to a FSM for PointerEvent.
final _downEvent = <int, bool>{};
// Scheduler used for frame callbacks.
var _scheduler;
// [onPointerDataCallback] used to dispatch pointer data callbacks.
var _callback;
// Offset used for resampling.
final _samplingOffset;
// Flag to track if a frame callback has been scheduled.
var _frameCallbackScheduled = false;
// Current sample time for resampling.
var _sampleTime = Duration();
// Resamplers used to filter incoming touch events.
final _resamplers = <int, PointerDataResampler>{};
PointerEventsListener({
SchedulerBinding scheduler,
ui.PointerDataPacketCallback callback,
Duration samplingOffset,
}) : _scheduler = scheduler,
_samplingOffset = samplingOffset ?? _defaultSamplingOffset {
_callback = callback;
}
/// Starts listening to pointer events. Also overrides the original
/// [ui.window.onPointerDataPacket] callback to a NOP since we
/// inject the pointer events received from the [Presentation] service.
void listen(PresentationProxy presentation) {
setCallbackState();
presentation
.capturePointerEventsHack(_pointerCaptureListenerBinding.wrap(this));
}
void setCallbackState() {
_scheduler ??= SchedulerBinding.instance;
_callback = ui.window.onPointerDataPacket;
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {};
}
/// Stops listening to pointer events. Also restores the
/// [ui.window.onPointerDataPacket] callback.
void stop() {
if (_callback != null) {
_cleanupPointerEvents();
_pointerCaptureListenerBinding.close();
// Restore the original pointer events callback on the window.
ui.window.onPointerDataPacket = _callback;
_callback = null;
_lastPointerEvent.clear();
_downEvent.clear();
}
}
void _cleanupPointerEvents() {
for (final lastEvent in _lastPointerEvent.values.toList()) {
if (lastEvent.phase != PointerEventPhase.remove &&
lastEvent.type != PointerEventType.mouse) {
onPointerEvent(_clone(lastEvent, PointerEventPhase.remove, lastEvent.x,
lastEvent.y, lastEvent.eventTime));
}
}
}
PointerEvent _clone(PointerEvent event, PointerEventPhase phase, double x,
double y, int eventTime) {
return PointerEvent(
buttons: event.buttons,
deviceId: event.deviceId,
eventTime: eventTime,
phase: phase,
pointerId: event.pointerId,
radiusMajor: event.radiusMajor,
radiusMinor: event.radiusMinor,
type: event.type,
x: x,
y: y);
}
/// |PointerCaptureListener|.
@override
Future<void> onPointerEvent(PointerEvent event) async {
_onPointerEvent(event);
}
void _onPointerEvent(PointerEvent event) {
if (_callback == null) {
return;
}
final eventArguments = <String, int>{
'eventTimeUs': event.eventTime ~/ 1000,
};
Timeline.timeSync('PointerEventsListener.onPointerEvent', () {
if (_kindFromPointerEvent(event) != ui.PointerDeviceKind.touch) {
final packet = _getPacket(event);
if (packet != null) {
_callback(ui.PointerDataPacket(data: [packet]));
}
return;
}
final frameTime =
_scheduler.currentSystemFrameTimeStamp.inMicroseconds * 1000;
// Sanity check event time by clamping to frameTime.
final eventTime =
event.eventTime < frameTime ? event.eventTime : frameTime;
final packet =
_getPacket(_clone(event, event.phase, event.x, event.y, eventTime));
if (packet != null) {
var resampler = _resamplers.putIfAbsent(
packet.device, () => PointerDataResampler());
resampler.addData(packet);
_dispatchEvents();
}
}, arguments: eventArguments);
}
void _dispatchEvents() {
for (var resampler in _resamplers.values) {
final packets = resampler.sample(_sampleTime);
if (packets.isNotEmpty) {
Timeline.timeSync('PointerEventsListener.dispatchPackets', () {
_callback(ui.PointerDataPacket(data: packets));
});
}
// Schedule frame callback if another call to `sample` is needed.
// We need the frame callback to determine sample time. This
// however makes us produce frames whenever touch points are
// present. Probably OK as we'll likely receive an update to
// the touch point location each frame that will result in us
// actually having to produce a frame.
if (resampler.hasPendingData() || _downEvent.containsValue(true)) {
_scheduleFrameCallback();
}
}
}
void _scheduleFrameCallback() {
if (_frameCallbackScheduled) {
return;
}
Timeline.timeSync('PointerEventsListener.scheduleFrameCallback', () {
_frameCallbackScheduled = true;
_scheduler.scheduleFrameCallback((_) {
Timeline.timeSync('PointerEventsListener.onFrameCallback', () {
_frameCallbackScheduled = false;
_sampleTime =
_scheduler.currentSystemFrameTimeStamp + _samplingOffset;
_dispatchEvents();
});
});
});
}
ui.PointerChange _changeFromPointerEvent(PointerEvent event) {
final lastEvent = _lastPointerEvent[event.pointerId] ?? event;
switch (event.phase) {
case PointerEventPhase.add:
return ui.PointerChange.add;
case PointerEventPhase.hover:
return ui.PointerChange.hover;
case PointerEventPhase.down:
_downEvent[event.pointerId] = true;
return ui.PointerChange.down;
case PointerEventPhase.move:
// If move is the first event, convert to `add` event. Otherwise,
// flutter pointer state machine throws an exception.
if (event.type != PointerEventType.mouse &&
_lastPointerEvent[event.pointerId] == null) {
return ui.PointerChange.add;
}
// If move event was seen before down event, convert to `down` event.
if (event.type != PointerEventType.mouse &&
_downEvent[event.pointerId] != true) {
_downEvent[event.pointerId] = true;
return ui.PointerChange.down;
}
// For mouse, return a hover event if no buttons were pressed.
if (event.type == PointerEventType.mouse && event.buttons == 0) {
return ui.PointerChange.hover;
}
// Check if this is a boundary condition and convert to up/down event.
if (lastEvent?.phase == PointerEventPhase.move) {
if (_outside(lastEvent) && _inside(event)) {
return ui.PointerChange.down;
}
if (_inside(lastEvent) && _outside(event)) {
return ui.PointerChange.cancel;
}
}
return ui.PointerChange.move;
case PointerEventPhase.up:
_downEvent[event.pointerId] = false;
return ui.PointerChange.up;
case PointerEventPhase.remove:
return ui.PointerChange.remove;
case PointerEventPhase.cancel:
default:
return ui.PointerChange.cancel;
}
}
ui.PointerDeviceKind _kindFromPointerEvent(PointerEvent event) {
switch (event.type) {
case PointerEventType.mouse:
return ui.PointerDeviceKind.mouse;
case PointerEventType.stylus:
return ui.PointerDeviceKind.stylus;
case PointerEventType.invertedStylus:
return ui.PointerDeviceKind.invertedStylus;
case PointerEventType.touch:
default:
return ui.PointerDeviceKind.touch;
}
}
ui.PointerData _getPacket(PointerEvent event) {
final lastEvent = _lastPointerEvent[event.pointerId];
// Only allow add and remove pointer events from outside the window bounds.
// For other events, we drop them if the last two were outside the window
// bounds. If any of current event or last event lies inside the window,
// we generate a synthetic down or up event.
if (event.phase != PointerEventPhase.add &&
event.phase != PointerEventPhase.remove &&
_outside(event) &&
_outside(lastEvent ?? event)) {
_lastPointerEvent[event.pointerId] = event;
return null;
}
// Calculate the offset between two events.
final delta = lastEvent != null
? ui.Offset(event.x - lastEvent.x, event.y - lastEvent.y)
: ui.Offset(0, 0);
// Convert from PointerEvent to PointerData.
final data = ui.PointerData(
buttons: event.buttons,
device: event.pointerId,
timeStamp: Duration(microseconds: event.eventTime ~/ 1000),
change: _changeFromPointerEvent(event),
kind: _kindFromPointerEvent(event),
physicalDeltaX: delta.dx * ui.window.devicePixelRatio,
physicalDeltaY: delta.dy * ui.window.devicePixelRatio,
physicalX: event.x * ui.window.devicePixelRatio,
physicalY: event.y * ui.window.devicePixelRatio,
pointerIdentifier: event.pointerId,
synthesized: false,
);
// Remember this event for checking boundary condition on the next event.
_lastPointerEvent[event.pointerId] = event;
return data;
}
bool _inside(PointerEvent event) {
return event != null &&
event.x * ui.window.devicePixelRatio >= 0 &&
event.x * ui.window.devicePixelRatio < ui.window.physicalSize.width &&
event.y * ui.window.devicePixelRatio >= 0 &&
event.y * ui.window.devicePixelRatio < ui.window.physicalSize.height;
}
bool _outside(PointerEvent event) => !_inside(event);
}