-
Notifications
You must be signed in to change notification settings - Fork 29.7k
System mouse cursors #54171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System mouse cursors #54171
Conversation
b1e644f to
e64ca1b
Compare
48f59a7 to
277adb0
Compare
gspencergoog
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.
Mostly just doc comments. It looks pretty good overall.
| /// defines a kind of mouse cursor, such as an arrow, a pointing hand, or an | ||
| /// I-beam. | ||
| /// | ||
| /// Internally, when the mouse pointer moves, it finds the front-most region |
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.
region as in MouseRegion? Maybe say [MouseRegion] then? Or is this a different region?
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.
In some way this is intentionally put in a vague way, because the answer depends on the cases. It can be an AnnotatedRegionLayer<MouseTrackerAnnotation>, RenderMouseRegion, or MouseRegion. There are also other render objects or widgets that do not directly use (Render)MouseRegion but directly push annotations, such as PlatformView, among a few.
The only thing we're certain of is that cursors have to be assigned to logical regions of the screen, because mouse tracking is based on positions.
| // Define the actions when a pointer should change to a prepared cursor by | ||
| // calling system functions. | ||
| Future<void> _handleActivateCursor(int device, PreparedMouseCursor cursor) async { | ||
| if (cursor is NoopMouseCursor) |
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.
Maybe I missunderstod what NoopMouseCursor does, but don't you have to tell the system that flutter is no longer controlling the cursor?
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.
"Not controlling the cursor" is only effective when the pointer moves across the "cursor boundary". When the pointer is moving within the "cursor boundary" is entirely covered by NoopMouseCursor, the cursor won't change at all.
|
The analyzer is unhappy. |
|
@Hixie @goderbauer I made some major changes:
Can you take a look? |
| MouseTrackerAnnotation result; | ||
| for (final MouseTrackerAnnotation annotation in annotations) { | ||
| if (annotation.cursor != null) { | ||
| result = annotation; | ||
| break; | ||
| } | ||
| } | ||
| return result ?? const _FallbackAnnotation(); |
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.
Feel free to ignore, but I find the following structure slightly clearer:
| MouseTrackerAnnotation result; | |
| for (final MouseTrackerAnnotation annotation in annotations) { | |
| if (annotation.cursor != null) { | |
| result = annotation; | |
| break; | |
| } | |
| } | |
| return result ?? const _FallbackAnnotation(); | |
| for (final MouseTrackerAnnotation annotation in annotations) { | |
| if (annotation.cursor != null) { | |
| return annotation; | |
| } | |
| } | |
| return const _FallbackAnnotation(); |
| final bool hadState = _mouseCursorStates.containsKey(device); | ||
| _mouseCursorStates.putIfAbsent(device, () { | ||
| final _MouseCursorState state = _MouseCursorState(); | ||
| state.onCursorChange = () { |
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 seems as if all _MouseCursorState always have the same onCursorChange method. Why not make it an instance method (and pass the device in in the constructor)?
| return state; | ||
| }); | ||
|
|
||
| final _MouseCursorState state = _mouseCursorStates[device]; |
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.
putIfAbsent above returns exactly this. Just assign it to state instead of looking it up again here?
| return; | ||
| } | ||
|
|
||
| final bool hadState = _mouseCursorStates.containsKey(device); |
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.
looks like this value is only used for asserts. Maybe rename it to debugHadState and wrap its assignment in an assert so it only happens when asserts are enabled?
| // prevents instantiation and extension. | ||
| MouseCursorController._(); | ||
|
|
||
| static MethodChannel get _channel => SystemChannels.mouseCursor; |
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.
nit: the indirection seems unnecessary, just use SystemChannels.mouseCursor below?
| @override | ||
| @protected | ||
| Future<void> performActivate(int device) async { | ||
| } |
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.
nit: add a comment to the body indicating why it's empty.
| String get debugDescription => ''; | ||
| } | ||
|
|
||
| /// A mouse cursor that is standard on the platform that the application is |
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.
nit: "standard" seems strange. Isn't it just one that ships with the platform or is available there?
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 "native"?
/// A mouse cursor that is natively supported on the platform that the
/// application is running on.| } | ||
|
|
||
| @override | ||
| String get debugDescription => ''; |
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 am surprised that the empty string is used here.
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 added a comment to explain:
// The [debugDescription] is '' so that its toString() returns 'NoopMouseCursor()'.| /// one on the screen in hit-test order, or [SystemMouseCursors.basic] if no | ||
| /// others can be found. | ||
| /// | ||
| /// The [MouseTrackerAnnotation] is immutable and does not support varying |
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 still have some questions about the "immutable" and the change notification part. Maybe we can do a quick video sync?
| // It uses [LinkedHashSet] to keep the insertion order. | ||
| LinkedHashSet<MouseTrackerAnnotation> get annotations => _annotations; | ||
| LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>(); | ||
| LinkedHashSet<MouseTrackerAnnotation> _annotations = <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>; |
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.
Why this change? The old version (without the cast) seemed cleaner...
|
Avoiding the repaint for when cursors change adds a lot of complexibility. I am not sure if that's worth it, TBH. The repaint shouldn't be that expensive and I would image that when you need to change the cursor, you'd also be changing other aspects of the UI to "justify" the new cursor. For that, you'd have to do a repaint anyways... |
|
@goderbauer Can you take a look at the new design, which I call "session-based": Overview of design:
A demo of implementing image cursor can be found at dkwingsmt#1. |
cb9a0fe to
0201c21
Compare
|
Just adding myself to this so I can see when it's ready as we need it for our flutter project. https://github.com/getcouragenow |
|
I think the default behavior for this on web should be the cursor autochanges when it detects you are hovering a button, text, etc. Will this be the behavior for web or will it be necessary to add an extra widget on every clickable, text widget we want to show a different cursor on? Many thanks for implementing this! |
goderbauer
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.
I like this design!
(haven't looked closely at the tests yet)
| /// The last event that the device observed before the update. | ||
| /// | ||
| /// If the update is triggered by a frame, it is not null, since the pointer | ||
| /// must have been added before. If the update is triggered by an 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.
Is this only null for specific events (e.g. pointer added)? Maybe document that?
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 changed it to
/// The last event that the device observed before the update.
///
/// If the update is triggered by a frame, the [previousEvent] is never null,
/// since the pointer must have been added before.
///
/// If the update is triggered by a pointer event, the [previousEvent] is not
/// null except for cases where the event is the first event observed by the
/// pointer (which is not necessarily a [PointerAddedEvent]).| logEnters.clear(); | ||
| }); | ||
|
|
||
| testWidgets('Changing whether MouseRegion.cursor is null is effective and repaints', (WidgetTester tester) async { |
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 have a test to verify that the cursor of the MouseRegion underneath the one that changed it cursor to null becomes active?
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.
Good idea. I changed this test by adding a background MouseRegion
|
@goderbauer I think I've addressed all comments. Can you take a look again? |
|
Besides the comments, I also changed |
goderbauer
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 after comment is resolved
Description
This PR adds the basic framework for the mouse cursor system. While only system cursors are supported for now, the APIs are designed with the consideration of asynchronously loaded cursors (image cursors).
With this PR:
MouseRegion,RenderMouseRegionandMouseTrackerAnnotationhave a new propertyMouseCursor cursorthat can be used to customize mouse cursors of pointers that are hovering over the region.Inkwelland a few other widgets also have a new parameterMouseCursor mouseCursorThis PR is only the framework part of the system. Without the upcoming engine change, these changes won't take effect, but will not break existing apps either.
For more information about the design, see https://flutter.dev/go/system-mouse-cursorEdit: this design doc is out of dateRelated Issues
Tests
I added the following tests:
MouseTrackerMouseRegionMouseCursorControllerChecklist
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.