diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/wayland/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/wayland/src/cursor.rs | 169 |
2 files changed, 153 insertions, 17 deletions
diff --git a/crates/wayland/Cargo.toml b/crates/wayland/Cargo.toml index 96d0e50..5f06617 100644 --- a/crates/wayland/Cargo.toml +++ b/crates/wayland/Cargo.toml @@ -33,6 +33,7 @@ memmap2 = "0.9" # actually resolves to, so both see the same wire types. wayland-scanner = "0.31.10" wayland-backend = { version = "0.3.15", features = ["server_system"] } +xcursor = "0.3.11" [dependencies.smithay] version = "0.7" diff --git a/crates/wayland/src/cursor.rs b/crates/wayland/src/cursor.rs index d9bc3be..624b3b3 100644 --- a/crates/wayland/src/cursor.rs +++ b/crates/wayland/src/cursor.rs @@ -12,21 +12,23 @@ //! 1. **The client's own cursor surface** (`CursorImageStatus::Surface`) -- //! a terminal's I-beam, a browser's hand, an app's resize arrows. Drawn //! from its surface tree, offset by the hotspot the client declared. -//! 2. **A built-in arrow**, for when no client has set an image (over -//! srdwm's own decorations and the desktop) or asked for a named shape -//! we have no art for. +//! 2. **The system's real XCursor theme arrow** (`load_theme_arrow`), +//! resolved from `XCURSOR_THEME`/`XCURSOR_SIZE` or, failing that, GTK's +//! own `gtk-cursor-theme-name`/`-size` - for when no client has set an +//! image (over srdwm's own decorations and the desktop) or asked for a +//! named shape we have no art for. +//! 3. **A built-in hand-rasterized arrow**, only if theme loading found +//! nothing at all - no theme installed, an unreadable file, whatever. +//! A cursor that is always present beats a prettier one that sometimes +//! isn't there, the same reasoning as `decoration.rs`'s font fallback; +//! this is the same bitmap that used to be the *only* arrow. //! //! `CursorImageStatus::Hidden` is honoured, so a client that hides the //! pointer still gets its way. Named shapes we have dedicated art for //! (text entry, the four resize directions, crosshair, move, and the -//! pointing-hand link-hover shape) render as that shape; anything else -//! falls back to the arrow. -//! -//! The built-in arrow is deliberate rather than loading an XCursor theme: -//! theme loading pulls in a dependency, needs a theme to actually be -//! installed, and has a search-path fallback story of its own. A cursor that -//! is always present beats a prettier one that sometimes isn't there - the -//! same reasoning as `decoration.rs`'s font fallback. +//! pointing-hand link-hover shape) still render as that hand-drawn shape, +//! not a theme lookup - only the plain default arrow goes through theme +//! resolution. use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::utils::{Logical, Point}; @@ -168,6 +170,10 @@ fn add_white_halo(buf: &mut [u8]) { #[derive(Clone)] pub(crate) struct CursorBuffers { pub(crate) arrow: smithay::backend::renderer::element::memory::MemoryRenderBuffer, + /// `arrow`'s hotspot - (0, 0), the bitmap's top-left tip, for the + /// built-in fallback, but a real XCursor theme's own `xhot`/`yhot` when + /// `load_theme_arrow` found one. See that function's doc comment. + pub(crate) arrow_hotspot: (i32, i32), pub(crate) text: smithay::backend::renderer::element::memory::MemoryRenderBuffer, pub(crate) ns_resize: smithay::backend::renderer::element::memory::MemoryRenderBuffer, pub(crate) ew_resize: smithay::backend::renderer::element::memory::MemoryRenderBuffer, @@ -368,8 +374,21 @@ fn upload(data: Vec<u8>) -> smithay::backend::renderer::element::memory::MemoryR pub(crate) const CENTERED_HOTSPOT: (i32, i32) = (CURSOR_SIZE / 2, CURSOR_SIZE / 2); pub(crate) fn make_buffers() -> CursorBuffers { + let (arrow, arrow_hotspot) = match load_theme_arrow() { + Some(theme_arrow) => { + use smithay::backend::allocator::Fourcc; + use smithay::backend::renderer::element::memory::MemoryRenderBuffer; + use smithay::utils::Transform; + ( + MemoryRenderBuffer::from_slice(&theme_arrow.bgra, Fourcc::Argb8888, theme_arrow.size, 1, Transform::Normal, None), + theme_arrow.hotspot, + ) + } + None => (make_buffer(), (0, 0)), + }; CursorBuffers { - arrow: make_buffer(), + arrow, + arrow_hotspot, text: upload(text_bitmap()), ns_resize: upload(straight_resize_bitmap(false)), ew_resize: upload(straight_resize_bitmap(true)), @@ -445,8 +464,11 @@ where // No client image. A named shape we have dedicated art for gets it // (centered on the pointer - these are all symmetric shapes, unlike // the arrow); anything else (Default, or one of the many shapes we - // don't draw, e.g. Grab/Pointer/Crosshair) falls back to the arrow, - // whose hotspot is its tip instead, at the origin. + // don't draw, e.g. Grab/Pointer/Crosshair) falls back to the arrow. + // The arrow's own hotspot is `buffers.arrow_hotspot` - (0, 0), the + // bitmap's tip, for the built-in fallback, but a real theme's `xhot`/ + // `yhot` (not necessarily the top-left corner at all) when + // `load_theme_arrow` found one. let (buffer, at) = match status { CursorImageStatus::Named(icon) => match icon { CursorIcon::Text | CursorIcon::VerticalText => { @@ -467,9 +489,9 @@ where CursorIcon::Crosshair => (&buffers.crosshair, (local.0 - CENTERED_HOTSPOT.0, local.1 - CENTERED_HOTSPOT.1)), CursorIcon::Move => (&buffers.move_icon, (local.0 - CENTERED_HOTSPOT.0, local.1 - CENTERED_HOTSPOT.1)), CursorIcon::Pointer => (&buffers.pointer, (local.0 - POINTER_HOTSPOT.0, local.1 - POINTER_HOTSPOT.1)), - _ => (&buffers.arrow, (local.0, local.1)), + _ => (&buffers.arrow, (local.0 - buffers.arrow_hotspot.0, local.1 - buffers.arrow_hotspot.1)), }, - _ => (&buffers.arrow, (local.0, local.1)), + _ => (&buffers.arrow, (local.0 - buffers.arrow_hotspot.0, local.1 - buffers.arrow_hotspot.1)), }; let at = (at.0 as f64, at.1 as f64); match MemoryRenderBufferRenderElement::from_buffer(renderer, at, buffer, None, None, None, Kind::Cursor) { @@ -482,8 +504,106 @@ where } +/// Resolves which XCursor theme to load the real arrow from, and at what +/// size. +/// +/// `XCURSOR_THEME`/`XCURSOR_SIZE` are the standard override, but nothing +/// sets them on a session started this way (confirmed live) - so the +/// fallback below, reading GTK's own `gtk-cursor-theme-name`/ +/// `gtk-cursor-theme-size` straight out of `settings.ini`, is what actually +/// resolves the theme apps on the same session are themed with in practice. +/// Without it, `xcursor::CursorTheme::load`'s own search lands on +/// `/usr/share/icons/default/index.theme`, which inherits Adwaita on a +/// machine with no `~/.icons/default` override - not what GTK reports +/// (confirmed live: Sweet-cursors), so the compositor's own pointer would +/// keep not matching every app's client-drawn cursor even after this. +fn theme_and_size() -> (String, u32) { + if let Ok(theme) = std::env::var("XCURSOR_THEME") { + if !theme.is_empty() { + let size = std::env::var("XCURSOR_SIZE").ok().and_then(|s| s.parse().ok()).unwrap_or(CURSOR_SIZE as u32); + return (theme, size); + } + } + if let Some(home) = std::env::var_os("HOME") { + let settings = std::path::Path::new(&home).join(".config/gtk-3.0/settings.ini"); + if let Ok(contents) = std::fs::read_to_string(&settings) { + let mut name = None; + let mut size = None; + for line in contents.lines() { + let line = line.trim(); + if let Some(v) = line.strip_prefix("gtk-cursor-theme-name=") { + name = Some(v.trim().to_string()); + } else if let Some(v) = line.strip_prefix("gtk-cursor-theme-size=") { + size = v.trim().parse().ok(); + } + } + if let Some(name) = name { + return (name, size.unwrap_or(CURSOR_SIZE as u32)); + } + } + } + ("default".to_string(), CURSOR_SIZE as u32) +} + +/// A loaded XCursor image, converted to what `make_buffers` needs to upload +/// it: BGRA8888 pixels, pixel dimensions, and hotspot. +struct ThemeArrow { + bgra: Vec<u8>, + size: (i32, i32), + hotspot: (i32, i32), +} + +/// Loads the real arrow cursor (`left_ptr`) from the resolved XCursor theme, +/// picking whichever bundled nominal size is closest to the target. +/// +/// Converts pixels from the crate's RGBA byte order (`Image::pixels_rgba`, +/// straight off disk) to the BGRA order every buffer in this file uses for +/// `Fourcc::Argb8888` - see `arrow_bitmap`'s own per-pixel byte order. +/// XCursor pixel data is already premultiplied alpha per the file format +/// spec, same as every bitmap built here, so only the channel order needs +/// converting, not the alpha itself. +/// +/// Returns `None` on any failure - theme or icon not found, corrupt file, +/// a pixel count that doesn't match the declared dimensions - so the +/// caller falls back to the built-in bitmap arrow, which is the entire +/// reason that fallback exists: see this module's own doc comment. +fn load_theme_arrow() -> Option<ThemeArrow> { + let (theme, size) = theme_and_size(); + let path = xcursor::CursorTheme::load(&theme).load_icon("left_ptr")?; + let bytes = std::fs::read(&path).ok()?; + let images = xcursor::parser::parse_xcursor(&bytes)?; + let image = images.into_iter().min_by_key(|img| (img.size as i64 - size as i64).abs())?; + if image.width == 0 || image.height == 0 || image.pixels_rgba.len() != (image.width * image.height * 4) as usize { + return None; + } + Some(ThemeArrow { + bgra: rgba_to_bgra(&image.pixels_rgba), + size: (image.width as i32, image.height as i32), + hotspot: (image.xhot as i32, image.yhot as i32), + }) +} + +/// Per-pixel R,G,B,A -> B,G,R,A channel reorder - the byte order `Fourcc:: +/// Argb8888` buffers use everywhere else in this file (see `arrow_bitmap`'s +/// own doc comment), versus the straight-off-disk order `xcursor::parser` +/// hands back in `Image::pixels_rgba`. Alpha is untouched: XCursor pixel +/// data is already premultiplied per the file format spec, same as every +/// bitmap built here. +fn rgba_to_bgra(pixels_rgba: &[u8]) -> Vec<u8> { + let mut bgra = Vec::with_capacity(pixels_rgba.len()); + for px in pixels_rgba.chunks_exact(4) { + bgra.push(px[2]); + bgra.push(px[1]); + bgra.push(px[0]); + bgra.push(px[3]); + } + bgra +} + /// The built-in arrow as an uploadable buffer. Built once at startup rather -/// than per frame - the bitmap never changes. +/// than per frame - the bitmap never changes. Kept as a fallback for when +/// `load_theme_arrow` finds nothing - see this module's doc comment on why +/// a cursor that's always present beats a prettier one that sometimes isn't. pub(crate) fn make_buffer() -> smithay::backend::renderer::element::memory::MemoryRenderBuffer { use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBuffer; @@ -496,6 +616,21 @@ mod tests { use super::*; #[test] + fn rgba_to_bgra_reorders_channels_and_leaves_alpha_alone() { + // Same fixture xcursor's own rgba_to_argb test uses, so the two + // conversions are easy to cross-check by eye: R=0x12, G=0x34, + // B=0x56, A=0x78. + let rgba = [0x12, 0x34, 0x56, 0x78]; + assert_eq!(rgba_to_bgra(&rgba), vec![0x56, 0x34, 0x12, 0x78]); + } + + #[test] + fn rgba_to_bgra_handles_multiple_pixels_independently() { + let rgba = [0x01, 0x02, 0x03, 0x04, 0xaa, 0xbb, 0xcc, 0xdd]; + assert_eq!(rgba_to_bgra(&rgba), vec![0x03, 0x02, 0x01, 0x04, 0xcc, 0xbb, 0xaa, 0xdd]); + } + + #[test] fn arrow_is_the_expected_size_and_has_an_opaque_tip() { let buf = arrow_bitmap(); assert_eq!(buf.len(), (CURSOR_SIZE * CURSOR_SIZE * 4) as usize); |