Allow styling the cursor in `MouseEventHandler`

Antonio Scandurra , Max Brunsfeld , and Nathan Sobo created

Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs                          | 33 ++++++++++++++++++++++++-
gpui/src/elements/mouse_event_handler.rs | 28 +++++++++++++++++++--
gpui/src/platform.rs                     |  9 +++++++
gpui/src/platform/mac/platform.rs        | 16 +++++++++++
gpui/src/platform/test.rs                |  7 +++++
zed/src/workspace/sidebar.rs             |  6 +++
6 files changed, 92 insertions(+), 7 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     elements::ElementBox,
     executor,
     keymap::{self, Keystroke},
-    platform::{self, Platform, PromptLevel, WindowOptions},
+    platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::{post_inc, timeout},
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
@@ -25,7 +25,10 @@ use std::{
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
     rc::{self, Rc},
-    sync::{Arc, Weak},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc, Weak,
+    },
     time::Duration,
 };
 
@@ -661,6 +664,7 @@ pub struct MutableAppContext {
     pending_effects: VecDeque<Effect>,
     pending_flushes: usize,
     flushing_effects: bool,
+    next_cursor_style_handle_id: Arc<AtomicUsize>,
 }
 
 impl MutableAppContext {
@@ -700,6 +704,7 @@ impl MutableAppContext {
             pending_effects: VecDeque::new(),
             pending_flushes: 0,
             flushing_effects: false,
+            next_cursor_style_handle_id: Default::default(),
         }
     }
 
@@ -1456,6 +1461,16 @@ impl MutableAppContext {
         self.presenters_and_platform_windows = presenters;
     }
 
+    pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle {
+        self.platform.set_cursor_style(style);
+        let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst);
+        CursorStyleHandle {
+            id,
+            next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(),
+            platform: self.platform(),
+        }
+    }
+
     fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
         let callbacks = self.subscriptions.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
@@ -3029,6 +3044,20 @@ impl<T> Drop for ElementStateHandle<T> {
     }
 }
 
+pub struct CursorStyleHandle {
+    id: usize,
+    next_cursor_style_handle_id: Arc<AtomicUsize>,
+    platform: Arc<dyn Platform>,
+}
+
+impl Drop for CursorStyleHandle {
+    fn drop(&mut self) {
+        if self.id + 1 == self.next_cursor_style_handle_id.load(SeqCst) {
+            self.platform.set_cursor_style(CursorStyle::Arrow);
+        }
+    }
+}
+
 #[must_use]
 pub enum Subscription {
     Subscription {

gpui/src/elements/mouse_event_handler.rs 🔗

@@ -2,23 +2,26 @@ use std::ops::DerefMut;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext,
-    MutableAppContext, PaintContext, SizeConstraint,
+    platform::CursorStyle,
+    CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
+    LayoutContext, MutableAppContext, PaintContext, SizeConstraint,
 };
 use serde_json::json;
 
 pub struct MouseEventHandler {
     state: ElementStateHandle<MouseState>,
     child: ElementBox,
+    cursor_style: Option<CursorStyle>,
     click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
     drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
 }
 
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Default)]
 pub struct MouseState {
     pub hovered: bool,
     pub clicked: bool,
     prev_drag_position: Option<Vector2F>,
+    cursor_style_handle: Option<CursorStyleHandle>,
 }
 
 impl MouseEventHandler {
@@ -33,11 +36,17 @@ impl MouseEventHandler {
         Self {
             state: state_handle,
             child,
+            cursor_style: None,
             click_handler: None,
             drag_handler: None,
         }
     }
 
+    pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
+        self.cursor_style = Some(cursor);
+        self
+    }
+
     pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
         self.click_handler = Some(Box::new(handler));
         self
@@ -78,6 +87,7 @@ impl Element for MouseEventHandler {
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
+        let cursor_style = self.cursor_style;
         let click_handler = self.click_handler.as_mut();
         let drag_handler = self.drag_handler.as_mut();
 
@@ -88,6 +98,15 @@ impl Element for MouseEventHandler {
                 let mouse_in = bounds.contains_point(*position);
                 if state.hovered != mouse_in {
                     state.hovered = mouse_in;
+                    if let Some(cursor_style) = cursor_style {
+                        if !state.clicked {
+                            if state.hovered {
+                                state.cursor_style_handle = Some(cx.set_cursor_style(cursor_style));
+                            } else {
+                                state.cursor_style_handle = None;
+                            }
+                        }
+                    }
                     cx.notify();
                     true
                 } else {
@@ -108,6 +127,9 @@ impl Element for MouseEventHandler {
                 state.prev_drag_position = None;
                 if !handled_in_child && state.clicked {
                     state.clicked = false;
+                    if !state.hovered {
+                        state.cursor_style_handle = None;
+                    }
                     cx.notify();
                     if let Some(handler) = click_handler {
                         if bounds.contains_point(*position) {

gpui/src/platform.rs 🔗

@@ -47,6 +47,8 @@ pub trait Platform: Send + Sync {
 
     fn write_credentials(&self, url: &str, username: &str, password: &[u8]);
     fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)>;
+
+    fn set_cursor_style(&self, style: CursorStyle);
 }
 
 pub(crate) trait ForegroundPlatform {
@@ -114,6 +116,13 @@ pub enum PromptLevel {
     Critical,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub enum CursorStyle {
+    Arrow,
+    ResizeLeftRight,
+    PointingHand,
+}
+
 pub trait FontSystem: Send + Sync {
     fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
     fn select_font(

gpui/src/platform/mac/platform.rs 🔗

@@ -1,6 +1,9 @@
 use super::{BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{
-    executor, keymap::Keystroke, platform, AnyAction, ClipboardItem, Event, Menu, MenuItem,
+    executor,
+    keymap::Keystroke,
+    platform::{self, CursorStyle},
+    AnyAction, ClipboardItem, Event, Menu, MenuItem,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -544,6 +547,17 @@ impl platform::Platform for MacPlatform {
             Some((username.to_string(), password.bytes().to_vec()))
         }
     }
+
+    fn set_cursor_style(&self, style: CursorStyle) {
+        unsafe {
+            let cursor: id = match style {
+                CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
+                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+            };
+            let _: () = msg_send![cursor, set];
+        }
+    }
 }
 
 unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {

gpui/src/platform/test.rs 🔗

@@ -1,3 +1,4 @@
+use super::CursorStyle;
 use crate::{AnyAction, ClipboardItem};
 use parking_lot::Mutex;
 use pathfinder_geometry::vector::Vector2F;
@@ -13,6 +14,7 @@ pub struct Platform {
     dispatcher: Arc<dyn super::Dispatcher>,
     fonts: Arc<dyn super::FontSystem>,
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
+    cursor: Mutex<CursorStyle>,
 }
 
 #[derive(Default)]
@@ -84,6 +86,7 @@ impl Platform {
             dispatcher: Arc::new(Dispatcher),
             fonts: Arc::new(super::current::FontSystem::new()),
             current_clipboard_item: Default::default(),
+            cursor: Mutex::new(CursorStyle::Arrow),
         }
     }
 }
@@ -129,6 +132,10 @@ impl super::Platform for Platform {
     fn read_credentials(&self, _: &str) -> Option<(String, Vec<u8>)> {
         None
     }
+
+    fn set_cursor_style(&self, style: CursorStyle) {
+        *self.cursor.lock() = style;
+    }
 }
 
 impl Window {

zed/src/workspace/sidebar.rs 🔗

@@ -1,6 +1,8 @@
 use super::Workspace;
 use crate::Settings;
-use gpui::{action, elements::*, AnyViewHandle, MutableAppContext, RenderContext};
+use gpui::{
+    action, elements::*, platform::CursorStyle, AnyViewHandle, MutableAppContext, RenderContext,
+};
 use std::{cell::RefCell, rc::Rc};
 
 pub struct Sidebar {
@@ -88,6 +90,7 @@ impl Sidebar {
                         .with_height(line_height + 16.0)
                         .boxed()
                     })
+                    .with_cursor_style(CursorStyle::PointingHand)
                     .on_click(move |cx| {
                         cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
                     })
@@ -135,6 +138,7 @@ impl Sidebar {
                 .with_style(&settings.theme.workspace.sidebar.resize_handle)
                 .boxed()
         })
+        .with_cursor_style(CursorStyle::ResizeLeftRight)
         .on_drag(move |delta, cx| {
             let prev_width = *width.borrow();
             match side {