Skip to content

Commit 0c2ee84

Browse files
authored
[web] Notify engine of handled PointerScrollEvents. (#145500)
Notifies the engine when `PointerSignalEvents` have been ignored by the framework, through the `ui.PointerData.respond` method. This allows the web to "preventDefault" (or not) on `wheel` events. ## Issues * Fixes (partially): flutter/flutter#139263 ## Tests * Added tests to ensure `respond` is called at the right time, with the right value. ## Demo * https://dit-multiview-scroll.web.app <details> <summary> ## Previous versions </summary> 1. Modified `PointerScrollEvent`, not shippable. 2. Modified when events were handled, instead of the opposite. </details>
1 parent 5efb67b commit 0c2ee84

File tree

7 files changed

+130
-4
lines changed

7 files changed

+130
-4
lines changed

packages/flutter/lib/src/gestures/converter.dart

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ abstract final class PointerEventConverter {
283283
position: position,
284284
scrollDelta: scrollDelta,
285285
embedderId: datum.embedderId,
286+
onRespond: datum.respond,
286287
);
287288
case ui.PointerSignalKind.scrollInertiaCancel:
288289
return PointerScrollInertiaCancelEvent(

packages/flutter/lib/src/gestures/events.dart

+45-4
Original file line numberDiff line numberDiff line change
@@ -1717,7 +1717,7 @@ class _TransformedPointerUpEvent extends _TransformedPointerEvent with _CopyPoin
17171717
/// events in a widget tree.
17181718
/// * [PointerSignalResolver], which provides an opt-in mechanism whereby
17191719
/// participating agents may disambiguate an event's target.
1720-
abstract class PointerSignalEvent extends PointerEvent {
1720+
abstract class PointerSignalEvent extends PointerEvent with _RespondablePointerEvent {
17211721
/// Abstract const constructor. This constructor enables subclasses to provide
17221722
/// const constructors so that they can be used in const expressions.
17231723
const PointerSignalEvent({
@@ -1731,6 +1731,27 @@ abstract class PointerSignalEvent extends PointerEvent {
17311731
});
17321732
}
17331733

1734+
/// A function that implements the [PointerSignalEvent.respond] method.
1735+
typedef RespondPointerEventCallback = void Function({required bool allowPlatformDefault});
1736+
1737+
mixin _RespondablePointerEvent on PointerEvent {
1738+
/// Sends a response to the native embedder for the [PointerSignalEvent].
1739+
///
1740+
/// The parameter [allowPlatformDefault] allows the platform to perform the
1741+
/// default action associated with the native event when it's set to `true`.
1742+
///
1743+
/// This method can be called any number of times, but once `allowPlatformDefault`
1744+
/// is set to `true`, it can't be set to `false` again.
1745+
///
1746+
/// The implementation of this method is configured through the `onRespond`
1747+
/// parameter of the [PointerSignalEvent] constructor.
1748+
///
1749+
/// See also [RespondPointerEventCallback].
1750+
void respond({
1751+
required bool allowPlatformDefault,
1752+
}) {}
1753+
}
1754+
17341755
mixin _CopyPointerScrollEvent on PointerEvent {
17351756
/// The amount to scroll, in logical pixels.
17361757
Offset get scrollDelta;
@@ -1760,6 +1781,7 @@ mixin _CopyPointerScrollEvent on PointerEvent {
17601781
double? tilt,
17611782
bool? synthesized,
17621783
int? embedderId,
1784+
RespondPointerEventCallback? onRespond,
17631785
}) {
17641786
return PointerScrollEvent(
17651787
viewId: viewId ?? this.viewId,
@@ -1769,6 +1791,7 @@ mixin _CopyPointerScrollEvent on PointerEvent {
17691791
position: position ?? this.position,
17701792
scrollDelta: scrollDelta,
17711793
embedderId: embedderId ?? this.embedderId,
1794+
onRespond: onRespond ?? (this as PointerScrollEvent).respond,
17721795
).transformed(transform);
17731796
}
17741797
}
@@ -1794,7 +1817,8 @@ class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescriptio
17941817
super.position,
17951818
this.scrollDelta = Offset.zero,
17961819
super.embedderId,
1797-
});
1820+
RespondPointerEventCallback? onRespond,
1821+
}) : _onRespond = onRespond;
17981822

17991823
@override
18001824
final Offset scrollDelta;
@@ -1812,6 +1836,15 @@ class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescriptio
18121836
super.debugFillProperties(properties);
18131837
properties.add(DiagnosticsProperty<Offset>('scrollDelta', scrollDelta));
18141838
}
1839+
1840+
final RespondPointerEventCallback? _onRespond;
1841+
1842+
@override
1843+
void respond({required bool allowPlatformDefault}) {
1844+
if (_onRespond != null) {
1845+
_onRespond!(allowPlatformDefault: allowPlatformDefault);
1846+
}
1847+
}
18151848
}
18161849

18171850
class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _CopyPointerScrollEvent implements PointerScrollEvent {
@@ -1834,6 +1867,14 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy
18341867
super.debugFillProperties(properties);
18351868
properties.add(DiagnosticsProperty<Offset>('scrollDelta', scrollDelta));
18361869
}
1870+
1871+
@override
1872+
RespondPointerEventCallback? get _onRespond => original._onRespond;
1873+
1874+
@override
1875+
void respond({required bool allowPlatformDefault}) {
1876+
original.respond(allowPlatformDefault: allowPlatformDefault);
1877+
}
18371878
}
18381879

18391880
mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent {
@@ -1905,7 +1946,7 @@ class PointerScrollInertiaCancelEvent extends PointerSignalEvent with _PointerEv
19051946
}
19061947
}
19071948

1908-
class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEvent with _CopyPointerScrollInertiaCancelEvent implements PointerScrollInertiaCancelEvent {
1949+
class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEvent with _CopyPointerScrollInertiaCancelEvent, _RespondablePointerEvent implements PointerScrollInertiaCancelEvent {
19091950
_TransformedPointerScrollInertiaCancelEvent(this.original, this.transform);
19101951

19111952
@override
@@ -1996,7 +2037,7 @@ class PointerScaleEvent extends PointerSignalEvent with _PointerEventDescription
19962037
}
19972038
}
19982039

1999-
class _TransformedPointerScaleEvent extends _TransformedPointerEvent with _CopyPointerScaleEvent implements PointerScaleEvent {
2040+
class _TransformedPointerScaleEvent extends _TransformedPointerEvent with _CopyPointerScaleEvent, _RespondablePointerEvent implements PointerScaleEvent {
20002041
_TransformedPointerScaleEvent(this.original, this.transform);
20012042

20022043
@override

packages/flutter/lib/src/gestures/pointer_signal_resolver.dart

+5
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ class PointerSignalResolver {
9696
void resolve(PointerSignalEvent event) {
9797
if (_firstRegisteredCallback == null) {
9898
assert(_currentEvent == null);
99+
// Nothing in the framework/app wants to handle the `event`. Allow the
100+
// platform to trigger any default native actions.
101+
event.respond(
102+
allowPlatformDefault: true
103+
);
99104
return;
100105
}
101106
assert(_isSameEvent(_currentEvent!, event));

packages/flutter/lib/src/widgets/scrollable.dart

+7
Original file line numberDiff line numberDiff line change
@@ -916,14 +916,21 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
916916
void _receivedPointerSignal(PointerSignalEvent event) {
917917
if (event is PointerScrollEvent && _position != null) {
918918
if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
919+
// The handler won't use the `event`, so allow the platform to trigger
920+
// any default native actions.
921+
event.respond(allowPlatformDefault: true);
919922
return;
920923
}
921924
final double delta = _pointerSignalEventDelta(event);
922925
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
923926
// Only express interest in the event if it would actually result in a scroll.
924927
if (delta != 0.0 && targetScrollOffset != position.pixels) {
925928
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
929+
return;
926930
}
931+
// The `event` won't result in a scroll, so allow the platform to trigger
932+
// any default native actions.
933+
event.respond(allowPlatformDefault: true);
927934
} else if (event is PointerScrollInertiaCancelEvent) {
928935
position.pointerScroll(0);
929936
// Don't use the pointer signal resolver, all hit-tested scrollables should stop.

packages/flutter/test/gestures/pointer_signal_resolver_test.dart

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ void main() {
4242
tester.resolver.resolve(tester.event);
4343
});
4444

45+
test('Resolving with no entries should notify engine of no-op', () {
46+
bool allowedPlatformDefault = false;
47+
final PointerSignalTester tester = PointerSignalTester();
48+
tester.event = PointerScrollEvent(
49+
onRespond: ({required bool allowPlatformDefault}) {
50+
allowedPlatformDefault = allowPlatformDefault;
51+
},
52+
);
53+
tester.resolver.resolve(tester.event);
54+
expect(allowedPlatformDefault, isTrue,
55+
reason: 'Should have called respond with allowPlatformDefault: true');
56+
});
57+
4558
test('First entry should always win', () {
4659
final PointerSignalTester tester = PointerSignalTester();
4760
final TestPointerSignalListener first = tester.addListener();

packages/flutter/test/widgets/scrollable_test.dart

+57
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,63 @@ void main() {
453453
expect(getScrollOffset(tester), 0.0);
454454
});
455455

456+
testWidgets('Engine is notified of ignored pointer signals (no scroll physics)', (WidgetTester tester) async {
457+
await pumpTest(tester, debugDefaultTargetPlatformOverride, scrollable: false);
458+
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
459+
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
460+
// Create a hover event so that |testPointer| has a location when generating the scroll.
461+
testPointer.hover(scrollEventLocation);
462+
463+
bool allowedPlatformDefault = false;
464+
await tester.sendEventToBinding(
465+
testPointer.scroll(
466+
const Offset(0.0, 20.0),
467+
onRespond: ({required bool allowPlatformDefault}) {
468+
allowedPlatformDefault = allowPlatformDefault;
469+
},
470+
));
471+
472+
expect(allowedPlatformDefault, isTrue,
473+
reason: 'Engine should be notified of ignored scroll pointer signals.');
474+
}, variant: TargetPlatformVariant.all());
475+
476+
testWidgets('Engine is notified of rejected scroll events (wrong direction)', (WidgetTester tester) async {
477+
await pumpTest(
478+
tester,
479+
debugDefaultTargetPlatformOverride,
480+
scrollDirection: Axis.horizontal,
481+
);
482+
483+
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
484+
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
485+
// Create a hover event so that |testPointer| has a location when generating the scroll.
486+
testPointer.hover(scrollEventLocation);
487+
488+
// Horizontal input is accepted
489+
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
490+
await tester.sendEventToBinding(
491+
testPointer.scroll(
492+
const Offset(0.0, 10.0),
493+
onRespond: ({required bool allowPlatformDefault}) {
494+
fail('The engine should not be notified when the scroll is accepted.');
495+
},
496+
));
497+
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
498+
await tester.pump();
499+
500+
// Vertical input not accepted
501+
bool allowedPlatformDefault = false;
502+
await tester.sendEventToBinding(
503+
testPointer.scroll(
504+
const Offset(0.0, 20.0),
505+
onRespond: ({required bool allowPlatformDefault}) {
506+
allowedPlatformDefault = allowPlatformDefault;
507+
},
508+
));
509+
expect(allowedPlatformDefault, isTrue,
510+
reason: 'Engine should be notified when scroll is rejected by the scrollable.');
511+
}, variant: TargetPlatformVariant.all());
512+
456513
testWidgets('Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse', (WidgetTester tester) async {
457514
ScrollDirection? lastUserScrollingDirection;
458515

packages/flutter_test/lib/src/test_pointer.dart

+2
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ class TestPointer {
288288
PointerScrollEvent scroll(
289289
Offset scrollDelta, {
290290
Duration timeStamp = Duration.zero,
291+
RespondPointerEventCallback? onRespond,
291292
}) {
292293
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
293294
assert(location != null);
@@ -297,6 +298,7 @@ class TestPointer {
297298
device: _device,
298299
position: location!,
299300
scrollDelta: scrollDelta,
301+
onRespond: onRespond,
300302
);
301303
}
302304

0 commit comments

Comments
 (0)