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 2aa0408e652e9471d1c63e075fbfe609646a1921..62f1d091c78988d826695984f89a643c1001c835 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -730,7 +730,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); } } } @@ -1530,9 +1530,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 8fcc7033e137a92d4509957f2d505a27fe3f2417..2b99277ce928835892cc660a947543185960af23 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}, @@ -211,7 +212,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, @@ -1501,22 +1502,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); @@ -1773,6 +1760,78 @@ impl X11ClientState { }) .expect("Failed to initialize refresh timer") } + + 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: