x11rb/cursor/
mod.rs

1//! Utility functions for working with X11 cursors
2//!
3//! The code in this module is only available when the `cursor` feature of the library is enabled.
4
5use crate::connection::Connection;
6use crate::cookie::Cookie as X11Cookie;
7use crate::errors::{ConnectionError, ReplyOrIdError};
8use crate::protocol::render::{self, Pictformat};
9use crate::protocol::xproto::{self, FontWrapper, Window};
10use crate::resource_manager::Database;
11use crate::NONE;
12use xcursor::parser::Image;
13
14mod find_cursor;
15mod parse_cursor;
16
17/// The level of cursor support of the X11 server
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum RenderSupport {
20    /// Render extension not available
21    None,
22
23    /// Static cursor support (CreateCursor added in RENDER 0.5)
24    StaticCursor,
25
26    /// Animated cursor support (CreateAnimCursor added in RENDER 0.8)
27    AnimatedCursor,
28}
29
30/// A cookie for creating a `Handle`
31#[derive(Debug)]
32pub struct Cookie<'a, 'b, C: Connection> {
33    screen: &'a xproto::Screen,
34    resource_database: &'b Database,
35    render_info: Option<(
36        X11Cookie<'a, C, render::QueryVersionReply>,
37        X11Cookie<'a, C, render::QueryPictFormatsReply>,
38    )>,
39}
40
41impl<C: Connection> Cookie<'_, '_, C> {
42    /// Get the handle from the replies from the X11 server
43    pub fn reply(self) -> Result<Handle, ReplyOrIdError> {
44        let mut render_version = (0, 0);
45        let mut picture_format = NONE;
46        if let Some((version, formats)) = self.render_info {
47            let version = version.reply()?;
48            render_version = (version.major_version, version.minor_version);
49            picture_format = find_format(&formats.reply()?);
50        }
51        Self::from_replies(
52            self.screen,
53            self.resource_database,
54            render_version,
55            picture_format,
56        )
57    }
58
59    /// Get the handle from the replies from the X11 server
60    pub fn reply_unchecked(self) -> Result<Option<Handle>, ReplyOrIdError> {
61        let mut render_version = (0, 0);
62        let mut picture_format = NONE;
63        if let Some((version, formats)) = self.render_info {
64            match (version.reply_unchecked()?, formats.reply_unchecked()?) {
65                (Some(version), Some(formats)) => {
66                    render_version = (version.major_version, version.minor_version);
67                    picture_format = find_format(&formats);
68                }
69                _ => return Ok(None),
70            }
71        }
72        Ok(Some(Self::from_replies(
73            self.screen,
74            self.resource_database,
75            render_version,
76            picture_format,
77        )?))
78    }
79
80    fn from_replies(
81        screen: &xproto::Screen,
82        resource_database: &Database,
83        render_version: (u32, u32),
84        picture_format: Pictformat,
85    ) -> Result<Handle, ReplyOrIdError> {
86        let render_support = if render_version.0 >= 1 || render_version.1 >= 8 {
87            RenderSupport::AnimatedCursor
88        } else if render_version.0 >= 1 || render_version.1 >= 5 {
89            RenderSupport::StaticCursor
90        } else {
91            RenderSupport::None
92        };
93        let theme = std::env::var("XCURSOR_THEME").ok().or_else(|| {
94            resource_database
95                .get_string("Xcursor.theme", "")
96                .map(|theme| theme.to_string())
97        });
98        let cursor_size = match resource_database.get_value("Xcursor.size", "") {
99            Ok(Some(value)) => value,
100            _ => 0,
101        };
102        let xft_dpi = match resource_database.get_value("Xft.dpi", "") {
103            Ok(Some(value)) => value,
104            _ => 0,
105        };
106        let cursor_size = get_cursor_size(cursor_size, xft_dpi, screen);
107        Ok(Handle {
108            root: screen.root,
109            picture_format,
110            render_support,
111            theme,
112            cursor_size,
113        })
114    }
115}
116
117/// A handle necessary for loading cursors
118#[derive(Debug)]
119pub struct Handle {
120    root: Window,
121    picture_format: Pictformat,
122    render_support: RenderSupport,
123    theme: Option<String>,
124    cursor_size: u32,
125}
126
127impl Handle {
128    /// Create a new cursor handle for creating cursors on the given screen.
129    ///
130    /// The `resource_database` is used to look up settings like the current cursor theme and the
131    /// cursor size to use.
132    ///
133    /// This function returns a cookie that can be used to later get the actual handle.
134    ///
135    /// If you want this function not to block, you should prefetch the RENDER extension's data on
136    /// the connection.
137    #[allow(clippy::new_ret_no_self)]
138    pub fn new<'a, 'b, C: Connection>(
139        conn: &'a C,
140        screen: usize,
141        resource_database: &'b Database,
142    ) -> Result<Cookie<'a, 'b, C>, ConnectionError> {
143        let screen = &conn.setup().roots[screen];
144        let render_info = if conn
145            .extension_information(render::X11_EXTENSION_NAME)?
146            .is_some()
147        {
148            let render_version = render::query_version(conn, 0, 8)?;
149            let render_pict_format = render::query_pict_formats(conn)?;
150            Some((render_version, render_pict_format))
151        } else {
152            None
153        };
154        Ok(Cookie {
155            screen,
156            resource_database,
157            render_info,
158        })
159    }
160
161    /// Loads the specified cursor, either from the cursor theme or by falling back to the X11
162    /// "cursor" font.
163    pub fn load_cursor<C>(&self, conn: &C, name: &str) -> Result<xproto::Cursor, ReplyOrIdError>
164    where
165        C: Connection,
166    {
167        load_cursor(conn, self, name)
168    }
169}
170
171fn open_cursor(theme: &Option<String>, name: &str) -> Option<find_cursor::Cursor> {
172    theme
173        .as_ref()
174        .and_then(|theme| find_cursor::find_cursor(theme, name))
175        .or_else(|| find_cursor::find_cursor("default", name))
176}
177
178fn create_core_cursor<C: Connection>(
179    conn: &C,
180    cursor: u16,
181) -> Result<xproto::Cursor, ReplyOrIdError> {
182    let result = conn.generate_id()?;
183    let cursor_font = FontWrapper::open_font(conn, b"cursor")?;
184    let _ = xproto::create_glyph_cursor(
185        conn,
186        result,
187        cursor_font.font(),
188        cursor_font.font(),
189        cursor,
190        cursor + 1,
191        // foreground color
192        0,
193        0,
194        0,
195        // background color
196        u16::MAX,
197        u16::MAX,
198        u16::MAX,
199    )?;
200    Ok(result)
201}
202
203fn create_render_cursor<C: Connection>(
204    conn: &C,
205    handle: &Handle,
206    image: &Image,
207    storage: &mut Option<(xproto::Pixmap, xproto::Gcontext, u16, u16)>,
208) -> Result<render::Animcursorelt, ReplyOrIdError> {
209    let to_u16 = |input: u32| {
210        u16::try_from(input).expect(
211            "xcursor-rs has a 16 bit limit on cursor size, but some number does not fit into u16?!",
212        )
213    };
214
215    let (width, height) = (to_u16(image.width), to_u16(image.height));
216
217    // Get a pixmap of the right size and a gc for it
218    let (pixmap, gc) = match *storage {
219        Some((pixmap, gc, w, h)) if (w, h) == (width, height) => (pixmap, gc),
220        _ => {
221            let (pixmap, gc) = if let Some((pixmap, gc, _, _)) = storage {
222                let _ = xproto::free_gc(conn, *gc)?;
223                let _ = xproto::free_pixmap(conn, *pixmap)?;
224                (*pixmap, *gc)
225            } else {
226                (conn.generate_id()?, conn.generate_id()?)
227            };
228            let _ = xproto::create_pixmap(conn, 32, pixmap, handle.root, width, height)?;
229            let _ = xproto::create_gc(conn, gc, pixmap, &Default::default())?;
230
231            *storage = Some((pixmap, gc, width, height));
232            (pixmap, gc)
233        }
234    };
235
236    let _ = xproto::put_image(
237        conn,
238        xproto::ImageFormat::Z_PIXMAP,
239        pixmap,
240        gc,
241        width,
242        height,
243        0,
244        0,
245        0,
246        32,
247        &image.pixels_rgba,
248    )?;
249
250    let picture = render::PictureWrapper::create_picture(
251        conn,
252        pixmap,
253        handle.picture_format,
254        &Default::default(),
255    )?;
256    let cursor = conn.generate_id()?;
257    let _ = render::create_cursor(
258        conn,
259        cursor,
260        picture.picture(),
261        to_u16(image.xhot),
262        to_u16(image.yhot),
263    )?;
264
265    Ok(render::Animcursorelt {
266        cursor,
267        delay: image.delay,
268    })
269}
270
271fn load_cursor<C: Connection>(
272    conn: &C,
273    handle: &Handle,
274    name: &str,
275) -> Result<xproto::Cursor, ReplyOrIdError> {
276    // Find the right cursor, load it directly if it is a core cursor
277    let cursor_file = match open_cursor(&handle.theme, name) {
278        None => return Ok(NONE),
279        Some(find_cursor::Cursor::CoreChar(c)) => return create_core_cursor(conn, c),
280        Some(find_cursor::Cursor::File(f)) => f,
281    };
282
283    // We have to load a file and use RENDER to create a cursor
284    if handle.render_support == RenderSupport::None {
285        return Ok(NONE);
286    }
287
288    // Load the cursor from the file
289    use std::io::BufReader;
290    let images = parse_cursor::parse_cursor(&mut BufReader::new(cursor_file), handle.cursor_size)
291        .or(Err(crate::errors::ParseError::InvalidValue))?;
292    let mut images = &images[..];
293
294    // No animated cursor support? Only use the first image
295    if handle.render_support == RenderSupport::StaticCursor {
296        images = &images[0..1];
297    }
298
299    // Now transfer the cursors to the X11 server
300    let mut storage = None;
301    let cursors = images
302        .iter()
303        .map(|image| create_render_cursor(conn, handle, image, &mut storage))
304        .collect::<Result<Vec<_>, _>>()?;
305    if let Some((pixmap, gc, _, _)) = storage {
306        let _ = xproto::free_gc(conn, gc)?;
307        let _ = xproto::free_pixmap(conn, pixmap)?;
308    }
309
310    if cursors.len() == 1 {
311        Ok(cursors[0].cursor)
312    } else {
313        let result = conn.generate_id()?;
314        let _ = render::create_anim_cursor(conn, result, &cursors)?;
315        for elem in cursors {
316            let _ = xproto::free_cursor(conn, elem.cursor)?;
317        }
318        Ok(result)
319    }
320}
321
322fn find_format(reply: &render::QueryPictFormatsReply) -> Pictformat {
323    reply
324        .formats
325        .iter()
326        .filter(|format| {
327            format.type_ == render::PictType::DIRECT
328                && format.depth == 32
329                && format.direct.red_shift == 16
330                && format.direct.red_mask == 0xff
331                && format.direct.green_shift == 8
332                && format.direct.green_mask == 0xff
333                && format.direct.blue_shift == 0
334                && format.direct.blue_mask == 0xff
335                && format.direct.alpha_shift == 24
336                && format.direct.alpha_mask == 0xff
337        })
338        .map(|format| format.id)
339        .next()
340        .expect("The X11 server is missing the RENDER ARGB_32 standard format!")
341}
342
343fn get_cursor_size(rm_cursor_size: u32, rm_xft_dpi: u32, screen: &xproto::Screen) -> u32 {
344    if let Some(size) = std::env::var("XCURSOR_SIZE")
345        .ok()
346        .and_then(|s| s.parse().ok())
347    {
348        return size;
349    }
350    if rm_cursor_size > 0 {
351        return rm_cursor_size;
352    }
353    if rm_xft_dpi > 0 {
354        return rm_xft_dpi * 16 / 72;
355    }
356    u32::from(screen.height_in_pixels.min(screen.width_in_pixels) / 48)
357}