1use std::{fmt, fs, io};
19
20use wayrs_client::object::Proxy;
21use wayrs_client::protocol::*;
22use wayrs_client::Connection;
23
24use crate::shm_alloc::{BufferSpec, ShmAlloc};
25
26use xcursor::parser::{parse_xcursor_stream, Image};
27
28use wayrs_protocols::cursor_shape_v1::*;
29pub use wp_cursor_shape_device_v1::Shape as CursorShape;
30
31#[derive(Debug)]
32pub enum CursorError {
33 DefaultCursorNotFound,
34 ThemeParseError,
35 ReadError(io::Error),
36}
37
38impl std::error::Error for CursorError {}
39
40impl fmt::Display for CursorError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::DefaultCursorNotFound => f.write_str("default cursor not found"),
44 Self::ThemeParseError => f.write_str("theme could not be parsed"),
45 Self::ReadError(error) => error.fmt(f),
46 }
47 }
48}
49
50impl From<io::Error> for CursorError {
51 fn from(value: io::Error) -> Self {
52 Self::ReadError(value)
53 }
54}
55
56#[derive(Debug)]
59pub struct CursorTheme(CursorThemeImp);
60
61#[derive(Debug)]
62enum CursorThemeImp {
63 Server {
64 manager: WpCursorShapeManagerV1,
65 },
66 Client {
67 compositor: WlCompositor,
68 cursor_size: u32,
69 theme: xcursor::CursorTheme,
70 },
71}
72
73#[derive(Debug)]
75pub struct CursorImage(CursorImageImp);
76
77#[derive(Debug)]
78enum CursorImageImp {
79 Server { shape: CursorShape },
80 Client { cursor_size: u32, imgs: Vec<Image> },
81}
82
83#[derive(Debug)]
86pub struct ThemedPointer {
87 pointer: WlPointer,
88 imp: ThemedPointerImp,
89}
90
91#[derive(Debug)]
92enum ThemedPointerImp {
93 Server { device: WpCursorShapeDeviceV1 },
94 Client { surface: WlSurface },
95}
96
97impl CursorTheme {
98 pub fn new<D>(conn: &mut Connection<D>, compositor: WlCompositor) -> Self {
100 if let Ok(manager) = conn.bind_singleton(1) {
101 return Self(CursorThemeImp::Server { manager });
102 }
103
104 let theme = xcursor::CursorTheme::load(
105 std::env::var("XCURSOR_THEME")
106 .as_deref()
107 .unwrap_or("default"),
108 );
109
110 let cursor_size = std::env::var("XCURSOR_SIZE")
111 .ok()
112 .and_then(|x| x.parse().ok())
113 .unwrap_or(24);
114
115 Self(CursorThemeImp::Client {
116 compositor,
117 cursor_size,
118 theme,
119 })
120 }
121
122 pub fn get_image(&self, shape: CursorShape) -> Result<CursorImage, CursorError> {
126 match &self.0 {
127 CursorThemeImp::Server { .. } => Ok(CursorImage(CursorImageImp::Server { shape })),
128 CursorThemeImp::Client {
129 cursor_size, theme, ..
130 } => {
131 let theme_path = theme
132 .load_icon(stringify_cursor_shape(shape))
133 .or_else(|| theme.load_icon("default"))
134 .ok_or(CursorError::DefaultCursorNotFound)?;
135
136 let mut reader = io::BufReader::new(fs::File::open(theme_path)?);
137 let mut imgs = match parse_xcursor_stream(&mut reader) {
138 Ok(imgs) => imgs,
139 Err(e) if e.kind() == io::ErrorKind::Other => {
140 return Err(CursorError::ThemeParseError);
141 }
142 Err(e) => {
143 return Err(e.into());
144 }
145 };
146
147 if imgs.is_empty() {
148 return Err(CursorError::DefaultCursorNotFound);
149 }
150
151 imgs.sort_unstable_by_key(|img| img.size);
152
153 Ok(CursorImage(CursorImageImp::Client {
154 cursor_size: *cursor_size,
155 imgs,
156 }))
157 }
158 }
159 }
160
161 pub fn get_themed_pointer<D>(
162 &self,
163 conn: &mut Connection<D>,
164 pointer: WlPointer,
165 ) -> ThemedPointer {
166 ThemedPointer {
167 pointer,
168 imp: match &self.0 {
169 CursorThemeImp::Server { manager } => ThemedPointerImp::Server {
170 device: manager.get_pointer(conn, pointer),
171 },
172 CursorThemeImp::Client { compositor, .. } => ThemedPointerImp::Client {
173 surface: compositor.create_surface(conn),
174 },
175 },
176 }
177 }
178}
179
180impl ThemedPointer {
181 pub fn set_cursor<D>(
192 &self,
193 conn: &mut Connection<D>,
194 shm: &mut ShmAlloc,
195 image: &CursorImage,
196 scale: u32,
197 serial: u32,
198 ) {
199 match (&self.imp, &image.0) {
200 (ThemedPointerImp::Server { device }, CursorImageImp::Server { shape }) => {
201 device.set_shape(conn, serial, *shape);
202 }
203 (
204 ThemedPointerImp::Client { surface },
205 CursorImageImp::Client { cursor_size, imgs },
206 ) => {
207 let scale = if surface.version() >= 3 { scale } else { 1 };
208 let target_size = cursor_size * scale;
209
210 let image = match imgs.binary_search_by_key(&target_size, |img| img.size) {
211 Ok(indx) => &imgs[indx],
212 Err(0) => imgs.first().unwrap(),
213 Err(indx) if indx >= imgs.len() => imgs.last().unwrap(),
214 Err(indx) => {
215 let a = &imgs[indx - 1];
216 let b = &imgs[indx];
217 if target_size - a.size < b.size - target_size {
218 a
219 } else {
220 b
221 }
222 }
223 };
224
225 let (buffer, canvas) = shm
226 .alloc_buffer(
227 conn,
228 BufferSpec {
229 width: image.width,
230 height: image.height,
231 stride: image.width * 4,
232 format: wl_shm::Format::Argb8888,
233 },
234 )
235 .expect("could not allocate frame shm buffer");
236
237 assert_eq!(image.pixels_rgba.len(), canvas.len());
238 canvas.copy_from_slice(&image.pixels_rgba);
239
240 surface.attach(conn, Some(buffer.into_wl_buffer()), 0, 0);
241 surface.damage(conn, 0, 0, i32::MAX, i32::MAX);
242 if surface.version() >= 3 {
243 surface.set_buffer_scale(conn, scale as i32);
244 }
245 surface.commit(conn);
246
247 self.pointer.set_cursor(
248 conn,
249 serial,
250 Some(*surface),
251 (image.xhot / scale) as i32,
252 (image.yhot / scale) as i32,
253 );
254 }
255 _ => panic!("ThemedPointer and CursorImage implementation mismatch"),
256 }
257 }
258
259 pub fn hide_cursor<D>(&self, conn: &mut Connection<D>, serial: u32) {
263 self.pointer.set_cursor(conn, serial, None, 0, 0);
264 }
265
266 pub fn destroy<D>(self, conn: &mut Connection<D>) {
270 match &self.imp {
271 ThemedPointerImp::Server { device } => device.destroy(conn),
272 ThemedPointerImp::Client { surface } => surface.destroy(conn),
273 }
274 }
275}
276
277fn stringify_cursor_shape(shape: CursorShape) -> &'static str {
278 const NAMES: &[&str] = &[
279 "default",
280 "context-menu",
281 "help",
282 "pointer",
283 "progress",
284 "wait",
285 "cell",
286 "crosshair",
287 "text",
288 "vertical-text",
289 "alias",
290 "copy",
291 "move",
292 "no-drop",
293 "not-allowed",
294 "grab",
295 "grabbing",
296 "e-resize",
297 "n-resize",
298 "ne-resize",
299 "nw-resize",
300 "s-resize",
301 "se-resize",
302 "sw-resize",
303 "w-resize",
304 "ew-resize",
305 "ns-resize",
306 "nesw-resize",
307 "nwse-resize",
308 "col-resize",
309 "row-resize",
310 "all-scroll",
311 "zoom-in",
312 "zoom-out",
313 ];
314 NAMES
315 .get(u32::from(shape).saturating_sub(1) as usize)
316 .unwrap_or(&"default")
317}