cranpose_ui/pointer_icon_session.rs
1//! The pointer icon the platform should be drawing right now.
2//!
3//! The shell resolves the hovered region's [`PointerIcon`] on every pointer
4//! move and records it here; the platform layer reads the pending change back
5//! and applies it to the window it owns (winit's `Window::set_cursor` on
6//! desktop, the canvas's CSS `cursor` on the web). The indirection exists
7//! because neither side can call the other directly: `cranpose-ui` cannot reach
8//! a window handle, and the platform backend is not on the stack when a
9//! modifier declares its icon.
10//!
11//! Reading is a poll rather than a callback because a windowing system wants
12//! the cursor set from its own event loop, holding the handles only that loop
13//! has: the platform asks for the change at the point where it can act on it.
14//!
15//! The session is stored per [`AppContext`](crate::render_state::AppContext),
16//! like the clipboard and text-input sessions, so each window in a multi-window
17//! app carries its own pointer icon.
18
19use std::cell::RefCell;
20
21use cranpose_ui_graphics::PointerIcon;
22
23/// The icon one window's pointer shows, with the change its platform has not
24/// applied yet. The app context keeps one for callers of the free functions
25/// below; a shell keeps one per surface, because the OS owns cursors per
26/// window.
27pub struct PointerIconState {
28 current: RefCell<PointerIcon>,
29 pending: RefCell<Option<PointerIcon>>,
30}
31
32impl Default for PointerIconState {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl PointerIconState {
39 /// A session showing the platform default with no change pending.
40 pub fn new() -> Self {
41 Self {
42 current: RefCell::new(PointerIcon::DEFAULT),
43 pending: RefCell::new(None),
44 }
45 }
46
47 /// Requests `icon`; a request for the icon already held changes nothing.
48 pub fn set(&self, icon: PointerIcon) {
49 if *self.current.borrow() == icon {
50 return;
51 }
52 *self.current.borrow_mut() = icon.clone();
53 *self.pending.borrow_mut() = Some(icon);
54 }
55
56 /// The change the platform has not applied yet, taken.
57 pub fn take_change(&self) -> Option<PointerIcon> {
58 self.pending.borrow_mut().take()
59 }
60
61 /// Offers the held icon to the platform again, as a pending change.
62 pub fn refresh(&self) {
63 *self.pending.borrow_mut() = Some(self.current.borrow().clone());
64 }
65
66 /// The icon currently requested, applied or not.
67 pub fn current(&self) -> PointerIcon {
68 self.current.borrow().clone()
69 }
70}
71
72/// Requests `icon` as the pointer's appearance.
73///
74/// Called by the shell once per pointer move with whatever the hovered region
75/// asks for. Setting the icon the session already holds does nothing, so a
76/// pointer travelling across one region does not re-upload its cursor.
77pub fn set_pointer_icon(icon: PointerIcon) {
78 crate::render_state::with_pointer_icon_session(|state| state.set(icon));
79}
80
81/// Takes the pointer icon change the platform has not applied yet, leaving
82/// nothing behind.
83///
84/// Returns `None` when the icon has not changed since the last call, which is
85/// the common case: a platform backend calls this after every batch of input
86/// and touches its window only when something comes back.
87pub fn take_pointer_icon_change() -> Option<PointerIcon> {
88 crate::render_state::with_pointer_icon_session(|state| state.take_change())
89}
90
91/// Offers the icon the session already holds to the platform again.
92///
93/// A windowing system resets the cursor to its own default on the way back
94/// into a window — when the application is activated, or when the pointer
95/// crosses in — without telling the application what it drew. Nothing in the
96/// hovered region has changed, so no change would be reported and the platform
97/// default would stay on screen over a region that names its own cursor. The
98/// platform layer calls this at those moments so the next poll re-applies what
99/// the region already asked for.
100pub fn refresh_pointer_icon() {
101 crate::render_state::with_pointer_icon_session(|state| state.refresh());
102}
103
104/// The pointer icon currently requested, whether or not the platform has
105/// applied it yet.
106pub fn current_pointer_icon() -> PointerIcon {
107 crate::render_state::with_pointer_icon_session(|state| state.current())
108}
109
110#[cfg(test)]
111#[path = "tests/pointer_icon_session_tests.rs"]
112mod tests;