Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@moffatman
Copy link
Contributor

Start sending PointerPanZoom events from iPadOS when the trackpad is used

Fixes flutter/flutter#23604

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides].
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See [testing the engine] for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the [CLA].
  • All existing and new tests are passing.

@chinmaygarde
Copy link
Member

cc @jmagman @gaaclarke

Copy link
Member

@jmagman jmagman left a 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
Copy link
Member

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?

Copy link
Contributor Author

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?

packet->SetPointerData(/*index=*/0, pointer_data);

[_engine.get() dispatchPointerDataPacket:std::move(packet)];
_mouseState.flutter_state_is_added = pointer_data.change != flutter::PointerData::Change::kRemove;
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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.

packet->SetPointerData(/*index=*/0, pointer_data);

[_engine.get() dispatchPointerDataPacket:std::move(packet)];
_mouseState.flutter_state_is_added = pointer_data.change != flutter::PointerData::Change::kRemove;
Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@moffatman moffatman force-pushed the trackpad_gestures_ipad branch from beb0e0d to 13f6de5 Compare March 22, 2022 23:24
@moffatman moffatman requested review from chunhtai and jmagman March 29, 2022 17:23
@zanderso
Copy link
Member

From PR review triage: It looks like @moffatman has responded to feedback. @chunhtai @cyanglaz could you give this another look please?

Comment on lines 1863 to 1914
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);
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

this looks correct

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;
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as panEvent

@moffatman moffatman force-pushed the trackpad_gestures_ipad branch 3 times, most recently from b70df69 to 4eb5e62 Compare April 6, 2022 00:51
@zanderso
Copy link
Member

zanderso commented Apr 7, 2022

What is the status of this PR?

@moffatman
Copy link
Contributor Author

I think I should request re-review, will do that now

@moffatman moffatman requested a review from cyanglaz April 7, 2022 21:04
@jmagman
Copy link
Member

jmagman commented Apr 19, 2022

Friendly ping @cyanglaz

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;
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@moffatman moffatman force-pushed the trackpad_gestures_ipad branch from 4eb5e62 to bf1bb56 Compare April 19, 2022 23:25
@zanderso
Copy link
Member

From PR triage: @cyanglaz it looks like this one is ready for another look.

@jmagman
Copy link
Member

jmagman commented May 9, 2022

This is on @cyanglaz's radar, he will test locally on his iPad to verify when he gets a chance. Thanks for your patience! 🙂

Copy link
Contributor

@dkwingsmt dkwingsmt left a 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.

[invocation setArgument:&deltaY atIndex:3];
[invocation invokeWithTarget:flutterView];

// The hover pointer is removed after ~3.5 seconds, this ensures that all events are received
Copy link
Contributor

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.

@moffatman moffatman force-pushed the trackpad_gestures_ipad branch 2 times, most recently from 5514101 to e7a31aa Compare May 11, 2022 19:12
@chinmaygarde
Copy link
Member

@dkwingsmt I think this is good for another review.

Copy link
Contributor

@dkwingsmt dkwingsmt left a 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!

@dkwingsmt dkwingsmt requested a review from cyanglaz May 13, 2022 11:24
@moffatman
Copy link
Contributor Author

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 kAdd being synthesized for the hover pointer in pointer_data_packet_converter.cc, it's already the practice for other pointer identifiers.

@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.

@moffatman moffatman force-pushed the trackpad_gestures_ipad branch from 5837d01 to 385061a Compare June 7, 2022 03:40
@moffatman moffatman force-pushed the trackpad_gestures_ipad branch 2 times, most recently from 787966b to 80cb056 Compare June 8, 2022 02:36
@moffatman moffatman force-pushed the trackpad_gestures_ipad branch from 80cb056 to bbb6f80 Compare June 8, 2022 03:05
@moffatman moffatman force-pushed the trackpad_gestures_ipad branch from bbb6f80 to 0b09015 Compare June 8, 2022 14:22
@hellohuanlin
Copy link
Contributor

@moffatman i saw you pushed multiple updates. is this PR ready for review again?

@moffatman
Copy link
Contributor Author

@hellohuanlin Yes, please take a look again. I added the "Add" event synthesis so that all cases are consistent.

@moffatman moffatman requested a review from dkwingsmt June 9, 2022 21:49
Copy link
Contributor

@hellohuanlin hellohuanlin left a 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);
Copy link
Contributor

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.");
Copy link
Contributor

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?

Copy link
Contributor Author

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+.

Copy link
Contributor

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

Copy link
Member

@jmagman jmagman Jun 11, 2022

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.

_discreteScrollingPanGestureRecognizer =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
_discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
_discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
Copy link
Contributor

@dkwingsmt dkwingsmt Jun 14, 2022

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
Copy link
Contributor

@dkwingsmt dkwingsmt Jun 14, 2022

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 UIApplicationSupportsIndirectInputEvents set in Info.plist, the platform dispatches mouse clicks as UITouch with touch.type of UITouchTypeIndirectPointer and a different identifier for each click. They are translated into Flutter pointer events with type of kMouse and different device IDs. These devices must be terminated with kRemove events then the touches end, otherwise they will keep triggering hover events.

The touches_to_remove_count tracks 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.

Copy link
Contributor Author

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.

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 14, 2022

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?

Copy link
Contributor Author

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:

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 14, 2022

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 UIApplicationSupportsIndirectInputEvents in Info.plist returns YES, then the platform dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of UITouchTypeIndirectPointer and different identifiers for each click. They are translated into Flutter pointer events with type of kMouse and different device IDs. These devices must be terminated with kRemove events when the touches end, otherwise they will keep triggering hover events.

If the UIApplicationSupportsIndirectInputEvents in Info.plist returns NO, then the platform dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of UITouchTypeIndirectPointer and different identifiers for each click. They are translated into Flutter pointer events with type of kTouch and different device IDs. Removing these devices is neither necessary nor harmful.

Therefore Flutter always removes these devices. The touches_to_remove_count tracks 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:

Copy link
Contributor Author

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.

Copy link
Contributor

@dkwingsmt dkwingsmt left a comment

Choose a reason for hiding this comment

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

LGTM. Doc suggestion.

@dkwingsmt dkwingsmt added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Jun 14, 2022
@fluttergithubbot fluttergithubbot merged commit 3a195fd into flutter:main Jun 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jun 14, 2022
houhuayong pushed a commit to houhuayong/engine that referenced this pull request Jun 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

platform-ios waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should support trackpad gestures from host

9 participants