diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f29ff23d5..8d34610814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/druid-shell/src/mouse.rs b/druid-shell/src/mouse.rs index 938e2b8af1..a20e4cda53 100644 --- a/druid-shell/src/mouse.rs +++ b/druid-shell/src/mouse.rs @@ -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, @@ -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), } diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index 9947f34b12..b19e700771 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -164,7 +164,7 @@ pub(crate) struct WindowState { deferred_queue: RefCell>, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct CustomCursor(gdk::Cursor); impl WindowBuilder { diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index a02db38c9a..8f43d80732 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -146,7 +146,7 @@ struct ViewState { text: PietText, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] // TODO: support custom cursors pub struct CustomCursor; diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index 6dc8dc0bef..c94b6d1fb0 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -100,7 +100,7 @@ struct WindowState { } // TODO: support custom cursors -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct CustomCursor; impl WindowState { diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index 3e4cfe9fbc..e51c0e1218 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -237,9 +237,10 @@ struct DxgiState { swap_chain: *mut IDXGISwapChain1, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct CustomCursor(Arc); +#[derive(PartialEq)] struct HCursor(HCURSOR); impl Drop for HCursor { diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 5ebf1d7159..ed7feef20b 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -532,7 +532,7 @@ struct PresentData { last_ust: Option, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct CustomCursor(xproto::Cursor); impl Window { diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 813ad82cfd..b8263b72e0 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -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" diff --git a/druid/examples/cursor.rs b/druid/examples/cursor.rs index 2f5fd1eb24..a7596210a1 100644 --- a/druid/examples/cursor.rs +++ b/druid/examples/cursor.rs @@ -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 @@ -44,28 +42,31 @@ impl> Controller 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 { 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 {}) @@ -75,31 +76,33 @@ fn ui_builder() -> impl Widget { #[derive(Clone, Data, Lens)] struct AppState { - cursor: Rc, - custom: Option>, + cursor: Cursor, + custom: Option, // 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 { - 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() { @@ -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, }; diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 542faab245..e6be0470ba 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -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; @@ -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, - pub(crate) cursor: &'a mut Option, pub(crate) is_handled: bool, pub(crate) is_root: bool, } @@ -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, pub(crate) prev_env: Option<&'a Env>, pub(crate) env: &'a Env, } @@ -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; } }); diff --git a/druid/src/core.rs b/druid/src/core.rs index 33ab292d1c..ff22022552 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -21,7 +21,7 @@ use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; use crate::util::ExtendDrain; use crate::{ - ArcStr, BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, + ArcStr, BoxConstraints, Color, Command, Cursor, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, Notification, PaintCtx, Region, RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, }; @@ -125,6 +125,11 @@ pub(crate) struct WidgetState { pub(crate) children_changed: bool, /// Associate timers with widgets that requested them. pub(crate) timers: HashMap, + /// The cursor that was set using one of the context methods. + pub(crate) cursor_change: CursorChange, + /// The result of merging up children cursors. This gets cleared when merging state up (unlike + /// cursor_change, which is persistent). + pub(crate) cursor: Option, } /// Methods by which a widget can attempt to change focus state. @@ -140,6 +145,18 @@ pub(crate) enum FocusChange { Previous, } +/// The possible cursor states for a widget. +#[derive(Clone)] +pub(crate) enum CursorChange { + /// No cursor has been set. + Default, + /// Someone set a cursor, but if a child widget also set their cursor then we'll use theirs + /// instead of ours. + Set(Cursor), + /// Someone set a cursor, and we'll use it regardless of what the children say. + Override(Cursor), +} + impl> WidgetPod { /// Create a new widget pod. /// @@ -569,7 +586,6 @@ impl> WidgetPod { state: parent_ctx.state, widget_state: &mut self.state, notifications: parent_ctx.notifications, - cursor: parent_ctx.cursor, is_handled: false, is_root: false, }; @@ -759,7 +775,6 @@ impl> WidgetPod { if recurse { let mut notifications = VecDeque::new(); let mut inner_ctx = EventCtx { - cursor: ctx.cursor, state: ctx.state, widget_state: &mut self.state, notifications: &mut notifications, @@ -795,14 +810,12 @@ impl> WidgetPod { env: &Env, ) { let EventCtx { - cursor, state, notifications: parent_notifications, .. } = ctx; let mut sentinal = VecDeque::new(); let mut inner_ctx = EventCtx { - cursor, state, notifications: &mut sentinal, widget_state: &mut self.state, @@ -983,7 +996,6 @@ impl> WidgetPod { let mut child_ctx = UpdateCtx { state: ctx.state, widget_state: &mut self.state, - cursor: ctx.cursor, prev_env, env, }; @@ -1031,6 +1043,8 @@ impl WidgetState { children: Bloom::new(), children_changed: false, timers: HashMap::new(), + cursor_change: CursorChange::Default, + cursor: None, } } @@ -1069,6 +1083,21 @@ impl WidgetState { self.request_update |= child_state.request_update; self.request_focus = child_state.request_focus.take().or(self.request_focus); self.timers.extend_drain(&mut child_state.timers); + + // We reset `child_state.cursor` no matter what, so that on the every pass through the tree, + // things will be recalculated just from `cursor_change`. + let child_cursor = child_state.cursor.take(); + if let CursorChange::Override(cursor) = &self.cursor_change { + self.cursor = Some(cursor.clone()); + } else if child_state.has_active || child_state.is_hot { + self.cursor = child_cursor; + } + + if self.cursor.is_none() { + if let CursorChange::Set(cursor) = &self.cursor_change { + self.cursor = Some(cursor.clone()); + } + } } #[inline] diff --git a/druid/src/mouse.rs b/druid/src/mouse.rs index ed006a6096..bdb2c5ae9b 100644 --- a/druid/src/mouse.rs +++ b/druid/src/mouse.rs @@ -15,7 +15,7 @@ //! The mousey bits use crate::kurbo::{Point, Vec2}; -use crate::{Modifiers, MouseButton, MouseButtons}; +use crate::{Cursor, Data, Modifiers, MouseButton, MouseButtons}; /// The state of the mouse for a click, mouse-up, move, or wheel event. /// @@ -94,3 +94,9 @@ impl From for MouseEvent { } } } + +impl Data for Cursor { + fn same(&self, other: &Cursor) -> bool { + self == other + } +} diff --git a/druid/src/widget/split.rs b/druid/src/widget/split.rs index 22313a76b1..3d2c0053a8 100644 --- a/druid/src/widget/split.rs +++ b/druid/src/widget/split.rs @@ -299,11 +299,15 @@ impl Widget for Split { ctx.request_layout(); } - if ctx.is_hot() && self.bar_hit_test(ctx.size(), mouse.pos) || ctx.is_active() { - match self.split_axis { - Axis::Horizontal => ctx.set_cursor(&Cursor::ResizeLeftRight), - Axis::Vertical => ctx.set_cursor(&Cursor::ResizeUpDown), - }; + if ctx.is_hot() || ctx.is_active() { + if self.bar_hit_test(ctx.size(), mouse.pos) { + match self.split_axis { + Axis::Horizontal => ctx.set_cursor(&Cursor::ResizeLeftRight), + Axis::Vertical => ctx.set_cursor(&Cursor::ResizeUpDown), + }; + } else { + ctx.clear_cursor(); + } } } _ => {} diff --git a/druid/src/window.rs b/druid/src/window.rs index f99181d349..c12690eb07 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -184,11 +184,6 @@ impl Window { _ => (), } - let mut cursor = match event { - Event::MouseMove(..) => Some(Cursor::Arrow), - _ => None, - }; - let event = match event { Event::Timer(token) => { if let Some(widget_id) = self.timers.get(&token) { @@ -217,7 +212,6 @@ impl Window { ContextState::new::(queue, &self.ext_handle, &self.handle, self.id, self.focus); let mut notifications = VecDeque::new(); let mut ctx = EventCtx { - cursor: &mut cursor, state: &mut state, notifications: &mut notifications, widget_state: &mut widget_state, @@ -252,8 +246,10 @@ impl Window { } } - if let Some(cursor) = cursor { + if let Some(cursor) = &widget_state.cursor { self.handle.set_cursor(&cursor); + } else if matches!(event, Event::MouseMove(..)) { + self.handle.set_cursor(&Cursor::Arrow); } self.post_event_processing(&mut widget_state, queue, data, env, false); @@ -286,18 +282,16 @@ impl Window { let mut widget_state = WidgetState::new(self.root.id(), Some(self.size)); let mut state = ContextState::new::(queue, &self.ext_handle, &self.handle, self.id, self.focus); - let mut cursor = None; let mut update_ctx = UpdateCtx { widget_state: &mut widget_state, - cursor: &mut cursor, state: &mut state, prev_env: None, env, }; self.root.update(&mut update_ctx, data, env); - if let Some(cursor) = cursor { - self.handle.set_cursor(&cursor); + if let Some(cursor) = &widget_state.cursor { + self.handle.set_cursor(cursor); } self.post_event_processing(&mut widget_state, queue, data, env, false);