Skip to main content

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;