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}