Skip to main content

damascene_core/tree/
node.rs

1//! Core [`El`] node data shape.
2
3use crate::anim::Timing;
4use crate::image::{Image, ImageFit};
5use crate::layout::{LayoutFn, VirtualItems};
6use crate::math::{MathDisplay, MathExpr};
7use crate::metrics::{ComponentSize, MetricsRole};
8use crate::shader::ShaderBinding;
9use crate::style::StyleProfile;
10
11use super::geometry::Sides;
12use super::identity::HoverAlpha;
13use super::layout_types::{Align, Axis, Justify, Size};
14use super::semantics::{Kind, Source, SurfaceRole};
15use super::text_types::{FontFamily, FontWeight, TextAlign, TextOverflow, TextRole, TextWrap};
16use crate::color::Color;
17
18/// Where the stock focus ring is drawn relative to the focusable node.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20pub enum FocusRingPlacement {
21    /// Draw the ring outside the layout rect, using the paint-overflow band.
22    #[default]
23    Outside,
24    /// Draw the ring just inside the layout rect. Use for tightly-stacked
25    /// focusable rows where adjacent siblings intentionally share edges.
26    Inside,
27}
28
29/// The core tree node.
30///
31/// Construct via the component builders (`text`, `button`, `card`,
32/// `column`, …) and chain modifiers (`.padding`, `.gap`, `.fill`, …).
33/// Avoid building `El` directly — the builders set polished defaults.
34///
35/// `#[non_exhaustive]` — `El` is meant to be built through the
36/// component constructors, not by struct-literal syntax. Direct
37/// construction from outside this crate is intentionally disabled
38/// so adding new layout/style fields stays a non-breaking change.
39#[derive(Clone, Debug)]
40#[non_exhaustive]
41pub struct El {
42    pub kind: Kind,
43    pub style_profile: StyleProfile,
44    pub key: Option<String>,
45    pub block_pointer: bool,
46    /// Expand this element's pointer hit target beyond its transformed
47    /// layout rect. Layout-neutral and paint-neutral: siblings don't
48    /// move, the element doesn't draw larger, and focus rings / shadows
49    /// still use [`Self::paint_overflow`].
50    ///
51    /// Use sparingly for controls with deliberately small visuals but
52    /// larger intended targets (resize handles, compact icon affordances,
53    /// row chrome). Hover, press, cursor, tooltip, and click routing all
54    /// share this expanded target, so the invisible area behaves like
55    /// the visible control. Ancestor clips still bound hit-testing.
56    pub hit_overflow: Sides,
57    pub focusable: bool,
58    pub focus_ring_placement: FocusRingPlacement,
59    /// Show the focus ring on this node even when focus arrived via
60    /// pointer (i.e. the runtime's `focus_visible` is `false`). Default
61    /// behavior matches the web platform's `:focus-visible` heuristic
62    /// — ring on Tab, no ring on click. Widgets like text inputs and
63    /// text areas opt in here because the ring is a meaningful
64    /// "this surface is now the active editing target" affordance even
65    /// when activated by mouse, beyond what the caret alone shows.
66    pub always_show_focus_ring: bool,
67    /// When true, this node is a pointer target for the library's
68    /// text-selection manager: pointer-down inside its rect starts (or
69    /// extends) the global [`crate::selection::Selection`] anchored at
70    /// this node's `key`. The leaf must also carry an explicit
71    /// `.key(...)` — same convention as focusable widgets — so the
72    /// selection survives tree rebuilds.
73    ///
74    /// Set via [`Self::selectable`]. Coordinates with focus on a
75    /// per-pointer-event basis: pointer-down on a focusable widget
76    /// transfers focus and clears selection; pointer-down on a
77    /// selectable-only leaf moves selection without disturbing focus.
78    pub selectable: bool,
79    /// When true, a touch contact starting on this node (or any
80    /// descendant) is treated as a drag rather than a pan/scroll
81    /// gesture. The runner's touch-scroll synthesis defaults to
82    /// "scroll wins" on touch — every interactive widget loses
83    /// touch drag to the nearest enclosing scrollable, matching
84    /// platform behavior where you can pan over a button. Widgets
85    /// that legitimately need a touch drag — sliders, scrubbers,
86    /// resize handles — opt in here so the runner commits to drag
87    /// instead of cancelling the press.
88    ///
89    /// Inherits along the ancestor path: a press on a slider's
90    /// thumb child consumes touch drag if the slider's outer
91    /// surface set the flag. Has no effect on mouse / pen pointers
92    /// — those follow the historical "press + move = drag" model.
93    pub consumes_touch_drag: bool,
94    /// Optional source-backed selection payload. Plain text leaves
95    /// select/copy their rendered [`Self::text`]. Rich text systems can
96    /// attach a [`crate::selection::SelectionSource`] so pointer
97    /// positions resolve through rendered text but copy returns the
98    /// original driving syntax (for example Markdown or TeX).
99    pub selection_source: Option<crate::selection::SelectionSource>,
100    /// When true, all key events (other than registered hotkeys) route
101    /// to this node as raw `KeyDown` instead of being interpreted by
102    /// the library's defaults (Tab traversal, Enter/Space activation,
103    /// Escape escape). Used by text-input widgets that need to consume
104    /// Tab/Enter/etc. as text or editing actions. Implies `focusable`
105    /// at the runner — the flag only takes effect when the node is
106    /// also the focused target.
107    pub capture_keys: bool,
108    /// When true, this node's paint opacity is multiplied by the
109    /// nearest focusable ancestor's focus envelope (0..1). The library
110    /// already animates that envelope on focus / blur; flagged nodes
111    /// fade in and out with the same easing without any app-side
112    /// focus tracking.
113    ///
114    /// Used by `text_input`'s caret bar — the caret only paints when
115    /// the input is focused, fading via the standard focus animation.
116    /// Documented in `widget_kit.md` as part of the public surface.
117    pub alpha_follows_focused_ancestor: bool,
118    /// When true, this node's paint opacity is also multiplied by the
119    /// runtime's caret blink alpha. Combine with
120    /// `alpha_follows_focused_ancestor` (the caret should blink only
121    /// while the input is focused) — the two compose multiplicatively.
122    /// Used by `text_input` / `text_area`'s caret bar.
123    pub blink_when_focused: bool,
124    /// When true, this node's hover and press visual envelopes are
125    /// borrowed from its nearest focusable ancestor instead of being
126    /// driven by its own (always-zero) envelope.
127    ///
128    /// The hit-test only ever resolves to a focusable target, so a
129    /// child of an interactive container — a slider thumb, a select
130    /// trigger's chevron, the dot inside a radio — never receives
131    /// hover or press envelopes of its own. Flagged children pick up
132    /// the ancestor's envelopes so they can lighten / darken / ring
133    /// out alongside the surface that captured the input.
134    ///
135    /// Used by `slider`'s thumb so grabbing the slider visibly
136    /// reacts on the thumb itself, mirroring shadcn's
137    /// `hover:ring-4 hover:ring-ring/50`.
138    pub state_follows_interactive_ancestor: bool,
139    /// When `Some`, this node's paint opacity is bound to the
140    /// **subtree interaction envelope** — `max` of the hover, focus,
141    /// and press envelopes for the subtree rooted here. The drawn
142    /// alpha interpolates from `rest` (no interaction anywhere in the
143    /// subtree) to `peak` (full interaction), then composes
144    /// multiplicatively with the existing [`Self::opacity`] /
145    /// inherited opacity stack.
146    ///
147    /// "Interaction" includes hovering, pressing, or keyboard-focusing
148    /// any descendant — so a hover-revealed close icon stays visible
149    /// when its tab is keyboard-focused, and an action pill stays
150    /// visible when the cursor moves to one of its focusable buttons.
151    /// Mirrors CSS's "this element OR any descendant is hot."
152    ///
153    /// Layout-neutral — the element's geometry stays fixed regardless
154    /// of interaction state. Use for hover-revealed close buttons,
155    /// secondary actions on list rows, hover-only validation icons,
156    /// and other "show on interaction" patterns whose visibility
157    /// shouldn't shift the surrounding layout.
158    pub hover_alpha: Option<HoverAlpha>,
159    pub source: Source,
160
161    /// Lint kinds the bundle's lint pass should treat as intentional on
162    /// this node. Each entry suppresses one
163    /// [`crate::bundle::lint::FindingKind`] on this specific node only —
164    /// it does **not** propagate to descendants, and it only matches
165    /// findings whose attribution target *is* this node. Authored via
166    /// [`Self::allow_lint`]. Whole-class or pattern-based suppression
167    /// (e.g. `DuplicateId`, which has no per-node target) is the
168    /// province of [`crate::bundle::lint::LintReport::retain`].
169    ///
170    /// Stock widgets do not call [`Self::allow_lint`]; the showcase
171    /// fixture does not call it either (it is a dogfood standard that
172    /// every showcase lint be addressed in widgets / layout rather than
173    /// silenced). User apps may opt out per-node when a finding is
174    /// genuinely intentional in their domain.
175    pub allow_lint: Vec<crate::bundle::lint::FindingKind>,
176
177    // Layout
178    pub axis: Axis,
179    pub gap: f32,
180    pub padding: Sides,
181    pub align: Align,
182    pub justify: Justify,
183    pub width: Size,
184    pub height: Size,
185    /// Optional lower bound on the resolved width in logical pixels.
186    /// Layout clamps the final width up to this value after `width`
187    /// resolves (Hug, Fill, or even Fixed) — useful for "this card
188    /// shrinks to fit content but never below 200px." A `Hug` parent
189    /// of a min-clamped child reflects the clamped intrinsic so the
190    /// parent grows accordingly. Composes additively with
191    /// [`Self::max_width`] and conflict-resolves to the lower bound
192    /// when both apply.
193    pub min_width: Option<f32>,
194    /// Optional upper bound on the resolved width in logical pixels.
195    /// Layout clamps the final width down to this value after `width`
196    /// resolves. Useful for capping a `Fill` child so it doesn't grow
197    /// past a readable column width. Conflict-resolves with
198    /// [`Self::min_width`] in favour of the lower bound when both
199    /// apply (matches CSS `min-width` over `max-width` precedence).
200    pub max_width: Option<f32>,
201    /// Optional lower bound on the resolved height in logical pixels.
202    /// See [`Self::min_width`] for the semantic.
203    pub min_height: Option<f32>,
204    /// Optional upper bound on the resolved height in logical pixels.
205    /// See [`Self::max_width`] for the semantic.
206    pub max_height: Option<f32>,
207    /// Optional t-shirt size for stock widgets. `None` means the active
208    /// theme supplies the component-class default.
209    pub component_size: Option<ComponentSize>,
210    /// Optional theme-facing metrics role. Stock widgets set this so
211    /// the theme can resolve default height/padding/radius before
212    /// layout; app-defined widgets can set the same role to opt into
213    /// identical sizing behavior.
214    pub metrics_role: Option<MetricsRole>,
215    /// Author-overrode layout metrics. Stock constructors set defaults
216    /// without these flags; public modifiers flip them so theme metrics
217    /// do not clobber explicit app choices.
218    pub explicit_width: bool,
219    pub explicit_height: bool,
220    pub explicit_padding: bool,
221    pub explicit_gap: bool,
222    pub explicit_radius: bool,
223    pub explicit_font_family: bool,
224    /// Author overrode the monospace font face for this node — theme
225    /// application leaves [`Self::mono_font_family`] alone when set.
226    pub explicit_mono_font_family: bool,
227    /// Author opted this node into the monospace family via
228    /// [`Self::mono`]. Role modifiers ([`Self::caption`], [`Self::label`],
229    /// [`Self::body`], [`Self::title`], [`Self::heading`],
230    /// [`Self::display`]) leave [`Self::font_mono`] alone when this flag
231    /// is set, so the natural reading order `text(s).mono().caption()`
232    /// keeps the mono family. Without this guard, role application
233    /// silently resets `font_mono = false`. The [`Self::code`] role
234    /// always forces `font_mono = true` regardless.
235    pub explicit_mono: bool,
236
237    // Visual style — these still live on `El` because the modifier API
238    // (`.fill(c)`, `.radius(r)`, `.shadow(s)`) is what users type. The
239    // renderer translates them into a [`ShaderBinding`] for
240    // `stock::rounded_rect` (or whatever `shader_override` specifies)
241    // when emitting [`crate::ir::DrawOp`]s.
242    pub fill: Option<Color>,
243    /// Alternate fill used when the nearest focusable ancestor's focus
244    /// envelope is below 1.0; the painter linearly interpolates from
245    /// `dim_fill` toward `fill` as the envelope approaches 1.0. Used by
246    /// `text_input` / `text_area` selection bands so the highlight
247    /// remains visible (in a muted color) even when the input loses
248    /// focus, matching the macOS convention.
249    pub dim_fill: Option<Color>,
250    pub stroke: Option<Color>,
251    pub stroke_width: f32,
252    /// Corner radii in logical pixels. Authored as a scalar in the
253    /// common case (`.radius(tokens::RADIUS_MD)` works via
254    /// [`super::geometry::Corners::from`]); per-corner shapes use
255    /// [`super::geometry::Corners::top`],
256    /// [`super::geometry::Corners::bottom`], etc. The painter clamps each corner to
257    /// half the shorter side.
258    pub radius: super::geometry::Corners,
259    pub shadow: f32,
260    pub surface_role: SurfaceRole,
261    /// Permit this element to paint outside its layout bounds. The
262    /// outset enlarges the quad geometry handed to the shader (and
263    /// any focus / shadow / glow visuals are positioned in the
264    /// overflow band) while leaving the layout rect — and therefore
265    /// sibling positions and hit-testing — unchanged. Subject to
266    /// ancestor clip rects: a focused widget inside a `clip()`ped
267    /// parent has its overflow clipped, same as any other paint.
268    pub paint_overflow: Sides,
269    /// Clip this element's own paint and descendants to its computed rect.
270    /// Used by scroll panes, host-painted regions, overlays, and any region
271    /// where overflow should not leak visually or receive events.
272    pub clip: bool,
273    /// This element is a vertical scroll viewport. The layout pass reads
274    /// the offset from `UiState`'s scroll-offset side map keyed by
275    /// `computed_id`, clamps it to `[0, content_h - viewport_h]`, and
276    /// writes the clamped value back. Set automatically by [`crate::scroll()`].
277    pub scrollable: bool,
278    /// Which edge of a [`Kind::Scroll`] container the offset sticks to
279    /// when engaged. `None` (default) means the stored offset is the
280    /// only source of truth — content changes do not shift it. `End`
281    /// matches egui's `ScrollArea::stick_to_bottom(true)`: the offset
282    /// stays glued to the tail across content growth (chat-log idiom).
283    /// `Start` is the symmetric "stick to head" — useful for virtual
284    /// lists that grow at the top (commit logs / activity-feed reverse
285    /// order) so newly added rows stay visible. No effect on
286    /// non-scrollable nodes. Opt in with [`Self::pin_start`] or
287    /// [`Self::pin_end`].
288    ///
289    /// The "is the pin currently engaged" bit lives in
290    /// [`crate::state::UiState`]'s scroll subsystem, keyed by
291    /// `computed_id`; layout reads it each frame to decide whether to
292    /// snap the stored offset to the pinned edge before clamping.
293    pub pin_policy: crate::tree::PinPolicy,
294    /// Treat this element's focusable children as a single arrow-navigable
295    /// group: while a focused element is one of the direct children,
296    /// `Up` / `Down` / `Home` / `End` move focus among the group's
297    /// focusable siblings instead of being routed as a `KeyDown`. Tab
298    /// traversal is unchanged.
299    ///
300    /// Used by `popover_panel` so menu items in a dropdown are
301    /// keyboard-navigable; available to any user widget that wants the
302    /// same semantics.
303    pub arrow_nav_siblings: bool,
304    /// Tooltip text. When set, the runtime synthesizes a hover-driven
305    /// tooltip layer anchored to this node — appearing after the
306    /// hover delay elapses, fading in with the standard envelope, and
307    /// dismissed when the pointer leaves or presses the node. The
308    /// trigger doesn't have to be focusable or keyed; the runtime
309    /// anchors the tooltip via the trigger's `computed_id`.
310    pub tooltip: Option<String>,
311    /// Pointer cursor declared for this element. `None` falls through
312    /// to whatever an ancestor declared, else [`crate::cursor::Cursor::Default`].
313    /// Resolution lives in [`crate::state::UiState::cursor`]: if a
314    /// press is captured, the cursor follows the press target;
315    /// otherwise the hovered node is walked root-ward for the first
316    /// explicit declaration. Disabled state is *not* auto-mapped —
317    /// widgets that want [`crate::cursor::Cursor::NotAllowed`] when disabled set it
318    /// explicitly in their build closure.
319    pub cursor: Option<crate::cursor::Cursor>,
320    /// Cursor to show *only while a press is captured at this exact
321    /// node*. Powers the natural Grab → Grabbing transition: the
322    /// slider sets `cursor=Grab` + `cursor_pressed=Grabbing`, and the
323    /// resolver picks the latter while the press anchors here. Unlike
324    /// [`Self::cursor`], this does **not** walk up: an ancestor's
325    /// `cursor_pressed` doesn't apply to a descendant press target.
326    /// The press target's own `cursor` is the fallback when this is
327    /// `None`.
328    pub cursor_pressed: Option<crate::cursor::Cursor>,
329    /// Override the implicit `stock::rounded_rect` binding for this
330    /// node's surface. The escape hatch a user crate uses to bind a
331    /// custom shader (e.g. `liquid_glass`).
332    pub shader_override: Option<ShaderBinding>,
333    /// Second escape hatch: author-supplied layout function that
334    /// positions this node's direct children. When set, the layout
335    /// pass calls the function instead of running its column/row/
336    /// overlay distribution. The library still recurses into each
337    /// child and still drives hit-test / focus / animation / scroll
338    /// off the rects the function returns. See [`LayoutFn`] for the
339    /// contract.
340    pub layout_override: Option<LayoutFn>,
341    /// Virtualized list state. Set by [`crate::virtual_list`] (and only
342    /// on `Kind::VirtualList` nodes). The layout pass uses this to
343    /// realize only the rows whose rect intersects the viewport. The
344    /// node is automatically `scrollable` + `clip`.
345    pub virtual_items: Option<VirtualItems>,
346    /// Show a draggable vertical scrollbar thumb when this node is
347    /// scrollable and its content overflows the viewport. The thumb
348    /// overlays the right edge of the viewport — it does not reflow
349    /// children. No effect on non-scrollable nodes. Defaults to
350    /// `false`; the [`crate::scroll()`] and [`crate::virtual_list()`]
351    /// constructors flip it on by default. Authors disable with
352    /// [`Self::no_scrollbar`].
353    pub scrollbar: bool,
354
355    // Text
356    pub text: Option<String>,
357    pub text_color: Option<Color>,
358    pub text_align: TextAlign,
359    pub text_wrap: TextWrap,
360    pub text_overflow: TextOverflow,
361    pub text_role: TextRole,
362    pub text_max_lines: Option<usize>,
363    pub font_size: f32,
364    pub line_height: f32,
365    pub font_family: FontFamily,
366    /// Monospace face used when [`Self::font_mono`] is set (or when the
367    /// node carries [`TextRole::Code`]). Stamped by theme application
368    /// from [`crate::Theme::mono_font_family`] unless the author set it
369    /// explicitly via [`Self::mono_font_family`].
370    pub mono_font_family: FontFamily,
371    pub font_weight: FontWeight,
372    pub font_mono: bool,
373    /// Italic styling. Author-set via [`Self::italic`]; honoured when
374    /// this El is a styled text leaf inside an [`Kind::Inlines`] parent
375    /// and (best-effort) on standalone text Els.
376    pub text_italic: bool,
377    /// Underline styling. Author-set via [`Self::underline`].
378    pub text_underline: bool,
379    /// Strikethrough styling. Author-set via [`Self::strikethrough`].
380    pub text_strikethrough: bool,
381    /// Link target URL. When set on a text leaf inside [`Kind::Inlines`],
382    /// the run renders as a link (themed) and runs sharing a URL group
383    /// together for hit-test. Author-set via [`Self::link`].
384    pub text_link: Option<String>,
385    /// Inline-run background. When set on a text leaf inside
386    /// [`Kind::Inlines`], the shaped span paints a solid quad behind
387    /// its glyphs (one rect per line if the span wraps). No effect on
388    /// standalone text Els — author wraps in a styled `row()` for
389    /// chip-shaped surfaces. Author-set via [`Self::background`].
390    pub text_bg: Option<Color>,
391
392    // Math
393    /// Native math expression rendered through Damascene's math box layout.
394    /// Set by [`crate::tree::math`], [`crate::tree::math_inline`], and
395    /// [`crate::tree::math_block`].
396    pub math: Option<std::sync::Arc<MathExpr>>,
397    pub math_display: MathDisplay,
398
399    // Icon
400    pub icon: Option<crate::icons::svg::IconSource>,
401    pub icon_stroke_width: f32,
402
403    /// Raster image. When set together with [`Kind::Image`] (or any
404    /// kind, though [`crate::image`] is the idiomatic builder) the
405    /// `draw_ops` pass emits a [`crate::ir::DrawOp::Image`] projected
406    /// per [`Self::image_fit`] and tinted by [`Self::image_tint`].
407    /// Layout intrinsic is the image's natural pixel size when both
408    /// `width` and `height` are `Hug`.
409    pub image: Option<Image>,
410    /// Multiply each sampled pixel by this colour (RGBA `[0..1]`). Most
411    /// raster art wants `None` (no tint); set it for monochrome assets
412    /// (icon-style PNGs) the app wants to recolour.
413    pub image_tint: Option<Color>,
414    /// How the image projects into the resolved rect. Defaults to
415    /// `ImageFit::Contain` — preserves aspect ratio and letterboxes.
416    pub image_fit: ImageFit,
417
418    /// App-owned GPU texture source for [`Kind::Surface`] elements.
419    /// Set via [`Self::surface_source`] (typically through the
420    /// [`crate::tree::surface`] builder).
421    pub surface_source: Option<crate::surface::SurfaceSource>,
422    /// How the surface texture composes with widgets painted below it.
423    /// Defaults to [`crate::surface::SurfaceAlpha::Premultiplied`].
424    pub surface_alpha: crate::surface::SurfaceAlpha,
425    /// How the surface texture projects into the resolved rect.
426    /// Defaults to [`ImageFit::Fill`] — stretch to the rect, ignoring
427    /// aspect ratio. `Contain` / `Cover` / `None` mirror the
428    /// corresponding modes on [`crate::tree::image`].
429    pub surface_fit: ImageFit,
430    /// Affine applied to the texture quad in destination space, around
431    /// the centre of the post-fit rect. Defaults to identity.
432    /// Composes after [`Self::surface_fit`]: the fit projection picks
433    /// the destination rect, then this matrix transforms it (rotate,
434    /// scale, translate, shear). The auto-clip scissor still clamps
435    /// to the El's content rect, so transforms that move the texture
436    /// outside that rect are cropped.
437    pub surface_transform: crate::affine::Affine2,
438
439    /// Scene specification for [`Kind::Scene3D`] elements. Set via the
440    /// [`crate::tree::chart3d`] builder. `draw_ops` resolves it (camera
441    /// auto-framed against the marks' bounds) into a `DrawOp::Scene3D`.
442    /// Boxed to keep `El` small — most Els carry no scene.
443    pub scene_source: Option<Box<crate::scene::SceneSpec>>,
444
445    /// Vector asset for [`Kind::Vector`] elements. Set via
446    /// [`Self::vector_source`] (typically through the
447    /// [`crate::tree::vector`] builder). The asset's view box determines
448    /// the natural aspect ratio.
449    pub vector_source: Option<std::sync::Arc<crate::vector::VectorAsset>>,
450    /// Render policy for [`Self::vector_source`]. Defaults to
451    /// [`crate::vector::VectorRenderMode::Painted`] so authored vector
452    /// paint is preserved unless the caller explicitly opts into mask
453    /// rendering.
454    pub vector_render_mode: crate::vector::VectorRenderMode,
455
456    pub children: Vec<El>,
457
458    /// Paint-time alpha multiplier in `[0, 1]`. Default `1.0`. Multiplies
459    /// the alpha channel of `fill`, `stroke`, and text colour at draw
460    /// time. Layout-neutral. App-driven changes are eased when
461    /// [`Self::animate`] is set.
462    pub opacity: f32,
463    /// Paint-time offset in logical pixels. Default `(0.0, 0.0)`.
464    /// **Subtree-inheriting**: descendants paint at their computed rect
465    /// plus all ancestor `translate` accumulated through the paint
466    /// recursion. Use this to slide a sidebar / drawer / list-item
467    /// without re-running layout. App-driven changes are eased when
468    /// [`Self::animate`] is set.
469    pub translate: (f32, f32),
470    /// Per-node uniform scale around the computed-rect centre. Default
471    /// `1.0`. Scales this node's surface quad and (if it carries text)
472    /// its glyph run together. **Not** subtree-inheriting — descendants
473    /// keep their own scale. Use this for tap-bounce on a button. App-
474    /// driven changes are eased when [`Self::animate`] is set.
475    pub scale: f32,
476    /// Opt-in app-driven prop interpolation. When `Some(timing)`, the
477    /// animation tracker eases `fill` / `text_color` / `stroke` /
478    /// `opacity` / `translate` / `scale` between rebuilds — the value
479    /// the build closure produces becomes the spring/tween target;
480    /// `current` carries over from last frame. State visuals (hover /
481    /// press / focus ring) keep their own library defaults regardless.
482    pub animate: Option<Timing>,
483
484    /// Inside-out redraw deadline: when `Some(d)` and this El is
485    /// visible (rect intersects the viewport), Damascene asks the host to
486    /// schedule the next frame within `d`. Aggregated across the tree
487    /// via `min` and surfaced as
488    /// [`crate::runtime::PrepareResult::next_redraw_in`]; the host
489    /// drives the loop, Damascene mediates by visibility.
490    ///
491    /// Use this for any widget whose paint depends on time (animated
492    /// images, video frames written via `surface()`, custom shaders
493    /// that don't go through the `samples_time` registration path,
494    /// hover-and-fade effects implemented outside the built-in
495    /// animation tracker). `Duration::ZERO` means "next frame ASAP";
496    /// non-zero values let the host pace at lower-than-display
497    /// cadence.
498    pub redraw_within: Option<std::time::Duration>,
499
500    /// Stable path-based ID, filled by the layout pass. Used as the
501    /// key for every side map that holds per-node bookkeeping in
502    /// [`crate::state::UiState`] — computed rects, interaction state,
503    /// state-envelope amounts, scroll offsets, in-flight animations.
504    pub computed_id: String,
505}