x11: Cursor style support (#11237)

apricotbucket28 created

Adds cursor style support to X11

![image](https://github.com/zed-industries/zed/assets/71973804/e5a2414f-4d80-4963-93d2-e4a15878a718)


Release Notes:

- N/A

Change summary

crates/gpui/Cargo.toml                       |  1 
crates/gpui/src/platform/linux/x11/client.rs | 69 +++++++++++++++++++--
2 files changed, 61 insertions(+), 9 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -120,6 +120,7 @@ x11rb = { version = "0.13.0", features = [
     "xkb",
     "randr",
     "xinput",
+    "cursor",
     "resource_manager",
 ] }
 xkbcommon = { version = "0.7", features = ["wayland", "x11"] }

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

@@ -3,19 +3,21 @@ use std::ops::Deref;
 use std::rc::{Rc, Weak};
 use std::time::{Duration, Instant};
 
-use calloop::{EventLoop, LoopHandle};
+use calloop::generic::{FdWrapper, Generic};
+use calloop::{EventLoop, LoopHandle, RegistrationToken};
 use collections::HashMap;
 use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
 use copypasta::ClipboardProvider;
 
 use util::ResultExt;
 use x11rb::connection::{Connection, RequestConnection};
+use x11rb::cursor;
 use x11rb::errors::ConnectionError;
 use x11rb::protocol::randr::ConnectionExt as _;
 use x11rb::protocol::xinput::{ConnectionExt, ScrollClass};
 use x11rb::protocol::xkb::ConnectionExt as _;
-use x11rb::protocol::xproto::ConnectionExt as _;
-use x11rb::protocol::{randr, xinput, xkb, xproto, Event};
+use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
+use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
 use x11rb::resource_manager::Database;
 use x11rb::xcb_ffi::XCBConnection;
 use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
@@ -36,10 +38,6 @@ use super::{
 use super::{button_of_key, modifiers_from_state};
 use crate::platform::linux::is_within_click_distance;
 use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
-use calloop::{
-    generic::{FdWrapper, Generic},
-    RegistrationToken,
-};
 
 pub(crate) struct WindowRef {
     window: X11WindowStatePtr,
@@ -72,6 +70,10 @@ pub struct X11ClientState {
     pub(crate) focused_window: Option<xproto::Window>,
     pub(crate) xkb: xkbc::State,
 
+    pub(crate) cursor_handle: cursor::Handle,
+    pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
+    pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
+
     pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
     pub(crate) scroll_x: Option<f32>,
     pub(crate) scroll_y: Option<f32>,
@@ -93,6 +95,8 @@ impl X11ClientStatePtr {
             state.loop_handle.remove(window_ref.refresh_event_token);
         }
 
+        state.cursor_styles.remove(&x_window);
+
         if state.windows.is_empty() {
             state.common.signal.stop();
         }
@@ -123,6 +127,9 @@ impl X11Client {
         xcb_connection
             .prefetch_extension_information(randr::X11_EXTENSION_NAME)
             .unwrap();
+        xcb_connection
+            .prefetch_extension_information(render::X11_EXTENSION_NAME)
+            .unwrap();
         xcb_connection
             .prefetch_extension_information(xinput::X11_EXTENSION_NAME)
             .unwrap();
@@ -210,6 +217,11 @@ impl X11Client {
             .map(|dpi: f32| dpi / 96.0)
             .unwrap_or(1.0);
 
+        let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)
+            .unwrap()
+            .reply()
+            .unwrap();
+
         let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
         let primary = X11ClipboardContext::<Primary>::new().unwrap();
 
@@ -254,6 +266,10 @@ impl X11Client {
             focused_window: None,
             xkb: xkb_state,
 
+            cursor_handle,
+            cursor_styles: HashMap::default(),
+            cursor_cache: HashMap::default(),
+
             scroll_class_data,
             scroll_x: None,
             scroll_y: None,
@@ -672,8 +688,43 @@ impl LinuxClient for X11Client {
         Box::new(window)
     }
 
-    //todo(linux)
-    fn set_cursor_style(&self, _style: CursorStyle) {}
+    fn set_cursor_style(&self, style: CursorStyle) {
+        let mut state = self.0.borrow_mut();
+        let Some(focused_window) = state.focused_window else {
+            return;
+        };
+        let current_style = state
+            .cursor_styles
+            .get(&focused_window)
+            .unwrap_or(&CursorStyle::Arrow);
+        if *current_style == style {
+            return;
+        }
+
+        let cursor = match state.cursor_cache.get(&style) {
+            Some(cursor) => *cursor,
+            None => {
+                let cursor = state
+                    .cursor_handle
+                    .load_cursor(&state.xcb_connection, &style.to_icon_name())
+                    .expect("failed to load cursor");
+                state.cursor_cache.insert(style, cursor);
+                cursor
+            }
+        };
+
+        state.cursor_styles.insert(focused_window, style);
+        state
+            .xcb_connection
+            .change_window_attributes(
+                focused_window,
+                &ChangeWindowAttributesAux {
+                    cursor: Some(cursor),
+                    ..Default::default()
+                },
+            )
+            .expect("failed to change window cursor");
+    }
 
     fn open_uri(&self, uri: &str) {
         open_uri_internal(uri, None);