gpui: Keep drag cursor style when dragging (#24797)

Jason Lee created

Release Notes:

- Improve to keep drag cursor style on dragging resize handles.

---

### Before


https://github.com/user-attachments/assets/d4100d01-ac02-42b8-b923-9f2b4633c458

### After


https://github.com/user-attachments/assets/b5a450cd-c6de-4b39-a79c-2d73fcbad209

With example:

```
cargo run -p gpui --example drag_drop
```


https://github.com/user-attachments/assets/4cba1966-1578-40ce-a435-64ec11bcace5

Change summary

crates/gpui/examples/drag_drop.rs | 137 +++++++++++++++++++++++++++++++++
crates/gpui/src/app.rs            |  11 +
crates/gpui/src/elements/div.rs   |   9 +
crates/gpui/src/window.rs         |   1 
4 files changed, 153 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/examples/drag_drop.rs 🔗

@@ -0,0 +1,137 @@
+use gpui::{
+    App, Application, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds,
+    WindowOptions, div, prelude::*, px, rgb, size,
+};
+
+#[derive(Clone, Copy)]
+struct DragInfo {
+    ix: usize,
+    color: Hsla,
+    position: Point<Pixels>,
+}
+
+impl DragInfo {
+    fn new(ix: usize, color: Hsla) -> Self {
+        Self {
+            ix,
+            color,
+            position: Point::default(),
+        }
+    }
+
+    fn position(mut self, pos: Point<Pixels>) -> Self {
+        self.position = pos;
+        self
+    }
+}
+
+impl Render for DragInfo {
+    fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
+        let size = gpui::size(px(120.), px(50.));
+
+        div()
+            .pl(self.position.x - size.width.half())
+            .pt(self.position.y - size.height.half())
+            .child(
+                div()
+                    .flex()
+                    .justify_center()
+                    .items_center()
+                    .w(size.width)
+                    .h(size.height)
+                    .bg(self.color.opacity(0.5))
+                    .text_color(gpui::white())
+                    .text_xs()
+                    .shadow_md()
+                    .child(format!("Item {}", self.ix)),
+            )
+    }
+}
+
+struct DragDrop {
+    drop_on: Option<DragInfo>,
+}
+
+impl DragDrop {
+    fn new() -> Self {
+        Self { drop_on: None }
+    }
+}
+
+impl Render for DragDrop {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let items = [gpui::blue(), gpui::red(), gpui::green()];
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .gap_5()
+            .bg(gpui::white())
+            .justify_center()
+            .items_center()
+            .text_color(rgb(0x333333))
+            .child(div().text_xl().text_center().child("Drop & Drop"))
+            .child(
+                div()
+                    .w_full()
+                    .mb_10()
+                    .justify_center()
+                    .flex()
+                    .flex_row()
+                    .gap_4()
+                    .items_center()
+                    .children(items.into_iter().enumerate().map(|(ix, color)| {
+                        let drag_info = DragInfo::new(ix, color);
+
+                        div()
+                            .id(("item", ix))
+                            .size_32()
+                            .flex()
+                            .justify_center()
+                            .items_center()
+                            .border_2()
+                            .border_color(color)
+                            .text_color(color)
+                            .cursor_move()
+                            .hover(|this| this.bg(color.opacity(0.2)))
+                            .child(format!("Item ({})", ix))
+                            .on_drag(drag_info, |info: &DragInfo, position, _, cx| {
+                                cx.new(|_| info.position(position))
+                            })
+                    })),
+            )
+            .child(
+                div()
+                    .id("drop-target")
+                    .w_128()
+                    .h_32()
+                    .flex()
+                    .justify_center()
+                    .items_center()
+                    .border_3()
+                    .border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black()))
+                    .when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5)))
+                    .on_drop(cx.listener(|this, info: &DragInfo, _, _| {
+                        this.drop_on = Some(*info);
+                    }))
+                    .child("Drop items here"),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| DragDrop::new()),
+        )
+        .unwrap();
+
+        cx.activate(true);
+    });
+}

crates/gpui/src/app.rs 🔗

@@ -31,10 +31,10 @@ use util::ResultExt;
 
 use crate::{
     Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset,
-    AssetSource, BackgroundExecutor, Bounds, ClipboardItem, DispatchPhase, DisplayId, EventEmitter,
-    FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId,
-    Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
-    PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
+    AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
+    EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
+    LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
+    Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
     ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
     Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, current_platform, hash,
     init_app_menus,
@@ -1803,6 +1803,9 @@ pub struct AnyDrag {
     /// This is used to render the dragged item in the same place
     /// on the original element that the drag was initiated
     pub cursor_offset: Point<Pixels>,
+
+    /// The cursor style to use while dragging
+    pub cursor_style: Option<CursorStyle>,
 }
 
 /// Contains state associated with a tooltip. You'll only need this struct if you're implementing

crates/gpui/src/elements/div.rs 🔗

@@ -1615,7 +1615,11 @@ impl Interactivity {
                                             global_id, hitbox, &style, window, cx,
                                         );
 
-                                        if !cx.has_active_drag() {
+                                        if let Some(drag) = cx.active_drag.as_ref() {
+                                            if let Some(mouse_cursor) = drag.cursor_style {
+                                                window.set_cursor_style(mouse_cursor, None);
+                                            }
+                                        } else {
                                             if let Some(mouse_cursor) = style.mouse_cursor {
                                                 window.set_cursor_style(mouse_cursor, Some(hitbox));
                                             }
@@ -1838,6 +1842,7 @@ impl Interactivity {
                 }
             });
         }
+        let drag_cursor_style = self.base_style.as_ref().mouse_cursor;
 
         let mut drag_listener = mem::take(&mut self.drag_listener);
         let drop_listeners = mem::take(&mut self.drop_listeners);
@@ -1929,6 +1934,7 @@ impl Interactivity {
                                         view: drag,
                                         value: drag_value,
                                         cursor_offset,
+                                        cursor_style: drag_cursor_style,
                                     });
                                     pending_mouse_down.take();
                                     window.refresh();
@@ -2269,6 +2275,7 @@ impl Interactivity {
                     }
                 }
 
+                style.mouse_cursor = drag.cursor_style;
                 cx.active_drag = Some(drag);
             }
         }

crates/gpui/src/window.rs 🔗

@@ -3093,6 +3093,7 @@ impl Window {
                             value: Arc::new(paths.clone()),
                             view: cx.new(|_| paths).into(),
                             cursor_offset: position,
+                            cursor_style: None,
                         });
                     }
                     PlatformInput::MouseMove(MouseMoveEvent {