aboutsummaryrefslogtreecommitdiff
path: root/src/cursor.rs
blob: 1ed3600f5586d5fcc68a3530add3f6a721ddc8e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::Read;

use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Physical, Point, Transform};
use xcursor::parser::{parse_xcursor, Image};
use xcursor::CursorTheme;

static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba");

pub struct Cursor {
    images: Vec<Image>,
    size: i32,
    cache: HashMap<i32, (TextureBuffer<GlesTexture>, Point<i32, Physical>)>,
}

impl Cursor {
    /// Load the said theme as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE`
    /// env variables.
    pub fn load(theme: &str, size: u8) -> Self {
        env::set_var("XCURSOR_THEME", theme);
        env::set_var("XCURSOR_SIZE", size.to_string());

        let images = match load_xcursor(theme) {
            Ok(images) => images,
            Err(err) => {
                warn!("error loading xcursor default cursor: {err:?}");

                vec![Image {
                    size: 32,
                    width: 64,
                    height: 64,
                    xhot: 1,
                    yhot: 1,
                    delay: 1,
                    pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA),
                    pixels_argb: vec![],
                }]
            }
        };

        Self {
            images,
            size: size as i32,
            cache: HashMap::new(),
        }
    }

    pub fn get(
        &mut self,
        renderer: &mut GlesRenderer,
        scale: i32,
    ) -> (TextureBuffer<GlesTexture>, Point<i32, Physical>) {
        self.cache
            .entry(scale)
            .or_insert_with_key(|scale| {
                let _span = tracy_client::span!("create cursor texture");

                let size = self.size * scale;

                let nearest_image = self
                    .images
                    .iter()
                    .min_by_key(|image| (size - image.size as i32).abs())
                    .unwrap();
                let frame = self
                    .images
                    .iter()
                    .find(move |image| {
                        image.width == nearest_image.width && image.height == nearest_image.height
                    })
                    .unwrap();

                let texture = TextureBuffer::from_memory(
                    renderer,
                    &frame.pixels_rgba,
                    Fourcc::Abgr8888,
                    (frame.width as i32, frame.height as i32),
                    false,
                    *scale,
                    Transform::Normal,
                    None,
                )
                .unwrap();
                (texture, (frame.xhot as i32, frame.yhot as i32).into())
            })
            .clone()
    }

    pub fn get_cached_hotspot(&self, scale: i32) -> Option<Point<i32, Physical>> {
        self.cache.get(&scale).map(|(_, hotspot)| *hotspot)
    }
}

fn load_xcursor(theme: &str) -> anyhow::Result<Vec<Image>> {
    let _span = tracy_client::span!();

    let theme = CursorTheme::load(theme);
    let path = theme
        .load_icon("default")
        .ok_or_else(|| anyhow!("no default icon"))?;
    let mut file = File::open(path).context("error opening cursor icon file")?;
    let mut buf = vec![];
    file.read_to_end(&mut buf)
        .context("error reading cursor icon file")?;
    let images = parse_xcursor(&buf).context("error parsing cursor icon file")?;

    Ok(images)
}