mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Adds allowPlatformDefault for wheel signals. (flutter/engine#51566)
Adds a function to each 'wheel' DataPacket sent to the framework so it can signal whether to `allowPlatformDefault` or not. The current default is to always `preventDefault` on browser events that get sent to the framework. This PR enables the framework to call a method on the `DataPacket`s to `allowPlatformDefault: true`, if the framework won't handle the Signal (signals are handled synchronously on the framework). This lets the engine "wait" for the framework to decide whether to `preventDefault` on a `wheel` event or not. ## Issues * Needed for: https://github.com/flutter/flutter/issues/139263 ## Tests * Added unit tests for the feature in the engine repo, veryfing whether the event has had its `defaultPrevented` or not. * Manually tested in a demo app (see below) ## Demo * https://dit-multiview-scroll.web.app <details> <summary> ## Previous approaches </summary> 1. Add a `handled` bool property to `PointerDataPacket` that the framework can write to (brittle) 2. Modifications to the `PlatformDispatcher` so the framework can `acknowledgePointerData` with a `PointerDataResponse` (fffffatttt change) 3. `acknowledge` function in `PointerDataPacket` </details> > [!IMPORTANT] > * Related: https://github.com/flutter/flutter/pull/145500 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
parent
8d062af40c
commit
6eacdf2be8
@ -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.
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
/// 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)';
|
||||
|
||||
|
||||
@ -34,6 +34,8 @@ enum PointerSignalKind {
|
||||
unknown
|
||||
}
|
||||
|
||||
typedef PointerDataRespondCallback = void Function({bool allowPlatformDefault});
|
||||
|
||||
class PointerData {
|
||||
const PointerData({
|
||||
this.viewId = 0,
|
||||
@ -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;
|
||||
@ -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)';
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
|
||||
|
||||
@ -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]!;
|
||||
@ -154,6 +155,7 @@ class PointerDataConverter {
|
||||
scrollDeltaX: scrollDeltaX,
|
||||
scrollDeltaY: scrollDeltaY,
|
||||
scale: scale,
|
||||
onRespond: onRespond,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
@ -796,6 +799,7 @@ class PointerDataConverter {
|
||||
scrollDeltaX: scrollDeltaX,
|
||||
scrollDeltaY: scrollDeltaY,
|
||||
scale: scale,
|
||||
onRespond: onRespond,
|
||||
)
|
||||
);
|
||||
case ui.PointerSignalKind.none:
|
||||
|
||||
@ -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',
|
||||
() {
|
||||
@ -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) {
|
||||
|
||||
@ -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.');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user