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    /// A file read or an ioctl failed: the cursor file, a buffer allocation, a
84    /// property read, or a commit. Carries the underlying error's text.
85    #[error("io: {0}")]
86    Io(String),
87
88    /// No plane on this CRTC can carry a cursor, and legacy was not allowed.
89    #[error("no plane on this CRTC can carry a cursor")]
90    NoPlane,
91
92    /// The only available path cannot carry this cursor.
93    ///
94    /// Today that means legacy `drmModeSetCursor`, on a controller with no
95    /// cursor hardware at all, or with a rotation the legacy call cannot
96    /// express. A caller told this can composite a
97    /// cursor instead, which beats a renderer that fails on its first show.
98    #[error("no supported cursor path is available")]
99    Unsupported,
100
101    /// The plane is missing a property every plane is required to have.
102    ///
103    /// A broken driver rather than anything a caller did.
104    #[error("the plane lacks a required property")]
105    MissingProperty,
106
107    /// A caller-named plane does not exist, cannot feed this CRTC, or can take
108    /// none of [`CURSOR_FORMATS`].
109    ///
110    /// Distinct from [`CursorError::NoPlane`]: the caller overrode the policy
111    /// and the override does not work. Falling back silently would leave them
112    /// on a plane they did not ask for.
113    #[error("the requested plane cannot carry a cursor on this CRTC")]
114    PlaneUnusable,
115}