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// cursor_replace(handle, f, w, h, hot) -> bool (E3) a new drawing for a live handle (the
15// platform's cursor is made again when it is shown next)
16// cursor_free(handle) (E3) the slot is free again (cursor_new may hand it out); a
17// hit box showing it gets the crosshair
18// cursor_next(shape) (E3) the cursor of the next hit box: any CUR_* -- the system
19// shapes CUR_X0 .. CUR_X_END (not-allowed, grab, the resize
20// shapes ...) that do not fit into the 4 flag bits, CUR_CUSTOM
21// cursor_at(x, y) -> shape (ctx.rs) the cursor at a point (tests, an engine's own hover)
22// Internal: cursor_resolve(shape) -> the plat shape for a hit box cursor (CUR_CUSTOM: the
23// custom cursor made for the current window's dpi with plat::cursor_create, rebuilt when
24// that dpi changes; the crosshair when there is none or the platform has no custom
25// cursors, e.g. headless runs).
26
27use alloc::rc::Rc;
28use alloc::vec::Vec;
29
30use super::cmd::Gfx;
31use super::consts::*;
32use super::Ui;
33use gfx::{Surface, FMT_PARGB};
34
35/// a cursor's draw function: f(g, dpi) draws into the cursor surface
36pub type CursorDraw = Rc<dyn Fn(&mut Gfx, i32)>;
37
38// one custom cursor slot [UC_*]
39#[derive(Clone)]
40struct UCur {
41 f: CursorDraw,
42 w: u32,
43 h: u32,
44 hot: u32,
45 /// plat shape (-1 none, -2 could not be made at dpi)
46 plat: i32,
47 dpi: u32,
48}
49
50/// the cursor slots and the current custom cursor
51#[derive(Default)]
52pub(crate) struct S {
53 slots: Vec<Option<UCur>>,
54 cur: u32,
55}
56
57// uc_px(dip, dpi) -> ceil(dip * dpi / 96)
58fn uc_px(dip: u32, dpi: u32) -> u32 {
59 dip.wrapping_mul(dpi).wrapping_add(95) / 96
60}
61
62impl Ui {
63 /// ui_cursor_new(f, w, h, hot) -> handle (0 = none)
64 pub fn cursor_new(&mut self, f: CursorDraw, w: u32, h: u32, hot: u32) -> u32 {
65 let s = &mut self.ucur.slots;
66 if s.is_empty() {
67 s.resize(CUR_USER_MAX as usize, None);
68 }
69 let Some(i) = s.iter().position(|x| x.is_none()) else { return 0 };
70 s[i] = Some(UCur { f, w, h, hot, plat: -1, dpi: 0 });
71 i as u32 + 1
72 }
73
74 /// ui_cursor_custom(handle): the cursor of CUR_CUSTOM hit boxes
75 pub fn cursor_custom(&mut self, h: u32) {
76 self.ucur.cur = h;
77 }
78
79 /// ui_cursor_current -> handle
80 pub fn cursor_current(&self) -> u32 {
81 self.ucur.cur
82 }
83
84 /// (E3) the cursor of the next hit box (canvas, button, hit_rect ...): any CUR_* shape --
85 /// also those beyond CUR_CUSTOM's 4 bits (CUR_NOT_ALLOWED, CUR_GRAB, CUR_SIZE_NE ...), a
86 /// custom cursor through CUR_CUSTOM; it wins over the hit box's flags. A platform without
87 /// a shape shows plat::cursor_fallback's
88 pub fn cursor_next(&mut self, shape: u32) {
89 self.wp.lay.next_cursor = shape;
90 }
91
92 /// (E3) a new drawing for a live handle (f, w x h DIP, hot spot x | y << 16): the platform's
93 /// cursor is made again at the next show -> false for a handle that is not live
94 pub fn cursor_replace(&mut self, handle: u32, f: CursorDraw, w: u32, h: u32, hot: u32) -> bool {
95 let i = handle.wrapping_sub(1);
96 let Some(Some(s)) = self.ucur.slots.get_mut(i as usize).filter(|_| i < CUR_USER_MAX) else { return false };
97 if s.plat >= 0 {
98 plat::cursor_destroy(s.plat);
99 }
100 *s = UCur { f, w, h, hot, plat: -1, dpi: 0 };
101 true
102 }
103
104 /// (E3) free a handle of cursor_new: its platform cursor goes, the slot may be handed out
105 /// again; when it was the custom cursor, CUR_CUSTOM shows the crosshair until another is set
106 pub fn cursor_free(&mut self, handle: u32) {
107 let i = handle.wrapping_sub(1);
108 if i >= CUR_USER_MAX {
109 return;
110 }
111 if let Some(slot) = self.ucur.slots.get_mut(i as usize) {
112 if let Some(s) = slot.take() {
113 if s.plat >= 0 {
114 plat::cursor_destroy(s.plat);
115 }
116 }
117 }
118 if self.ucur.cur == handle {
119 self.ucur.cur = 0;
120 }
121 }
122
123 // uc_rec(handle): the slot of a live handle
124 fn uc_rec(&self, h: u32) -> Option<&UCur> {
125 let i = h.wrapping_sub(1);
126 if i >= CUR_USER_MAX {
127 return None;
128 }
129 self.ucur.slots.get(i as usize)?.as_ref()
130 }
131
132 /// ui_cursor_render(handle, dpi) -> the cursor drawn at dpi into a new surface
133 pub fn cursor_render(&mut self, h: u32, dpi: i32) -> Option<Surface> {
134 let r = self.uc_rec(h)?.clone();
135 if (dpi as u32).wrapping_sub(24) > 960 - 24 {
136 return None; // 25 % .. 1000 %
137 }
138 let w = uc_px(r.w, dpi as u32);
139 let hh = uc_px(r.h, dpi as u32);
140 if w.wrapping_sub(1) >= CUR_MAX_PX as u32 || hh.wrapping_sub(1) >= CUR_MAX_PX as u32 {
141 return None;
142 }
143 let mut sf = Surface::new(w as i32, hh as i32, FMT_PARGB);
144 if sf.px.is_empty() {
145 return None;
146 }
147 self.dc.begin(&sf);
148 {
149 let mut g = Gfx { dc: &mut self.dc, sf: &mut sf, text: &mut self.text };
150 (r.f)(&mut g, dpi);
151 }
152 self.dc.end();
153 Some(sf)
154 }
155
156 /// ui_cursor_resolve(shape) -> the plat shape: CUR_CUSTOM becomes the custom cursor made
157 /// for the current window's dpi (the crosshair without one)
158 pub(crate) fn cursor_resolve(&mut self, shape: u32) -> u32 {
159 if shape != CUR_CUSTOM {
160 return shape;
161 }
162 let h = self.ucur.cur;
163 let Some(r) = self.uc_rec(h).cloned() else { return CUR_CROSS };
164 let dpi = plat::cursor_dpi(self.wp.rt.dpi) as u32; // dpi to draw for
165 if r.dpi == dpi {
166 if r.plat >= 0 {
167 return r.plat as u32;
168 }
169 if r.plat == -2 {
170 return CUR_CROSS; // failed at this dpi already
171 }
172 }
173 // a new image: forget the old cursor (the platform shows the arrow meanwhile)
174 if r.plat >= 0 {
175 plat::cursor_destroy(r.plat);
176 }
177 let i = (h - 1) as usize;
178 if let Some(s) = self.ucur.slots[i].as_mut() {
179 s.plat = -2;
180 s.dpi = dpi;
181 }
182 let Some(sf) = self.cursor_render(h, dpi as i32) else { return CUR_CROSS };
183 // rows of exactly w * 4 bytes for the platform
184 let (w, hh) = (sf.w as usize, sf.h as usize);
185 let mut px = Vec::with_capacity(w * hh * 4);
186 for y in 0..hh {
187 let o = y * sf.stride as usize;
188 px.extend_from_slice(&sf.px[o..o + w * 4]);
189 }
190 // hot spot in px
191 let hx = ((r.hot & 0xFFFF).wrapping_mul(dpi).wrapping_add(48) / 96) as i32;
192 let hy = ((r.hot >> 16).wrapping_mul(dpi).wrapping_add(48) / 96) as i32;
193 let id = plat::cursor_create(&px, w as i32, hh as i32, hx, hy, dpi as i32);
194 if id >= 0 {
195 if let Some(s) = self.ucur.slots[i].as_mut() {
196 s.plat = id;
197 }
198 return id as u32;
199 }
200 CUR_CROSS
201 }
202}