Skip to main content

aetna_core/tree/
constructors.rs

1//! Free constructors for common [`El`] tree shapes.
2//!
3//! Kept separate from the core `El` type so the central node definition
4//! stays focused on fields and chainable modifiers.
5
6use std::panic::Location;
7use std::sync::Arc;
8
9use crate::image::Image;
10use crate::layout::VirtualItems;
11use crate::math::{MathDisplay, MathExpr};
12
13use super::layout_types::{Align, Axis, Size};
14use super::node::El;
15use super::semantics::Kind;
16
17/// A vertical container — the layout fallback.
18///
19/// Reach for a named widget first: [`card`] / [`titled_card`] for boxed
20/// surfaces; [`sidebar`] for nav rails; [`toolbar`] for page headers;
21/// [`item`] for object rows; [`form_item`] / [`field_row`] for stacked
22/// fields. `column` is the right answer when no widget shape fits.
23///
24/// Defaults match CSS flex's `display: flex; flex-direction: column`:
25/// `axis = Column`, `align = Stretch`, `width = Hug`, `height = Hug`,
26/// `gap = 0`. Children shrink to content on the main axis (height)
27/// and stretch to the column's width on the cross axis.
28///
29/// To claim the parent's extent (the analog of `width: 100%` /
30/// `flex: 1`), set `.width(Size::Fill(1.0))` /
31/// `.height(Size::Fill(1.0))`. To space children apart, set
32/// `.gap(tokens::SPACE_*)` — CSS-style opt-in spacing.
33///
34/// Switch `align` to `Center` / `Start` / `End` and children shrink
35/// to their content width so the alignment can position them — the
36/// same as CSS `align-items` non-stretch semantics.
37///
38/// **Smell:** `column([...]).fill(CARD).stroke(BORDER).radius(...)`
39/// reinvents [`card`]; `column([...]).fill(CARD).stroke(BORDER).width(SIDEBAR_WIDTH)`
40/// reinvents [`sidebar`]. Use the named widget — same recipe, the right
41/// surface role, less to forget.
42///
43/// [`card`]: crate::widgets::card::card
44/// [`titled_card`]: crate::widgets::card::titled_card
45/// [`sidebar`]: crate::widgets::sidebar::sidebar
46/// [`toolbar`]: crate::widgets::toolbar::toolbar
47/// [`item`]: crate::widgets::item::item
48/// [`form_item`]: crate::widgets::form::form_item
49/// [`field_row`]: crate::widgets::form::field_row
50#[track_caller]
51pub fn column<I, E>(children: I) -> El
52where
53    I: IntoIterator<Item = E>,
54    E: Into<El>,
55{
56    El::new(Kind::Group)
57        .at_loc(Location::caller())
58        .children(children)
59        .axis(Axis::Column)
60}
61
62/// A horizontal container — the layout fallback.
63///
64/// Reach for a named widget first: [`item`] for clickable object rows
65/// (recent file, repo, project, person, asset entry — anywhere you'd
66/// otherwise build a focusable row with stacked text and trailing
67/// buttons); [`toolbar`] for page chrome; [`field_row`] for label +
68/// control; [`tabs_list`] for segmented controls; [`breadcrumb_list`] /
69/// [`pagination_content`] for navigation rows. `row` is the right
70/// answer when no widget shape fits.
71///
72/// Defaults match CSS flex's `display: flex; flex-direction: row`:
73/// `axis = Row`, `align = Stretch`, `width = Hug`, `height = Hug`,
74/// `gap = 0`. Children shrink to content on the main axis (width)
75/// and stretch to the row's height on the cross axis.
76///
77/// `Stretch` is the cross-axis default the same way `align-items:
78/// stretch` is in CSS. For typical content rows (`[icon, text,
79/// button]`) you almost always want `.align(Center)` to vertically
80/// center the children — the CSS-Tailwind muscle memory of
81/// `flex items-center`. Without it, smaller fixed-size children
82/// (badges, icons) sit at the top of the row, just like CSS does.
83///
84/// To space children apart, set `.gap(tokens::SPACE_*)` — opt-in
85/// like CSS.
86///
87/// **Smell:** a focusable, keyed `row([column([t1, t2]), button, button])`
88/// used as a clickable resource entry — that's [`item`], not a hand-rolled
89/// row. The named widget gives you hover, press, focus, the rail, and
90/// the slots (`item_media`, `item_content`, `item_actions`) for free.
91///
92/// [`item`]: crate::widgets::item::item
93/// [`toolbar`]: crate::widgets::toolbar::toolbar
94/// [`field_row`]: crate::widgets::form::field_row
95/// [`tabs_list`]: crate::widgets::tabs::tabs_list
96/// [`breadcrumb_list`]: crate::widgets::breadcrumb::breadcrumb_list
97/// [`pagination_content`]: crate::widgets::pagination::pagination_content
98#[track_caller]
99pub fn row<I, E>(children: I) -> El
100where
101    I: IntoIterator<Item = E>,
102    E: Into<El>,
103{
104    El::new(Kind::Group)
105        .at_loc(Location::caller())
106        .children(children)
107        .axis(Axis::Row)
108}
109
110/// An overlay stack; children share the parent's rect.
111///
112/// For modals, sheets, popovers, and tooltips reach for the named
113/// widget instead — [`dialog`], [`sheet`], [`popover`], `.tooltip(...)`.
114/// `stack` is the layered-visuals primitive (focus rings, custom
115/// badges painted over content) that those widgets compose against.
116///
117/// [`dialog`]: crate::widgets::dialog::dialog
118/// [`sheet`]: crate::widgets::sheet::sheet
119/// [`popover`]: crate::widgets::popover::popover
120#[track_caller]
121pub fn stack<I, E>(children: I) -> El
122where
123    I: IntoIterator<Item = E>,
124    E: Into<El>,
125{
126    El::new(Kind::Group)
127        .at_loc(Location::caller())
128        .children(children)
129        .axis(Axis::Overlay)
130}
131
132/// A vertical scroll viewport. Children stack as in [`column()`]; the
133/// container clips overflow and translates content by the current scroll
134/// offset. Wheel events over the viewport update the offset.
135///
136/// Give it a `.key("...")` so the offset persists by name across
137/// rebuilds — without a key, the offset is keyed by sibling index and
138/// resets if structure shifts.
139#[track_caller]
140pub fn scroll<I, E>(children: I) -> El
141where
142    I: IntoIterator<Item = E>,
143    E: Into<El>,
144{
145    El::new(Kind::Scroll)
146        .at_loc(Location::caller())
147        .children(children)
148        .axis(Axis::Column)
149        .width(Size::Fill(1.0))
150        .height(Size::Fill(1.0))
151        .clip()
152        .scrollable()
153        .scrollbar()
154}
155
156/// Block whose direct children flow inline (text leaves + embeds +
157/// hard breaks). Models HTML's `<p>` shape: heterogeneous children,
158/// attributed runs, optional inline embeds. Children are styled via
159/// the existing modifier chain (`.bold()`, `.italic()`, `.color(c)`,
160/// `.code()`, `.link(url)`, etc.) — there is no parallel
161/// `RichText`/`TextRun` type.
162///
163/// ```ignore
164/// text_runs([
165///     text("Aetna — "),
166///     text("rich text").bold(),
167///     text(" composition."),
168///     hard_break(),
169///     text("Custom shaders, custom layouts, "),
170///     text("virtual_list").code(),
171///     text(" — and inline runs."),
172/// ])
173/// ```
174#[track_caller]
175pub fn text_runs<I, E>(children: I) -> El
176where
177    I: IntoIterator<Item = E>,
178    E: Into<El>,
179{
180    El::new(Kind::Inlines)
181        .at_loc(Location::caller())
182        .axis(Axis::Column)
183        .align(Align::Start)
184        .width(Size::Fill(1.0))
185        .children(children)
186}
187
188/// Forced line break inside a [`text_runs`] block. Mirrors HTML's
189/// `<br>`. Outside an `Inlines` parent, lays out as a zero-size leaf.
190#[track_caller]
191pub fn hard_break() -> El {
192    El::new(Kind::HardBreak)
193        .at_loc(Location::caller())
194        .width(Size::Hug)
195        .height(Size::Hug)
196}
197
198#[track_caller]
199pub fn math(expr: impl Into<Arc<MathExpr>>) -> El {
200    math_inline(expr)
201}
202
203#[track_caller]
204pub fn math_inline(expr: impl Into<Arc<MathExpr>>) -> El {
205    El::new(Kind::Math)
206        .at_loc(Location::caller())
207        .math_expr(expr)
208        .math_display(MathDisplay::Inline)
209        .width(Size::Hug)
210        .height(Size::Hug)
211}
212
213#[track_caller]
214pub fn math_block(expr: impl Into<Arc<MathExpr>>) -> El {
215    El::new(Kind::Math)
216        .at_loc(Location::caller())
217        .math_expr(expr)
218        .math_display(MathDisplay::Block)
219        .width(Size::Fill(1.0))
220        .height(Size::Hug)
221}
222
223/// Virtualized vertical list of `count` rows of fixed height
224/// `row_height`. The library calls `build_row(i)` only for indices
225/// whose rect intersects the visible viewport, then lays them out at
226/// the scroll-shifted Y. `.gap(...)` contributes spacing between rows,
227/// matching column-style layout. Authors typically key rows with a
228/// stable identifier (`button("foo").key("msg-abc")`) so hover/press/
229/// focus state survives scrolling.
230///
231/// The returned El defaults to `Size::Fill(1.0)` on both axes (it's a
232/// viewport — its size is decided by the parent). `Size::Hug` would
233/// defeat virtualization and panics at layout time.
234#[track_caller]
235pub fn virtual_list<F>(count: usize, row_height: f32, build_row: F) -> El
236where
237    F: Fn(usize) -> El + Send + Sync + 'static,
238{
239    let mut el = El::new(Kind::VirtualList)
240        .at_loc(Location::caller())
241        .axis(Axis::Column)
242        .align(Align::Stretch)
243        .width(Size::Fill(1.0))
244        .height(Size::Fill(1.0))
245        .clip()
246        .scrollable()
247        .scrollbar();
248    el.virtual_items = Some(VirtualItems::new(count, row_height, build_row));
249    el
250}
251
252/// Variable-height variant of [`virtual_list`]. `row_key(i)` must
253/// return a stable identity for the logical row at `i`; the dynamic
254/// list uses those identities to preserve an in-viewport anchor while
255/// rows are inserted, removed, measured, or remeasured at a new width.
256/// Each row sizes itself from its own content (`Size::Hug` or
257/// `Size::Fixed` on the main axis); `estimated_row_height` is used for
258/// unmeasured rows when the library positions the visible window and
259/// computes the scrollbar thumb. `.gap(...)` contributes spacing
260/// between rows, matching column-style layout.
261///
262/// Use this when row heights are content-driven (diff hunks, expanded
263/// rows, comment threads) and a single `row_height` would either waste
264/// space or truncate. For genuinely uniform lists prefer
265/// [`virtual_list`] — its O(1) range math is cheaper and free of any
266/// estimate/measure jitter.
267#[track_caller]
268pub fn virtual_list_dyn<K, F>(
269    count: usize,
270    estimated_row_height: f32,
271    row_key: K,
272    build_row: F,
273) -> El
274where
275    K: Fn(usize) -> String + Send + Sync + 'static,
276    F: Fn(usize) -> El + Send + Sync + 'static,
277{
278    let mut el = El::new(Kind::VirtualList)
279        .at_loc(Location::caller())
280        .axis(Axis::Column)
281        .align(Align::Stretch)
282        .width(Size::Fill(1.0))
283        .height(Size::Fill(1.0))
284        .clip()
285        .scrollable()
286        .scrollbar();
287    el.virtual_items = Some(VirtualItems::new_dyn(
288        count,
289        estimated_row_height,
290        row_key,
291        build_row,
292    ));
293    el
294}
295
296/// A `Fill(1)` filler. Inside a `row` it pushes siblings to the right;
297/// inside a `column` it pushes siblings to the bottom.
298#[track_caller]
299pub fn spacer() -> El {
300    El::new(Kind::Spacer)
301        .at_loc(Location::caller())
302        .width(Size::Fill(1.0))
303        .height(Size::Fill(1.0))
304}
305
306/// A raster image element. The El hugs the image's natural pixel
307/// size by default; set [`El::width`] / [`El::height`] for an
308/// explicit box, and [`El::image_fit`] to control projection.
309///
310/// ```
311/// use aetna_core::prelude::*;
312/// let pixels = vec![0u8; 4 * 4 * 4];
313/// let img = Image::from_rgba8(4, 4, pixels);
314/// let _ = image(img).image_fit(ImageFit::Cover).radius(8.0);
315/// ```
316#[track_caller]
317pub fn image(img: impl Into<Image>) -> El {
318    El::new(Kind::Image).at_loc(Location::caller()).image(img)
319}
320
321/// An app-supplied vector asset. By default Aetna preserves authored
322/// fills, strokes, and gradients through the painted vector path; call
323/// [`El::vector_mask`] when the asset should be treated as a one-colour
324/// coverage mask. Companion to [`crate::icon`] for content that
325/// doesn't fit icon conventions: arbitrary-aspect bounding boxes,
326/// programmatic construction each frame. Pairs with
327/// [`crate::vector::PathBuilder`] for ergonomic path construction.
328///
329/// # Sizing
330///
331/// The default size matches the asset's view-box dimensions in logical
332/// pixels. Set [`El::width`] / [`El::height`] / [`El::fill_size`] to
333/// override. Painted vectors are tessellated into the resolved rect;
334/// mask vectors sample the backend MSDF atlas across that rect.
335///
336/// # Caching
337///
338/// The asset's [`VectorAsset::content_hash`](crate::vector::VectorAsset::content_hash)
339/// is the backend cache key. Apps that build the same shape twice (two
340/// commits sharing a merge connector geometry, two flowchart edges with
341/// the same arc) can share backend work; per-frame-unique geometry gets
342/// one cache entry per unique shape.
343///
344/// ```ignore
345/// use aetna_core::prelude::*;
346/// use aetna_core::tree::Color;
347///
348/// let curve = PathBuilder::new()
349///     .move_to(0.0, 0.0)
350///     .cubic_to(20.0, 0.0, 0.0, 60.0, 20.0, 60.0)
351///     .stroke_solid(Color::rgb(80, 200, 240), 2.0)
352///     .stroke_line_cap(VectorLineCap::Round)
353///     .build();
354/// let asset = VectorAsset::from_paths([0.0, 0.0, 20.0, 60.0], vec![curve]);
355/// let _ = vector(asset);
356/// ```
357#[track_caller]
358pub fn vector(asset: crate::vector::VectorAsset) -> El {
359    let [_, _, vw, vh] = asset.view_box;
360    El::new(Kind::Vector)
361        .at_loc(Location::caller())
362        .width(Size::Fixed(vw.max(0.0)))
363        .height(Size::Fixed(vh.max(0.0)))
364        .vector_source(std::sync::Arc::new(asset))
365}
366
367/// An app-owned-texture surface. Aetna composites the texture into
368/// the paint stream at the El's resolved rect — no upload, no per-frame
369/// copy. The default size matches the texture's pixel dimensions; set
370/// [`El::width`] / [`El::height`] (or `.fill_size()`) for an explicit
371/// box.
372///
373/// # Sizing, projection, and transforms
374///
375/// The texture's pixel dimensions are **independent of the rendered
376/// size**. By default the widget stretches the texture across the
377/// resolved rect ([`crate::image::ImageFit::Fill`]); reach for
378/// [`El::surface_fit`] to letterbox-preserve aspect ratio
379/// ([`crate::image::ImageFit::Contain`]), crop-cover
380/// ([`crate::image::ImageFit::Cover`]), or paint at natural size
381/// ([`crate::image::ImageFit::None`]). [`El::surface_transform`]
382/// composes an affine on top — rotate, mirror, zoom/pan — applied
383/// around the centre of the post-fit rect.
384///
385/// Picking a sizing strategy:
386/// - For pixel-accurate display, size the widget to the texture's
387///   pixel dimensions (the default constructor does this for you).
388/// - For a 3D viewport or video frame whose source resolution should
389///   track the rendered size, the app should re-allocate its texture
390///   to match the resolved rect (read it via `UiState::rect_of_key`
391///   after `prepare()`).
392/// - For an animated image whose natural dimensions are fixed
393///   (decoded GIF / WebP / APNG, decoded video frame),
394///   `surface_fit(Contain)` letterboxes into any layout rect with
395///   no per-resize allocation.
396///
397/// # z-order, scissor, hit-test
398///
399/// The widget participates in layout, scissor, scrolling, hit-test,
400/// and z-order like any other El: siblings declared before this one
401/// paint underneath, siblings after paint on top. The auto-clip
402/// scissor clamps painted content to the El's content rect — affines
403/// or `Cover` projections that overflow are cropped.
404///
405/// ```ignore
406/// // Pseudocode — the AppTexture comes from a backend constructor.
407/// use aetna_core::prelude::*;
408/// let tex: AppTexture = /* aetna_wgpu::app_texture(...) */ todo!();
409/// let _ = surface(tex)
410///     .fill_size()
411///     .surface_fit(ImageFit::Contain)
412///     .surface_alpha(SurfaceAlpha::Opaque)
413///     .surface_transform(Affine2::rotate(0.1));
414/// ```
415#[track_caller]
416pub fn surface(texture: crate::surface::AppTexture) -> El {
417    let (w, h) = texture.size_px();
418    El::new(Kind::Surface)
419        .at_loc(Location::caller())
420        .width(Size::Fixed(w as f32))
421        .height(Size::Fixed(h as f32))
422        .surface_source(crate::surface::SurfaceSource::Texture(texture))
423}
424
425/// A 1-pixel separator line.
426#[track_caller]
427pub fn divider() -> El {
428    El::new(Kind::Divider)
429        .at_loc(Location::caller())
430        .height(Size::Fixed(1.0))
431        .width(Size::Fill(1.0))
432        .fill(crate::tokens::BORDER)
433}
434
435// ---------- &str → El convenience ----------
436//
437// Lets `titled_card("Title", ["a body line"])` work without `text(...)`.
438
439impl From<&str> for El {
440    fn from(s: &str) -> Self {
441        crate::widgets::text::text(s)
442    }
443}
444
445impl From<String> for El {
446    fn from(s: String) -> Self {
447        crate::widgets::text::text(s)
448    }
449}
450
451impl From<&String> for El {
452    fn from(s: &String) -> Self {
453        crate::widgets::text::text(s.as_str())
454    }
455}