Refactor mouse hit testing system: Direct mouse hit test#59883
Refactor mouse hit testing system: Direct mouse hit test#59883fluttergithubbot merged 29 commits intoflutter:masterfrom
Conversation
Matrix4.* rises from 3.4% to 4.1%
|
nit: in the PR description:
You probably mean "regular hit testing system on the render tree"? |
|
Also: looks like cirrus had a bad day. |
| synthesized: event?.synthesized, | ||
| transform: event?.transform, | ||
| original: event?.original as PointerEnterEvent, | ||
| original: null, |
There was a problem hiding this comment.
Why are we forcing this to null now instead of keeping the original event if available?
There was a problem hiding this comment.
Short version: In order to prevent the type system complaining, either we set this to null, which satisfies the current definition of original, or we change the type of original in the constructor, which also changes the definition of original.
Long version: An error will be thrown in this as-cast when converting other event types using fromMouseEvent (e.g. from PointerDownEvent to PointerEnterEvent). This as-cast was not here when this constructor was first created, but is reasonable since the constructor requires original to be PointerEnterEvent, which also makes sense since original was added for the transformed constructor to transform between coordinate space. So if we think it this way, this original should be null because original was not defined to suit in the type converting case.
| cursor: effectiveMouseCursor, | ||
| onEnter: (PointerEnterEvent event) => _handleHover(true), | ||
| onExit: (PointerExitEvent event) => _handleHover(false), | ||
| return MouseRegion( |
There was a problem hiding this comment.
Why are we switching this around? Even when it is disabled it should still respond to hovers?
There was a problem hiding this comment.
Yes. A widget should never turn off the mouse region when it's disabled.
The mouse cursor algorithm detects the first MouseTrackerAnnotation found and uses its cursor. If the MouseRegion is hidden behind the IgnorePointer then the widget will never be able to show its disabled cursor, and the cursor will incorrectly be the one of the widget behind it, as if the TextField does not exist. A test that is violated can be found at
The reason why this isn't found earlier is probably because IgnorePointer and AbsorbPointer failed to support mouse events (incorrectly, see #35213).
| @@ -201,9 +202,17 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> | |||
| // Layer hit testing is done using device pixels, so we have to convert | |||
There was a problem hiding this comment.
Why do we still need to have a separate hit testing pass for mouse events? Can that not be integrated with the regular hit testing now as well?
There was a problem hiding this comment.
Without mouse,
- During hover events, no regular hit test is done.
- During down events, a regular hit test is done, so this is an extra pass, but down events happen at such a low frequency that we might not really care.
- During move events, no regular hit test is done (the result of down events is reused).
Conclusion: The only "extra pass" is the ones at down events, which do not really matter.
There was a problem hiding this comment.
I am more concerned about the extra code complexity that we have to deal with because the paths are not unified.
There was a problem hiding this comment.
If the mouse device update is triggered by a pointer event, then the code path is indeed more complicated than necessary:
GestureBinding.dispatchEvent -> PointerRouter.route -> MouseTracker._handleEvent -> hit test
Maybe we could've done the hit test at GestureBinding.dispatchEvent and pass it all the way to MouseTracker._handleEvent.
But the mouse device update can also be triggered after every frame, during the post-frame callbacks:
SchedulerBinding.handleDrawFrame -> MouseTracker._updateAllDevices -> hit test
In this case MouseTracker have to initiate a hit test for each device, without the participation of GestureBinding at all.
I'm not saying that a much more unified structure can not be achieved at all, but for now I'd rather touch as few things as possible in order to see the performance impact of the current PR.
| _localTransforms.removeLast(); | ||
| else | ||
| _transforms.removeLast(); | ||
| assert(_transforms.isNotEmpty); |
There was a problem hiding this comment.
This assert is a little odd. Why do we assert after removing that there's still something in _transforms? Shouldn't we assert before?
There was a problem hiding this comment.
The _transforms should never be empty, because there should at least be the pre-seeded identity matrix.
Of course it shouldn't be empty before the operation either, but that would be detected by removeLast anyway.
| @@ -201,9 +202,17 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> | |||
| // Layer hit testing is done using device pixels, so we have to convert | |||
There was a problem hiding this comment.
I am more concerned about the extra code complexity that we have to deal with because the paths are not unified.
|
This is breaking customer app, please see #60995 |
Description
This PR refactors mouse hit testing so that it uses the regular mouse hit testing system instead of the current layer-based approach. Now
hitTestMouseTrackersperforms a regular hit testing on the target position, and picks the render objects that inheritsMouseTrackerAnnotation.This change will bring several benefits, including addressing several issues:
What prevented this design from happening in the beginning was performance. Mouse hit testing is done far more frequently than render-object-based regular hit testing, but the render tree is considered too large to be efficiently hit tested.
To resolve this, this PR introduces a new method
HitTestResult.pushOffsetto allow a few optimizations (introduced in this design doc). Local benchmarks show that the resulting performance is on par with, or even possibly better the layer-based approach.Related Issues
Besides the issues mentioned above, it also:
Tests
I added the following tests:
Changed tests:
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.