aetna_core/tree/identity.rs
1//! Identity, source, and interaction-flag modifiers for [`El`].
2
3use std::panic::Location;
4
5use super::node::El;
6use super::semantics::{Kind, Source};
7
8/// Configuration for [`El::hover_alpha`] — the rest and peak alpha
9/// endpoints for a node whose opacity binds to the **subtree
10/// interaction envelope** (max of hover, focus, and press over the
11/// subtree rooted at this node).
12///
13/// `rest` is the drawn alpha when no descendant of this node is
14/// currently the active hover, focus, or press target. `peak` is the
15/// drawn alpha at full envelope. Linear interpolation between the two
16/// follows the eased subtree envelope (0..1).
17///
18/// Both fields are clamped to `[0.0, 1.0]` by [`El::hover_alpha`].
19/// Typical use is `rest < peak` ("reveal on interaction"), but the
20/// representation accepts `rest > peak` ("fade out on interaction") and
21/// sub-1.0 peaks for subtle affordances.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct HoverAlpha {
24 pub rest: f32,
25 pub peak: f32,
26}
27
28impl El {
29 pub fn new(kind: Kind) -> Self {
30 Self {
31 kind,
32 ..Default::default()
33 }
34 }
35
36 // ---- Identity / source ----
37 pub fn key(mut self, k: impl Into<String>) -> Self {
38 self.key = Some(k.into());
39 self
40 }
41
42 pub fn block_pointer(mut self) -> Self {
43 self.block_pointer = true;
44 self
45 }
46
47 pub fn focusable(mut self) -> Self {
48 self.focusable = true;
49 self
50 }
51
52 /// Show the focus ring on this node even when focus arrived via
53 /// pointer click. Default focus-ring behavior follows the web
54 /// platform's `:focus-visible` rule — ring on Tab, no ring on
55 /// click. Widgets where the ring is meaningful regardless of
56 /// source — text input, text area — opt in here so clicking into
57 /// the field still raises the "now active" affordance. Implies
58 /// nothing about focusability; pair with `.focusable()`.
59 pub fn always_show_focus_ring(mut self) -> Self {
60 self.always_show_focus_ring = true;
61 self
62 }
63
64 /// Opt this node into the library's text-selection system. The
65 /// node must also carry an explicit `.key(...)`; selection requires
66 /// stable identity across rebuilds the same way focus does.
67 pub fn selectable(mut self) -> Self {
68 self.selectable = true;
69 self
70 }
71
72 /// Opt this node into raw key capture when focused. While this
73 /// node is the focused target, the library's Tab/Enter/Escape
74 /// defaults are bypassed and raw `KeyDown` events are delivered for
75 /// the widget to interpret. Implies `focusable`.
76 pub fn capture_keys(mut self) -> Self {
77 self.capture_keys = true;
78 self.focusable = true;
79 self
80 }
81
82 /// Multiply this element's paint opacity by the nearest focusable
83 /// ancestor's focus envelope.
84 pub fn alpha_follows_focused_ancestor(mut self) -> Self {
85 self.alpha_follows_focused_ancestor = true;
86 self
87 }
88
89 /// Multiply this node's paint opacity by the runtime's caret blink
90 /// alpha.
91 pub fn blink_when_focused(mut self) -> Self {
92 self.blink_when_focused = true;
93 self
94 }
95
96 /// Borrow hover and press visual envelopes from the nearest
97 /// focusable ancestor.
98 pub fn state_follows_interactive_ancestor(mut self) -> Self {
99 self.state_follows_interactive_ancestor = true;
100 self
101 }
102
103 /// Bind this element's paint opacity to the subtree interaction
104 /// envelope — the `max` of hover, focus, and press for the subtree
105 /// rooted at this element.
106 ///
107 /// At rest (no descendant is the active hover, focus, or press
108 /// target) the element paints at `rest`. At full envelope it paints
109 /// at `peak`. Both are clamped to `[0.0, 1.0]`, with linear
110 /// interpolation in between following the eased envelope.
111 ///
112 /// "Subtree" matches CSS `:hover` semantics: hovering, focusing, or
113 /// pressing *any descendant* keeps the element revealed. A
114 /// hover-revealed close icon stays visible while the cursor moves
115 /// across the tab body or while the tab is keyboard-focused; an
116 /// action pill stays visible while the cursor moves between
117 /// focusable buttons inside it. The trigger isn't strictly
118 /// "hover" — focus and press also count — but `hover` is the
119 /// dominant case and the name reflects it.
120 ///
121 /// Layout-neutral — the element keeps its computed rect at all
122 /// times. Use for hover-revealed close buttons, secondary actions
123 /// on list rows, hover-only validation icons, and other
124 /// "show on interaction" patterns where the surrounding layout
125 /// shouldn't shift.
126 ///
127 /// # Beyond alpha
128 ///
129 /// For the other common hover affordances — Material-style lift
130 /// (`translate_y`), button-pop (`scale`), tint shift (`fill`) —
131 /// drive the prop from app code using
132 /// [`crate::BuildCx::is_hovering_within`] plus
133 /// [`Self::animate`]:
134 ///
135 /// ```ignore
136 /// fn build(&self, cx: &BuildCx) -> El {
137 /// let lifted = cx.is_hovering_within("card");
138 /// card([...])
139 /// .key("card")
140 /// .focusable()
141 /// .translate(0.0, if lifted { -2.0 } else { 0.0 })
142 /// .scale(if lifted { 1.02 } else { 1.0 })
143 /// .animate(Timing::SPRING_QUICK)
144 /// }
145 /// ```
146 ///
147 /// `is_hovering_within` reads the same subtree predicate
148 /// `hover_alpha` consumes (CSS `:hover`-style cascade). `animate`
149 /// eases the prop between the two build values across frames, so
150 /// the transition is smooth without per-channel declarative API.
151 /// `hover_alpha` itself is the alpha-channel shorthand — it skips
152 /// the boolean-to-value conversion and the per-node `animate`
153 /// allocation, since alpha is the dominant hover affordance.
154 pub fn hover_alpha(mut self, rest: f32, peak: f32) -> Self {
155 self.hover_alpha = Some(HoverAlpha {
156 rest: rest.clamp(0.0, 1.0),
157 peak: peak.clamp(0.0, 1.0),
158 });
159 self
160 }
161
162 pub fn at(mut self, file: &'static str, line: u32) -> Self {
163 self.source = Source {
164 file,
165 line,
166 from_library: false,
167 };
168 self
169 }
170
171 /// Set source from a `Location` (used internally by
172 /// `#[track_caller]` constructors).
173 pub fn at_loc(mut self, loc: &'static Location<'static>) -> Self {
174 self.source = Source::from_caller(loc);
175 self
176 }
177
178 /// Mark this El as constructed inside an aetna library closure
179 /// where `#[track_caller]` doesn't reach user code (e.g. the
180 /// `.map(|item| ...)` body inside `tabs_list`, `radio_group`,
181 /// etc.). The lint pass uses this flag to walk blame attribution
182 /// upward to the nearest user-source ancestor instead of pointing
183 /// findings at aetna-core internals. User code never needs to call
184 /// this.
185 pub fn from_library(mut self) -> Self {
186 self.source.from_library = true;
187 self
188 }
189}