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