Skip to main content

drmkit_cursor/
lib.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Cursor themes, and the cursors in them.
5//!
6//! [`Theme`] indexes the `XCursor` themes installed on a machine and resolves a
7//! cursor name -- following each theme's `Inherits=` chain and the alias table
8//! that maps modern names onto whatever legacy name a theme happens to ship --
9//! to a file on disk. It reads no pixels, which keeps it cheap and testable
10//! without a display.
11//!
12//! [`Cursor`] is the other half: it reads one of those files into frames,
13//! hotspots and timing. A cursor file is untrusted input, so the parser bounds
14//! every length against the bytes actually present before allocating anything
15//! -- see the `xcursor` module for why that is not delegated to a crate.
16
17mod alias;
18mod cursor;
19mod pingpong;
20mod plane;
21mod renderer;
22mod size;
23mod sprite;
24mod stage;
25mod theme;
26mod xcursor;
27
28#[cfg(test)]
29mod tests;
30
31pub use cursor::{Cursor, Frame};
32pub use pingpong::PingPong;
33pub use plane::{CURSOR_FORMATS, PlanePath, SelectedPlane, select_plane};
34pub use renderer::{Paused, Renderer, RendererConfig};
35pub use size::{preferred_size, probe_size};
36pub use sprite::{Placement, Rotation, compose};
37pub use stage::{PlaneState, hardware_rotation, plane_origin, stage};
38pub use theme::{Theme, ThemeResolution, default_search_paths};
39
40/// What can go wrong finding a cursor.
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42#[non_exhaustive]
43pub enum CursorError {
44    /// The cursor name is empty, `.`, `..`, or contains a path separator.
45    ///
46    /// A cursor name is an identifier, not a path. Refusing the ambiguous
47    /// cases keeps a caller-supplied name from composing into somewhere else
48    /// on the filesystem.
49    #[error("not a usable cursor name")]
50    InvalidName,
51
52    /// No cursor theme directory was readable at all.
53    ///
54    /// Distinct from [`CursorError::NotFound`]: this one means nothing is
55    /// installed, which is a different thing to say to a user.
56    #[error("no cursor themes are installed")]
57    NoThemes,
58
59    /// No theme on the search path has this cursor.
60    #[error("no theme provides that cursor")]
61    NotFound,
62
63    /// The file is not a cursor, is truncated, or contradicts itself.
64    #[error("malformed cursor file")]
65    Malformed,
66
67    /// The file declares more frames, or larger frames, than are accepted.
68    ///
69    /// Separate from [`CursorError::Malformed`] because such a file may be
70    /// perfectly well-formed -- it is refused for being unreasonable, not for
71    /// being broken, and that is worth saying differently.
72    #[error("cursor file exceeds the loader's limits")]
73    TooLarge,
74
75    /// The file parsed but holds no images.
76    #[error("cursor file contains no images")]
77    NoImages,
78
79    /// Caller-supplied pixels do not describe an image.
80    #[error("not a usable cursor image")]
81    InvalidImage,
82
83    /// The file could not be read.
84    #[error("io: {0}")]
85    Io(String),
86
87    /// No plane on this CRTC can carry a cursor, and legacy was not allowed.
88    #[error("no plane on this CRTC can carry a cursor")]
89    NoPlane,
90
91    /// The only available path is one this does not implement yet.
92    ///
93    /// Today that means legacy `drmModeSetCursor`, on a controller with no
94    /// plane that can carry a cursor. A caller told this can composite a
95    /// cursor instead, which beats a renderer that fails on its first show.
96    #[error("no supported cursor path is available")]
97    Unsupported,
98
99    /// The plane is missing a property every plane is required to have.
100    ///
101    /// A broken driver rather than anything a caller did.
102    #[error("the plane lacks a required property")]
103    MissingProperty,
104
105    /// A caller-named plane does not exist, cannot feed this CRTC, or cannot
106    /// take `ARGB8888`.
107    ///
108    /// Distinct from [`CursorError::NoPlane`]: the caller overrode the policy
109    /// and the override does not work. Falling back silently would leave them
110    /// on a plane they did not ask for.
111    #[error("the requested plane cannot carry a cursor on this CRTC")]
112    PlaneUnusable,
113}