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