diff --git a/masonry/src/contexts.rs b/masonry/src/contexts.rs index fa0c4c9b0..8886304e3 100644 --- a/masonry/src/contexts.rs +++ b/masonry/src/contexts.rs @@ -16,9 +16,7 @@ use crate::render_root::{MutateCallback, RenderRootSignal, RenderRootState}; use crate::text::TextBrush; use crate::tree_arena::{ArenaMutChildren, ArenaRefChildren}; use crate::widget::{WidgetMut, WidgetRef, WidgetState}; -use crate::{ - AllowRawMut, BoxConstraints, CursorIcon, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod, -}; +use crate::{AllowRawMut, BoxConstraints, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod}; // Note - Most methods defined in this file revolve around `WidgetState` fields. // Consider reading `WidgetState` documentation (especially the documented naming scheme) @@ -371,32 +369,13 @@ impl_context_method!( // --- MARK: CURSOR --- // Cursor-related impls. impl_context_method!(EventCtx<'_>, { - // TODO - Rewrite doc - /// Set the cursor icon. + /// Notifies Masonry that the cursor returned by [`Widget::get_cursor`] has changed. /// - /// This setting will be retained until [`clear_cursor`] is called, but it will only take - /// effect when this widget [`is_hovered`] and/or [`has_pointer_capture`]. 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.) - /// - /// [`clear_cursor`]: EventCtx::clear_cursor - /// [`override_cursor`]: EventCtx::override_cursor - /// [`is_hovered`]: EventCtx::is_hovered - /// [`has_pointer_capture`]: EventCtx::has_pointer_capture - pub fn set_cursor(&mut self, cursor: &CursorIcon) { - trace!("set_cursor {:?}", cursor); - self.widget_state.cursor = Some(*cursor); - } - - /// 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) { - trace!("clear_cursor"); - self.widget_state.cursor = None; + /// This is mostly meant for cases where the cursor changes even if the pointer doesn't + /// move, because the nature of the widget has changed somehow. + pub fn cursor_icon_changed(&mut self) { + trace!("cursor_icon_changed"); + self.global_state.needs_pointer_pass = true; } }); diff --git a/masonry/src/passes/update.rs b/masonry/src/passes/update.rs index d57441ce9..655bc6185 100644 --- a/masonry/src/passes/update.rs +++ b/masonry/src/passes/update.rs @@ -10,7 +10,9 @@ use crate::passes::event::run_on_pointer_event_pass; use crate::passes::{merge_state_up, recurse_on_children}; use crate::render_root::{RenderRoot, RenderRootSignal, RenderRootState}; use crate::tree_arena::ArenaMut; -use crate::{PointerEvent, RegisterCtx, Update, UpdateCtx, Widget, WidgetId, WidgetState}; +use crate::{ + PointerEvent, QueryCtx, RegisterCtx, Update, UpdateCtx, Widget, WidgetId, WidgetState, +}; // --- MARK: HELPERS --- fn get_id_path(root: &RenderRoot, widget_id: Option) -> Vec { @@ -235,6 +237,11 @@ fn update_disabled_for_widget( pub(crate) fn run_update_disabled_pass(root: &mut RenderRoot) { let _span = info_span!("update_disabled").entered(); + // If a widget was enabled or disabled, the pointer icon may need to change. + if root.root_state().needs_update_disabled { + root.global_state.needs_pointer_pass = true; + } + let (root_widget, root_state) = root.widget_arena.get_pair_mut(root.root.id()); update_disabled_for_widget(&mut root.global_state, root_widget, root_state, false); } @@ -627,9 +634,21 @@ pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot) { .pointer_capture_target .or(next_hovered_widget); - let new_cursor = if let Some(cursor_source) = cursor_source { + let new_cursor = if let (Some(cursor_source), Some(pos)) = (cursor_source, pointer_pos) { let (widget, state) = root.widget_arena.get_pair(cursor_source); - state.item.cursor.unwrap_or(widget.item.get_cursor()) + + let ctx = QueryCtx { + global_state: &root.global_state, + widget_state_children: state.children, + widget_children: widget.children, + widget_state: state.item, + }; + + if state.item.is_disabled { + CursorIcon::Default + } else { + widget.item.get_cursor(&ctx, pos) + } } else { CursorIcon::Default }; diff --git a/masonry/src/testing/helper_widgets.rs b/masonry/src/testing/helper_widgets.rs index accb1f5e2..a2cf4745c 100644 --- a/masonry/src/testing/helper_widgets.rs +++ b/masonry/src/testing/helper_widgets.rs @@ -386,7 +386,7 @@ impl Widget for ModularWidget { None } - fn get_cursor(&self) -> CursorIcon { + fn get_cursor(&self, _ctx: &QueryCtx, _pos: Point) -> CursorIcon { CursorIcon::Default } @@ -586,8 +586,8 @@ impl Widget for Recorder { self.child.get_debug_text() } - fn get_cursor(&self) -> CursorIcon { - self.child.get_cursor() + fn get_cursor(&self, ctx: &QueryCtx, pos: Point) -> CursorIcon { + self.child.get_cursor(ctx, pos) } fn get_child_at_pos<'c>( diff --git a/masonry/src/widget/prose.rs b/masonry/src/widget/prose.rs index d8647144d..ecd3f1530 100644 --- a/masonry/src/widget/prose.rs +++ b/masonry/src/widget/prose.rs @@ -15,7 +15,7 @@ use crate::widget::label::LABEL_X_PADDING; use crate::widget::{LineBreaking, WidgetMut}; use crate::{ AccessCtx, AccessEvent, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, PaintCtx, - PointerEvent, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, + PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, }; /// The prose widget is a widget which displays text which can be @@ -150,15 +150,12 @@ impl Widget for Prose { } } PointerEvent::PointerMove(state) => { - if !ctx.is_disabled() { - // TODO: Set cursor if over link - ctx.set_cursor(&CursorIcon::Text); - if ctx.has_pointer_capture() - && self.text_layout.pointer_move(inner_origin, state) - { - // We might have changed text colours, so we need to re-request a layout - ctx.request_layout(); - } + if !ctx.is_disabled() + && ctx.has_pointer_capture() + && self.text_layout.pointer_move(inner_origin, state) + { + // We might have changed text colours, so we need to re-request a layout + ctx.request_layout(); } } PointerEvent::PointerUp(button, state) => { @@ -265,6 +262,11 @@ impl Widget for Prose { } } + fn get_cursor(&self, _ctx: &QueryCtx, _pos: Point) -> CursorIcon { + // TODO: Set cursor if over link + CursorIcon::Text + } + fn accessibility_role(&self) -> Role { Role::Document } diff --git a/masonry/src/widget/split.rs b/masonry/src/widget/split.rs index b66d94845..f82833c0b 100644 --- a/masonry/src/widget/split.rs +++ b/masonry/src/widget/split.rs @@ -8,7 +8,6 @@ use smallvec::{smallvec, SmallVec}; use tracing::{trace_span, warn, Span}; use vello::Scene; -use crate::dpi::LogicalPosition; use crate::event::PointerButton; use crate::kurbo::Line; use crate::paint_scene_helpers::{fill_color, stroke}; @@ -16,7 +15,7 @@ use crate::widget::flex::Axis; use crate::widget::{WidgetMut, WidgetPod}; use crate::{ theme, AccessCtx, AccessEvent, BoxConstraints, Color, CursorIcon, EventCtx, LayoutCtx, - PaintCtx, Point, PointerEvent, Rect, RegisterCtx, Size, TextEvent, Widget, WidgetId, + PaintCtx, Point, PointerEvent, QueryCtx, Rect, RegisterCtx, Size, TextEvent, Widget, WidgetId, }; // TODO - Have child widget type as generic argument @@ -31,11 +30,6 @@ pub struct Split { min_bar_area: f64, // Integers only solid: bool, draggable: bool, - /// The split bar is hovered by the mouse. This state is locked to `true` if the - /// widget is active (the bar is being dragged) to avoid cursor and painting jitter - /// if the mouse moves faster than the layout and temporarily gets outside of the - /// bar area while still being dragged. - is_bar_hover: bool, /// Offset from the split point (bar center) to the actual mouse position when the /// bar was clicked. This is used to ensure a click without mouse move is a no-op, /// instead of re-centering the bar on the mouse. @@ -60,7 +54,6 @@ impl Split { min_bar_area: 6.0, solid: false, draggable: false, - is_bar_hover: false, click_offset: 0.0, child1: WidgetPod::new(child1).boxed(), child2: WidgetPod::new(child2).boxed(), @@ -199,7 +192,7 @@ impl Split { } /// Returns true if the provided mouse position is inside the splitter bar area. - fn bar_hit_test(&self, size: Size, mouse_pos: LogicalPosition) -> bool { + fn bar_hit_test(&self, size: Size, mouse_pos: Point) -> bool { let (edge1, edge2) = self.bar_edges(size); match self.split_axis { Axis::Horizontal => mouse_pos.x >= edge1 && mouse_pos.x <= edge2, @@ -375,7 +368,9 @@ impl Widget for Split { if self.draggable { match event { PointerEvent::PointerDown(PointerButton::Primary, state) => { - if self.bar_hit_test(ctx.size(), state.position) { + let mouse_pos = Point::new(state.position.x, state.position.y); + let local_mouse_pos = mouse_pos - ctx.window_origin().to_vec2(); + if self.bar_hit_test(ctx.size(), local_mouse_pos) { ctx.set_handled(); ctx.capture_pointer(); // Save the delta between the mouse click position and the split point @@ -383,26 +378,6 @@ impl Widget for Split { Axis::Horizontal => state.position.x, Axis::Vertical => state.position.y, } - self.bar_position(ctx.size()); - // If not already hovering, force and change cursor appropriately - if !self.is_bar_hover { - self.is_bar_hover = true; - match self.split_axis { - Axis::Horizontal => ctx.set_cursor(&CursorIcon::EwResize), - Axis::Vertical => ctx.set_cursor(&CursorIcon::NsResize), - }; - } - } - } - PointerEvent::PointerUp(PointerButton::Primary, state) => { - if ctx.has_pointer_capture() { - ctx.set_handled(); - // Depending on where the mouse cursor is when the button is released, - // the cursor might or might not need to be changed - self.is_bar_hover = - ctx.is_hovered() && self.bar_hit_test(ctx.size(), state.position); - if !self.is_bar_hover { - ctx.clear_cursor(); - } } } PointerEvent::PointerMove(state) => { @@ -418,21 +393,6 @@ impl Widget for Split { }; self.update_split_point(ctx.size(), effective_pos); ctx.request_layout(); - } else { - // If not active, set cursor when hovering state changes - let hover = - ctx.is_hovered() && self.bar_hit_test(ctx.size(), state.position); - if self.is_bar_hover != hover { - self.is_bar_hover = hover; - if hover { - match self.split_axis { - Axis::Horizontal => ctx.set_cursor(&CursorIcon::EwResize), - Axis::Vertical => ctx.set_cursor(&CursorIcon::NsResize), - }; - } else { - ctx.clear_cursor(); - } - } } } _ => {} @@ -556,6 +516,20 @@ impl Widget for Split { } } + fn get_cursor(&self, ctx: &QueryCtx, pos: Point) -> CursorIcon { + let local_mouse_pos = pos - ctx.window_origin().to_vec2(); + let is_bar_hovered = self.bar_hit_test(ctx.size(), local_mouse_pos); + + if ctx.has_pointer_capture() || is_bar_hovered { + match self.split_axis { + Axis::Horizontal => CursorIcon::EwResize, + Axis::Vertical => CursorIcon::NsResize, + } + } else { + CursorIcon::Default + } + } + fn accessibility_role(&self) -> Role { Role::Splitter } diff --git a/masonry/src/widget/textbox.rs b/masonry/src/widget/textbox.rs index 1d8369b58..9268ba572 100644 --- a/masonry/src/widget/textbox.rs +++ b/masonry/src/widget/textbox.rs @@ -15,7 +15,7 @@ use crate::text::{TextBrush, TextEditor, TextWithSelection}; use crate::widget::{LineBreaking, WidgetMut}; use crate::{ AccessCtx, AccessEvent, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, PaintCtx, - PointerEvent, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, + PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, }; const TEXTBOX_PADDING: f64 = 3.0; @@ -308,7 +308,7 @@ impl Widget for Textbox { ); } - fn get_cursor(&self) -> CursorIcon { + fn get_cursor(&self, _ctx: &QueryCtx, _pos: Point) -> CursorIcon { CursorIcon::Text } diff --git a/masonry/src/widget/widget.rs b/masonry/src/widget/widget.rs index bfa34ec67..1edc04c6b 100644 --- a/masonry/src/widget/widget.rs +++ b/masonry/src/widget/widget.rs @@ -220,9 +220,11 @@ pub trait Widget: AsAny { None } - // TODO - Document - // TODO - Add &UpdateCtx argument - fn get_cursor(&self) -> CursorIcon { + /// Return the cursor icon for this widget. + /// + /// **pos** - the mouse position in global coordinates (e.g. `(0,0)` is the top-left corner of the + /// window). + fn get_cursor(&self, ctx: &QueryCtx, pos: Point) -> CursorIcon { CursorIcon::Default } @@ -471,8 +473,8 @@ impl Widget for Box { self.deref().get_debug_text() } - fn get_cursor(&self) -> CursorIcon { - self.deref().get_cursor() + fn get_cursor(&self, ctx: &QueryCtx, pos: Point) -> CursorIcon { + self.deref().get_cursor(ctx, pos) } fn get_child_at_pos<'c>( diff --git a/masonry/src/widget/widget_state.rs b/masonry/src/widget/widget_state.rs index 31bc21603..b1f7e50a2 100644 --- a/masonry/src/widget/widget_state.rs +++ b/masonry/src/widget/widget_state.rs @@ -5,7 +5,7 @@ use vello::kurbo::{Insets, Point, Rect, Size, Vec2}; -use crate::{CursorIcon, WidgetId}; +use crate::WidgetId; // TODO - Reduce WidgetState size. // See https://github.com/linebender/xilem/issues/706 @@ -126,9 +126,6 @@ pub(crate) struct WidgetState { pub(crate) children_changed: bool, - // TODO - Remove and handle in WidgetRoot instead - pub(crate) cursor: Option, - // --- STATUS --- /// This widget has been disabled. pub(crate) is_explicitly_disabled: bool, @@ -192,7 +189,6 @@ impl WidgetState { needs_update_stashed: true, focus_chain: Vec::new(), children_changed: true, - cursor: None, update_focus_chain: true, #[cfg(debug_assertions)] widget_name,