From f1ddd336f6ec30d11ff0dfbc4fb330bb8edc02dc Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 16 Jun 2025 23:26:33 -0600 Subject: [PATCH] Cherry-pick "linux: Add mouse cursor icon name synonyms #32820" (#32833) Most of the default icon sets on Ubuntu do not use the names that were there. To fix, using the icon synonyms from the chromium source. This will probably fix some of the linux mouse cursor issues tracked in https://github.com/zed-industries/zed/issues/26141 Also adds a note in the load failure logs mentioning that misconfigured XCURSOR_PATH may be the issue. I ran into this because https://github.com/snapcrafters/alacritty/issues/21. On X11 also adds: Caching of load errors to log once for missing cursor icons. Fallback on default cursor icon. This way if there was a transition from a non-default icon to a missing icon it doesn't get stuck showing the non-default icon. Leaving release notes blank as I have other mouse cursor fixes and would prefer to just have one entry in the release notes. Release Notes: Linux: Fixed a couple bugs that can cause the mouse cursor to not appear or only appear as an arrow --- crates/gpui/src/platform/linux/platform.rs | 68 ++++++++----- .../gpui/src/platform/linux/wayland/client.rs | 11 ++- .../gpui/src/platform/linux/wayland/cursor.rs | 74 ++++++++------ crates/gpui/src/platform/linux/x11/client.rs | 99 +++++++++++++++---- 4 files changed, 172 insertions(+), 80 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 3c582ba999c5b3733398640d5fb7835a6bc5913c..aee05706d9207285bd855ddcd537faeaf2003fe1 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -647,44 +647,60 @@ pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result &'static str { - // 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 + pub(super) fn to_icon_names(&self) -> &'static [&'static str] { + // Based on cursor names from chromium: + // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 match self { - CursorStyle::Arrow => "left_ptr", - 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::ResizeUpLeftDownRight => "nwse-resize", - CursorStyle::ResizeUpRightDownLeft => "nesw-resize", - CursorStyle::ResizeColumn => "col-resize", - CursorStyle::ResizeRow => "row-resize", - CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", - CursorStyle::OperationNotAllowed => "not-allowed", - CursorStyle::DragLink => "alias", - CursorStyle::DragCopy => "copy", - CursorStyle::ContextualMenu => "context-menu", + CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME], + CursorStyle::IBeam => &["text", "xterm"], + CursorStyle::Crosshair => &["crosshair", "cross"], + CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"], + CursorStyle::OpenHand => &["openhand", "grab", "hand1"], + CursorStyle::PointingHand => &["pointer", "hand", "hand2"], + CursorStyle::ResizeLeft => &["w-resize", "left_side"], + CursorStyle::ResizeRight => &["e-resize", "right_side"], + CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"], + CursorStyle::ResizeUp => &["n-resize", "top_side"], + CursorStyle::ResizeDown => &["s-resize", "bottom_side"], + CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"], + CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"], + CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"], + CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"], + CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"], + CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"], + CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"], + CursorStyle::DragLink => &["alias"], + CursorStyle::DragCopy => &["copy"], + CursorStyle::ContextualMenu => &["context-menu"], CursorStyle::None => { #[cfg(debug_assertions)] panic!("CursorStyle::None should be handled separately in the client"); #[cfg(not(debug_assertions))] - "default" + &[DEFAULT_CURSOR_ICON_NAME] } } } } +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { + if let Ok(xcursor_path) = env::var("XCURSOR_PATH") { + log::warn!( + "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \ + XCURSOR_PATH overrides the default icon search. Its current value is '{}'", + message, + xcursor_path + ); + } else { + log::warn!("{:#}", message); + } +} + #[cfg(any(feature = "wayland", feature = "x11"))] impl crate::Keystroke { pub(super) fn from_xkb( diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 22ce0a60f8216c6510ff47ac752dd3f3dfffe69b..fb61d82838a1f6f31805349d9112f9ece35e9c81 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -704,7 +704,7 @@ impl LinuxClient for WaylandClient { let scale = focused_window.primary_output_scale(); state .cursor - .set_icon(&wl_pointer, serial, style.to_icon_name(), scale); + .set_icon(&wl_pointer, serial, style.to_icon_names(), scale); } } } @@ -1515,9 +1515,12 @@ impl Dispatch for WaylandClientStatePtr { cursor_shape_device.set_shape(serial, style.to_shape()); } else { let scale = window.primary_output_scale(); - state - .cursor - .set_icon(&wl_pointer, serial, style.to_icon_name(), scale); + state.cursor.set_icon( + &wl_pointer, + serial, + style.to_icon_names(), + scale, + ); } } drop(state); diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index 756447ecd21a666a556d0db92e2aad199644ea23..f15fb5dde23bf0bb729a0812624b5634cd120eec 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -1,4 +1,6 @@ use crate::Globals; +use crate::platform::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning}; +use anyhow::anyhow; use util::ResultExt; use wayland_client::Connection; @@ -82,47 +84,57 @@ impl Cursor { &mut self, wl_pointer: &WlPointer, serial_id: u32, - mut cursor_icon_name: &str, + mut cursor_icon_names: &[&str], scale: i32, ) { self.set_theme_size(self.size * scale as u32); - if let Some(theme) = &mut self.theme { - let mut buffer: Option<&CursorImageBuffer>; + let Some(theme) = &mut self.theme else { + log::warn!("Wayland: Unable to load cursor themes"); + return; + }; - 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 - ); + let mut buffer: &CursorImageBuffer; + 'outer: { + for cursor_icon_name in cursor_icon_names { + if let Some(cursor) = theme.get_cursor(cursor_icon_name) { + buffer = &cursor[0]; + break 'outer; + } + } + + if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) { + buffer = &cursor[0]; + log_cursor_icon_warning(anyhow!( + "wayland: Unable to get cursor icon {:?}. \ + Using default cursor icon: '{}'", + cursor_icon_names, + DEFAULT_CURSOR_ICON_NAME + )); } else { - buffer = None; - log::warn!("Linux: Wayland: Unable to get default cursor too!"); + log_cursor_icon_warning(anyhow!( + "wayland: Unable to fallback on default cursor icon '{}' for theme '{}'", + DEFAULT_CURSOR_ICON_NAME, + self.theme_name.as_deref().unwrap_or("default") + )); + return; } + } - if let Some(buffer) = &mut buffer { - let (width, height) = buffer.dimensions(); - let (hot_x, hot_y) = buffer.hotspot(); + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); - self.surface.set_buffer_scale(scale); + self.surface.set_buffer_scale(scale); - wl_pointer.set_cursor( - serial_id, - Some(&self.surface), - hot_x as i32 / scale, - hot_y as i32 / scale, - ); + wl_pointer.set_cursor( + serial_id, + Some(&self.surface), + hot_x as i32 / scale, + hot_y as i32 / scale, + ); - 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"); - } + self.surface.attach(Some(&buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 3c2e24b0c9be23de08b1f9313273532a09f6584e..ac9a17aaad12524d50dd3078b46155078a295191 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -8,7 +8,7 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::Context as _; +use anyhow::{Context as _, anyhow}; use calloop::{ EventLoop, LoopHandle, RegistrationToken, generic::{FdWrapper, Generic}, @@ -51,7 +51,8 @@ use crate::platform::{ LinuxCommon, PlatformWindow, blade::BladeContext, linux::{ - LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, + DEFAULT_CURSOR_ICON_NAME, LinuxClient, get_xkb_compose_state, is_within_click_distance, + log_cursor_icon_warning, open_uri_internal, platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}, reveal_path_internal, xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, @@ -197,7 +198,7 @@ pub struct X11ClientState { pub(crate) pre_key_char_down: Option, pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, - pub(crate) cursor_cache: HashMap, + pub(crate) cursor_cache: HashMap>, pointer_device_states: BTreeMap, @@ -1457,22 +1458,8 @@ impl LinuxClient for X11Client { return; } - let cursor = match state.cursor_cache.get(&style) { - Some(cursor) => *cursor, - None => { - let Some(cursor) = (match style { - CursorStyle::None => create_invisible_cursor(&state.xcb_connection).log_err(), - _ => state - .cursor_handle - .load_cursor(&state.xcb_connection, style.to_icon_name()) - .log_err(), - }) else { - return; - }; - - state.cursor_cache.insert(style, cursor); - cursor - } + let Some(cursor) = state.get_cursor_icon(style) else { + return; }; state.cursor_styles.insert(focused_window, style); @@ -1618,6 +1605,80 @@ impl LinuxClient for X11Client { } } +impl X11ClientState { + fn get_cursor_icon(&mut self, style: CursorStyle) -> Option { + if let Some(cursor) = self.cursor_cache.get(&style) { + return *cursor; + } + + let mut result; + match style { + CursorStyle::None => match create_invisible_cursor(&self.xcb_connection) { + Ok(loaded_cursor) => result = Ok(loaded_cursor), + Err(err) => result = Err(err.context("error while creating invisible cursor")), + }, + _ => 'outer: { + let mut errors = String::new(); + let cursor_icon_names = style.to_icon_names(); + for cursor_icon_name in cursor_icon_names { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, cursor_icon_name) + { + Ok(loaded_cursor) => { + if loaded_cursor != x11rb::NONE { + result = Ok(loaded_cursor); + break 'outer; + } + } + Err(err) => { + errors.push_str(&err.to_string()); + errors.push('\n'); + } + } + } + if errors.is_empty() { + result = Err(anyhow!( + "errors while loading cursor icons {:?}:\n{}", + cursor_icon_names, + errors + )); + } else { + result = Err(anyhow!("did not find cursor icons {:?}", cursor_icon_names)); + } + } + }; + + let cursor = match result { + Ok(cursor) => Some(cursor), + Err(err) => { + match self + .cursor_handle + .load_cursor(&self.xcb_connection, DEFAULT_CURSOR_ICON_NAME) + { + Ok(default) => { + log_cursor_icon_warning(err.context(format!( + "x11: error loading cursor icon, falling back on default icon '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + Some(default) + } + Err(default_err) => { + log_cursor_icon_warning(err.context(default_err).context(format!( + "x11: error loading default cursor fallback '{}'", + DEFAULT_CURSOR_ICON_NAME + ))); + None + } + } + } + }; + + self.cursor_cache.insert(style, cursor); + cursor + } +} + // Adapted from: // https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {