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. Authors typically key rows with a stable
227/// identifier (`button("foo").key("msg-abc")`) so hover/press/focus
228/// state survives scrolling.
229///
230/// The returned El defaults to `Size::Fill(1.0)` on both axes (it's a
231/// viewport — its size is decided by the parent). `Size::Hug` would
232/// defeat virtualization and panics at layout time.
233#[track_caller]
234pub fn virtual_list<F>(count: usize, row_height: f32, build_row: F) -> El
235where
236    F: Fn(usize) -> El + Send + Sync + 'static,
237{
238    let mut el = El::new(Kind::VirtualList)
239        .at_loc(Location::caller())
240        .axis(Axis::Column)
241        .align(Align::Stretch)
242        .width(Size::Fill(1.0))
243        .height(Size::Fill(1.0))
244        .clip()
245        .scrollable()
246        .scrollbar();
247    el.virtual_items = Some(VirtualItems::new(count, row_height, build_row));
248    el
249}
250
251/// Variable-height variant of [`virtual_list`]. Each row sizes itself
252/// from its own content (`Size::Hug` or `Size::Fixed` on the main
253/// axis); `estimated_row_height` is used for unmeasured rows when the
254/// library positions the visible window and computes the scrollbar
255/// thumb. The first time a row enters the viewport its actual intrinsic
256/// height is measured at the viewport width and cached on `UiState`,
257/// so scroll math converges as the user scrolls.
258///
259/// Use this when row heights are content-driven (diff hunks, expanded
260/// rows, comment threads) and a single `row_height` would either waste
261/// space or truncate. For genuinely uniform lists prefer
262/// [`virtual_list`] — its O(1) range math is cheaper and free of any
263/// estimate/measure jitter.
264#[track_caller]
265pub fn virtual_list_dyn<F>(count: usize, estimated_row_height: f32, build_row: F) -> El
266where
267    F: Fn(usize) -> El + Send + Sync + 'static,
268{
269    let mut el = El::new(Kind::VirtualList)
270        .at_loc(Location::caller())
271        .axis(Axis::Column)
272        .align(Align::Stretch)
273        .width(Size::Fill(1.0))
274        .height(Size::Fill(1.0))
275        .clip()
276        .scrollable()
277        .scrollbar();
278    el.virtual_items = Some(VirtualItems::new_dyn(
279        count,
280        estimated_row_height,
281        build_row,
282    ));
283    el
284}
285
286/// A `Fill(1)` filler. Inside a `row` it pushes siblings to the right;
287/// inside a `column` it pushes siblings to the bottom.
288#[track_caller]
289pub fn spacer() -> El {
290    El::new(Kind::Spacer)
291        .at_loc(Location::caller())
292        .width(Size::Fill(1.0))
293        .height(Size::Fill(1.0))
294}
295
296/// A raster image element. The El hugs the image's natural pixel
297/// size by default; set [`El::width`] / [`El::height`] for an
298/// explicit box, and [`El::image_fit`] to control projection.
299///
300/// ```
301/// use aetna_core::prelude::*;
302/// let pixels = vec![0u8; 4 * 4 * 4];
303/// let img = Image::from_rgba8(4, 4, pixels);
304/// let _ = image(img).image_fit(ImageFit::Cover).radius(8.0);
305/// ```
306#[track_caller]
307pub fn image(img: impl Into<Image>) -> El {
308    El::new(Kind::Image).at_loc(Location::caller()).image(img)
309}
310
311/// An app-supplied vector asset. By default Aetna preserves authored
312/// fills, strokes, and gradients through the painted vector path; call
313/// [`El::vector_mask`] when the asset should be treated as a one-colour
314/// coverage mask. Companion to [`crate::icon`] for content that
315/// doesn't fit icon conventions: arbitrary-aspect bounding boxes,
316/// programmatic construction each frame. Pairs with
317/// [`crate::vector::PathBuilder`] for ergonomic path construction.
318///
319/// # Sizing
320///
321/// The default size matches the asset's view-box dimensions in logical
322/// pixels. Set [`El::width`] / [`El::height`] / [`El::fill_size`] to
323/// override. Painted vectors are tessellated into the resolved rect;
324/// mask vectors sample the backend MSDF atlas across that rect.
325///
326/// # Caching
327///
328/// The asset's [`VectorAsset::content_hash`](crate::vector::VectorAsset::content_hash)
329/// is the backend cache key. Apps that build the same shape twice (two
330/// commits sharing a merge connector geometry, two flowchart edges with
331/// the same arc) can share backend work; per-frame-unique geometry gets
332/// one cache entry per unique shape.
333///
334/// ```ignore
335/// use aetna_core::prelude::*;
336/// use aetna_core::tree::Color;
337///
338/// let curve = PathBuilder::new()
339///     .move_to(0.0, 0.0)
340///     .cubic_to(20.0, 0.0, 0.0, 60.0, 20.0, 60.0)
341///     .stroke_solid(Color::rgb(80, 200, 240), 2.0)
342///     .stroke_line_cap(VectorLineCap::Round)
343///     .build();
344/// let asset = VectorAsset::from_paths([0.0, 0.0, 20.0, 60.0], vec![curve]);
345/// let _ = vector(asset);
346/// ```
347#[track_caller]
348pub fn vector(asset: crate::vector::VectorAsset) -> El {
349    let [_, _, vw, vh] = asset.view_box;
350    El::new(Kind::Vector)
351        .at_loc(Location::caller())
352        .width(Size::Fixed(vw.max(0.0)))
353        .height(Size::Fixed(vh.max(0.0)))
354        .vector_source(std::sync::Arc::new(asset))
355}
356
357/// An app-owned-texture surface. Aetna composites the texture into
358/// the paint stream at the El's resolved rect — no upload, no per-frame
359/// copy. The default size matches the texture's pixel dimensions; set
360/// [`El::width`] / [`El::height`] (or `.fill_size()`) for an explicit
361/// box.
362///
363/// # Sizing, projection, and transforms
364///
365/// The texture's pixel dimensions are **independent of the rendered
366/// size**. By default the widget stretches the texture across the
367/// resolved rect ([`crate::image::ImageFit::Fill`]); reach for
368/// [`El::surface_fit`] to letterbox-preserve aspect ratio
369/// ([`crate::image::ImageFit::Contain`]), crop-cover
370/// ([`crate::image::ImageFit::Cover`]), or paint at natural size
371/// ([`crate::image::ImageFit::None`]). [`El::surface_transform`]
372/// composes an affine on top — rotate, mirror, zoom/pan — applied
373/// around the centre of the post-fit rect.
374///
375/// Picking a sizing strategy:
376/// - For pixel-accurate display, size the widget to the texture's
377///   pixel dimensions (the default constructor does this for you).
378/// - For a 3D viewport or video frame whose source resolution should
379///   track the rendered size, the app should re-allocate its texture
380///   to match the resolved rect (read it via `UiState::rect_of_key`
381///   after `prepare()`).
382/// - For an animated image whose natural dimensions are fixed
383///   (decoded GIF / WebP / APNG, decoded video frame),
384///   `surface_fit(Contain)` letterboxes into any layout rect with
385///   no per-resize allocation.
386///
387/// # z-order, scissor, hit-test
388///
389/// The widget participates in layout, scissor, scrolling, hit-test,
390/// and z-order like any other El: siblings declared before this one
391/// paint underneath, siblings after paint on top. The auto-clip
392/// scissor clamps painted content to the El's content rect — affines
393/// or `Cover` projections that overflow are cropped.
394///
395/// ```ignore
396/// // Pseudocode — the AppTexture comes from a backend constructor.
397/// use aetna_core::prelude::*;
398/// let tex: AppTexture = /* aetna_wgpu::app_texture(...) */ todo!();
399/// let _ = surface(tex)
400///     .fill_size()
401///     .surface_fit(ImageFit::Contain)
402///     .surface_alpha(SurfaceAlpha::Opaque)
403///     .surface_transform(Affine2::rotate(0.1));
404/// ```
405#[track_caller]
406pub fn surface(texture: crate::surface::AppTexture) -> El {
407    let (w, h) = texture.size_px();
408    El::new(Kind::Surface)
409        .at_loc(Location::caller())
410        .width(Size::Fixed(w as f32))
411        .height(Size::Fixed(h as f32))
412        .surface_source(crate::surface::SurfaceSource::Texture(texture))
413}
414
415/// A 1-pixel separator line.
416#[track_caller]
417pub fn divider() -> El {
418    El::new(Kind::Divider)
419        .at_loc(Location::caller())
420        .height(Size::Fixed(1.0))
421        .width(Size::Fill(1.0))
422        .fill(crate::tokens::BORDER)
423}
424
425// ---------- &str → El convenience ----------
426//
427// Lets `titled_card("Title", ["a body line"])` work without `text(...)`.
428
429impl From<&str> for El {
430    fn from(s: &str) -> Self {
431        crate::widgets::text::text(s)
432    }
433}
434
435impl From<String> for El {
436    fn from(s: String) -> Self {
437        crate::widgets::text::text(s)
438    }
439}
440
441impl From<&String> for El {
442    fn from(s: &String) -> Self {
443        crate::widgets::text::text(s.as_str())
444    }
445}