Files
feresaria/compositor/src/cursor.rs
T
Samuel Onoja 6f2951c6b0 Scope redraw requests and avoid redundant animation fallback wakeups
Instrument redraw sources, per-output request coalescing, and animation fallback scheduling. Route local visual changes to their affected outputs while preserving deferred grab and gesture updates.

Schedule fallback recovery using each orphaned animation output's refresh interval, suppress it while a normal frame chain exists, and retain requested final frames after scheduling failures. Add mixed-refresh, cursor footprint, frame-chain, and disconnect regressions and document measurement limits.
2026-10-02 01:21:11 +01:00

133 lines
3.8 KiB
Rust

use std::fs;
use std::sync::OnceLock;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::CursorIcon;
use smithay::utils::{Buffer, Point, Size, Transform};
use xcursor::CursorTheme;
use xcursor::parser::Image;
const DEFAULT_CURSOR_SIZE: u32 = 24;
#[derive(Debug, Clone)]
pub(crate) struct NamedCursor {
pub buffer: MemoryRenderBuffer,
pub hotspot: Point<i32, Buffer>,
pub size: Size<i32, Buffer>,
}
pub(crate) fn cursor_theme() -> CursorTheme {
let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
CursorTheme::load(&name)
}
pub(crate) fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon) -> NamedCursor {
let requested_size = cursor_size();
let image = cursor_names(icon)
.iter()
.find_map(|name| load_image(theme, name, requested_size));
match image {
Some(image) => cursor_from_image(image),
None => fallback_cursor(),
}
}
fn cursor_size() -> u32 {
static SIZE: OnceLock<u32> = OnceLock::new();
*SIZE.get_or_init(|| {
std::env::var("XCURSOR_SIZE")
.ok()
.and_then(|value| value.parse().ok())
.filter(|size| *size > 0)
.unwrap_or(DEFAULT_CURSOR_SIZE)
})
}
fn cursor_names(icon: CursorIcon) -> [&'static str; 3] {
match icon {
CursorIcon::Default => ["default", "left_ptr", "arrow"],
CursorIcon::Pointer => ["pointer", "hand2", "left_ptr"],
CursorIcon::Text => ["text", "xterm", "left_ptr"],
_ => [icon.name(), "default", "left_ptr"],
}
}
fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Image> {
let path = theme.load_icon(name)?;
let bytes = fs::read(path).ok()?;
xcursor::parser::parse_xcursor(&bytes)?.into_iter().min_by_key(|image| {
(
image.size.abs_diff(requested_size),
image.width.abs_diff(requested_size),
)
})
}
fn cursor_from_image(image: Image) -> NamedCursor {
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None);
NamedCursor {
buffer,
hotspot: Point::from((image.xhot as i32, image.yhot as i32)),
size,
}
}
fn fallback_cursor() -> NamedCursor {
const WIDTH: usize = 12;
const HEIGHT: usize = 18;
let mut pixels = vec![0_u8; WIDTH * HEIGHT * 4];
for y in 0..HEIGHT {
let body_width = (y / 2 + 1).min(8);
for x in 0..body_width {
let edge = x == 0 || x + 1 == body_width || y == 0;
let offset = (y * WIDTH + x) * 4;
let color = if edge { 0 } else { 255 };
pixels[offset..offset + 4].copy_from_slice(&[color, color, color, 255]);
}
}
NamedCursor {
buffer: MemoryRenderBuffer::from_slice(
&pixels,
Fourcc::Abgr8888,
Size::<i32, Buffer>::from((WIDTH as i32, HEIGHT as i32)),
1,
Transform::Normal,
None,
),
hotspot: Point::default(),
size: (WIDTH as i32, HEIGHT as i32).into(),
}
}
#[cfg(test)]
mod tests {
use smithay::input::pointer::CursorIcon;
use super::cursor_names;
#[test]
fn supplies_legacy_fallback_names_for_common_cursors() {
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
}
#[test]
fn falls_back_to_default_for_other_named_cursors() {
assert_eq!(
cursor_names(CursorIcon::Crosshair),
["crosshair", "default", "left_ptr"]
);
}
}