Fix cursors on some GNOME installations (#12914)

Mikayla Maki created

This PR adds support for `org.gnome.desktop.interface`'s `cursor-theme`
setting on Wayland. This should fix cursors not showing up on some GNOME
installs. This PR also adds the wiring to watch the current cursor theme
value.

Thanks to @apricotbucket28 for helping debug the issue.

Release Notes:

- N/A

Change summary

crates/gpui/src/platform/linux/wayland/client.rs     | 32 ++++
crates/gpui/src/platform/linux/wayland/cursor.rs     | 45 ++++++
crates/gpui/src/platform/linux/x11/client.rs         |  3 
crates/gpui/src/platform/linux/xdg_desktop_portal.rs | 95 ++++++++++---
4 files changed, 145 insertions(+), 30 deletions(-)

Detailed changes

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

@@ -63,7 +63,9 @@ use crate::platform::linux::is_within_click_distance;
 use crate::platform::linux::wayland::cursor::Cursor;
 use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
 use crate::platform::linux::wayland::window::WaylandWindow;
-use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
+use crate::platform::linux::xdg_desktop_portal::{
+    cursor_settings, Event as XDPEvent, XDPEventSource,
+};
 use crate::platform::linux::LinuxClient;
 use crate::platform::PlatformWindow;
 use crate::{
@@ -430,7 +432,7 @@ impl WaylandClient {
 
         let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
 
-        let cursor = Cursor::new(&conn, &globals, 24);
+        let mut cursor = Cursor::new(&conn, &globals, 24);
 
         handle
             .insert_source(XDPEventSource::new(&common.background_executor), {
@@ -446,10 +448,24 @@ impl WaylandClient {
                             }
                         }
                     }
+                    XDPEvent::CursorTheme(theme) => {
+                        if let Some(client) = client.0.upgrade() {
+                            let mut client = client.borrow_mut();
+                            client.cursor.set_theme(theme.as_str(), None);
+                        }
+                    }
+                    XDPEvent::CursorSize(size) => {
+                        if let Some(client) = client.0.upgrade() {
+                            let mut client = client.borrow_mut();
+                            client.cursor.set_size(size);
+                        }
+                    }
                 }
             })
             .unwrap();
 
+        let foreground = common.foreground_executor.clone();
+
         let mut state = Rc::new(RefCell::new(WaylandClientState {
             serial_tracker: SerialTracker::new(),
             globals,
@@ -511,6 +527,18 @@ impl WaylandClient {
             pending_open_uri: None,
         }));
 
+        foreground
+            .spawn({
+                let state = state.clone();
+                async move {
+                    if let Ok((theme, size)) = cursor_settings().await {
+                        let mut state = state.borrow_mut();
+                        state.cursor.set_theme(theme.as_str(), size);
+                    }
+                }
+            })
+            .detach();
+
         WaylandSource::new(conn, event_queue)
             .insert(handle)
             .unwrap();

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

@@ -1,14 +1,18 @@
 use crate::Globals;
 use util::ResultExt;
 
-use wayland_client::protocol::wl_pointer::WlPointer;
 use wayland_client::protocol::wl_surface::WlSurface;
+use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm};
 use wayland_client::Connection;
 use wayland_cursor::{CursorImageBuffer, CursorTheme};
 
 pub(crate) struct Cursor {
     theme: Option<CursorTheme>,
+    theme_name: Option<String>,
     surface: WlSurface,
+    size: u32,
+    shm: WlShm,
+    connection: Connection,
 }
 
 impl Drop for Cursor {
@@ -22,10 +26,49 @@ impl Cursor {
     pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
         Self {
             theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
+            theme_name: None,
             surface: globals.compositor.create_surface(&globals.qh, ()),
+            shm: globals.shm.clone(),
+            connection: connection.clone(),
+            size,
         }
     }
 
+    pub fn set_theme(&mut self, theme_name: &str, size: Option<u32>) {
+        if let Some(size) = size {
+            self.size = size;
+        }
+        if let Some(theme) =
+            CursorTheme::load_from_name(&self.connection, self.shm.clone(), theme_name, self.size)
+                .log_err()
+        {
+            self.theme = Some(theme);
+            self.theme_name = Some(theme_name.to_string());
+        } else if let Some(theme) =
+            CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err()
+        {
+            self.theme = Some(theme);
+            self.theme_name = None;
+        }
+    }
+
+    pub fn set_size(&mut self, size: u32) {
+        self.size = size;
+        self.theme = self
+            .theme_name
+            .as_ref()
+            .and_then(|name| {
+                CursorTheme::load_from_name(
+                    &self.connection,
+                    self.shm.clone(),
+                    name.as_str(),
+                    self.size,
+                )
+                .log_err()
+            })
+            .or_else(|| CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err());
+    }
+
     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>;

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

@@ -359,6 +359,9 @@ impl X11Client {
                             window.window.set_appearance(appearance);
                         }
                     }
+                    XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
+                        // noop, X11 manages this for us.
+                    }
                 }
             })
             .unwrap();

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

@@ -1,18 +1,18 @@
 //! Provides a [calloop] event source from [XDG Desktop Portal] events
 //!
-//! This module uses the [ashpd] crate and handles many async loop
-use std::future::Future;
+//! This module uses the [ashpd] crate
 
 use ashpd::desktop::settings::{ColorScheme, Settings};
-use calloop::channel::{Channel, Sender};
+use calloop::channel::Channel;
 use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
 use smol::stream::StreamExt;
-use util::ResultExt;
 
 use crate::{BackgroundExecutor, WindowAppearance};
 
 pub enum Event {
     WindowAppearance(WindowAppearance),
+    CursorTheme(String),
+    CursorSize(u32),
 }
 
 pub struct XDPEventSource {
@@ -23,34 +23,62 @@ impl XDPEventSource {
     pub fn new(executor: &BackgroundExecutor) -> Self {
         let (sender, channel) = calloop::channel::channel();
 
-        Self::spawn_observer(executor, Self::appearance_observer(sender.clone()));
+        let background = executor.clone();
 
-        Self { channel }
-    }
-
-    fn spawn_observer(
-        executor: &BackgroundExecutor,
-        to_spawn: impl Future<Output = Result<(), anyhow::Error>> + Send + 'static,
-    ) {
         executor
             .spawn(async move {
-                to_spawn.await.log_err();
+                let settings = Settings::new().await?;
+
+                if let Ok(mut cursor_theme_changed) = settings
+                    .receive_setting_changed_with_args(
+                        "org.gnome.desktop.interface",
+                        "cursor-theme",
+                    )
+                    .await
+                {
+                    let sender = sender.clone();
+                    background
+                        .spawn(async move {
+                            while let Some(theme) = cursor_theme_changed.next().await {
+                                let theme = theme?;
+                                sender.send(Event::CursorTheme(theme))?;
+                            }
+                            anyhow::Ok(())
+                        })
+                        .detach();
+                }
+
+                if let Ok(mut cursor_size_changed) = settings
+                    .receive_setting_changed_with_args::<u32>(
+                        "org.gnome.desktop.interface",
+                        "cursor-size",
+                    )
+                    .await
+                {
+                    let sender = sender.clone();
+                    background
+                        .spawn(async move {
+                            while let Some(size) = cursor_size_changed.next().await {
+                                let size = size?;
+                                sender.send(Event::CursorSize(size))?;
+                            }
+                            anyhow::Ok(())
+                        })
+                        .detach();
+                }
+
+                let mut appearance_changed = settings.receive_color_scheme_changed().await?;
+                while let Some(scheme) = appearance_changed.next().await {
+                    sender.send(Event::WindowAppearance(WindowAppearance::from_native(
+                        scheme,
+                    )))?;
+                }
+
+                anyhow::Ok(())
             })
-            .detach()
-    }
-
-    async fn appearance_observer(sender: Sender<Event>) -> Result<(), anyhow::Error> {
-        let settings = Settings::new().await?;
+            .detach();
 
-        // We observe the color change during the execution of the application
-        let mut stream = settings.receive_color_scheme_changed().await?;
-        while let Some(scheme) = stream.next().await {
-            sender.send(Event::WindowAppearance(WindowAppearance::from_native(
-                scheme,
-            )))?;
-        }
-
-        Ok(())
+        Self { channel }
     }
 }
 
@@ -142,3 +170,16 @@ pub fn should_auto_hide_scrollbars(executor: &BackgroundExecutor) -> Result<bool
         Ok(auto_hide)
     })
 }
+
+pub async fn cursor_settings() -> Result<(String, Option<u32>), anyhow::Error> {
+    let settings = Settings::new().await?;
+    let cursor_theme = settings
+        .read::<String>("org.gnome.desktop.interface", "cursor-theme")
+        .await?;
+    let cursor_size = settings
+        .read::<u32>("org.gnome.desktop.interface", "cursor-size")
+        .await
+        .ok();
+
+    Ok((cursor_theme, cursor_size))
+}