cranpose_ui/
pointer_icon_session.rs1use std::cell::RefCell;
20
21use cranpose_ui_graphics::PointerIcon;
22
23pub(crate) struct PointerIconState {
24 current: RefCell<PointerIcon>,
25 pending: RefCell<Option<PointerIcon>>,
26}
27
28impl PointerIconState {
29 pub(crate) fn new() -> Self {
30 Self {
31 current: RefCell::new(PointerIcon::DEFAULT),
32 pending: RefCell::new(None),
33 }
34 }
35
36 fn set(&self, icon: PointerIcon) {
37 if *self.current.borrow() == icon {
38 return;
39 }
40 *self.current.borrow_mut() = icon.clone();
41 *self.pending.borrow_mut() = Some(icon);
42 }
43
44 fn take_change(&self) -> Option<PointerIcon> {
45 self.pending.borrow_mut().take()
46 }
47
48 fn refresh(&self) {
49 *self.pending.borrow_mut() = Some(self.current.borrow().clone());
50 }
51
52 fn current(&self) -> PointerIcon {
53 self.current.borrow().clone()
54 }
55}
56
57pub fn set_pointer_icon(icon: PointerIcon) {
63 crate::render_state::with_pointer_icon_session(|state| state.set(icon));
64}
65
66pub fn take_pointer_icon_change() -> Option<PointerIcon> {
73 crate::render_state::with_pointer_icon_session(|state| state.take_change())
74}
75
76pub fn refresh_pointer_icon() {
86 crate::render_state::with_pointer_icon_session(|state| state.refresh());
87}
88
89pub fn current_pointer_icon() -> PointerIcon {
92 crate::render_state::with_pointer_icon_session(|state| state.current())
93}
94
95#[cfg(test)]
96mod tests {
97 use cranpose_ui_graphics::{CursorIcon, ImageBitmap};
98
99 use super::*;
100 use crate::render_state::AppContext;
101
102 fn custom_icon() -> PointerIcon {
103 PointerIcon::custom(
104 ImageBitmap::from_rgba8(4, 4, vec![255; 64]).expect("bitmap"),
105 1,
106 2,
107 )
108 .expect("icon")
109 }
110
111 #[test]
112 fn a_fresh_session_holds_the_default_icon_and_no_change() {
113 let context = AppContext::new();
114 context.enter(|| {
115 assert_eq!(current_pointer_icon(), PointerIcon::DEFAULT);
116 assert_eq!(take_pointer_icon_change(), None);
117 });
118 }
119
120 #[test]
121 fn setting_a_new_icon_yields_one_change() {
122 let context = AppContext::new();
123 context.enter(|| {
124 set_pointer_icon(PointerIcon::POINTER);
125 assert_eq!(current_pointer_icon(), PointerIcon::POINTER);
126 assert_eq!(take_pointer_icon_change(), Some(PointerIcon::POINTER));
127 assert_eq!(take_pointer_icon_change(), None);
128 });
129 }
130
131 #[test]
132 fn re_setting_the_same_icon_reports_no_change() {
133 let context = AppContext::new();
134 context.enter(|| {
135 set_pointer_icon(PointerIcon::TEXT);
136 assert_eq!(take_pointer_icon_change(), Some(PointerIcon::TEXT));
137 set_pointer_icon(PointerIcon::TEXT);
138 assert_eq!(take_pointer_icon_change(), None);
139 });
140 }
141
142 #[test]
143 fn the_latest_icon_wins_when_the_platform_has_not_polled() {
144 let context = AppContext::new();
145 context.enter(|| {
146 set_pointer_icon(PointerIcon::POINTER);
147 set_pointer_icon(PointerIcon::System(CursorIcon::Crosshair));
148 assert_eq!(
149 take_pointer_icon_change(),
150 Some(PointerIcon::System(CursorIcon::Crosshair))
151 );
152 assert_eq!(take_pointer_icon_change(), None);
153 });
154 }
155
156 #[test]
157 fn custom_icons_round_trip_through_the_session() {
158 let context = AppContext::new();
159 context.enter(|| {
160 let icon = custom_icon();
161 set_pointer_icon(icon.clone());
162 assert_eq!(take_pointer_icon_change(), Some(icon.clone()));
163 set_pointer_icon(icon);
164 assert_eq!(take_pointer_icon_change(), None);
165 });
166 }
167
168 #[test]
169 fn a_refresh_offers_the_icon_the_region_already_asked_for() {
170 let context = AppContext::new();
171 context.enter(|| {
172 set_pointer_icon(PointerIcon::POINTER);
173 assert_eq!(take_pointer_icon_change(), Some(PointerIcon::POINTER));
174 assert_eq!(take_pointer_icon_change(), None);
175
176 refresh_pointer_icon();
177 assert_eq!(
178 take_pointer_icon_change(),
179 Some(PointerIcon::POINTER),
180 "coming back to the window re-applies the region's own cursor"
181 );
182 });
183 }
184
185 #[test]
186 fn a_refresh_on_a_window_that_asked_for_nothing_restores_the_default() {
187 let context = AppContext::new();
188 context.enter(|| {
189 refresh_pointer_icon();
190 assert_eq!(take_pointer_icon_change(), Some(PointerIcon::DEFAULT));
191 });
192 }
193
194 #[test]
195 fn each_app_context_carries_its_own_icon() {
196 let first = AppContext::new();
197 let second = AppContext::new();
198 first.enter(|| set_pointer_icon(PointerIcon::POINTER));
199 second.enter(|| {
200 assert_eq!(current_pointer_icon(), PointerIcon::DEFAULT);
201 assert_eq!(take_pointer_icon_change(), None);
202 });
203 first.enter(|| {
204 assert_eq!(take_pointer_icon_change(), Some(PointerIcon::POINTER));
205 });
206 }
207}