Skip to content

Commit

Permalink
Make the cursor API stateful and widget-local. (#1433)
Browse files Browse the repository at this point in the history
  • Loading branch information
jneem authored Dec 4, 2020
1 parent 9e83942 commit 3eb1a84
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 84 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ You can find its changes [documented below](#060---2020-06-01).
- `Delegate::command` now returns `Handled`, not `bool` ([#1298] by [@jneem])
- `TextBox` selects all contents when tabbed to on macOS ([#1283] by [@cmyr])
- All Image formats are now optional, reducing compile time and binary size by default ([#1340] by [@JAicewizard])
- The `Cursor` API has changed to a stateful one ([#1433] by [@jneem])

### Deprecated

Expand Down Expand Up @@ -542,6 +543,7 @@ Last release without a changelog :(
[#1361]: https://github.com/linebender/druid/pull/1361
[#1371]: https://github.com/linebender/druid/pull/1371
[#1410]: https://github.com/linebender/druid/pull/1410
[#1433]: https://github.com/linebender/druid/pull/1433
[#1438]: https://github.com/linebender/druid/pull/1438
[#1441]: https://github.com/linebender/druid/pull/1441

Expand Down
7 changes: 4 additions & 3 deletions druid-shell/src/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,9 @@ impl std::fmt::Debug for MouseButtons {
}

//NOTE: this currently only contains cursors that are included by default on
//both Windows and macOS. We may want to provide polyfills for various additional cursors,
//and we will also want to add some mechanism for adding custom cursors.
//both Windows and macOS. We may want to provide polyfills for various additional cursors.
/// Mouse cursors.
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub enum Cursor {
/// The default arrow cursor.
Arrow,
Expand All @@ -254,6 +253,8 @@ pub enum Cursor {
NotAllowed,
ResizeLeftRight,
ResizeUpDown,
// The platform cursor should be small. Any image data that it uses should be shared (i.e.
// behind an `Arc` or using a platform API that does the sharing).
Custom(platform::window::CustomCursor),
}

Expand Down
2 changes: 1 addition & 1 deletion druid-shell/src/platform/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ pub(crate) struct WindowState {
deferred_queue: RefCell<Vec<DeferredOp>>,
}

#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct CustomCursor(gdk::Cursor);

impl WindowBuilder {
Expand Down
2 changes: 1 addition & 1 deletion druid-shell/src/platform/mac/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ struct ViewState {
text: PietText,
}

#[derive(Clone)]
#[derive(Clone, PartialEq)]
// TODO: support custom cursors
pub struct CustomCursor;

Expand Down
2 changes: 1 addition & 1 deletion druid-shell/src/platform/web/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ struct WindowState {
}

// TODO: support custom cursors
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct CustomCursor;

impl WindowState {
Expand Down
3 changes: 2 additions & 1 deletion druid-shell/src/platform/windows/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,10 @@ struct DxgiState {
swap_chain: *mut IDXGISwapChain1,
}

#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct CustomCursor(Arc<HCursor>);

#[derive(PartialEq)]
struct HCursor(HCURSOR);

impl Drop for HCursor {
Expand Down
2 changes: 1 addition & 1 deletion druid-shell/src/platform/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ struct PresentData {
last_ust: Option<u64>,
}

#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct CustomCursor(xproto::Cursor);

impl Window {
Expand Down
4 changes: 2 additions & 2 deletions druid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ piet-common = { version = "=0.2.0-pre6", features = ["png"] }

[[example]]
name = "cursor"
required-features = ["image"]
required-features = ["image", "png"]

[[example]]
name = "image"
required-features = ["image"]
required-features = ["image", "png"]

[[example]]
name = "invalidation"
Expand Down
75 changes: 39 additions & 36 deletions druid/examples/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ use druid::{
use druid::widget::prelude::*;
use druid::widget::{Button, Controller};

use std::rc::Rc;

/// This Controller switches the current cursor based on the selection.
/// The crucial part of this code is actually making and initialising
/// the cursor. This happens here. Because we cannot make the cursor
Expand All @@ -44,28 +42,31 @@ impl<W: Widget<AppState>> Controller<AppState, W> for CursorArea {
data: &mut AppState,
env: &Env,
) {
match event {
Event::WindowConnected => {
data.custom = ctx.window().make_cursor(&data.custom_desc).map(Rc::new);
}
Event::MouseMove(_) => {
// Because the cursor is reset to the default on every `MouseMove`
// event we have to explicitly overwrite this every event.
ctx.set_cursor(&data.cursor);
}
_ => {}
if let Event::WindowConnected = event {
data.custom = ctx.window().make_cursor(&data.custom_desc);
}
child.event(ctx, event, data, env);
}

fn update(
&mut self,
child: &mut W,
ctx: &mut UpdateCtx,
old_data: &AppState,
data: &AppState,
env: &Env,
) {
if data.cursor != old_data.cursor {
ctx.set_cursor(&data.cursor);
}
child.update(ctx, old_data, data, env);
}
}

fn ui_builder() -> impl Widget<AppState> {
Button::new("Change cursor")
.on_click(|ctx, data: &mut AppState, _env| {
data.cursor = next_cursor(&data.cursor, data.custom.clone());
// After changing the cursor, we need to update the active cursor
// via the context in order for the change to take effect immediately.
ctx.set_cursor(&data.cursor);
.on_click(|_ctx, data: &mut AppState, _env| {
data.next_cursor();
})
.padding(50.0)
.controller(CursorArea {})
Expand All @@ -75,31 +76,33 @@ fn ui_builder() -> impl Widget<AppState> {

#[derive(Clone, Data, Lens)]
struct AppState {
cursor: Rc<Cursor>,
custom: Option<Rc<Cursor>>,
cursor: Cursor,
custom: Option<Cursor>,
// To see what #[data(ignore)] does look at the docs.rs page on `Data`:
// https://docs.rs/druid/0.6.0/druid/trait.Data.html
#[data(ignore)]
custom_desc: CursorDesc,
}

fn next_cursor(c: &Cursor, custom: Option<Rc<Cursor>>) -> Rc<Cursor> {
Rc::new(match c {
Cursor::Arrow => Cursor::IBeam,
Cursor::IBeam => Cursor::Crosshair,
Cursor::Crosshair => Cursor::OpenHand,
Cursor::OpenHand => Cursor::NotAllowed,
Cursor::NotAllowed => Cursor::ResizeLeftRight,
Cursor::ResizeLeftRight => Cursor::ResizeUpDown,
Cursor::ResizeUpDown => {
if let Some(custom) = custom {
return custom;
} else {
Cursor::Arrow
impl AppState {
fn next_cursor(&mut self) {
self.cursor = match self.cursor {
Cursor::Arrow => Cursor::IBeam,
Cursor::IBeam => Cursor::Crosshair,
Cursor::Crosshair => Cursor::OpenHand,
Cursor::OpenHand => Cursor::NotAllowed,
Cursor::NotAllowed => Cursor::ResizeLeftRight,
Cursor::ResizeLeftRight => Cursor::ResizeUpDown,
Cursor::ResizeUpDown => {
if let Some(custom) = &self.custom {
custom.clone()
} else {
Cursor::Arrow
}
}
}
Cursor::Custom(_) => Cursor::Arrow,
})
Cursor::Custom(_) => Cursor::Arrow,
};
}
}

pub fn main() {
Expand All @@ -110,7 +113,7 @@ pub fn main() {
let custom_desc = CursorDesc::new(cursor_image, (0.0, 0.0));

let data = AppState {
cursor: Rc::new(Cursor::Arrow),
cursor: Cursor::Arrow,
custom: None,
custom_desc,
};
Expand Down
49 changes: 34 additions & 15 deletions druid/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::{
time::Duration,
};

use crate::core::{CommandQueue, FocusChange, WidgetState};
use crate::core::{CommandQueue, CursorChange, FocusChange, WidgetState};
use crate::env::KeyLike;
use crate::piet::{Piet, PietText, RenderContext};
use crate::shell::Region;
Expand Down Expand Up @@ -67,7 +67,6 @@ pub struct EventCtx<'a, 'b> {
pub(crate) state: &'a mut ContextState<'b>,
pub(crate) widget_state: &'a mut WidgetState,
pub(crate) notifications: &'a mut VecDeque<Notification>,
pub(crate) cursor: &'a mut Option<Cursor>,
pub(crate) is_handled: bool,
pub(crate) is_root: bool,
}
Expand Down Expand Up @@ -95,7 +94,6 @@ pub struct LifeCycleCtx<'a, 'b> {
pub struct UpdateCtx<'a, 'b> {
pub(crate) state: &'a mut ContextState<'b>,
pub(crate) widget_state: &'a mut WidgetState,
pub(crate) cursor: &'a mut Option<Cursor>,
pub(crate) prev_env: Option<&'a Env>,
pub(crate) env: &'a Env,
}
Expand Down Expand Up @@ -258,20 +256,41 @@ impl_context_method!(
impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, {
/// Set the cursor icon.
///
/// Call this when handling a mouse move event, to set the cursor for the
/// widget. A container widget can safely call this method, then recurse
/// to its children, as a sequence of calls within an event propagation
/// only has the effect of the last one (ie no need to worry about
/// flashing).
/// This setting will be retained until [`clear_cursor`] is called, but it will only take
/// effect when this widget is either [`hot`] or [`active`]. If a child widget also sets a
/// cursor, the child widget's cursor will take precedence. (If that isn't what you want, use
/// [`override_cursor`] instead.)
///
/// This method is expected to be called mostly from the [`MouseMove`]
/// event handler, but can also be called in response to other events,
/// for example pressing a key to change the behavior of a widget, or
/// in response to data changes.
///
/// [`MouseMove`]: enum.Event.html#variant.MouseMove
/// [`clear_cursor`]: EventCtx::clear_cursor
/// [`override_cursor`]: EventCtx::override_cursor
/// [`hot`]: EventCtx::is_hot
/// [`active`]: EventCtx::is_active
pub fn set_cursor(&mut self, cursor: &Cursor) {
*self.cursor = Some(cursor.clone());
self.widget_state.cursor_change = CursorChange::Set(cursor.clone());
}

/// Override the cursor icon.
///
/// This setting will be retained until [`clear_cursor`] is called, but it will only take
/// effect when this widget is either [`hot`] or [`active`]. This will override the cursor
/// preferences of a child widget. (If that isn't what you want, use [`set_cursor`] instead.)
///
/// [`clear_cursor`]: EventCtx::clear_cursor
/// [`set_cursor`]: EventCtx::override_cursor
/// [`hot`]: EventCtx::is_hot
/// [`active`]: EventCtx::is_active
pub fn override_cursor(&mut self, cursor: &Cursor) {
self.widget_state.cursor_change = CursorChange::Override(cursor.clone());
}

/// Clear the cursor icon.
///
/// This undoes the effect of [`set_cursor`] and [`override_cursor`].
///
/// [`override_cursor`]: EventCtx::override_cursor
/// [`set_cursor`]: EventCtx::set_cursor
pub fn clear_cursor(&mut self) {
self.widget_state.cursor_change = CursorChange::Default;
}
});

Expand Down
Loading

0 comments on commit 3eb1a84

Please sign in to comment.