wayland: Implement cursor-shape-v1 (#11106)

apricotbucket28 created

Fixes wrong cursor icons and sizes on KDE 6 (and possibly other
compositors)

Gnome still doesn't support this protocol, so to fix cursor settings
there we'll need to read `gsettings`.

Before:

![image](https://github.com/zed-industries/zed/assets/71973804/f0c3dc1b-2bed-43f7-b579-df6c9c0b547f)

After:

![image](https://github.com/zed-industries/zed/assets/71973804/6de639f9-653d-4896-80ca-b7c69641eded)


Release Notes:

- N/A

Change summary

crates/gpui/src/platform.rs                      |  2 
crates/gpui/src/platform/linux/platform.rs       | 53 +++++++++
crates/gpui/src/platform/linux/wayland/client.rs | 99 ++++++++++-------
crates/gpui/src/platform/linux/wayland/cursor.rs | 80 ++++---------
4 files changed, 136 insertions(+), 98 deletions(-)

Detailed changes

crates/gpui/src/platform.rs 🔗

@@ -715,7 +715,7 @@ pub enum PromptLevel {
 }
 
 /// The style of the cursor (pointer)
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum CursorStyle {
     /// The default cursor
     Arrow,

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -28,6 +28,7 @@ use futures::channel::oneshot;
 use parking_lot::Mutex;
 use time::UtcOffset;
 use wayland_client::Connection;
+use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
 use xkbcommon::xkb::{self, Keycode, Keysym, State};
 
 use crate::platform::linux::wayland::WaylandClient;
@@ -501,6 +502,58 @@ pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
     Ok(result)
 }
 
+impl CursorStyle {
+    pub(super) fn to_shape(&self) -> Shape {
+        match self {
+            CursorStyle::Arrow => Shape::Default,
+            CursorStyle::IBeam => Shape::Text,
+            CursorStyle::Crosshair => Shape::Crosshair,
+            CursorStyle::ClosedHand => Shape::Grabbing,
+            CursorStyle::OpenHand => Shape::Grab,
+            CursorStyle::PointingHand => Shape::Pointer,
+            CursorStyle::ResizeLeft => Shape::WResize,
+            CursorStyle::ResizeRight => Shape::EResize,
+            CursorStyle::ResizeLeftRight => Shape::EwResize,
+            CursorStyle::ResizeUp => Shape::NResize,
+            CursorStyle::ResizeDown => Shape::SResize,
+            CursorStyle::ResizeUpDown => Shape::NsResize,
+            CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
+            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
+            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
+            CursorStyle::DragLink => Shape::Alias,
+            CursorStyle::DragCopy => Shape::Copy,
+            CursorStyle::ContextualMenu => Shape::ContextMenu,
+        }
+    }
+
+    pub(super) fn to_icon_name(&self) -> String {
+        // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
+        // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
+        // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
+        match self {
+            CursorStyle::Arrow => "arrow",
+            CursorStyle::IBeam => "text",
+            CursorStyle::Crosshair => "crosshair",
+            CursorStyle::ClosedHand => "grabbing",
+            CursorStyle::OpenHand => "grab",
+            CursorStyle::PointingHand => "pointer",
+            CursorStyle::ResizeLeft => "w-resize",
+            CursorStyle::ResizeRight => "e-resize",
+            CursorStyle::ResizeLeftRight => "ew-resize",
+            CursorStyle::ResizeUp => "n-resize",
+            CursorStyle::ResizeDown => "s-resize",
+            CursorStyle::ResizeUpDown => "ns-resize",
+            CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
+            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
+            CursorStyle::OperationNotAllowed => "not-allowed",
+            CursorStyle::DragLink => "alias",
+            CursorStyle::DragCopy => "copy",
+            CursorStyle::ContextualMenu => "context-menu",
+        }
+        .to_string()
+    }
+}
+
 impl Keystroke {
     pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
         let mut modifiers = modifiers;

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -34,6 +34,10 @@ use wayland_client::{
     },
     Connection, Dispatch, Proxy, QueueHandle,
 };
+use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
+use wayland_protocols::wp::cursor_shape::v1::client::{
+    wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
+};
 use wayland_protocols::wp::fractional_scale::v1::client::{
     wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
 };
@@ -68,6 +72,7 @@ const MIN_KEYCODE: u32 = 8;
 pub struct Globals {
     pub qh: QueueHandle<WaylandClientStatePtr>,
     pub compositor: wl_compositor::WlCompositor,
+    pub cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
     pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
     pub wm_base: xdg_wm_base::XdgWmBase,
     pub shm: wl_shm::WlShm,
@@ -93,6 +98,7 @@ impl Globals {
                     (),
                 )
                 .unwrap(),
+            cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(),
             data_device_manager: globals
                 .bind(
                     &qh,
@@ -112,9 +118,11 @@ impl Globals {
 }
 
 pub(crate) struct WaylandClientState {
-    serial: u32,
+    serial: u32, // todo(linux): storing a general serial is wrong
+    pointer_serial: u32,
     globals: Globals,
     wl_pointer: Option<wl_pointer::WlPointer>,
+    cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
     data_device: Option<wl_data_device::WlDataDevice>,
     // Surface to Window mapping
     windows: HashMap<ObjectId, WaylandWindowStatePtr>,
@@ -137,7 +145,7 @@ pub(crate) struct WaylandClientState {
     mouse_focused_window: Option<WaylandWindowStatePtr>,
     keyboard_focused_window: Option<WaylandWindowStatePtr>,
     loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
-    cursor_icon_name: String,
+    cursor_style: Option<CursorStyle>,
     cursor: Cursor,
     clipboard: Option<Clipboard>,
     primary: Option<Primary>,
@@ -197,6 +205,9 @@ impl WaylandClientStatePtr {
             if let Some(wl_pointer) = &state.wl_pointer {
                 wl_pointer.release();
             }
+            if let Some(cursor_shape_device) = &state.cursor_shape_device {
+                cursor_shape_device.destroy();
+            }
             if let Some(data_device) = &state.data_device {
                 data_device.release();
             }
@@ -289,8 +300,10 @@ impl WaylandClient {
 
         let mut state = Rc::new(RefCell::new(WaylandClientState {
             serial: 0,
+            pointer_serial: 0,
             globals,
             wl_pointer: None,
+            cursor_shape_device: None,
             data_device,
             output_scales: outputs,
             windows: HashMap::default(),
@@ -330,8 +343,8 @@ impl WaylandClient {
             mouse_focused_window: None,
             keyboard_focused_window: None,
             loop_handle: handle.clone(),
-            cursor_icon_name: "arrow".to_string(),
             enter_token: None,
+            cursor_style: None,
             cursor,
             clipboard: Some(clipboard),
             primary: Some(primary),
@@ -375,39 +388,28 @@ impl LinuxClient for WaylandClient {
     }
 
     fn set_cursor_style(&self, style: CursorStyle) {
-        // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
-        // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
-        // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
-        let cursor_icon_name = match style {
-            CursorStyle::Arrow => "arrow",
-            CursorStyle::IBeam => "text",
-            CursorStyle::Crosshair => "crosshair",
-            CursorStyle::ClosedHand => "grabbing",
-            CursorStyle::OpenHand => "grab",
-            CursorStyle::PointingHand => "pointer",
-            CursorStyle::ResizeLeft => "w-resize",
-            CursorStyle::ResizeRight => "e-resize",
-            CursorStyle::ResizeLeftRight => "ew-resize",
-            CursorStyle::ResizeUp => "n-resize",
-            CursorStyle::ResizeDown => "s-resize",
-            CursorStyle::ResizeUpDown => "ns-resize",
-            CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
-            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
-            CursorStyle::OperationNotAllowed => "not-allowed",
-            CursorStyle::DragLink => "alias",
-            CursorStyle::DragCopy => "copy",
-            CursorStyle::ContextualMenu => "context-menu",
-        }
-        .to_string();
-
         let mut state = self.0.borrow_mut();
-        state.cursor_icon_name = cursor_icon_name.clone();
-        if state.mouse_focused_window.is_some() {
-            let wl_pointer = state
-                .wl_pointer
-                .clone()
-                .expect("window is focused by pointer");
-            state.cursor.set_icon(&wl_pointer, &cursor_icon_name);
+
+        let need_update = state
+            .cursor_style
+            .map_or(true, |current_style| current_style != style);
+
+        if need_update {
+            let serial = state.pointer_serial;
+            state.cursor_style = Some(style);
+
+            if let Some(cursor_shape_device) = &state.cursor_shape_device {
+                cursor_shape_device.set_shape(serial, style.to_shape());
+            } else if state.mouse_focused_window.is_some() {
+                // cursor-shape-v1 isn't supported, set the cursor using a surface.
+                let wl_pointer = state
+                    .wl_pointer
+                    .clone()
+                    .expect("window is focused by pointer");
+                state
+                    .cursor
+                    .set_icon(&wl_pointer, serial, &style.to_icon_name());
+            }
         }
     }
 
@@ -516,6 +518,8 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
 }
 
 delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
+delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
+delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
 delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager);
 delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
 delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
@@ -684,7 +688,13 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
             if capabilities.contains(wl_seat::Capability::Pointer) {
                 let client = state.get_client();
                 let mut state = client.borrow_mut();
-                state.wl_pointer = Some(seat.get_pointer(qh, ()));
+                let pointer = seat.get_pointer(qh, ());
+                state.cursor_shape_device = state
+                    .globals
+                    .cursor_shape_manager
+                    .as_ref()
+                    .map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
+                state.wl_pointer = Some(pointer);
             }
         }
     }
@@ -889,7 +899,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
     ) {
         let mut client = this.get_client();
         let mut state = client.borrow_mut();
-        let cursor_icon_name = state.cursor_icon_name.clone();
 
         match event {
             wl_pointer::Event::Enter {
@@ -899,17 +908,21 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 surface_y,
                 ..
             } => {
-                state.serial = serial;
+                state.pointer_serial = serial;
                 state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
 
                 if let Some(window) = get_window(&mut state, &surface.id()) {
                     state.enter_token = Some(());
                     state.mouse_focused_window = Some(window.clone());
-                    state.cursor.mark_dirty();
-                    state.cursor.set_serial_id(serial);
-                    state
-                        .cursor
-                        .set_icon(&wl_pointer, cursor_icon_name.as_str());
+                    if let Some(style) = state.cursor_style {
+                        if let Some(cursor_shape_device) = &state.cursor_shape_device {
+                            cursor_shape_device.set_shape(serial, style.to_shape());
+                        } else {
+                            state
+                                .cursor
+                                .set_icon(&wl_pointer, serial, &style.to_icon_name());
+                        }
+                    }
                     drop(state);
                     window.set_focused(true);
                 }

crates/gpui/src/platform/linux/wayland/cursor.rs 🔗

@@ -8,9 +8,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme};
 
 pub(crate) struct Cursor {
     theme: Option<CursorTheme>,
-    current_icon_name: Option<String>,
     surface: WlSurface,
-    serial_id: u32,
 }
 
 impl Drop for Cursor {
@@ -24,65 +22,39 @@ impl Cursor {
     pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
         Self {
             theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
-            current_icon_name: None,
             surface: globals.compositor.create_surface(&globals.qh, ()),
-            serial_id: 0,
         }
     }
 
-    pub fn mark_dirty(&mut self) {
-        self.current_icon_name = None;
-    }
-
-    pub fn set_serial_id(&mut self, serial_id: u32) {
-        self.serial_id = serial_id;
-    }
-
-    pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: &str) {
-        let need_update = self
-            .current_icon_name
-            .as_ref()
-            .map_or(true, |current_icon_name| {
-                current_icon_name != cursor_icon_name
-            });
-
-        if need_update {
-            if let Some(theme) = &mut self.theme {
-                let mut buffer: Option<&CursorImageBuffer>;
-
-                if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
-                    buffer = Some(&cursor[0]);
-                } else if let Some(cursor) = theme.get_cursor("default") {
-                    buffer = Some(&cursor[0]);
-                    cursor_icon_name = "default";
-                    log::warn!(
-                        "Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
-                        cursor_icon_name
-                    );
-                } else {
-                    buffer = None;
-                    log::warn!("Linux: Wayland: Unable to get default cursor too!");
-                }
-
-                if let Some(buffer) = &mut buffer {
-                    let (width, height) = buffer.dimensions();
-                    let (hot_x, hot_y) = buffer.hotspot();
+    pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) {
+        if let Some(theme) = &mut self.theme {
+            let mut buffer: Option<&CursorImageBuffer>;
+
+            if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
+                buffer = Some(&cursor[0]);
+            } else if let Some(cursor) = theme.get_cursor("default") {
+                buffer = Some(&cursor[0]);
+                cursor_icon_name = "default";
+                log::warn!(
+                    "Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
+                    cursor_icon_name
+                );
+            } else {
+                buffer = None;
+                log::warn!("Linux: Wayland: Unable to get default cursor too!");
+            }
 
-                    wl_pointer.set_cursor(
-                        self.serial_id,
-                        Some(&self.surface),
-                        hot_x as i32,
-                        hot_y as i32,
-                    );
-                    self.surface.attach(Some(&buffer), 0, 0);
-                    self.surface.damage(0, 0, width as i32, height as i32);
-                    self.surface.commit();
+            if let Some(buffer) = &mut buffer {
+                let (width, height) = buffer.dimensions();
+                let (hot_x, hot_y) = buffer.hotspot();
 
-                    self.current_icon_name = Some(cursor_icon_name.to_string());
-                }
-            } else {
-                log::warn!("Linux: Wayland: Unable to load cursor themes");
+                wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32);
+                self.surface.attach(Some(&buffer), 0, 0);
+                self.surface.damage(0, 0, width as i32, height as i32);
+                self.surface.commit();
             }
+        } else {
+            log::warn!("Linux: Wayland: Unable to load cursor themes");
         }
     }
 }