Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/smithay/wayland-rs/llms.txt

Use this file to discover all available pages before exploring further.

wayland-cursor provides a pure-Rust re-implementation of the libwayland-cursor functionality. It reads XCursor theme files from the system, selects the appropriate image size, uploads cursor pixel data into a Wayland shared-memory pool, and exposes WlBuffer handles you can attach directly to a wl_surface. Animated cursors are fully supported via a frame-timing API.
Version covered by this reference: 0.31.14. Full API documentation is available at docs.rs/wayland-cursor.

Installation

[dependencies]
wayland-cursor = "0.31.14"
# wayland-client is a required transitive dependency; you likely already have it
wayland-client = "0.31"
No additional system libraries are required — wayland-cursor uses rustix for shared-memory management and xcursor for parsing XCursor files.

CursorTheme

CursorTheme represents a fully loaded cursor theme. It owns the Wayland shared-memory pool into which all cursor images are uploaded. You create one per Wayland connection and reuse it for all cursor lookups.
pub struct CursorTheme { /* private */ }

Constructors

MethodSignatureDescription
loadfn load(conn: &Connection, shm: WlShm, size: u32) -> Result<Self, InvalidId>Load from system defaults. Reads XCURSOR_THEME and XCURSOR_SIZE environment variables; falls back to the "default" theme.
load_orfn load_or(conn: &Connection, shm: WlShm, name: &str, size: u32) -> Result<Self, InvalidId>Load using name as the fallback theme if XCURSOR_THEME is not set.
load_from_namefn load_from_name(conn: &Connection, shm: WlShm, name: &str, size: u32) -> Result<Self, InvalidId>Load a specific named theme, ignoring all environment variables.
load is equivalent to load_or(conn, shm, "default", size). All three constructors create the backing wl_shm_pool via the provided WlShm global and return Err(InvalidId) only if the WlShm object is no longer valid.

Methods

impl CursorTheme {
    pub fn get_cursor(&mut self, name: &str) -> Option<&Cursor>;
    pub fn set_fallback<F>(&mut self, fallback: F)
    where
        F: Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync + 'static;
}
MethodDescription
get_cursor(name)Look up a cursor by its XCursor name (e.g. "default", "wait", "crosshair"). Returns None if neither the theme nor any parent theme provides it. Lazily parses and uploads on first call.
set_fallback(f)Register a closure that is called with (name, size) when a cursor is missing from the system theme. The closure should return the raw bytes of an XCursor file, or None to decline.
The XCURSOR_THEME and XCURSOR_SIZE environment variables are read at load / load_or call time. Set them before creating your CursorTheme if you need to override them programmatically.

Cursor

A Cursor holds one or more CursorImageBuffer frames. Static cursors have exactly one frame; animated cursors have multiple, each with an associated display duration.
#[derive(Debug, Clone)]
pub struct Cursor { /* private */ }

Methods

impl Cursor {
    pub fn frame_and_duration(&self, millis: u32) -> FrameAndDuration;
    pub fn image_count(&self) -> usize;
}
MethodSignatureDescription
frame_and_durationfn frame_and_duration(&self, millis: u32) -> FrameAndDurationGiven the elapsed time in milliseconds since the animation started, returns which frame to display and how many milliseconds remain until the next frame. Time wraps automatically over the full animation duration.
image_countfn image_count(&self) -> usizeReturns the total number of frames in this cursor.

Indexing

Cursor implements Index<usize> — use array indexing to retrieve a specific frame as a CursorImageBuffer:
let buffer: &CursorImageBuffer = &cursor[frame_index];

FrameAndDuration

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FrameAndDuration {
    /// The index of the frame to display.
    pub frame_index: usize,
    /// Milliseconds for which this frame should remain visible.
    pub frame_duration: u32,
}

CursorImageBuffer

A single frame of cursor pixel data, backed by a WlBuffer in the shared-memory pool. CursorImageBuffer derefs to WlBuffer, so you can attach it directly to a wl_surface.
#[derive(Debug, Clone)]
pub struct CursorImageBuffer { /* private */ }

Methods

impl CursorImageBuffer {
    pub fn dimensions(&self) -> (u32, u32);
    pub fn hotspot(&self) -> (u32, u32);
    pub fn delay(&self) -> u32;
}
MethodReturn typeDescription
dimensions()(u32, u32)Pixel width and height of this frame as (width, height).
hotspot()(u32, u32)Position of the pointer hotspot within the image as (xhot, yhot).
delay()u32Display duration of this frame in milliseconds (same value as FrameAndDuration::frame_duration).
CursorImageBuffer derefs to WlBuffer:
impl Deref for CursorImageBuffer {
    type Target = WlBuffer;
    fn deref(&self) -> &WlBuffer { ... }
}
The WlBuffer backing each CursorImageBuffer is managed internally by wayland-cursor. Do not call wl_buffer.destroy() on it directly.

Animation loop pattern

For animated cursors, use frame_and_duration to drive a frame-advance loop. Record a start time, then on each iteration compute elapsed milliseconds and ask the cursor which frame to show.
use std::time::{Duration, Instant};
use wayland_cursor::CursorTheme;

fn run_cursor_loop(
    cursor_surface: &wayland_client::protocol::wl_surface::WlSurface,
    cursor: &wayland_cursor::Cursor,
) {
    let start_time = Instant::now();
    loop {
        let millis = start_time.elapsed().as_millis() as u32;
        let fr_info = cursor.frame_and_duration(millis);

        let buffer = &cursor[fr_info.frame_index];
        let hotspot = buffer.hotspot();
        cursor_surface.attach(Some(buffer), 0, 0);
        cursor_surface.damage(0, 0, i32::MAX, i32::MAX);
        cursor_surface.commit();

        std::thread::sleep(Duration::from_millis(fr_info.frame_duration as u64));
    }
}
For static cursors, image_count() returns 1 and frame_and_duration always returns frame_index: 0.

Complete usage example

The following example shows the full lifecycle: connecting to Wayland, obtaining the wl_shm global, loading a theme, looking up a cursor, and attaching its buffer to a surface.
use wayland_client::{Connection, protocol::wl_shm::WlShm};
use wayland_cursor::CursorTheme;
use std::time::{Instant, Duration};

fn example(
    connection: &Connection,
    shm: WlShm,
    cursor_surface: &wayland_client::protocol::wl_surface::WlSurface,
) {
    // Load the default cursor theme at 24 logical pixels.
    // Respects the XCURSOR_THEME / XCURSOR_SIZE environment variables.
    let mut cursor_theme = CursorTheme::load(connection, shm, 24)
        .expect("failed to create shm pool for cursor theme");

    // Look up the "default" cursor (the standard pointer arrow).
    let cursor = cursor_theme
        .get_cursor("default")
        .expect("theme does not provide a 'default' cursor");

    // Run an animation loop.
    let start = Instant::now();
    loop {
        let elapsed_ms = start.elapsed().as_millis() as u32;
        let fr = cursor.frame_and_duration(elapsed_ms);

        let buf = &cursor[fr.frame_index];
        let (hx, hy) = buf.hotspot();

        // Attach the buffer and set the hotspot via wl_pointer.set_cursor.
        cursor_surface.attach(Some(buf), 0, 0);
        cursor_surface.commit();

        // Sleep until the frame expires. In a real app, integrate with your
        // event loop timer instead.
        std::thread::sleep(Duration::from_millis(fr.frame_duration as u64));
    }
}
Use set_fallback to embed a minimal cursor image in your binary as a last resort when the system theme is incomplete:
cursor_theme.set_fallback(|name, _size| {
    if name == "default" {
        Some(std::borrow::Cow::Borrowed(include_bytes!("../assets/default.xcur")))
    } else {
        None
    }
});

Build docs developers (and LLMs) love