-
Notifications
You must be signed in to change notification settings - Fork 6k
iPad trackpad gestures #31591
iPad trackpad gestures #31591
Conversation
jmagman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cyanglaz if you have bandwidth can you take a look at the logic here and validate it makes sense?
| fml::scoped_nsobject<UIScrollView> _scrollView; | ||
| fml::scoped_nsobject<UIHoverGestureRecognizer> _hoverGestureRecognizer API_AVAILABLE(ios(13.4)); | ||
| fml::scoped_nsobject<UIPanGestureRecognizer> _panGestureRecognizer API_AVAILABLE(ios(13.4)); | ||
| fml::scoped_nsobject<UIPanGestureRecognizer> _discretePanGestureRecognizer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you're copying the _panGestureRecognizer pattern, but I'm not sure why it's a scoped_nsobject other than someone didn't want to figure out the MRC memory management pattern. Can you try re-writing this with the new ivars as vanilla Objective-C objects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really know what I'm doing with Objective-C, can you verify what I changed it to is what you are meaning here?
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
| packet->SetPointerData(/*index=*/0, pointer_data); | ||
|
|
||
| [_engine.get() dispatchPointerDataPacket:std::move(packet)]; | ||
| _mouseState.flutter_state_is_added = pointer_data.change != flutter::PointerData::Change::kRemove; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm un-sure about this, it looks fine.
CC @chunhtai do you mind take a look at the logics about pointers in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to assume the engine shell will synthesize add event when the change is not kRemove? why not always synthesize the add event in embedding so that you don't need to guess what engine shell may be doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correct events will be sent to the shell. Nothing needs to be synthesized. This is to synchronize so that in hoverEvent and scrollEvent we don't send the same event twice.
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
| packet->SetPointerData(/*index=*/0, pointer_data); | ||
|
|
||
| [_engine.get() dispatchPointerDataPacket:std::move(packet)]; | ||
| _mouseState.flutter_state_is_added = pointer_data.change != flutter::PointerData::Change::kRemove; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to assume the engine shell will synthesize add event when the change is not kRemove? why not always synthesize the add event in embedding so that you don't need to guess what engine shell may be doing
|
|
||
| packet->SetPointerData(pointer_index++, pointer_data); | ||
|
|
||
| if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to check flutter_state_is_added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Engine shell does not need to synthesize. The event will be kAdd as needed based on state of the gesture recognizer (see generatePointerDataForMouse). Use of flutter_state_is_added is only to synchronize between hover recognizer and scroll recognizer.
flutter_state_is_added is only for the mouse cursor (hovering only). The quoted code is for each UITouch (it's because UIKit uses touch mechanism to send mouse clicks).
| // only does this hover logic when device kind is flutter::PointerData::DeviceKind::kMouse. | ||
| size_t touches_to_remove_count = 0; | ||
| for (UITouch* touch in touches) { | ||
| if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here check for flutter_state_is_added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above comment, flutter_state_is_added is only for the mouse hover cursor.
beb0e0d to
13f6de5
Compare
|
From PR review triage: It looks like @moffatman has responded to feedback. @chunhtai @cyanglaz could you give this another look please? |
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
| flutter::PointerData add_pointer_data = pointer_data; | ||
| add_pointer_data.signal_kind = flutter::PointerData::SignalKind::kNone; | ||
| add_pointer_data.change = flutter::PointerData::Change::kAdd; | ||
| _mouseState.flutter_state_is_added = true; | ||
| packet->SetPointerData(/*index=*/0, add_pointer_data); | ||
| packet->SetPointerData(/*index=*/1, pointer_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is manually adding a "add" event. Is this something we normally do? Is there a side effect for this?
@chunhtai might know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks correct
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
| pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc. | ||
| pointer_data.scale = 1; | ||
| } else { | ||
| pointer_data.change = flutter::PointerData::Change::kPanZoomEnd; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about other states like cancel or failed? I looks like we do have a kCancel in PointerData:Change
I'm not familiar with the PointerData, @chunhtai might be able to confirm if other UIGestureRecognizerState needs to be mapped to different PointerData:Change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For trackpad gesture we will only have kPanZoomStart, kPanZoomUpdate, kPanZoomEnd. I didn't make a separate PanZoomCancelled since I didn't find any situation where it would be used differently than PanZoomEnd in the framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good. cc @chunhtai to confirm as he is more knowledgable on gestures in flutter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably still use cancel just to be safe, also can this be a switch statements and put assert on the case that are unexpected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated to use switch so it's more obvious what UIKit phase maps to which flutter phase.
| pointer_data.scale = recognizer.scale; | ||
| pointer_data.rotation = _rotationGestureRecognizer.rotation; | ||
| } else { | ||
| pointer_data.change = flutter::PointerData::Change::kPanZoomEnd; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as panEvent
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
b70df69 to
4eb5e62
Compare
|
What is the status of this PR? |
|
I think I should request re-review, will do that now |
|
Friendly ping @cyanglaz |
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
| pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc. | ||
| pointer_data.scale = 1; | ||
| } else { | ||
| pointer_data.change = flutter::PointerData::Change::kPanZoomEnd; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good. cc @chunhtai to confirm as he is more knowledgable on gestures in flutter.
| [invocation setArgument:&deltaY atIndex:3]; | ||
| [invocation invokeWithTarget:flutterView]; | ||
|
|
||
| // The hover pointer is removed after ~3.5 seconds, this ensures that all events are received |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the 3.5 seconds defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the observed behavior on iPadOS. After around 3.5 seconds of not moving the mouse, the system will remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a risk of flaky test. It is possible to find a different way to test this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could end the test without checking that the pointer is removed after a delay, since it's just an observed behaviour of the iPad, you're right that it could change with any update. The rest of the test can stay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the better question to ask is whether our implementation relies on this behavior, such as relying on this to send the pointer removal. If so we should keep this test, explain why it's important, and note that this behavior is volatile.
4eb5e62 to
bf1bb56
Compare
|
From PR triage: @cyanglaz it looks like this one is ready for another look. |
|
This is on @cyanglaz's radar, he will test locally on his iPad to verify when he gets a chance. Thanks for your patience! 🙂 |
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
dkwingsmt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor changes. I see that it's really hard to test! Great job.
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
| [invocation setArgument:&deltaY atIndex:3]; | ||
| [invocation invokeWithTarget:flutterView]; | ||
|
|
||
| // The hover pointer is removed after ~3.5 seconds, this ensures that all events are received |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the better question to ask is whether our implementation relies on this behavior, such as relying on this to send the pointer removal. If so we should keep this test, explain why it's important, and note that this behavior is volatile.
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
5514101 to
e7a31aa
Compare
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
|
@dkwingsmt I think this is good for another review. |
dkwingsmt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks for taking care all of them!
|
I realized if we only send discrete scroll events on a pointer, it doesn't need to be removed, since it won't ever trigger any hover events. So the code is simpler now, no need to coordinate the reuse of the hover pointer. Also, I will now rely on @hellohuanlin: In regards to pointer arena, we still need to use the pointer arena within flutter, as even though we know some of its intent, we need to determine which widget will receive the trackpad gesture. For example: nested pannable/scrollable widgets. |
5837d01 to
385061a
Compare
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
787966b to
80cb056
Compare
Clicking-and-dragging with a mouse was causing erroneous scroll data to be generated.
80cb056 to
bbb6f80
Compare
bbb6f80 to
0b09015
Compare
|
@moffatman i saw you pushed multiple updates. is this PR ready for review again? |
|
@hellohuanlin Yes, please take a look again. I added the "Add" event synthesis so that all cases are consistent. |
hellohuanlin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great. Thanks for your effort addressing feedbacks.
| synthesized_add_event.synthesized = 1; | ||
| synthesized_add_event.buttons = 0; | ||
| state = EnsurePointerState(synthesized_add_event); | ||
| converted_pointers.push_back(synthesized_add_event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice! thanks for moving this into the converter
| supportsPointerInteraction = | ||
| performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector); | ||
| } | ||
| XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you know if any of our CI test devices supports pointer interaction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This testing support depends on iOS 15 anyway, so I don't think it's run yet. Right now I believe the simulator in CI is iPhone 8 on iOS 13. We need iPad on iOS15+.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmagman probably knows who can help with setting up test devices for iOS15
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do this in the simulator, we shouldn't need physical devices, like this:
| -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 8' \ |
We need to get flutter/flutter#85558 done so these tests actually run 🙂
It's fine to just match the pattern of the tests around it for now, as long as it passes for you locally.
testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m
Outdated
Show resolved
Hide resolved
| _discreteScrollingPanGestureRecognizer = | ||
| [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)]; | ||
| _discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete; | ||
| _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add your explanation here as comments? I've copied it as below. Modify as you see fit.
Disallow all touch types. When using trackpad scroll, the platform doesn't send touch events, instead it sends scroll events, so disallowing touch types doesn't stop the trackpad callback. If touch events are allowed, touching the screen will be consumed by the pan gesture recognizer instead of being passed through to flutter via touchesBegan etc.
| return; | ||
| } | ||
|
|
||
| // When UIApplicationSupportsIndirectInputEvents is set in Info.plist, mouse clicks will be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever unset UIApplicationSupportsIndirectInputEvents? If not we might change the "if" to a more decisive word. Also here's a my two cents of entire paragraph. Take as you see fit.
With
UIApplicationSupportsIndirectInputEventsset inInfo.plist, the platform dispatches mouse clicks asUITouchwithtouch.typeofUITouchTypeIndirectPointerand a different identifier for each click. They are translated into Flutter pointer events withtypeofkMouseand different device IDs. These devices must be terminated withkRemoveevents then the touches end, otherwise they will keep triggering hover events.The
touches_to_remove_counttracks how many removal events are needed in this group of touches to properly allocate space for the packet. The removal event of a touch is synthesized immediately after its normal event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote it that way since UIApplicationSupportsIndirectInputEvents is false by default, I need to make a PR to migrate it to true. But whether that key is true or false, each mouse click will always have a unique identifier, since it comes in as a UITouch. An accommodation is needed since mouse events with device kind touch don't trigger MouseRegion / mouse tracker and there are no side effects from creating a lot of single-use devices in the framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, firstly, what exactly is the "mouse device" you're talking about? (It's mostly for the next question)
Secondly, do you mean that the behavior as observed in Flutter will vary based on UIApplicationSupportsIndirectInputEvents, so that if it's false the resulting events will have type touch, and if it's true the events will have type mouse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I say mouse device, I guess I mean a sequence of pointer events with a unique identifier and device-kind kMouse. Those sequences have different side effects in the framework than sequences with a unique identifier but device-kind kTouch.
For point 2, yes, that's correct, the event type will change depending on UIApplicationSupportsIndirectInputEvents, as well as a few other things:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I've also found https://developer.apple.com/documentation/uikit/pointer_interactions, which explains that an indirect pointer stands for the trackpad, while direct pointer stands for finger touches or pencils. We probably want to document this behavioral difference in the framework.
Then for the documentation, how about this:
If the
UIApplicationSupportsIndirectInputEventsinInfo.plistreturns YES, then the platform dispatches indirect pointer touches (trackpad clicks) asUITouchwith a type ofUITouchTypeIndirectPointerand different identifiers for each click. They are translated into Flutter pointer events withtypeofkMouseand different device IDs. These devices must be terminated withkRemoveevents when the touches end, otherwise they will keep triggering hover events.If the
UIApplicationSupportsIndirectInputEventsinInfo.plistreturns NO, then the platform dispatches indirect pointer touches (trackpad clicks) asUITouchwith a type ofUITouchTypeIndirectPointerand different identifiers for each click. They are translated into Flutter pointer events withtypeofkTouchand different device IDs. Removing these devices is neither necessary nor harmful.Therefore Flutter always removes these devices. The
touches_to_remove_counttracks how many remove events are needed in this group of touches to properly allocate space for the packet. The remove event of a touch is synthesized immediately after its normal event.See also:
- https://developer.apple.com/documentation/uikit/pointer_interactions?language=objc
- https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents?language=objc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great summarization, I'll put it in.
dkwingsmt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Doc suggestion.
Start sending PointerPanZoom events from iPadOS when the trackpad is used
Fixes flutter/flutter#23604
Pre-launch Checklist
writing and running engine tests.
///).