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