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}