Skip to main content

aetna_core/state/
cursor.rs

1//! Cursor resolution for [`UiState`](super::UiState).
2
3use crate::cursor::Cursor;
4use crate::tree::El;
5
6use super::UiState;
7
8impl UiState {
9    /// Resolved pointer cursor for the current frame.
10    ///
11    /// Picks the cursor in this order:
12    /// 1. If [`Self::pressed`] is set:
13    ///    a. If the press target itself declares `.cursor_pressed(...)`,
14    ///    use that — drives the slider's Grab → Grabbing transition and
15    ///    the more general "press has its own affordance" idiom. Does
16    ///    not inherit: an ancestor's `cursor_pressed` doesn't apply to a
17    ///    descendant press target.
18    ///    b. Otherwise walk from the press target up to `root` for the
19    ///    first explicit `.cursor(...)`. Press capture wins so a button
20    ///    drag that wanders onto a text region doesn't flicker the
21    ///    cursor mid-press.
22    /// 2. Else if the pointer is over a text-link run
23    ///    (`hovered_link` is `Some`), use [`Cursor::Pointer`].
24    ///    Link runs aren't keyed hit-test targets, so this branch sits
25    ///    parallel to the keyed-hover lookup. Beats any
26    ///    `.cursor(Cursor::Text)` declared on a containing paragraph
27    ///    — link affordance is more specific than panel affordance.
28    /// 3. Else if [`Self::hovered`] is set, walk from the hovered
29    ///    target up to `root` for the first explicit declaration —
30    ///    so a panel that sets `.cursor(Move)` once propagates to
31    ///    children that don't override.
32    /// 4. Else [`Cursor::Default`].
33    ///
34    /// Disabled state isn't auto-mapped to [`Cursor::NotAllowed`];
35    /// widgets that want that affordance branch in their build closure.
36    pub fn cursor(&self, root: &El) -> Cursor {
37        if let Some(pressed) = &self.pressed {
38            let id = pressed.node_id.as_str();
39            if let Some(c) = cursor_pressed_at_target(root, id) {
40                return c;
41            }
42            return cursor_for_target(root, id).unwrap_or(Cursor::Default);
43        }
44        if self.hovered_link.is_some() {
45            return Cursor::Pointer;
46        }
47        if let Some(hovered) = &self.hovered {
48            return cursor_for_target(root, hovered.node_id.as_str()).unwrap_or(Cursor::Default);
49        }
50        Cursor::Default
51    }
52}
53
54/// Find the node by `target_id` and return its `cursor_pressed`, if
55/// any. Unlike [`cursor_for_target`] this does **not** walk up — only
56/// the literal press target's `cursor_pressed` matters (an ancestor's
57/// pressed-cursor declaration shouldn't override a descendant press).
58fn cursor_pressed_at_target(root: &El, target_id: &str) -> Option<Cursor> {
59    fn walk(node: &El, target_id: &str) -> Option<Option<Cursor>> {
60        if node.computed_id == target_id {
61            return Some(node.cursor_pressed);
62        }
63        for c in &node.children {
64            if let Some(found) = walk(c, target_id) {
65                return Some(found);
66            }
67        }
68        None
69    }
70    walk(root, target_id).flatten()
71}
72
73/// Resolve the cursor a node by `target_id` should display by walking
74/// from `root` down, carrying the closest-ancestor cursor declaration.
75/// Returns the target's own cursor if declared, else the nearest
76/// ancestor's, else `None` when no ancestor (or the target itself)
77/// declared one. Returns `None` when `target_id` isn't in the tree —
78/// callers fall back to the default in that case.
79fn cursor_for_target(root: &El, target_id: &str) -> Option<Cursor> {
80    fn walk(node: &El, target_id: &str, inherited: Option<Cursor>) -> Option<Option<Cursor>> {
81        let here = node.cursor.or(inherited);
82        if node.computed_id == target_id {
83            return Some(here);
84        }
85        for c in &node.children {
86            if let Some(found) = walk(c, target_id, here) {
87                return Some(found);
88            }
89        }
90        None
91    }
92    walk(root, target_id, None).flatten()
93}