Skip to main content

ui/
cursor.rs

1// cursor -- custom pointer cursors drawn by the app, at the dpi of the window
2//                                                                     [src/ui/ui_cursor.s]
3//
4//   cursor_new(f, w, h, hot) -> handle 1..CUR_USER_MAX (0 = no slot left)
5//        a cursor of w x h DIP with the hot spot hot = x | y << 16 (DIP); f(g, dpi) draws it
6//        at `dpi` into g (FMT_PARGB, transparent; physical px: ceil(w * dpi / 96) x
7//        ceil(h * dpi / 96)) -- called again for every new dpi
8//   cursor_custom(handle)      the cursor that hit boxes with CUR_CUSTOM show (their
9//                              next_flags(CUR_CUSTOM << HB_CURSOR_SHIFT)); set it each
10//                              frame before the widget; 0 = the crosshair
11//   cursor_render(handle, dpi) -> Some(surface)   the cursor drawn into a new surface
12//                              (FMT_PARGB; tests, previews)
13//   cursor_current() -> handle the handle CUR_CUSTOM stands for (0 = none)
14// Internal: cursor_resolve(shape) -> the plat shape for a hit box cursor (CUR_CUSTOM: the
15// custom cursor made for the current window's dpi with plat::cursor_create, rebuilt when
16// that dpi changes; the crosshair when there is none or the platform has no custom
17// cursors, e.g. headless runs).
18
19use alloc::rc::Rc;
20use alloc::vec::Vec;
21
22use super::cmd::Gfx;
23use super::consts::*;
24use super::Ui;
25use gfx::{Surface, FMT_PARGB};
26
27/// a cursor's draw function: f(g, dpi) draws into the cursor surface
28pub type CursorDraw = Rc<dyn Fn(&mut Gfx, i32)>;
29
30// one custom cursor slot  [UC_*]
31#[derive(Clone)]
32struct UCur {
33    f: CursorDraw,
34    w: u32,
35    h: u32,
36    hot: u32,
37    /// plat shape (-1 none, -2 could not be made at dpi)
38    plat: i32,
39    dpi: u32,
40}
41
42/// the cursor slots and the current custom cursor
43#[derive(Default)]
44pub(crate) struct S {
45    slots: Vec<Option<UCur>>,
46    cur: u32,
47}
48
49// uc_px(dip, dpi) -> ceil(dip * dpi / 96)
50fn uc_px(dip: u32, dpi: u32) -> u32 {
51    dip.wrapping_mul(dpi).wrapping_add(95) / 96
52}
53
54impl Ui {
55    /// ui_cursor_new(f, w, h, hot) -> handle (0 = none)
56    pub fn cursor_new(&mut self, f: CursorDraw, w: u32, h: u32, hot: u32) -> u32 {
57        let s = &mut self.ucur.slots;
58        if s.is_empty() {
59            s.resize(CUR_USER_MAX as usize, None);
60        }
61        let Some(i) = s.iter().position(|x| x.is_none()) else { return 0 };
62        s[i] = Some(UCur { f, w, h, hot, plat: -1, dpi: 0 });
63        i as u32 + 1
64    }
65
66    /// ui_cursor_custom(handle): the cursor of CUR_CUSTOM hit boxes
67    pub fn cursor_custom(&mut self, h: u32) {
68        self.ucur.cur = h;
69    }
70
71    /// ui_cursor_current -> handle
72    pub fn cursor_current(&self) -> u32 {
73        self.ucur.cur
74    }
75
76    // uc_rec(handle): the slot of a live handle
77    fn uc_rec(&self, h: u32) -> Option<&UCur> {
78        let i = h.wrapping_sub(1);
79        if i >= CUR_USER_MAX {
80            return None;
81        }
82        self.ucur.slots.get(i as usize)?.as_ref()
83    }
84
85    /// ui_cursor_render(handle, dpi) -> the cursor drawn at dpi into a new surface
86    pub fn cursor_render(&mut self, h: u32, dpi: i32) -> Option<Surface> {
87        let r = self.uc_rec(h)?.clone();
88        if (dpi as u32).wrapping_sub(24) > 960 - 24 {
89            return None; // 25 % .. 1000 %
90        }
91        let w = uc_px(r.w, dpi as u32);
92        let hh = uc_px(r.h, dpi as u32);
93        if w.wrapping_sub(1) >= CUR_MAX_PX as u32 || hh.wrapping_sub(1) >= CUR_MAX_PX as u32 {
94            return None;
95        }
96        let mut sf = Surface::new(w as i32, hh as i32, FMT_PARGB);
97        if sf.px.is_empty() {
98            return None;
99        }
100        self.dc.begin(&sf);
101        {
102            let mut g = Gfx { dc: &mut self.dc, sf: &mut sf, text: &mut self.text };
103            (r.f)(&mut g, dpi);
104        }
105        self.dc.end();
106        Some(sf)
107    }
108
109    /// ui_cursor_resolve(shape) -> the plat shape: CUR_CUSTOM becomes the custom cursor made
110    /// for the current window's dpi (the crosshair without one)
111    pub(crate) fn cursor_resolve(&mut self, shape: u32) -> u32 {
112        if shape != CUR_CUSTOM {
113            return shape;
114        }
115        let h = self.ucur.cur;
116        let Some(r) = self.uc_rec(h).cloned() else { return CUR_CROSS };
117        let dpi = plat::cursor_dpi(self.wp.rt.dpi) as u32; // dpi to draw for
118        if r.dpi == dpi {
119            if r.plat >= 0 {
120                return r.plat as u32;
121            }
122            if r.plat == -2 {
123                return CUR_CROSS; // failed at this dpi already
124            }
125        }
126        // a new image: forget the old cursor (the platform shows the arrow meanwhile)
127        if r.plat >= 0 {
128            plat::cursor_destroy(r.plat);
129        }
130        let i = (h - 1) as usize;
131        if let Some(s) = self.ucur.slots[i].as_mut() {
132            s.plat = -2;
133            s.dpi = dpi;
134        }
135        let Some(sf) = self.cursor_render(h, dpi as i32) else { return CUR_CROSS };
136        // rows of exactly w * 4 bytes for the platform
137        let (w, hh) = (sf.w as usize, sf.h as usize);
138        let mut px = Vec::with_capacity(w * hh * 4);
139        for y in 0..hh {
140            let o = y * sf.stride as usize;
141            px.extend_from_slice(&sf.px[o..o + w * 4]);
142        }
143        // hot spot in px
144        let hx = ((r.hot & 0xFFFF).wrapping_mul(dpi).wrapping_add(48) / 96) as i32;
145        let hy = ((r.hot >> 16).wrapping_mul(dpi).wrapping_add(48) / 96) as i32;
146        let id = plat::cursor_create(&px, w as i32, hh as i32, hx, hy, dpi as i32);
147        if id >= 0 {
148            if let Some(s) = self.ucur.slots[i].as_mut() {
149                s.plat = id;
150            }
151            return id as u32;
152        }
153        CUR_CROSS
154    }
155}