srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-06-01 11:15:00 +0200
committersrdusr <[email protected]>2024-06-01 11:15:00 +0200
commit20cdd47b39f09aa19317d9d29fef6511b98c1233 (patch)
treee369fb583a66867cf13ddb2ee0ca782ac98014c3 /crates
parentae056d17d137463b70f34862b9e33a9813c4b381 (diff)
downloadsrdwm-20cdd47b39f09aa19317d9d29fef6511b98c1233.tar.gz
srdwm-20cdd47b39f09aa19317d9d29fef6511b98c1233.zip
cursor: load the real XCursor theme arrow instead of always drawing the built-in bitmap
The compositor's own pointer - over decorations and the desktop, and as the fallback for any named shape with no dedicated art - was always the hand-rasterized 24px ARROW bitmap, regardless of what theme the rest of the session was using. Confirmed live (by the AGS peer session) that this machine's actual GTK cursor theme is Sweet-cursors at size 24 (both gtk-3.0 and gtk-4.0 settings.ini, and gsettings, agree), installed and present, but nothing read it - XCURSOR_THEME/XCURSOR_SIZE aren't set on this work either, so even naive env-based theme loading would have found nothing. load_theme_arrow (cursor.rs) now resolves a theme name and size -- XCURSOR_THEME/XCURSOR_SIZE first, falling back to GTK's own gtk-cursor-theme-name/-size out of settings.ini, since that's what's actually authoritative in practice here - and loads left_ptr via the `xcursor` crate (the same one anvil and other smithay compositors use), picking whichever nominal size the theme ships is closest to the target. XCursor pixel data comes back as straight RGBA off disk; only the channel order needs converting to the BGRA every other buffer in this file uses for Fourcc::Argb8888 (see arrow_bitmap's own byte order) - the data is already premultiplied alpha per the file format spec, same as everything else built here, so no premultiplication step. Falls back to the existing built-in bitmap arrow on any failure (theme/icon missing, corrupt file, a pixel count that doesn't match the declared dimensions) - same "always present beats prettier but sometimes absent" reasoning the built-in arrow's own doc comment already gave for not doing this at all, kept intact as the fallback rather than replaced. The built-in arrow's hotspot was implicitly (0, 0) - its tip, baked into where render_elements positioned it. A real theme's hotspot is data (CursorBuffers::arrow_hotspot), not necessarily the bitmap's corner, so both `_ =>` arrow arms in render_elements now subtract it like every other named shape already does. Verified live on this machine: resolves theme="Sweet-cursors" size=24 from settings.ini, loads a real 30x30 left_ptr image with hotspot (4,4) -- checked with a temporary probe test, removed before committing. Two lightweight unit tests (rgba_to_bgra_*) cover the channel-reorder byte math without needing a theme installed, so they run everywhere. cargo build --workspace, cargo clippy -p srdwm-wayland (0 new warnings), cargo test -p srdwm-wayland cursor (13/13), cargo test -p srdwm-core (111/111) all pass. Known remaining gap, not addressed here: the loaded size still doesn't track output scale (a HiDPI output gets whatever size settings.ini says regardless of scale factor) - MemoryRenderBuffer's own scale param is hardcoded to 1 throughout this file already, a pre-existing limitation this change doesn't touch.
Diffstat (limited to 'crates')
-rw-r--r--crates/wayland/Cargo.toml1
-rw-r--r--crates/wayland/src/cursor.rs169
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);