1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! Identity, source, and interaction-flag modifiers for [`El`].
// Lock in full per-item documentation for this module (issue #73).
#![warn(missing_docs)]
use std::panic::Location;
use super::geometry::Sides;
use super::node::El;
use super::semantics::{Kind, Source};
/// Configuration for [`El::hover_alpha`] — the rest and peak alpha
/// endpoints for a node whose opacity binds to the **subtree
/// interaction envelope** (max of hover, focus, and press over the
/// subtree rooted at this node).
///
/// `rest` is the drawn alpha when no descendant of this node is
/// currently the active hover, focus, or press target. `peak` is the
/// drawn alpha at full envelope. Linear interpolation between the two
/// follows the eased subtree envelope (0..1).
///
/// Both fields are clamped to `[0.0, 1.0]` by [`El::hover_alpha`].
/// Typical use is `rest < peak` ("reveal on interaction"), but the
/// representation accepts `rest > peak` ("fade out on interaction") and
/// sub-1.0 peaks for subtle affordances.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HoverAlpha {
/// Drawn alpha when the subtree interaction envelope is 0 (no
/// hover, focus, or press on this node or any descendant).
pub rest: f32,
/// Drawn alpha at full interaction envelope (1.0).
pub peak: f32,
}
impl El {
/// Construct a bare element of the given [`Kind`] with all
/// modifiers at their defaults. App code usually reaches for the
/// catalog constructors (`column`, `button`, `card`, …) instead.
pub fn new(kind: Kind) -> Self {
Self {
kind,
..Default::default()
}
}
// ---- Identity / source ----
/// Give this node a stable identity across rebuilds. The key
/// becomes part of the node's `computed_id` (`role[key]` instead of
/// `role.index`), so focus, hover, scroll offsets, hit-testing, and
/// animation survive sibling reordering. Required for `.tooltip()`,
/// `.selectable()`, and anything else looked up by identity.
pub fn key(mut self, k: impl Into<String>) -> Self {
self.key = Some(k.into());
self
}
/// Make this node opaque to pointer hit-testing: pointer events
/// over its rect stop here instead of falling through to whatever
/// is painted beneath (scrims, modal surfaces).
pub fn block_pointer(mut self) -> Self {
self.block_pointer = true;
self
}
/// Expand this node's pointer hit target without changing layout
/// or paint. Hover, press, cursor, tooltip, and click routing all
/// use the expanded target; [`UiEvent::target_rect`][crate::UiEvent::target_rect]
/// still reports the node's transformed visual rect from layout.
///
/// Keep this conservative. It is for controls whose effective
/// interaction region is intentionally larger than their drawn
/// chrome, not for making unrelated gutters activate nearby UI.
pub fn hit_overflow(mut self, outset: impl Into<Sides>) -> Self {
self.hit_overflow = outset.into();
self
}
/// Include this node in keyboard focus traversal (Tab order
/// follows tree order). Focused nodes receive activation keys and
/// paint the stock focus ring; pair with `.key(...)` so focus
/// survives rebuilds.
pub fn focusable(mut self) -> Self {
self.focusable = true;
self
}
/// Suppress this keyed node's interaction-state visuals — the
/// hover-lighten, press-darken, and focus ring that a keyed node's
/// fill otherwise picks up from the pointer. Use it on a
/// keyed-but-decorative surface: one keyed purely for identity,
/// routing, or persistent state (a pan/zoom canvas background, a
/// graph node keyed only for click routing, a keyed layout anchor)
/// whose fill should stay static under the cursor.
///
/// The node still hit-tests and routes clicks/events normally — only
/// the visual state response is dropped (the underlying envelope is
/// never tracked, so it reads back at rest). [`Kind::Scrim`] and
/// [`crate::tree::viewport`] get this behavior automatically; this is
/// the opt-in for everything else.
pub fn no_hover(mut self) -> Self {
self.no_hover = true;
self
}
/// Show the focus ring on this node even when focus arrived via
/// pointer click. Default focus-ring behavior follows the web
/// platform's `:focus-visible` rule — ring on Tab, no ring on
/// click. Widgets where the ring is meaningful regardless of
/// source — text input, text area — opt in here so clicking into
/// the field still raises the "now active" affordance. Implies
/// nothing about focusability; pair with `.focusable()`.
pub fn always_show_focus_ring(mut self) -> Self {
self.always_show_focus_ring = true;
self
}
/// Opt this node into the library's text-selection system. The
/// node must also carry an explicit `.key(...)`; selection requires
/// stable identity across rebuilds the same way focus does.
pub fn selectable(mut self) -> Self {
self.selectable = true;
self
}
/// Opt this node into consuming touch drag. A touch contact that
/// starts on this node (or any descendant — the flag inherits
/// down the tree) is treated as a drag rather than a pan/scroll
/// gesture, suppressing the runner's touch-scroll synthesis.
/// Use on widgets whose primary interaction is dragging:
/// sliders, scrubbers, resize handles, draggable cards. No
/// effect on mouse / pen pointers.
pub fn consumes_touch_drag(mut self) -> Self {
self.consumes_touch_drag = true;
self
}
/// Attach source-backed copy/hit-test text for this selectable
/// node. The node still needs `.selectable().key(...)`; this only
/// changes how selection offsets map to copied text.
pub fn selection_source(mut self, source: crate::selection::SelectionSource) -> Self {
self.selection_source = Some(source);
self
}
/// Opt this node into raw key capture when focused. While this
/// node is the focused target, the library's traversal/activation
/// defaults are bypassed and raw `KeyDown` events are delivered for
/// the widget to interpret. Escape is still treated as "exit
/// editing": the raw `KeyDown` is delivered first, then focus is
/// cleared. Implies `focusable`.
pub fn capture_keys(mut self) -> Self {
self.capture_keys = true;
self.focusable = true;
self
}
/// Multiply this element's paint opacity by the nearest focusable
/// ancestor's focus envelope.
pub fn alpha_follows_focused_ancestor(mut self) -> Self {
self.alpha_follows_focused_ancestor = true;
self
}
/// Multiply this node's paint opacity by the runtime's caret blink
/// alpha.
pub fn blink_when_focused(mut self) -> Self {
self.blink_when_focused = true;
self
}
/// Borrow hover and press visual envelopes from the nearest
/// focusable ancestor.
pub fn state_follows_interactive_ancestor(mut self) -> Self {
self.state_follows_interactive_ancestor = true;
self
}
/// Bind this element's paint opacity to the subtree interaction
/// envelope — the `max` of hover, focus, and press for the subtree
/// rooted at this element.
///
/// At rest (no descendant is the active hover, focus, or press
/// target) the element paints at `rest`. At full envelope it paints
/// at `peak`. Both are clamped to `[0.0, 1.0]`, with linear
/// interpolation in between following the eased envelope.
///
/// "Subtree" matches CSS `:hover` semantics: hovering, focusing, or
/// pressing *any descendant* keeps the element revealed. A
/// hover-revealed close icon stays visible while the cursor moves
/// across the tab body or while the tab is keyboard-focused; an
/// action pill stays visible while the cursor moves between
/// focusable buttons inside it. The trigger isn't strictly
/// "hover" — focus and press also count — but `hover` is the
/// dominant case and the name reflects it.
///
/// Layout-neutral — the element keeps its computed rect at all
/// times. Use for hover-revealed close buttons, secondary actions
/// on list rows, hover-only validation icons, and other
/// "show on interaction" patterns where the surrounding layout
/// shouldn't shift.
///
/// # Beyond alpha
///
/// For the other common hover affordances — Material-style lift
/// (`translate_y`), button-pop (`scale`), tint shift (`fill`) —
/// drive the prop from app code using
/// [`crate::BuildCx::is_hovering_within`] plus
/// [`Self::animate`]:
///
/// ```ignore
/// fn build(&self, cx: &BuildCx) -> El {
/// let lifted = cx.is_hovering_within("card");
/// card([...])
/// .key("card")
/// .focusable()
/// .translate(0.0, if lifted { -2.0 } else { 0.0 })
/// .scale(if lifted { 1.02 } else { 1.0 })
/// .animate(Timing::SPRING_QUICK)
/// }
/// ```
///
/// `is_hovering_within` reads the same subtree predicate
/// `hover_alpha` consumes (CSS `:hover`-style cascade). `animate`
/// eases the prop between the two build values across frames, so
/// the transition is smooth without per-channel declarative API.
/// `hover_alpha` itself is the alpha-channel shorthand — it skips
/// the boolean-to-value conversion and the per-node `animate`
/// allocation, since alpha is the dominant hover affordance.
pub fn hover_alpha(mut self, rest: f32, peak: f32) -> Self {
self.hover_alpha = Some(HoverAlpha {
rest: rest.clamp(0.0, 1.0),
peak: peak.clamp(0.0, 1.0),
});
self
}
/// Set the source attribution (file + line) reported for this node
/// by lint findings and inspection dumps, marking it as user code.
/// Catalog constructors set this automatically via `#[track_caller]`;
/// see [`Self::at_loc`].
pub fn at(mut self, file: &'static str, line: u32) -> Self {
self.source = Source {
file,
line,
from_library: false,
};
self
}
/// Set source from a `Location` (used internally by
/// `#[track_caller]` constructors).
pub fn at_loc(mut self, loc: &'static Location<'static>) -> Self {
self.source = Source::from_caller(loc);
self
}
/// Mark this El as constructed inside an damascene library closure
/// where `#[track_caller]` doesn't reach user code (e.g. the
/// `.map(|item| ...)` body inside `tabs_list`, `radio_group`,
/// etc.). The lint pass uses this flag to walk blame attribution
/// upward to the nearest user-source ancestor instead of pointing
/// findings at damascene-core internals. User code never needs to call
/// this.
pub fn from_library(mut self) -> Self {
self.source.from_library = true;
self
}
/// Suppress a single [`crate::bundle::lint::FindingKind`] on this
/// node. The bundle's lint pass will skip findings of that kind
/// whose attribution target is this exact node — siblings,
/// descendants, and ancestors are unaffected, so a stray
/// suppression cannot silently swallow real bugs elsewhere in the
/// tree. Chain to silence multiple kinds:
/// `el.allow_lint(FindingKind::RawColor).allow_lint(FindingKind::MissingSurfaceFill)`.
///
/// Reach for this when a finding is *genuinely intentional* in your
/// app — a hand-rolled custom-shader surface where the raw color is
/// the point, a deliberately bare `Panel` you'll fill later, a
/// hover-reveal action whose hit-overflow collision is by design.
/// If you find yourself sprinkling it widely, the lint is probably
/// catching a real shape worth fixing.
///
/// Whole-class suppression (e.g. silencing every
/// [`crate::bundle::lint::FindingKind::DuplicateId`] at the bundle
/// boundary) lives on the [`crate::bundle::lint::LintReport`]
/// itself — see [`crate::bundle::lint::LintReport::retain`].
///
/// **Dogfood:** stock widgets and the damascene showcase fixture do
/// not call this — every finding raised inside damascene's own code
/// gets fixed at the source.
pub fn allow_lint(mut self, kind: crate::bundle::lint::FindingKind) -> Self {
if !self.allow_lint.contains(&kind) {
self.allow_lint.push(kind);
}
self
}
}