Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion lib/ui/pointer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ enum PointerSignalKind {
unknown
}

/// A function that implements the [PointerData.respond] method.
typedef PointerDataRespondCallback = void Function({bool allowPlatformDefault});

/// Information about the state of a pointer.
class PointerData {
/// Creates an object that represents the state of a pointer.
Expand Down Expand Up @@ -177,7 +180,8 @@ class PointerData {
this.panDeltaY = 0.0,
this.scale = 0.0,
this.rotation = 0.0,
});
PointerDataRespondCallback? onRespond,
}) : _onRespond = onRespond;

/// The ID of the [FlutterView] this [PointerEvent] originated from.
final int viewId;
Expand Down Expand Up @@ -380,6 +384,36 @@ class PointerData {
/// The current angle of the pan/zoom in radians, with 0.0 as the initial angle.
final double rotation;

// An optional function that allows the framework to respond to the event
// that triggered this PointerData instance.
final PointerDataRespondCallback? _onRespond;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can inline a function type here without creating a one-off typedef. Is this a guideline or something?

Copy link
Member Author

@ditman ditman May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can inline a function type here without creating a one-off typedef. Is this a guideline or something?

@kevmoo I went back and forth on this (I originally had this as an inline function type). The Style guide for Flutter repo that @goderbauer linked says this:

Prefer using typedefs to declare callbacks. Typedefs benefit from having documentation on the type itself and make it easier to read and find common callsites for the signature.

I don't have strong feelings about it (but when we were bikeshedding in parameter name, it was a chore to rename it in several, unconnected parts, and that's when I ended up adding the typedef... it's also nice to be able to add some dartdoc to the typedef).


/// Method that the framework/app can call to respond to the native event
/// that triggered this [PointerData].
///
/// The parameter [allowPlatformDefault] allows the platform to perform the
/// default action associated with the native event when it's set to `true`.
///
/// This method can be called any number of times, but once `allowPlatformDefault`
/// is set to `true`, it can't be set to `false` again.
///
/// If `allowPlatformDefault` is never set to `true`, the Flutter engine will
/// consume the event, so it won't be seen by the platform. In the web, this
/// means that `preventDefault` will be called in the DOM event that triggered
/// the `PointerData`. See [Event: preventDefault() method in MDN][EpDmiMDN].
///
/// The implementation of this method is configured through the `onRespond`
/// parameter of the [PointerData] constructor.
///
/// See also [PointerDataRespondCallback].
///
/// [EpDmiMDN]: https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
void respond({required bool allowPlatformDefault}) {
if (_onRespond != null) {
_onRespond(allowPlatformDefault: allowPlatformDefault);
}
}

@override
String toString() => 'PointerData(viewId: $viewId, x: $physicalX, y: $physicalY)';

Expand Down
12 changes: 11 additions & 1 deletion lib/web_ui/lib/pointer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ enum PointerSignalKind {
unknown
}

typedef PointerDataRespondCallback = void Function({bool allowPlatformDefault});

class PointerData {
const PointerData({
this.viewId = 0,
Expand Down Expand Up @@ -72,7 +74,8 @@ class PointerData {
this.panDeltaY = 0.0,
this.scale = 0.0,
this.rotation = 0.0,
});
PointerDataRespondCallback? onRespond,
}) : _onRespond = onRespond;
final int viewId;
final int embedderId;
final Duration timeStamp;
Expand Down Expand Up @@ -109,6 +112,13 @@ class PointerData {
final double panDeltaY;
final double scale;
final double rotation;
final PointerDataRespondCallback? _onRespond;

void respond({required bool allowPlatformDefault}) {
if (_onRespond != null) {
_onRespond(allowPlatformDefault: allowPlatformDefault);
}
}

@override
String toString() => 'PointerData(viewId: $viewId, x: $physicalX, y: $physicalY)';
Expand Down
25 changes: 17 additions & 8 deletions lib/web_ui/lib/src/engine/pointer_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ abstract class _BaseAdapter {
final List<_Listener> _listeners = <_Listener>[];
DomWheelEvent? _lastWheelEvent;
bool _lastWheelEventWasTrackpad = false;
bool _lastWheelEventAllowedDefault = false;

DomEventTarget get _viewTarget => _view.dom.rootElement;
DomEventTarget get _globalTarget => _view.embeddingStrategy.globalEventTarget;
Expand Down Expand Up @@ -706,6 +707,10 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
pressureMax: 1.0,
scrollDeltaX: deltaX,
scrollDeltaY: deltaY,
onRespond: ({bool allowPlatformDefault = false}) {
// Once `allowPlatformDefault` is `true`, never go back to `false`!
_lastWheelEventAllowedDefault |= allowPlatformDefault;
},
);
}
_lastWheelEvent = event;
Expand All @@ -722,17 +727,21 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
));
}

void _handleWheelEvent(DomEvent e) {
assert(domInstanceOfString(e, 'WheelEvent'));
final DomWheelEvent event = e as DomWheelEvent;
void _handleWheelEvent(DomEvent event) {
assert(domInstanceOfString(event, 'WheelEvent'));
if (_debugLogPointerEvents) {
print(event.type);
}
_callback(e, _convertWheelEventToPointerData(event));
// Prevent default so mouse wheel event doesn't get converted to
// a scroll event that semantic nodes would process.
//
event.preventDefault();
_lastWheelEventAllowedDefault = false;
// [ui.PointerData] can set the `_lastWheelEventAllowedDefault` variable
// to true, when the framework says so. See the implementation of `respond`
// when creating the PointerData object above.
_callback(event, _convertWheelEventToPointerData(event as DomWheelEvent));
// This works because the `_callback` is handled synchronously in the
// framework, so it's able to modify `_lastWheelEventAllowedDefault`.
if (!_lastWheelEventAllowedDefault) {
event.preventDefault();
}
}

/// For browsers that report delta line instead of pixels such as FireFox
Expand Down
4 changes: 4 additions & 0 deletions lib/web_ui/lib/src/engine/pointer_converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class PointerDataConverter {
required double scrollDeltaX,
required double scrollDeltaY,
required double scale,
ui.PointerDataRespondCallback? onRespond,
}) {
assert(globalPointerState.pointers.containsKey(device));
final _PointerDeviceState state = globalPointerState.pointers[device]!;
Expand Down Expand Up @@ -154,6 +155,7 @@ class PointerDataConverter {
scrollDeltaX: scrollDeltaX,
scrollDeltaY: scrollDeltaY,
scale: scale,
onRespond: onRespond,
);
}

Expand Down Expand Up @@ -263,6 +265,7 @@ class PointerDataConverter {
double scrollDeltaX = 0.0,
double scrollDeltaY = 0.0,
double scale = 1.0,
ui.PointerDataRespondCallback? onRespond,
}) {
if (_debugLogPointerConverter) {
print('>> view=$viewId device=$device change=$change buttons=$buttons');
Expand Down Expand Up @@ -796,6 +799,7 @@ class PointerDataConverter {
scrollDeltaX: scrollDeltaX,
scrollDeltaY: scrollDeltaY,
scale: scale,
onRespond: onRespond,
)
);
case ui.PointerSignalKind.none:
Expand Down
71 changes: 71 additions & 0 deletions lib/web_ui/test/engine/pointer_binding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,74 @@ void testMain() {
},
);

test('wheel event - preventDefault called', () {
// Synthesize a 'wheel' event.
final DomEvent event = _PointerEventContext().wheel(
buttons: 0,
clientX: 10,
clientY: 10,
deltaX: 10,
deltaY: 0,
);
rootElement.dispatchEvent(event);
// Check that the engine called `preventDefault` on the event.
expect(event.defaultPrevented, isTrue);
});

test('wheel event - framework can stop preventDefault (allowPlatformDefault)', () {
// The framework calls `data.respond(allowPlatformDefault: true)`
ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) {
packet.data.where(
(ui.PointerData datum) => datum.signalKind == ui.PointerSignalKind.scroll
).forEach(
(ui.PointerData datum) {
datum.respond(allowPlatformDefault: true);
}
);
};

// Synthesize a 'wheel' event.
final DomEvent event = _PointerEventContext().wheel(
buttons: 0,
clientX: 10,
clientY: 10,
deltaX: 10,
deltaY: 0,
);
rootElement.dispatchEvent(event);

// Check that the engine did NOT call `preventDefault` on the event.
expect(event.defaultPrevented, isFalse);
});

test('wheel event - once allowPlatformDefault is set to true, it cannot be rolled back', () {
// The framework calls `data.respond(allowPlatformDefault: true)`
ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) {
packet.data.where(
(ui.PointerData datum) => datum.signalKind == ui.PointerSignalKind.scroll
).forEach(
(ui.PointerData datum) {
datum.respond(allowPlatformDefault: false);
datum.respond(allowPlatformDefault: true);
datum.respond(allowPlatformDefault: false);
}
);
};

// Synthesize a 'wheel' event.
final DomEvent event = _PointerEventContext().wheel(
buttons: 0,
clientX: 10,
clientY: 10,
deltaX: 10,
deltaY: 0,
);
rootElement.dispatchEvent(event);

// Check that the engine did NOT call `preventDefault` on the event.
expect(event.defaultPrevented, isFalse);
});

test(
'does synthesize add or hover or move for scroll',
() {
Expand Down Expand Up @@ -3114,6 +3182,9 @@ mixin _ButtonedEventMixin on _BasicEventContext {
if (wheelDeltaX != null) 'wheelDeltaX': wheelDeltaX,
if (wheelDeltaY != null) 'wheelDeltaY': wheelDeltaY,
'ctrlKey': ctrlKey,
'cancelable': true,
'bubbles': true,
'composed': true,
});
// timeStamp can't be set in the constructor, need to override the getter.
if (timeStamp != null) {
Expand Down
22 changes: 16 additions & 6 deletions web_sdk/test/api_conform_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,22 @@ void main() {
i < uiTypeDef.functionType!.parameters.parameters.length &&
i < webTypeDef.functionType!.parameters.parameters.length;
i++) {
final SimpleFormalParameter uiParam =
(uiTypeDef.type as GenericFunctionType).parameters.parameters[i]
as SimpleFormalParameter;
final SimpleFormalParameter webParam =
(webTypeDef.type as GenericFunctionType).parameters.parameters[i]
as SimpleFormalParameter;
final FormalParameter uiFormalParam =
(uiTypeDef.type as GenericFunctionType).parameters.parameters[i];
final FormalParameter webFormalParam =
(webTypeDef.type as GenericFunctionType).parameters.parameters[i];

if (uiFormalParam.runtimeType != webFormalParam.runtimeType) {
failed = true;
print('Warning: lib/ui/ui.dart $typeDefName parameter $i '
'${uiFormalParam.name!.lexeme}} is of type ${uiFormalParam.runtimeType}, but of ${webFormalParam.runtimeType} in lib/web_ui/ui.dart.');
}

// This is not entirely true and can break, but this way we can support both positional and named params
// (The assumption that the parameter of a DefaultFormalParameter is a SimpleFormalParameter is a stretch)
final SimpleFormalParameter uiParam = ((uiFormalParam is DefaultFormalParameter) ? uiFormalParam.parameter : uiFormalParam) as SimpleFormalParameter;
final SimpleFormalParameter webParam = ((webFormalParam is DefaultFormalParameter) ? webFormalParam.parameter : uiFormalParam) as SimpleFormalParameter;

if (webParam.name == null) {
failed = true;
print('Warning: lib/web_ui/ui.dart $typeDefName parameter $i should have name.');
Expand Down