Skip to main content

damascene_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    /// Opt this node into consuming touch drag. A touch contact that
87    /// starts on this node (or any descendant — the flag inherits
88    /// down the tree) is treated as a drag rather than a pan/scroll
89    /// gesture, suppressing the runner's touch-scroll synthesis.
90    /// Use on widgets whose primary interaction is dragging:
91    /// sliders, scrubbers, resize handles, draggable cards. No
92    /// effect on mouse / pen pointers.
93    pub fn consumes_touch_drag(mut self) -> Self {
94        self.consumes_touch_drag = true;
95        self
96    }
97
98    /// Attach source-backed copy/hit-test text for this selectable
99    /// node. The node still needs `.selectable().key(...)`; this only
100    /// changes how selection offsets map to copied text.
101    pub fn selection_source(mut self, source: crate::selection::SelectionSource) -> Self {
102        self.selection_source = Some(source);
103        self
104    }
105
106    /// Opt this node into raw key capture when focused. While this
107    /// node is the focused target, the library's traversal/activation
108    /// defaults are bypassed and raw `KeyDown` events are delivered for
109    /// the widget to interpret. Escape is still treated as "exit
110    /// editing": the raw `KeyDown` is delivered first, then focus is
111    /// cleared. Implies `focusable`.
112    pub fn capture_keys(mut self) -> Self {
113        self.capture_keys = true;
114        self.focusable = true;
115        self
116    }
117
118    /// Multiply this element's paint opacity by the nearest focusable
119    /// ancestor's focus envelope.
120    pub fn alpha_follows_focused_ancestor(mut self) -> Self {
121        self.alpha_follows_focused_ancestor = true;
122        self
123    }
124
125    /// Multiply this node's paint opacity by the runtime's caret blink
126    /// alpha.
127    pub fn blink_when_focused(mut self) -> Self {
128        self.blink_when_focused = true;
129        self
130    }
131
132    /// Borrow hover and press visual envelopes from the nearest
133    /// focusable ancestor.
134    pub fn state_follows_interactive_ancestor(mut self) -> Self {
135        self.state_follows_interactive_ancestor = true;
136        self
137    }
138
139    /// Bind this element's paint opacity to the subtree interaction
140    /// envelope — the `max` of hover, focus, and press for the subtree
141    /// rooted at this element.
142    ///
143    /// At rest (no descendant is the active hover, focus, or press
144    /// target) the element paints at `rest`. At full envelope it paints
145    /// at `peak`. Both are clamped to `[0.0, 1.0]`, with linear
146    /// interpolation in between following the eased envelope.
147    ///
148    /// "Subtree" matches CSS `:hover` semantics: hovering, focusing, or
149    /// pressing *any descendant* keeps the element revealed. A
150    /// hover-revealed close icon stays visible while the cursor moves
151    /// across the tab body or while the tab is keyboard-focused; an
152    /// action pill stays visible while the cursor moves between
153    /// focusable buttons inside it. The trigger isn't strictly
154    /// "hover" — focus and press also count — but `hover` is the
155    /// dominant case and the name reflects it.
156    ///
157    /// Layout-neutral — the element keeps its computed rect at all
158    /// times. Use for hover-revealed close buttons, secondary actions
159    /// on list rows, hover-only validation icons, and other
160    /// "show on interaction" patterns where the surrounding layout
161    /// shouldn't shift.
162    ///
163    /// # Beyond alpha
164    ///
165    /// For the other common hover affordances — Material-style lift
166    /// (`translate_y`), button-pop (`scale`), tint shift (`fill`) —
167    /// drive the prop from app code using
168    /// [`crate::BuildCx::is_hovering_within`] plus
169    /// [`Self::animate`]:
170    ///
171    /// ```ignore
172    /// fn build(&self, cx: &BuildCx) -> El {
173    ///     let lifted = cx.is_hovering_within("card");
174    ///     card([...])
175    ///         .key("card")
176    ///         .focusable()
177    ///         .translate(0.0, if lifted { -2.0 } else { 0.0 })
178    ///         .scale(if lifted { 1.02 } else { 1.0 })
179    ///         .animate(Timing::SPRING_QUICK)
180    /// }
181    /// ```
182    ///
183    /// `is_hovering_within` reads the same subtree predicate
184    /// `hover_alpha` consumes (CSS `:hover`-style cascade). `animate`
185    /// eases the prop between the two build values across frames, so
186    /// the transition is smooth without per-channel declarative API.
187    /// `hover_alpha` itself is the alpha-channel shorthand — it skips
188    /// the boolean-to-value conversion and the per-node `animate`
189    /// allocation, since alpha is the dominant hover affordance.
190    pub fn hover_alpha(mut self, rest: f32, peak: f32) -> Self {
191        self.hover_alpha = Some(HoverAlpha {
192            rest: rest.clamp(0.0, 1.0),
193            peak: peak.clamp(0.0, 1.0),
194        });
195        self
196    }
197
198    pub fn at(mut self, file: &'static str, line: u32) -> Self {
199        self.source = Source {
200            file,
201            line,
202            from_library: false,
203        };
204        self
205    }
206
207    /// Set source from a `Location` (used internally by
208    /// `#[track_caller]` constructors).
209    pub fn at_loc(mut self, loc: &'static Location<'static>) -> Self {
210        self.source = Source::from_caller(loc);
211        self
212    }
213
214    /// Mark this El as constructed inside an damascene library closure
215    /// where `#[track_caller]` doesn't reach user code (e.g. the
216    /// `.map(|item| ...)` body inside `tabs_list`, `radio_group`,
217    /// etc.). The lint pass uses this flag to walk blame attribution
218    /// upward to the nearest user-source ancestor instead of pointing
219    /// findings at damascene-core internals. User code never needs to call
220    /// this.
221    pub fn from_library(mut self) -> Self {
222        self.source.from_library = true;
223        self
224    }
225
226    /// Suppress a single [`crate::bundle::lint::FindingKind`] on this
227    /// node. The bundle's lint pass will skip findings of that kind
228    /// whose attribution target is this exact node — siblings,
229    /// descendants, and ancestors are unaffected, so a stray
230    /// suppression cannot silently swallow real bugs elsewhere in the
231    /// tree. Chain to silence multiple kinds:
232    /// `el.allow_lint(FindingKind::RawColor).allow_lint(FindingKind::MissingSurfaceFill)`.
233    ///
234    /// Reach for this when a finding is *genuinely intentional* in your
235    /// app — a hand-rolled custom-shader surface where the raw color is
236    /// the point, a deliberately bare `Panel` you'll fill later, a
237    /// hover-reveal action whose hit-overflow collision is by design.
238    /// If you find yourself sprinkling it widely, the lint is probably
239    /// catching a real shape worth fixing.
240    ///
241    /// Whole-class suppression (e.g. silencing every
242    /// [`crate::bundle::lint::FindingKind::DuplicateId`] at the bundle
243    /// boundary) lives on the [`crate::bundle::lint::LintReport`]
244    /// itself — see [`crate::bundle::lint::LintReport::retain`].
245    ///
246    /// **Dogfood:** stock widgets and the damascene showcase fixture do
247    /// not call this — every finding raised inside damascene's own code
248    /// gets fixed at the source.
249    pub fn allow_lint(mut self, kind: crate::bundle::lint::FindingKind) -> Self {
250        if !self.allow_lint.contains(&kind) {
251            self.allow_lint.push(kind);
252        }
253        self
254    }
255}