Unverified Commit 884158a6 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Reland 3: Make LiveTestWidgetsFlutterBinding work with setSurfaceSize and live tests (#87239)

* Revert "Revert "Reland 2: Make LiveTestWidgetsFlutterBinding work with setSurfaceSize and live tests (#86912)" (#87233)"

This reverts commit 5b80d32a.

* Record tap positions
parent a76212f8
...@@ -11,11 +11,12 @@ void main() { ...@@ -11,11 +11,12 @@ void main() {
* because [matchesGoldenFile] does not use Skia Gold in its native package. * because [matchesGoldenFile] does not use Skia Gold in its native package.
*/ */
LiveTestWidgetsFlutterBinding(); LiveTestWidgetsFlutterBinding().framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps;
testWidgets('Should show event indicator for pointer events', (WidgetTester tester) async { testWidgets('Should show event indicator for pointer events', (WidgetTester tester) async {
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true); final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true);
final Widget target = Container( final List<Offset> taps = <Offset>[];
Widget target({bool recording = true}) => Container(
padding: const EdgeInsets.fromLTRB(20, 10, 25, 20), padding: const EdgeInsets.fromLTRB(20, 10, 25, 20),
child: animationSheet.record( child: animationSheet.record(
MaterialApp( MaterialApp(
...@@ -25,37 +26,105 @@ void main() { ...@@ -25,37 +26,105 @@ void main() {
border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)), border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
), ),
child: Center( child: Center(
child: GestureDetector( child: Container(
onTap: () {}, width: 40,
child: const Text('Test'), height: 40,
color: Colors.black,
child: GestureDetector(
onTapDown: (TapDownDetails details) {
taps.add(details.globalPosition);
},
),
), ),
), ),
), ),
), ),
recording: recording,
), ),
); );
await tester.pumpWidget(target); await tester.pumpWidget(target(recording: false));
await tester.pumpFrames(target, const Duration(milliseconds: 50)); await tester.pumpFrames(target(), const Duration(milliseconds: 50));
final TestGesture gesture1 = await tester.createGesture(); final TestGesture gesture1 = await tester.createGesture(pointer: 1);
await gesture1.down(tester.getCenter(find.byType(Text)) + const Offset(10, 10)); await gesture1.down(tester.getCenter(find.byType(GestureDetector)) + const Offset(10, 10));
expect(taps, equals(const <Offset>[Offset(130, 120)]));
taps.clear();
await tester.pumpFrames(target, const Duration(milliseconds: 100)); await tester.pumpFrames(target(), const Duration(milliseconds: 100));
final TestGesture gesture2 = await tester.createGesture(); final TestGesture gesture2 = await tester.createGesture(pointer: 2);
await gesture2.down(tester.getTopLeft(find.byType(Text)) + const Offset(30, -10)); await gesture2.down(tester.getTopLeft(find.byType(GestureDetector)) + const Offset(30, -10));
await gesture1.moveBy(const Offset(50, 50)); await gesture1.moveBy(const Offset(50, 50));
await tester.pumpFrames(target, const Duration(milliseconds: 100)); await tester.pumpFrames(target(), const Duration(milliseconds: 100));
await gesture1.up(); await gesture1.up();
await gesture2.up(); await gesture2.up();
await tester.pumpFrames(target, const Duration(milliseconds: 50)); await tester.pumpFrames(target(), const Duration(milliseconds: 50));
expect(taps, isEmpty);
await expectLater( await expectLater(
animationSheet.collate(6), animationSheet.collate(6),
matchesGoldenFile('LiveBinding.press.animation.png'), matchesGoldenFile('LiveBinding.press.animation.png'),
); );
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
testWidgets('Should show event indicator for pointer events with setSurfaceSize', (WidgetTester tester) async {
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true);
final List<Offset> taps = <Offset>[];
Widget target({bool recording = true}) => Container(
padding: const EdgeInsets.fromLTRB(20, 10, 25, 20),
child: animationSheet.record(
MaterialApp(
home: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 128, 128, 128),
border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
),
child: Center(
child: Container(
width: 40,
height: 40,
color: Colors.black,
child: GestureDetector(
onTapDown: (TapDownDetails details) {
taps.add(details.globalPosition);
},
),
),
),
),
),
recording: recording,
),
);
await tester.binding.setSurfaceSize(const Size(300, 300));
await tester.pumpWidget(target(recording: false));
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
final TestGesture gesture1 = await tester.createGesture(pointer: 1);
await gesture1.down(tester.getCenter(find.byType(GestureDetector)) + const Offset(10, 10));
expect(taps, equals(const <Offset>[Offset(130, 120)]));
taps.clear();
await tester.pumpFrames(target(), const Duration(milliseconds: 100));
final TestGesture gesture2 = await tester.createGesture(pointer: 2);
await gesture2.down(tester.getTopLeft(find.byType(GestureDetector)) + const Offset(30, -10));
await gesture1.moveBy(const Offset(50, 50));
await tester.pumpFrames(target(), const Duration(milliseconds: 100));
await gesture1.up();
await gesture2.up();
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
expect(taps, isEmpty);
await expectLater(
animationSheet.collate(6),
matchesGoldenFile('LiveBinding.press.animation.2.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
} }
...@@ -134,11 +134,11 @@ class AnimationSheetBuilder { ...@@ -134,11 +134,11 @@ class AnimationSheetBuilder {
/// The returned widget wraps `child` in a box with a fixed size specified by /// The returned widget wraps `child` in a box with a fixed size specified by
/// [frameSize]. The `key` is also applied to the returned widget. /// [frameSize]. The `key` is also applied to the returned widget.
/// ///
/// The `recording` defaults to true, which means the painted result of each /// The frame is only recorded if the `recording` argument is true, or during
/// frame will be stored and later available for [display]. If `recording` is /// a procedure that is wrapped within [recording]. In either case, the
/// false, then frames are not recorded. This is useful during the setup phase /// painted result of each frame will be stored and later available for
/// that shouldn't be recorded; if the target widget isn't wrapped in [record] /// [display]. If neither condition is met, the frames are not recorded, which
/// during the setup phase, the states will be lost when it starts recording. /// is useful during setup phases.
/// ///
/// The `child` must not be null. /// The `child` must not be null.
/// ///
...@@ -274,7 +274,10 @@ class AnimationSheetBuilder { ...@@ -274,7 +274,10 @@ class AnimationSheetBuilder {
/// ///
/// An example of using this method can be found at [AnimationSheetBuilder]. /// An example of using this method can be found at [AnimationSheetBuilder].
Future<ui.Image> collate(int cellsPerRow) async { Future<ui.Image> collate(int cellsPerRow) async {
return _collateFrames(await _frames, frameSize, cellsPerRow); final List<ui.Image> frames = await _frames;
assert(frames.isNotEmpty,
'No frames are collected. Have you forgot to set `recording` to true?');
return _collateFrames(frames, frameSize, cellsPerRow);
} }
/// Returns the smallest size that can contain all recorded frames. /// Returns the smallest size that can contain all recorded frames.
......
...@@ -119,6 +119,20 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding { ...@@ -119,6 +119,20 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding {
/// that actually needs to make a network call should provide its own /// that actually needs to make a network call should provide its own
/// `HttpClient` to the code making the call, so that it can appropriately mock /// `HttpClient` to the code making the call, so that it can appropriately mock
/// or fake responses. /// or fake responses.
///
/// ### Coordinate spaces
///
/// [TestWidgetsFlutterBinding] might be run on devices of different screen
/// sizes, while the testing widget is still told the same size to ensure
/// consistent results. Consequently, code that deals with positions (such as
/// pointer events or painting) must distinguish between two coordinate spaces:
///
/// * The _local coordinate space_ is the one used by the testing widget
/// (typically an 800 by 600 window, but can be altered by [setSurfaceSize]).
/// * The _global coordinate space_ is the one used by the device.
///
/// Positions can be transformed between coordinate spaces with [localToGlobal]
/// and [globalToLocal].
abstract class TestWidgetsFlutterBinding extends BindingBase abstract class TestWidgetsFlutterBinding extends BindingBase
with SchedulerBinding, with SchedulerBinding,
ServicesBinding, ServicesBinding,
...@@ -447,14 +461,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -447,14 +461,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
}); });
} }
/// Convert the given point from the global coordinate system (as used by /// Convert the given point from the global coordinate space to the local
/// pointer events from the device) to the coordinate system used by the /// one.
/// tests (an 800 by 600 window). ///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset globalToLocal(Offset point) => point; Offset globalToLocal(Offset point) => point;
/// Convert the given point from the coordinate system used by the tests (an /// Convert the given point from the local coordinate space to the global
/// 800 by 600 window) to the global coordinate system (as used by pointer /// one.
/// events from the device). ///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset localToGlobal(Offset point) => point; Offset localToGlobal(Offset point) => point;
/// The source of the current pointer event. /// The source of the current pointer event.
...@@ -462,15 +478,34 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -462,15 +478,34 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// The [pointerEventSource] is set as the `source` parameter of /// The [pointerEventSource] is set as the `source` parameter of
/// [handlePointerEventForSource] and can be used in the immediate enclosing /// [handlePointerEventForSource] and can be used in the immediate enclosing
/// [dispatchEvent]. /// [dispatchEvent].
///
/// When [handlePointerEvent] is called directly, [pointerEventSource]
/// is [TestBindingEventSource.device].
TestBindingEventSource get pointerEventSource => _pointerEventSource; TestBindingEventSource get pointerEventSource => _pointerEventSource;
TestBindingEventSource _pointerEventSource = TestBindingEventSource.device; TestBindingEventSource _pointerEventSource = TestBindingEventSource.device;
/// Dispatch an event to the targets found by a hit test on its position, /// Dispatch an event to the targets found by a hit test on its position,
/// and remember its source as [pointerEventSource]. /// and remember its source as [pointerEventSource].
/// ///
/// This method sets [pointerEventSource] to `source`, runs /// This method sets [pointerEventSource] to `source`, forwards the call to
/// [handlePointerEvent], then resets [pointerEventSource] to the previous /// [handlePointerEvent], then resets [pointerEventSource] to the previous
/// value. /// value.
///
/// If `source` is [TestBindingEventSource.device], then the `event` is based
/// in the global coordinate space (for definitions for coordinate spaces,
/// see [TestWidgetsFlutterBinding]) and the event is likely triggered by the
/// user physically interacting with the screen during a live test on a real
/// device (see [LiveTestWidgetsFlutterBinding]).
///
/// If `source` is [TestBindingEventSource.test], then the `event` is based
/// in the local coordinate space and the event is likely triggered by
/// programatically simulated pointer events, such as:
///
/// * [WidgetController.tap] and alike methods, as well as directly using
/// [TestGesture]. They are usually used in
/// [AutomatedTestWidgetsFlutterBinding] but sometimes in live tests too.
/// * [WidgetController.timedDrag] and alike methods. They are usually used
/// in macrobenchmarks.
void handlePointerEventForSource( void handlePointerEventForSource(
PointerEvent event, { PointerEvent event, {
TestBindingEventSource source = TestBindingEventSource.device, TestBindingEventSource source = TestBindingEventSource.device,
...@@ -482,7 +517,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -482,7 +517,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// to the previous value. /// to the previous value.
@protected @protected
void withPointerEventSource(TestBindingEventSource source, VoidCallback task) { void withPointerEventSource(TestBindingEventSource source, VoidCallback task) {
final TestBindingEventSource previousSource = source; final TestBindingEventSource previousSource = _pointerEventSource;
_pointerEventSource = source; _pointerEventSource = source;
try { try {
task(); task();
...@@ -1504,11 +1539,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1504,11 +1539,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
/// Events dispatched by [TestGesture] are not affected by this. /// Events dispatched by [TestGesture] are not affected by this.
HitTestDispatcher? deviceEventDispatcher; HitTestDispatcher? deviceEventDispatcher;
/// Dispatch an event to the targets found by a hit test on its position. /// Dispatch an event to the targets found by a hit test on its position.
/// ///
/// Apart from forwarding the event to [GestureBinding.dispatchEvent], /// If the [pointerEventSource] is [TestBindingEventSource.test], then
/// This also paint all events that's down on the screen. /// the event is forwarded to [GestureBinding.dispatchEvent] as usual;
/// additionally, down pointers are painted on the screen.
///
/// If the [pointerEventSource] is [TestBindingEventSource.device], then
/// the event, after being transformed to the local coordinate system, is
/// forwarded to [deviceEventDispatcher].
@override @override
void handlePointerEvent(PointerEvent event) { void handlePointerEvent(PointerEvent event) {
switch (pointerEventSource) { switch (pointerEventSource) {
...@@ -1530,8 +1569,12 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1530,8 +1569,12 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
break; break;
case TestBindingEventSource.device: case TestBindingEventSource.device:
if (deviceEventDispatcher != null) { if (deviceEventDispatcher != null) {
// The pointer events received with this source has a global position
// (see [handlePointerEventForSource]). Transform it to the local
// coordinate space used by the testing widgets.
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position));
withPointerEventSource(TestBindingEventSource.device, withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(event) () => super.handlePointerEvent(localEvent)
); );
} }
break; break;
...@@ -1545,9 +1588,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1545,9 +1588,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
super.dispatchEvent(event, hitTestResult); super.dispatchEvent(event, hitTestResult);
break; break;
case TestBindingEventSource.device: case TestBindingEventSource.device:
assert(hitTestResult != null); assert(hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent);
assert(deviceEventDispatcher != null); assert(deviceEventDispatcher != null);
deviceEventDispatcher!.dispatchEvent(event, hitTestResult!); if (hitTestResult != null)
deviceEventDispatcher!.dispatchEvent(event, hitTestResult);
break; break;
} }
} }
...@@ -1782,15 +1826,6 @@ class _LiveTestRenderView extends RenderView { ...@@ -1782,15 +1826,6 @@ class _LiveTestRenderView extends RenderView {
onNeedPaint(); onNeedPaint();
} }
@override
bool hitTest(HitTestResult result, { required Offset position }) {
final Matrix4 transform = configuration.toHitTestMatrix();
final double det = transform.invert();
assert(det != 0.0);
position = MatrixUtils.transformPoint(transform, position);
return super.hitTest(result, position: position);
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
assert(offset == Offset.zero); assert(offset == Offset.zero);
......
...@@ -59,6 +59,21 @@ export 'package:test_api/test_api.dart' hide ...@@ -59,6 +59,21 @@ export 'package:test_api/test_api.dart' hide
/// Signature for callback to [testWidgets] and [benchmarkWidgets]. /// Signature for callback to [testWidgets] and [benchmarkWidgets].
typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester); typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester);
// Return the last element that satisifes `test`, or return null if not found.
E? _lastWhereOrNull<E>(Iterable<E> list, bool Function(E) test) {
late E result;
bool foundMatching = false;
for (final E element in list) {
if (test(element)) {
result = element;
foundMatching = true;
}
}
if (foundMatching)
return result;
return null;
}
/// Runs the [callback] inside the Flutter test environment. /// Runs the [callback] inside the Flutter test environment.
/// ///
/// Use this function for testing custom [StatelessWidget]s and /// Use this function for testing custom [StatelessWidget]s and
...@@ -829,15 +844,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -829,15 +844,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
.map((HitTestEntry candidate) => candidate.target) .map((HitTestEntry candidate) => candidate.target)
.whereType<RenderObject>() .whereType<RenderObject>()
.first; .first;
final Element? innerTargetElement = collectAllElementsFrom( final Element? innerTargetElement = _lastWhereOrNull(
binding.renderViewElement!, collectAllElementsFrom(binding.renderViewElement!, skipOffstage: true),
skipOffstage: true, (Element element) => element.renderObject == innerTarget,
).cast<Element?>().lastWhere(
(Element? element) => element!.renderObject == innerTarget,
orElse: () => null,
); );
if (innerTargetElement == null) { if (innerTargetElement == null) {
printToConsole('No widgets found at ${binding.globalToLocal(event.position)}.'); printToConsole('No widgets found at ${event.position}.');
return; return;
} }
final List<Element> candidates = <Element>[]; final List<Element> candidates = <Element>[];
...@@ -850,7 +862,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -850,7 +862,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
int numberOfWithTexts = 0; int numberOfWithTexts = 0;
int numberOfTypes = 0; int numberOfTypes = 0;
int totalNumber = 0; int totalNumber = 0;
printToConsole('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:'); printToConsole('Some possible finders for the widgets at ${event.position}:');
for (final Element element in candidates) { for (final Element element in candidates) {
if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming
break; break;
......
...@@ -6,6 +6,13 @@ import 'package:flutter/foundation.dart'; ...@@ -6,6 +6,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
// Only check the initial lines of the message, since the message walks the
// entire widget tree back, and any changes to the widget tree break these
// tests if we check the entire message.
void _expectStartsWith(List<String?> actual, List<String?> matcher) {
expect(actual.sublist(0, matcher.length), equals(matcher));
}
void main() { void main() {
final _MockLiveTestWidgetsFlutterBinding binding = _MockLiveTestWidgetsFlutterBinding(); final _MockLiveTestWidgetsFlutterBinding binding = _MockLiveTestWidgetsFlutterBinding();
...@@ -14,8 +21,9 @@ void main() { ...@@ -14,8 +21,9 @@ void main() {
int invocations = 0; int invocations = 0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( Directionality(
home: Center( textDirection: TextDirection.ltr,
child: Center(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
invocations++; invocations++;
...@@ -42,39 +50,87 @@ void main() { ...@@ -42,39 +50,87 @@ void main() {
await tester.pump(); await tester.pump();
expect(invocations, 0); expect(invocations, 0);
expect(printedMessages, equals(''' _expectStartsWith(printedMessages, '''
Some possible finders for the widgets at Offset(400.0, 300.0): Some possible finders for the widgets at Offset(400.0, 300.0):
find.text('Test') find.text('Test')
find.widgetWithText(RawGestureDetector, 'Test') '''.trim().split('\n'));
find.byType(GestureDetector)
find.byType(Center)
find.widgetWithText(IgnorePointer, 'Test')
find.byType(FadeTransition)
find.byType(FractionalTranslation)
find.byType(SlideTransition)
find.widgetWithText(FocusTrap, 'Test')
find.widgetWithText(PrimaryScrollController, 'Test')
find.widgetWithText(PageStorage, 'Test')
'''.trim().split('\n')));
printedMessages.clear(); printedMessages.clear();
await binding.collectDebugPrints(printedMessages, () async { await binding.collectDebugPrints(printedMessages, () async {
await tester.tapAt(const Offset(1, 1)); await tester.tapAt(const Offset(1, 1));
}); });
expect(printedMessages, equals(''' expect(printedMessages, equals('''
Some possible finders for the widgets at Offset(1.0, 1.0): No widgets found at Offset(1.0, 1.0).
find.byType(MouseRegion) '''.trim().split('\n')));
find.byType(ExcludeSemantics) });
find.byType(BlockSemantics)
find.byType(ModalBarrier) testWidgets('Should print message on pointer events with setSurfaceSize', (WidgetTester tester) async {
find.byType(Overlay) final List<String?> printedMessages = <String?>[];
int invocations = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child:GestureDetector(
onTap: () {
invocations++;
},
child: const Text('Test'),
),
),
),
);
final Size originalSize = tester.binding.createViewConfiguration().size;
await tester.binding.setSurfaceSize(const Size(2000, 1800));
try {
await tester.pump();
final Offset widgetCenter = tester.getRect(find.byType(Text)).center;
expect(widgetCenter.dx, 1000);
expect(widgetCenter.dy, 900);
await binding.collectDebugPrints(printedMessages, () async {
await tester.tap(find.byType(Text));
});
await tester.pump();
expect(invocations, 0);
_expectStartsWith(printedMessages, '''
Some possible finders for the widgets at Offset(1000.0, 900.0):
find.text('Test')
'''.trim().split('\n'));
printedMessages.clear();
await binding.collectDebugPrints(printedMessages, () async {
await tester.tapAt(const Offset(1, 1));
});
expect(printedMessages, equals('''
No widgets found at Offset(1.0, 1.0).
'''.trim().split('\n'))); '''.trim().split('\n')));
} finally {
await tester.binding.setSurfaceSize(originalSize);
}
}); });
} }
class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding { class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
@override @override
TestBindingEventSource get pointerEventSource => TestBindingEventSource.device; void handlePointerEventForSource(
PointerEvent event, {
TestBindingEventSource source = TestBindingEventSource.device,
}) {
// In this test we use `WidgetTester.tap` to simulate real device touches.
// `WidgetTester.tap` sends events in the local coordinate system, while
// real devices touches sends event in the global coordinate system.
// See the documentation of [handlePointerEventForSource] for details.
if (source == TestBindingEventSource.test) {
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position));
return super.handlePointerEventForSource(globalEvent, source: TestBindingEventSource.device);
}
return super.handlePointerEventForSource(event, source: source);
}
List<String?>? _storeDebugPrints; List<String?>? _storeDebugPrints;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment