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}