From 2fe1293fbafa936568ca10f75af0095f2bc158fc Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 6 Jun 2025 22:56:27 +0200 Subject: [PATCH] Improve cursor style behavior for some draggable elements (#31965) Follow-up to #24797 This PR ensures some cursor styles do not change for draggable elements during dragging. The linked PR covered this on the higher level for draggable divs. However, e.g. the pane divider inbetween two editors is not a draggable div and thus still has the issue that the cursor style changes during dragging. This PR fixes this issue by setting the hitbox to `None` in cases where the element is currently being dragged, which ensures the cursor style is applied to the cursor no matter what during dragging. Namely, this change fixes this for - non-div pane dividers - minimap slider and the - editor scrollbars and implements it for the UI scrollbars (Notably, UI scrollbars do already have `cursor_default` on their parent container but would not keep this during dragging. I opted out on removing this from the parent containers until #30194 or a similar PR is merged). https://github.com/user-attachments/assets/f97859dd-5f1d-4449-ab92-c27f2d933c4a Release Notes: - N/A --- crates/editor/src/element.rs | 35 +++++++++++------- crates/gpui/examples/window_shadow.rs | 2 +- crates/gpui/src/elements/div.rs | 4 +- crates/gpui/src/elements/text.rs | 2 +- crates/gpui/src/window.rs | 39 ++++++++++++++------ crates/markdown/src/markdown.rs | 4 +- crates/terminal_view/src/terminal_element.rs | 4 +- crates/ui/src/components/indent_guides.rs | 2 +- crates/ui/src/components/scrollbar.rs | 25 ++++++++++--- crates/workspace/src/pane_group.rs | 12 +++++- crates/workspace/src/workspace.rs | 2 +- 11 files changed, 88 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 073aac287377e93c3e568f3a710f481f35b4ee67..fadabaf0352b66d62539ccfa0581806b0e3d56d1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5131,7 +5131,7 @@ impl EditorElement { let is_singleton = self.editor.read(cx).is_singleton(cx); let line_height = layout.position_map.line_height; - window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox)); + window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); for LineNumberLayout { shaped_line, @@ -5158,9 +5158,9 @@ impl EditorElement { // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox)); + window.set_cursor_style(CursorStyle::IBeam, &hitbox); } else { - window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox)); + window.set_cursor_style(CursorStyle::PointingHand, &hitbox); } } } @@ -5378,7 +5378,7 @@ impl EditorElement { .read(cx) .all_diff_hunks_expanded() { - window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox)); + window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); } } } @@ -5452,7 +5452,7 @@ impl EditorElement { |window| { let editor = self.editor.read(cx); if editor.mouse_cursor_hidden { - window.set_cursor_style(CursorStyle::None, None); + window.set_window_cursor_style(CursorStyle::None); } else if editor .hovered_link_state .as_ref() @@ -5460,13 +5460,10 @@ impl EditorElement { { window.set_cursor_style( CursorStyle::PointingHand, - Some(&layout.position_map.text_hitbox), + &layout.position_map.text_hitbox, ); } else { - window.set_cursor_style( - CursorStyle::IBeam, - Some(&layout.position_map.text_hitbox), - ); + window.set_cursor_style(CursorStyle::IBeam, &layout.position_map.text_hitbox); }; self.paint_lines_background(layout, window, cx); @@ -5607,6 +5604,7 @@ impl EditorElement { let Some(scrollbars_layout) = layout.scrollbars_layout.take() else { return; }; + let any_scrollbar_dragged = self.editor.read(cx).scroll_manager.any_scrollbar_dragged(); for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() { let hitbox = &scrollbar_layout.hitbox; @@ -5672,7 +5670,11 @@ impl EditorElement { BorderStyle::Solid, )); - window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox)); + if any_scrollbar_dragged { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, &hitbox); + } } }) } @@ -5740,7 +5742,7 @@ impl EditorElement { } }); - if self.editor.read(cx).scroll_manager.any_scrollbar_dragged() { + if any_scrollbar_dragged { window.on_mouse_event({ let editor = self.editor.clone(); move |_: &MouseUpEvent, phase, window, cx| { @@ -6126,6 +6128,7 @@ impl EditorElement { fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { if let Some(mut layout) = layout.minimap.take() { let minimap_hitbox = layout.thumb_layout.hitbox.clone(); + let dragging_minimap = self.editor.read(cx).scroll_manager.is_dragging_minimap(); window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| { window.with_element_namespace("minimap", |window| { @@ -6177,7 +6180,11 @@ impl EditorElement { }); }); - window.set_cursor_style(CursorStyle::Arrow, Some(&minimap_hitbox)); + if dragging_minimap { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, &minimap_hitbox); + } let minimap_axis = ScrollbarAxis::Vertical; let pixels_per_line = (minimap_hitbox.size.height / layout.max_scroll_top) @@ -6238,7 +6245,7 @@ impl EditorElement { } }); - if self.editor.read(cx).scroll_manager.is_dragging_minimap() { + if dragging_minimap { window.on_mouse_event({ let editor = self.editor.clone(); move |event: &MouseUpEvent, phase, window, cx| { diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index e75e50e31a5ca574bac93d7036ad8eaab6ef8001..06dde911330d0b82ba3584cf5fb8054f57920b93 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -61,7 +61,7 @@ impl Render for WindowShadow { CursorStyle::ResizeUpRightDownLeft } }, - Some(&hitbox), + &hitbox, ); }, ) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 4c96ede3ca118e34484a4519ab67076adde4bdf8..bbc3454923c488c9b9120a7a762ed5b85fba28ea 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1744,11 +1744,11 @@ impl Interactivity { if let Some(drag) = cx.active_drag.as_ref() { if let Some(mouse_cursor) = drag.cursor_style { - window.set_cursor_style(mouse_cursor, None); + window.set_window_cursor_style(mouse_cursor); } } else { if let Some(mouse_cursor) = style.mouse_cursor { - window.set_cursor_style(mouse_cursor, Some(hitbox)); + window.set_cursor_style(mouse_cursor, hitbox); } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 86cf4407b58036f82306cd3a637dc96320df92f6..014f617e2cfc74755908368f57060aeaeb38aa74 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -769,7 +769,7 @@ impl Element for InteractiveText { .iter() .any(|range| range.contains(&ix)) { - window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox)) + window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 20eaca0c5ea195ec33141e4588c1227fad18f5a3..8253320898bfcec9225be216bce3a5db4ea584a5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -408,7 +408,7 @@ pub(crate) type AnyMouseListener = #[derive(Clone)] pub(crate) struct CursorStyleRequest { - pub(crate) hitbox_id: Option, // None represents whole window + pub(crate) hitbox_id: HitboxId, pub(crate) style: CursorStyle, } @@ -622,6 +622,7 @@ pub(crate) struct Frame { pub(crate) input_handlers: Vec>, pub(crate) tooltip_requests: Vec>, pub(crate) cursor_styles: Vec, + window_cursor_style: Option, #[cfg(any(test, feature = "test-support"))] pub(crate) debug_bounds: FxHashMap>, #[cfg(any(feature = "inspector", debug_assertions))] @@ -666,6 +667,7 @@ impl Frame { input_handlers: Vec::new(), tooltip_requests: Vec::new(), cursor_styles: Vec::new(), + window_cursor_style: None, #[cfg(any(test, feature = "test-support"))] debug_bounds: FxHashMap::default(), @@ -691,6 +693,7 @@ impl Frame { self.window_control_hitboxes.clear(); self.deferred_draws.clear(); self.focus = None; + self.window_cursor_style = None; #[cfg(any(feature = "inspector", debug_assertions))] { @@ -699,6 +702,17 @@ impl Frame { } } + pub(crate) fn cursor_style(&self, window: &Window) -> Option { + self.window_cursor_style.or_else(|| { + self.cursor_styles.iter().rev().find_map(|request| { + request + .hitbox_id + .is_hovered(window) + .then_some(request.style) + }) + }) + } + pub(crate) fn hit_test(&self, position: Point) -> HitTest { let mut set_hover_hitbox_count = false; let mut hit_test = HitTest::default(); @@ -2157,14 +2171,23 @@ impl Window { /// Updates the cursor style at the platform level. This method should only be called /// during the prepaint phase of element drawing. - pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) { + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { self.invalidator.debug_assert_paint(); self.next_frame.cursor_styles.push(CursorStyleRequest { - hitbox_id: hitbox.map(|hitbox| hitbox.id), + hitbox_id: hitbox.id, style, }); } + /// Updates the cursor style for the entire window at the platform level. A cursor + /// style using this method will have precedence over any cursor style set using + /// `set_cursor_style`. This method should only be called during the prepaint + /// phase of element drawing. + pub fn set_window_cursor_style(&mut self, style: CursorStyle) { + self.invalidator.debug_assert_paint(); + self.next_frame.window_cursor_style = Some(style); + } + /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called /// during the paint phase of element drawing. pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { @@ -3245,15 +3268,7 @@ impl Window { if self.is_window_hovered() { let style = self .rendered_frame - .cursor_styles - .iter() - .rev() - .find(|request| { - request - .hitbox_id - .map_or(true, |hitbox_id| hitbox_id.is_hovered(self)) - }) - .map(|request| request.style) + .cursor_style(self) .unwrap_or(CursorStyle::Arrow); cx.platform.set_cursor_style(style); } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 626ffcef6f3f5771b60fe737f56fbe652e321781..172cda09bb69478eb26cbcf3d3a2bb3415d2cb01 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -576,9 +576,9 @@ impl MarkdownElement { .is_some(); if is_hovering_link { - window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox)); + window.set_cursor_style(CursorStyle::PointingHand, hitbox); } else { - window.set_cursor_style(CursorStyle::IBeam, Some(hitbox)); + window.set_cursor_style(CursorStyle::IBeam, hitbox); } let on_open_url = self.on_url_click.take(); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 2ea27fe5bb383cf2fdbbc1ac164e3df1f4639b5d..3c68c0501d822404d016243acac9df3caa221dd8 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -974,9 +974,9 @@ impl Element for TerminalElement { && bounds.contains(&window.mouse_position()) && self.terminal_view.read(cx).hover.is_some() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(&layout.hitbox)); + window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox); } else { - window.set_cursor_style(gpui::CursorStyle::IBeam, Some(&layout.hitbox)); + window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox); } let original_cursor = layout.cursor.take(); diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index f6f256323da98c17bd5261807c90a08aef956acb..01b3e2cf74f090173f6cffd173d59ae3003664ca 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -330,7 +330,7 @@ mod uniform_list { }); let mut hovered_hitbox_id = None; for (i, hitbox) in hitboxes.iter().enumerate() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(hitbox)); + window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); let indent_guide = &self.indent_guides[i]; let fill_color = if hitbox.is_hovered(window) { hovered_hitbox_id = Some(hitbox.id); diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 4ee2760c937417bae239e48e537f0f3769ac1810..2a8c4885acff5f3b5e75c7e2f6ae62335f9b8ebe 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -2,10 +2,10 @@ use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; use crate::{IntoElement, prelude::*, px, relative}; use gpui::{ - Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, - ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, IsZero, LayoutId, - ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, - ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad, + Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle, + Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, + IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, + ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad, }; pub struct Scrollbar { @@ -22,6 +22,12 @@ enum ThumbState { Dragging(Pixels), } +impl ThumbState { + fn is_dragging(&self) -> bool { + matches!(*self, ThumbState::Dragging(_)) + } +} + impl ScrollableHandle for UniformListScrollHandle { fn content_size(&self) -> Size { self.0.borrow().base_handle.content_size() @@ -236,7 +242,7 @@ impl Element for Scrollbar { _inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, + hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { @@ -244,7 +250,8 @@ impl Element for Scrollbar { window.with_content_mask(Some(ContentMask { bounds }), |window| { let axis = self.kind; let colors = cx.theme().colors(); - let thumb_base_color = match self.state.thumb_state.get() { + let thumb_state = self.state.thumb_state.get(); + let thumb_base_color = match thumb_state { ThumbState::Dragging(_) => colors.scrollbar_thumb_active_background, ThumbState::Hover => colors.scrollbar_thumb_hover_background, ThumbState::Inactive => colors.scrollbar_thumb_background, @@ -285,6 +292,12 @@ impl Element for Scrollbar { BorderStyle::default(), )); + if thumb_state.is_dragging() { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, hitbox); + } + let scroll = self.state.scroll_handle.clone(); enum ScrollbarMouseEvent { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7e5e77f97b5b82265ff26c93e89eacba0724f61d..4565cef34719cdf3d4c506e7ba73dedb8cc6e3de 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1281,7 +1281,17 @@ mod element { Axis::Vertical => CursorStyle::ResizeRow, Axis::Horizontal => CursorStyle::ResizeColumn, }; - window.set_cursor_style(cursor_style, Some(&handle.hitbox)); + + if layout + .dragged_handle + .borrow() + .is_some_and(|dragged_ix| dragged_ix == ix) + { + window.set_window_cursor_style(cursor_style); + } else { + window.set_cursor_style(cursor_style, &handle.hitbox); + } + window.paint_quad(gpui::fill( handle.divider_bounds, cx.theme().colors().pane_group_border, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e2b84ef1b9791b1e0af8a549e2979f2c4e953134..adf16c0910345a4f30ffe5af179ee558e525d859 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7411,7 +7411,7 @@ pub fn client_side_decorations( CursorStyle::ResizeUpRightDownLeft } }, - Some(&hitbox), + &hitbox, ); }, )