Skip to main content

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