Skip to content

Conversation

@dkwingsmt
Copy link
Contributor

@dkwingsmt dkwingsmt commented Apr 7, 2020

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:

  • Cursors are assigned to regions in a declarative way using data classes.
  • MouseRegion, RenderMouseRegion and MouseTrackerAnnotation have a new property MouseCursor cursor that can be used to customize mouse cursors of pointers that are hovering over the region.
  • Inkwell and a few other widgets also have a new parameter MouseCursor mouseCursor
  • Hidden cursor, uncontrolled cursor, and several system cursors are supported.

This 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-cursor Edit: this design doc is out of date

Related Issues

Tests

I added the following tests:

  • Many tests to the cursor aspect of MouseTracker
  • Test the cursor aspect of MouseRegion
  • Test MouseCursorController

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.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Did any tests fail when you ran them? Please read Handling breaking changes.

  • No, no existing tests failed, so this is not a breaking change.
  • Yes, this is a breaking change. If not, delete the remainder of this section.

@fluttergithubbot fluttergithubbot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Apr 7, 2020
@dkwingsmt dkwingsmt force-pushed the system-mouse-cursor-2 branch from b1e644f to e64ca1b Compare April 7, 2020 06:06
@dkwingsmt dkwingsmt force-pushed the system-mouse-cursor-2 branch from 48f59a7 to 277adb0 Compare April 14, 2020 20:59
@dkwingsmt dkwingsmt requested a review from gspencergoog April 14, 2020 21:07
Copy link
Contributor

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

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?

Copy link
Contributor Author

@dkwingsmt dkwingsmt Apr 16, 2020

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

@goderbauer
Copy link
Member

The analyzer is unhappy.

@fluttergithubbot fluttergithubbot added the a: tests "flutter test", flutter_test, or one of our tests label Apr 22, 2020
@dkwingsmt
Copy link
Contributor Author

dkwingsmt commented Apr 22, 2020

@Hixie @goderbauer I made some major changes:

  • PreparedMouseCursor now has a protected method performActivate, which is overridden by subclasses. It replaces the is type-checks in MouseTracker.
  • The method channel calls are split into a MouseCursorController class, which only has static methods, such as activateSystemCursor, and is to be used by PreparedMouseCursor.performActivate.
  • The cursor part of MouseTracker is split into a mixin MouseTrackerCursorMixin. The original MouseTracker is renamed to BaseMouseTracker, whereas the one with all mixins is the new MouseTracker.

Can you take a look?

Comment on lines 73 to 80
MouseTrackerAnnotation result;
for (final MouseTrackerAnnotation annotation in annotations) {
if (annotation.cursor != null) {
result = annotation;
break;
}
}
return result ?? const _FallbackAnnotation();
Copy link
Member

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:

Suggested change
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 = () {
Copy link
Member

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];
Copy link
Member

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);
Copy link
Member

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

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 {
}
Copy link
Member

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

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?

Copy link
Contributor Author

@dkwingsmt dkwingsmt May 4, 2020

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 => '';
Copy link
Member

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.

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

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

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

@goderbauer
Copy link
Member

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

@dkwingsmt
Copy link
Contributor Author

dkwingsmt commented May 4, 2020

@goderbauer Can you take a look at the new design, which I call "session-based":

Overview of design:

  • MouseCursor is an immutable class and stores data. Subclasses overrides the method createSession to create a MouseCursorSession object.
  • Every MouseCursor has a corresponding MouseCursorSession class, which represents the duration when a device displays a cursor, i.e. the tuple (device, cursor).
  • MouseCursorSession is a mutable class that stores states and provides methods activate and dispose to override. The activate will handle the method calls as well as resource fetching, caching, fallback cursors, etc.
  • MouseTrackerAnnotation stays const, and doesn't have listeners at all.
  • No more MouseTrackerRenderObjectMixin.
    • RenderMouseRegion implements variable cursor by implementing MouseTrackerAnnotation, and pushing itself as the annotation.

A demo of implementing image cursor can be found at dkwingsmt#1.

@winwisely99
Copy link

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

@ja2375
Copy link

ja2375 commented May 7, 2020

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!

Copy link
Member

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

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?

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 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 {
Copy link
Member

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?

Copy link
Contributor Author

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

@dkwingsmt
Copy link
Contributor Author

@goderbauer I think I've addressed all comments. Can you take a look again?

@dkwingsmt
Copy link
Contributor Author

Besides the comments, I also changed SystemMouseCursor, instead of int shapeCode, it now uses String kind, suggested by Stuart for clarity. Let me know if it's considered inappropreate.

Copy link
Member

@goderbauer goderbauer left a 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

@dkwingsmt dkwingsmt merged commit 7f8cb7f into flutter:master May 11, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

a: tests "flutter test", flutter_test, or one of our tests f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants