Skip to content
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

Make the cursor API stateful and widget-local. #1433

Merged
merged 6 commits into from
Dec 4, 2020
Merged

Make the cursor API stateful and widget-local. #1433

merged 6 commits into from
Dec 4, 2020

Conversation

jneem
Copy link
Collaborator

@jneem jneem commented Nov 27, 2020

This is an attempt at #1368. The point is that instead of calling set_cursor on every mouse move, we call it once and then it will take effect whenever the calling widget is hot or active.

Since this is a more "structured" API than the previous one, it's also a bit less powerful. This is because druid now takes care of bubbling and deciding between the various cursor requests, whereas in the previous API it was all under the caller's control. Having said that, I don't actually know of any realistic use-cases that the new API misses out on.

@jneem jneem mentioned this pull request Nov 27, 2020
@JAicewizard
Copy link
Contributor

If I am not mistaken, this only allows setting cursors for WidgetPod boundaries? Since that's where the WidgetState is located. I think that is fine since most users should be using WidgetPod anyways. I don't think that should be a problem, but might be worth documenting somewhere.

I guess this is sort of what I also imagined at first, except I hit a wall at having some widget state managed by druid. I guess this is exactly that except it only works for setting cursors within WidgetPod boundaries.

I took a quick look at the code and it seems very straight-forward to me, looks good.

@raphlinus
Copy link
Contributor

We definitely want to be able to set the cursor in Runebender to represent hovering over, eg, control points. Does this preclude that?

self.handle.set_cursor(&cursor);
} else if matches!(event, Event::MouseMove(..)) {
self.handle.set_cursor(&Cursor::Arrow);
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if it would only set the cursor when it actually changed. I think it was part of the nice to haves.
You could probably just compare the Rc since that should stay the same if the cursor doesn't change.

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 there are cases where you have to set the cursor because it was invalidated some other way, for example when the window loses focus. My personal feeling is that it is possible to track those other state changes but simpler not to, so set it every time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, there's an issue open about this (#112) but I didn't feel like taking it on.

@jneem
Copy link
Collaborator Author

jneem commented Nov 28, 2020

If I am not mistaken, this only allows setting cursors for WidgetPod boundaries? Since that's where the WidgetState is located. I think that is fine since most users should be using WidgetPod anyways. I don't think that should be a problem, but might be worth documenting somewhere.

If you call set_cursor once and forget about it, then yes, it will go according to the WidgetPod boundaries. But you can always track the location of the mouse and call set_cursor again if you want to do something more sophisticated.

@jneem
Copy link
Collaborator Author

jneem commented Nov 28, 2020

We definitely want to be able to set the cursor in Runebender to represent hovering over, eg, control points. Does this preclude that?

I don't think so, no. You'll call set_cursor when the mouse goes over a control point, and then clear_cursor (or set_cursor again) when it leaves the control point. The main difference between this and the previous state of things is that you used to have to call set_cursor on every mouse move; now you do it when you actually want to change the cursor.

@JAicewizard
Copy link
Contributor

I don't think so, no. You'll call set_cursor when the mouse goes over a control point, and then clear_cursor (or set_cursor again) when it leaves the control point. The main difference between this and the previous state of things is that you used to have to call set_cursor on every mouse move; now you do it when you actually want to change the cursor.

I think it does? but maybe I'm interpreting the question wrong. Because if you set the cursor within a widget(pod) it will automatically set the cursor to that when it hovers over that widget(pod) right?

@jneem
Copy link
Collaborator Author

jneem commented Nov 28, 2020

Right, but if you want to do something fancier (like only show a cursor while the mouse is over some more complicated subset of the WidgetPod), you can just listen to mouse moves and call set_cursor again (or clear_cursor) when appropriate.

Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on! have left my comments inline, nothing really major. I think this is definitely a more intuitive API.

data: &AppState,
env: &Env,
) {
if !Rc::ptr_eq(&data.cursor, &old_data.cursor) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer to just write this out as !data.cursor.same(&old_data.cursor), that should be more familiar to folks.

Copy link
Collaborator

@derekdreery derekdreery Dec 3, 2020

Choose a reason for hiding this comment

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

This brings up an interesting question: which is better

  • first.same(&second)
  • Data::same(&first, &second)

I prefer the second one, the reason being that I can immediately grok that we are using the fact that both types impl Data. I also find it easier to read. This is a subjective opinion though and I'd be interested in others' opinions.

Copy link
Member

Choose a reason for hiding this comment

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

I think the first is more idiomatic, but I see your point. We have definitely been using the first version everywhere, though, so I'm inclined to be consistent; understanding that same is the method on Data is a pretty fundamental druid concept.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I make Cursor implement PartialEq, so now this is just ==.

}

fn ui_builder() -> impl Widget<AppState> {
Button::new("Change cursor")
.on_click(|ctx, data: &mut AppState, _env| {
.on_click(|_ctx, data: &mut AppState, _env| {
data.cursor = next_cursor(&data.cursor, data.custom.clone());
Copy link
Member

Choose a reason for hiding this comment

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

totally unrelated to this but just noticing that it might make sense for this to just be a method on AppState? then we wouldn't need to pass in anything.

pub fn set_cursor(&mut self, cursor: &Cursor) {
*self.cursor = Some(cursor.clone());
self.widget_state.cursor_setting = CursorSetting::Set(Rc::new(cursor.clone()));
Copy link
Member

Choose a reason for hiding this comment

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

What's the motivation for using the Rc? is this so that we play well with Data? Or to avoid having cloning be expensive?

In either case I think it might be more ergonomic to solve this at the druid-shell level; for instance Cursor::Custom could have an Rc<platform::window::Cursor>, and we could add a Data impl for Cursor in druid, if we wanted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was worried about the cost. Which is silly, because I only did the druid-shell cursor thing recently, and I already forgot that it does the shared pointer thing already (within the platform-specific code). I've added the Data impl anyway.

@@ -140,6 +145,18 @@ pub(crate) enum FocusChange {
Previous,
}

/// The possible cursor states for a widget.
#[derive(Clone)]
pub(crate) enum CursorSetting {
Copy link
Member

Choose a reason for hiding this comment

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

Feels weird to have use a gerund for the name here, although it's not a big deal. I might match the naming of the FocusChange and go with CursorChange? or SetCursor? I don't think it's a big deal for an internal type but at least want to consider what some alternatives could be.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I went with CursorChange.

self.cursor = child_cursor;
} else if let CursorSetting::Set(cursor) = &child_state.cursor_setting {
self.cursor = Some(Rc::clone(cursor));
}
Copy link
Member

Choose a reason for hiding this comment

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

this logic is hurting my head a bit.

Is there a situation where the child_cursor.is_some() ends up unintentionally overriding the child's cursor_setting?

or... what if both a parent and a child have called set_cursor, and the mouse moves over to the child, and then back to the parent, does the parent end up correctly resetting the cursor? This happens at the grandparent, in its own merge_up?

I think I'm satisfied, but want to at least write that concern down.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've re-written the logic so that it's only merging self.cursor_change and child.cursor to make self.cursor. (i.e. child.cursor_change is no longer involved, so there's less multi-generational thinking involved)

I've also tried to make it clearer that cursor gets reset on every pass through the tree, whereas cursor_change is persistent.

@jneem
Copy link
Collaborator Author

jneem commented Dec 3, 2020

Thanks for the comments! I think I've addressed them all, and I also realized that I never updated Split to clear the cursor...

Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

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

Great, thanks again for taking this on!

druid-shell/src/mouse.rs Outdated Show resolved Hide resolved
Co-authored-by: Colin Rofls <colin@cmyr.net>
@jneem jneem merged commit 3eb1a84 into linebender:master Dec 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants