Improve cursor style behavior for some draggable elements (#31965)

Finn Evers created

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

Change summary

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(-)

Detailed changes

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| {

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);
                                             }
                                         }
 

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)
                         }
                     }
 

crates/gpui/src/window.rs 🔗

@@ -408,7 +408,7 @@ pub(crate) type AnyMouseListener =
 
 #[derive(Clone)]
 pub(crate) struct CursorStyleRequest {
-    pub(crate) hitbox_id: Option<HitboxId>, // 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<Option<PlatformInputHandler>>,
     pub(crate) tooltip_requests: Vec<Option<TooltipRequest>>,
     pub(crate) cursor_styles: Vec<CursorStyleRequest>,
+    window_cursor_style: Option<CursorStyle>,
     #[cfg(any(test, feature = "test-support"))]
     pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
     #[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<CursorStyle> {
+        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<Pixels>) -> 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);
         }

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();

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();

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);

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<Pixels> {
         self.0.borrow().base_handle.content_size()
@@ -236,7 +242,7 @@ impl Element for Scrollbar {
         _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _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 {

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,