wayrs_utils/
cursor.rs

1//! A simple but opinionated xcursor helper.
2//!
3//! # Example
4//!
5//! ```ignore
6//! // Do this once
7//! let cursor_theme = CursorTheme::new(conn, &globals, wl_compositor);
8//! let default_cursor = cursor_theme.get_image(CursorShape::Default).unwrap();
9//!
10//! // Do this when you bind a pointer
11//! let themed_pointer = cursor_theme.get_themed_pointer(conn, pointer);
12//!
13//! // Set cursor (on `wl_pointer.enter` or whenever you need to)
14//! themed_pointer.set_cursor(conn, shm, &default_cursor, surface_scale, enter_serial);
15//!
16//! ```
17
18use 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/// [`WpCursorShapeManagerV1`] wrapper which fallbacks to `xcursor` when `cursor-shape-v1` protocol
57/// extension is not supported.
58#[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/// A cursor image.
74#[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/// A wrapper around [`WlPointer`] with convenient [`set_cursor`](Self::set_cursor) and
84/// [`hide_cursor`](Self::hide_cursor) methods.
85#[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    /// Create new [`CursorTheme`], preferring the server-side implementation if possible.
99    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    /// Find and parse a cursor image.
123    ///
124    /// No-op if server-side implementation is used.
125    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    /// Set cursor image.
182    ///
183    /// Refer to [`WlPointer::set_cursor`] for more info.
184    ///
185    /// `shm` and `scale` are ignored if server-side implementation is used.
186    ///
187    /// # Panics
188    ///
189    /// This function may panic if the [`CursorShape`] was created form different [`CursorTheme`]
190    /// than this [`ThemedPointer`].
191    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    /// Hide cursor.
260    ///
261    /// Sets surface to NULL.
262    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    /// Destroy cursor's surface / cursor shape device.
267    ///
268    /// This function does not destroy the pointer.
269    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}