fenestra_core/element.rs
1//! The element IR: a plain tree of boxes and text, with typed styles,
2//! interaction variant overlays, and `Msg`-carrying handlers. `view()`
3//! rebuilds this tree on every redraw; it must stay cheap to construct.
4
5use peniko::Color;
6
7use crate::events::KeyInput;
8use crate::style::{Length, Paint, Style, TextAlign, TextWrap, ThemedFn, Transition};
9use crate::theme::Theme;
10use crate::tokens::{ShadowToken, TextSize, Weight};
11
12/// Maps a key press on a focused element to an optional message.
13pub(crate) type TypeAheadFn<Msg> = Box<dyn Fn(&str) -> Option<Msg>>;
14pub(crate) type KeyFn<Msg> = Box<dyn Fn(&KeyInput) -> Option<Msg>>;
15/// Maps a pointer position (as fractions of the element rect) to a message.
16pub(crate) type DragFn<Msg> = Box<dyn Fn(f32, f32) -> Option<Msg>>;
17/// Maps a recognized [`SwipeDir`] to a message.
18pub(crate) type SwipeFn<Msg> = Box<dyn Fn(SwipeDir) -> Msg>;
19/// Maps the edited text to a message.
20pub(crate) type InputFn<Msg> = Box<dyn Fn(&str) -> Msg>;
21/// Maps a dropped OS file path to a message.
22pub(crate) type FileDropFn<Msg> = Box<dyn Fn(&std::path::Path) -> Msg>;
23/// Maps an internal drag payload to an optional message on drop.
24pub(crate) type DropFn<Msg> = Box<dyn Fn(&str) -> Option<Msg>>;
25/// Resolves a control's content color (the color drawn *on* it) from the
26/// theme, for the uniform state-layer engine.
27pub(crate) type ContentFn = Box<dyn Fn(&Theme) -> Color>;
28
29/// What an element fundamentally is.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Kind {
32 /// A container box.
33 Box,
34 /// A text run (one style for the whole paragraph).
35 Text(String),
36 /// A paragraph of styled runs ([`rich_text`]): one wrapped layout,
37 /// per-span weight/color/size/family/italic.
38 Rich(Vec<Span>),
39 /// A themed hairline rule (resolved to `border_subtle`).
40 Divider,
41 /// A vector path (icons, check marks), scaled from its viewbox to the
42 /// element rect and painted in the resolved text color.
43 Path(PathData),
44 /// A single-line editable text field driven by parley's `PlainEditor`.
45 /// The app's value is the source of truth; edits emit `on_input`.
46 Input(InputData),
47 /// An RGBA8 image stretched to the element rect and clipped to its
48 /// corner radius.
49 Image(ImageData),
50}
51
52/// Payload for virtualized rows ([`Element::virtual_rows`]): only the
53/// scrolled-into-view window of rows is materialized each frame.
54pub struct VirtualData<Msg> {
55 pub(crate) count: usize,
56 pub(crate) row_height: f32,
57 /// Rows size themselves; `row_height` is the estimate for
58 /// unmaterialized ones (heights are measured and corrected).
59 pub(crate) variable: bool,
60 pub(crate) builder: std::rc::Rc<dyn Fn(usize) -> Element<Msg>>,
61}
62
63impl<Msg> Clone for VirtualData<Msg> {
64 fn clone(&self) -> Self {
65 Self {
66 count: self.count,
67 row_height: self.row_height,
68 variable: self.variable,
69 builder: std::rc::Rc::clone(&self.builder),
70 }
71 }
72}
73
74/// Payload for a container query ([`responsive`]): the closure rebuilds this
75/// container's subtree from its own available size, and `hint` is the size the
76/// first frame uses before any measurement exists.
77pub(crate) struct ResponsiveData<Msg> {
78 pub(crate) hint: (f32, f32),
79 pub(crate) f: std::rc::Rc<dyn Fn((f32, f32)) -> Element<Msg>>,
80}
81
82/// Payload for [`Kind::Image`].
83#[derive(Debug, Clone)]
84pub struct ImageData {
85 /// Decoded straight-alpha RGBA8 pixels, shared cheaply across frames.
86 pub image: peniko::ImageData,
87}
88
89impl PartialEq for ImageData {
90 /// Identity comparison (same pixel allocation and dimensions), not a
91 /// byte-by-byte one: rebuilt views share the `Arc`'d blob.
92 fn eq(&self, other: &Self) -> bool {
93 self.image.data.id() == other.image.data.id()
94 && self.image.width == other.image.width
95 && self.image.height == other.image.height
96 }
97}
98
99/// Payload for [`Kind::Input`].
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct InputData {
102 /// The current value (app state).
103 pub value: String,
104 /// Placeholder shown when the value is empty.
105 pub placeholder: String,
106 /// Multiline editing: text wraps to the element width, Enter inserts a
107 /// newline, and the measured height grows with the content.
108 pub multiline: bool,
109}
110
111/// Optical corrections for a [`Kind::Path`] (see [`crate::optical`]): geometric
112/// nudges that make a shape *look* right even though it measures "wrong". Both
113/// default to off, so a path renders byte-identically until opted in — set them
114/// per icon where the shape needs it, rather than auto-detecting (which would
115/// silently shift every existing icon).
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
117pub struct OpticalCorrection {
118 /// Scale the drawn path up by [`crate::optical::CIRCLE_OVERSHOOT`] about the
119 /// viewbox center, so a round or pointed icon reads the same visual size as
120 /// square-edged neighbors.
121 pub overshoot: bool,
122 /// Translate the path so its centroid (visual mass, [`crate::optical::centroid`])
123 /// sits at the viewbox center instead of its bounding-box center — the
124 /// classic "nudge the play triangle toward its point" correction.
125 pub center: bool,
126}
127
128/// Path payload for [`Kind::Path`].
129#[derive(Debug, Clone, PartialEq)]
130pub struct PathData {
131 /// The path, in viewbox coordinates.
132 pub path: std::sync::Arc<kurbo::BezPath>,
133 /// Design-space size the path was drawn in.
134 pub viewbox: (f64, f64),
135 /// Stroke width in viewbox units; `None` fills the path instead.
136 pub stroke: Option<f64>,
137 /// Optical corrections (overshoot / centroid centering); both off by default.
138 pub optical: OpticalCorrection,
139}
140
141/// How an overlay child opens and closes.
142#[derive(Debug, Clone, Copy, PartialEq)]
143pub enum OverlayMode {
144 /// Open while present in the tree; the app controls it (modals).
145 /// Outside clicks and Esc emit the overlay's `on_close` message.
146 Open,
147 /// Clicking the anchor (parent) toggles it; outside clicks and Esc
148 /// close it, and a click on any clickable inside closes it (menus).
149 Toggle,
150 /// Shows after hovering the anchor for the delay; never hit-tested.
151 Hover {
152 /// Hover delay in milliseconds.
153 delay_ms: f32,
154 },
155}
156
157/// Which screen edge a drawer/sheet overlay is anchored to.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum DrawerSide {
160 /// The left edge, filling the full canvas height.
161 Left,
162 /// The right edge, filling the full canvas height.
163 Right,
164 /// The top edge, filling the full canvas width.
165 Top,
166 /// The bottom edge, filling the full canvas width — a bottom sheet.
167 Bottom,
168}
169
170/// Where an overlay is positioned.
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum OverlayPlacement {
173 /// Below the anchor, left edges aligned, with a vertical gap. Flips
174 /// above when there is no room below.
175 Below {
176 /// Gap in logical px.
177 gap: f32,
178 },
179 /// Below the anchor, horizontally centered on it.
180 BelowCenter {
181 /// Gap in logical px.
182 gap: f32,
183 },
184 /// Centered in the canvas.
185 Center,
186 /// Pinned to the top-right of the canvas (toast stacks).
187 TopRight {
188 /// Margin from the canvas edges in logical px.
189 margin: f32,
190 },
191 /// At the pointer position when the overlay opened (context menus);
192 /// pinned there until it closes.
193 Pointer {
194 /// Offset from the pointer in logical px.
195 gap: f32,
196 },
197 /// Flush against a screen edge, filling that edge's full span (drawers and
198 /// sheets); slides in from off-canvas as it opens.
199 Edge {
200 /// Which edge to anchor to.
201 side: DrawerSide,
202 },
203 /// To the right of the anchor with top edges aligned (submenu flyouts);
204 /// flips to the anchor's left when there is no room on the right.
205 RightStart {
206 /// Gap from the anchor's right (or left, when flipped) edge in px.
207 gap: f32,
208 },
209}
210
211/// Marks an element as an overlay child of its parent (the anchor):
212/// excluded from normal layout, laid out against the canvas, painted after
213/// the root, and hit-tested first.
214#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct Overlay {
216 /// Open/close behavior.
217 pub mode: OverlayMode,
218 /// Positioning relative to the anchor or canvas.
219 pub placement: OverlayPlacement,
220 /// Dim everything beneath with black at 0.4 alpha.
221 pub backdrop: bool,
222 /// Tab cycles only inside this overlay while it is open.
223 pub trap_focus: bool,
224}
225
226impl Overlay {
227 /// A click-toggled menu below its anchor (select listboxes).
228 pub fn menu() -> Self {
229 Self {
230 mode: OverlayMode::Toggle,
231 placement: OverlayPlacement::Below { gap: 4.0 },
232 backdrop: false,
233 trap_focus: false,
234 }
235 }
236
237 /// A hover tooltip: 400ms delay, 6px below, centered, untouchable.
238 pub fn tooltip() -> Self {
239 Self {
240 mode: OverlayMode::Hover { delay_ms: 400.0 },
241 placement: OverlayPlacement::BelowCenter { gap: 6.0 },
242 backdrop: false,
243 trap_focus: false,
244 }
245 }
246
247 /// An app-driven centered modal with backdrop and focus trap.
248 pub fn modal() -> Self {
249 Self {
250 mode: OverlayMode::Open,
251 placement: OverlayPlacement::Center,
252 backdrop: true,
253 trap_focus: true,
254 }
255 }
256
257 /// An app-driven context menu pinned at the right-click position:
258 /// pair with `.on_right_click(open_msg)` on the target and
259 /// `.on_close(close_msg)` on the menu; item clicks are the app's cue
260 /// to close.
261 pub fn context() -> Self {
262 Self {
263 mode: OverlayMode::Open,
264 placement: OverlayPlacement::Pointer { gap: 2.0 },
265 backdrop: false,
266 trap_focus: false,
267 }
268 }
269
270 /// An app-driven toast stack pinned to the top-right: no backdrop, no
271 /// focus trap, and nothing closes it from outside — dismissal is the
272 /// stack's own buttons (or the app removing items).
273 pub fn toasts() -> Self {
274 Self {
275 mode: OverlayMode::Open,
276 placement: OverlayPlacement::TopRight { margin: 16.0 },
277 backdrop: false,
278 trap_focus: false,
279 }
280 }
281
282 /// An app-driven drawer/sheet flush to a screen `side`, with a backdrop and
283 /// focus trap; it slides in from that edge. Render it only while open;
284 /// `on_close` fires on Esc and an outside (scrim) click.
285 pub fn drawer(side: DrawerSide) -> Self {
286 Self {
287 mode: OverlayMode::Open,
288 placement: OverlayPlacement::Edge { side },
289 backdrop: true,
290 trap_focus: true,
291 }
292 }
293
294 /// A click-toggled submenu flyout to the right of its anchoring menu item
295 /// (flipping left at the canvas edge). Clicking or pressing Enter on the
296 /// anchor opens/closes it; outside clicks and Escape close it.
297 pub fn submenu() -> Self {
298 Self {
299 mode: OverlayMode::Toggle,
300 placement: OverlayPlacement::RightStart { gap: 2.0 },
301 backdrop: false,
302 trap_focus: false,
303 }
304 }
305}
306
307/// Accessible role and state of an element, projected into the platform
308/// accessibility tree by the shell (AccessKit) and exposed headlessly via
309/// `Frame::access_tree`. Text, image, and input leaves project
310/// automatically; kit widgets set the rest.
311#[derive(Debug, Clone, Copy, PartialEq)]
312pub enum Semantics {
313 /// An activatable button.
314 Button,
315 /// A checkbox: two-state, or tri-state when `mixed`.
316 Checkbox {
317 /// Whether it is checked.
318 checked: bool,
319 /// Whether it is in the indeterminate (mixed) state — projects
320 /// `aria-checked="mixed"`.
321 mixed: bool,
322 },
323 /// An on/off switch.
324 Switch {
325 /// Whether it is on.
326 on: bool,
327 },
328 /// One option of a radio group.
329 Radio {
330 /// Whether it is the selected option.
331 selected: bool,
332 },
333 /// A numeric slider.
334 Slider {
335 /// Current value.
336 value: f32,
337 /// Minimum value.
338 min: f32,
339 /// Maximum value.
340 max: f32,
341 },
342 /// An editable text field (value comes from the input element).
343 TextInput {
344 /// Whether it accepts newlines.
345 multiline: bool,
346 },
347 /// A button that opens a listbox of options.
348 ComboBox,
349 /// A modal dialog.
350 Dialog,
351 /// One tab of a tab strip.
352 Tab {
353 /// Whether it is the active tab.
354 selected: bool,
355 },
356 /// A transient notification (toasts).
357 Alert,
358 /// Static text (automatic for text leaves).
359 Label,
360 /// An image (automatic for image leaves).
361 Image,
362 /// A numeric input stepped by buttons / arrow keys (CSS `<input type=number>`).
363 Spinbutton {
364 /// Current value.
365 value: f32,
366 /// Minimum value.
367 min: f32,
368 /// Maximum value.
369 max: f32,
370 },
371 /// A scalar measurement within a known range (the HTML `<meter>`).
372 Meter {
373 /// Current value.
374 value: f32,
375 /// Minimum value.
376 min: f32,
377 /// Maximum value.
378 max: f32,
379 },
380 /// Task completion. `value` is the fraction `0.0..=1.0`, or `None` when the
381 /// progress is indeterminate.
382 ProgressBar {
383 /// Completed fraction `0.0..=1.0`, or `None` for indeterminate.
384 value: Option<f32>,
385 },
386}
387
388impl Semantics {
389 /// The ARIA role word for this role (`"button"`, `"textbox"`, …) — the same
390 /// vocabulary [`Frame::access_yaml`](crate::Frame::access_yaml) emits. The
391 /// public name for a node's role, stable across versions.
392 #[must_use]
393 pub fn aria_role(&self) -> &'static str {
394 crate::query::role_name(self)
395 }
396}
397
398/// One styled run of a [`rich_text`] paragraph. Unset properties
399/// inherit the paragraph's text style.
400#[derive(Debug, Clone, PartialEq)]
401pub struct Span {
402 pub(crate) text: String,
403 pub(crate) weight: Option<crate::tokens::Weight>,
404 pub(crate) color: Option<Color>,
405 pub(crate) size_px: Option<f32>,
406 pub(crate) family: Option<crate::tokens::FamilyRole>,
407 pub(crate) italic: bool,
408}
409
410/// A styled run for [`rich_text`].
411pub fn span(text: impl Into<String>) -> Span {
412 Span {
413 text: text.into(),
414 weight: None,
415 color: None,
416 size_px: None,
417 family: None,
418 italic: false,
419 }
420}
421
422impl Span {
423 /// Font weight for this run.
424 pub fn weight(mut self, weight: crate::tokens::Weight) -> Self {
425 self.weight = Some(weight);
426 self
427 }
428
429 /// Color for this run (route through theme tokens in app code).
430 pub fn color(mut self, color: Color) -> Self {
431 self.color = Some(color);
432 self
433 }
434
435 /// Font size in logical px for this run.
436 pub fn size_px(mut self, px: f32) -> Self {
437 self.size_px = Some(px);
438 self
439 }
440
441 /// Family role for this run.
442 pub fn family(mut self, family: crate::tokens::FamilyRole) -> Self {
443 self.family = Some(family);
444 self
445 }
446
447 /// Italic (synthesized when the face has no italic variant).
448 pub fn italic(mut self) -> Self {
449 self.italic = true;
450 self
451 }
452}
453
454/// A recognized swipe (flick) direction, delivered by
455/// [`Element::on_swipe`]. Screen axes: `Down` is toward the bottom.
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum SwipeDir {
458 /// A flick toward the top.
459 Up,
460 /// A flick toward the bottom.
461 Down,
462 /// A flick toward the start of the line (left in LTR).
463 Left,
464 /// A flick toward the end of the line (right in LTR).
465 Right,
466}
467
468/// Mouse cursor shown while hovering an element.
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum Cursor {
471 /// The platform default arrow.
472 Default,
473 /// Pointing hand (buttons, links).
474 Pointer,
475 /// Text I-beam.
476 Text,
477 /// Action not available.
478 NotAllowed,
479}
480
481/// How an element animates *out* when it leaves the tree (the counterpart of
482/// [`Element::enter`]). When an element tagged with [`Element::exit`] is
483/// removed, a paint-only snapshot ("ghost") is left in its place and animates
484/// toward these targets over `transition`, then is dropped — there is no live
485/// widget behind it (inputs collapse to a plain box). Inert under reduced
486/// motion: the element is removed immediately, so headless renders are
487/// unchanged.
488#[derive(Debug, Clone, Copy, PartialEq)]
489pub struct ExitAnim {
490 /// Timing of the exit (spring, or duration + easing).
491 pub transition: Transition,
492 /// Opacity the ghost fades to (0.0 = fully gone, the default).
493 pub opacity_to: f32,
494 /// Scale the ghost reaches about its center (1.0 = no scale change).
495 pub scale_to: f32,
496 /// Translation the ghost drifts by as it leaves, logical px `(dx, dy)`.
497 pub translate_to: (f32, f32),
498}
499
500/// One node in the view tree. `Msg` is the app's message type; handlers
501/// carry `Msg` values, not closures over state.
502pub struct Element<Msg> {
503 pub(crate) kind: Kind,
504 pub(crate) style: Style,
505 pub(crate) children: Vec<Element<Msg>>,
506 /// User key for stable identity (`.id()`).
507 pub(crate) key: Option<String>,
508 /// Where this element was constructed (file:line of the builder
509 /// call) — surfaced by `Frame::debug_tree` so dumps map back to
510 /// source. Captured via `#[track_caller]`, zero proc macros.
511 pub(crate) source: &'static std::panic::Location<'static>,
512 /// Forces children of a z-stack into the same grid cell.
513 pub(crate) stack: bool,
514 pub(crate) focusable: bool,
515 pub(crate) autofocus: bool,
516 /// Scroll containers only: keep pinned to the bottom while content
517 /// grows (chat/log pattern).
518 pub(crate) stick_bottom: bool,
519 pub(crate) cursor: Option<Cursor>,
520 pub(crate) disabled: bool,
521 pub(crate) on_click: Option<Msg>,
522 pub(crate) on_double_click: Option<Msg>,
523 pub(crate) on_right_click: Option<Msg>,
524 pub(crate) on_hover: Option<Msg>,
525 pub(crate) on_key: Option<KeyFn<Msg>>,
526 pub(crate) on_drag: Option<DragFn<Msg>>,
527 /// Fired once on pointer release after a captured drag (the gesture
528 /// that drove [`Self::on_drag`] ended). Only meaningful alongside
529 /// `on_drag`; used for drag lifecycles like column-resize commit.
530 pub(crate) on_drag_end: Option<Msg>,
531 /// Fired when a press-drag-release on this element is recognized as a swipe
532 /// (a fast flick past a small distance), with the dominant [`SwipeDir`].
533 pub(crate) on_swipe: Option<SwipeFn<Msg>>,
534 pub(crate) on_input: Option<InputFn<Msg>>,
535 pub(crate) on_close: Option<Msg>,
536 pub(crate) on_file_drop: Option<FileDropFn<Msg>>,
537 /// Payload announced when a pointer drag starts on this element.
538 pub(crate) drag_source: Option<String>,
539 pub(crate) on_drop: Option<DropFn<Msg>>,
540 pub(crate) overlay: Option<Overlay>,
541 /// Continuous rotation period in ms (spinners); paint-time, clock-driven.
542 pub(crate) spin: Option<f32>,
543 /// Looping keyframe timeline sampled from the frame clock.
544 pub(crate) keyframes: Option<crate::style::Keyframes>,
545 /// Virtualized rows: materialized from scroll state at build time.
546 pub(crate) virtual_rows: Option<VirtualData<Msg>>,
547 /// Container query: a transparent wrapper that `build` replaces with the
548 /// element its closure builds from this container's own measured size.
549 pub(crate) responsive: Option<ResponsiveData<Msg>>,
550 /// Accessible role and state (kit widgets set it; leaves auto-project).
551 pub(crate) semantics: Option<Semantics>,
552 /// Explicit accessible value (ARIA `valuetext`), overriding the input-derived
553 /// one. Widgets like spinbutton/meter set it to a formatted value string.
554 pub(crate) access_value: Option<String>,
555 /// Announce content changes to assistive technology (polite).
556 pub(crate) live: bool,
557 /// Static text: users can drag-select and copy it.
558 pub(crate) selectable: bool,
559 /// Fade-in transition seeded when the id first appears.
560 pub(crate) enter: Option<crate::style::Transition>,
561 /// Exit animation: a paint-only ghost lingers and animates out when the
562 /// id is removed from the tree (the counterpart of [`Self::enter`]).
563 pub(crate) exit: Option<ExitAnim>,
564 /// FLIP/shared-element layout animation: when the element's measured rect
565 /// moves between frames it slides from the old position to the new.
566 pub(crate) animate_layout: bool,
567 /// Buffered type-ahead while focused (1s window per keystroke).
568 pub(crate) on_type_ahead: Option<TypeAheadFn<Msg>>,
569 /// Accessible name (screen-reader label).
570 pub(crate) label: Option<String>,
571 pub(crate) themed: Option<ThemedFn>,
572 pub(crate) hover_style: Option<ThemedFn>,
573 pub(crate) active_style: Option<ThemedFn>,
574 pub(crate) focus_style: Option<ThemedFn>,
575 /// Uniform Material state layer: resolves the content color veiled over the
576 /// container on hover/focus/press/drag (replaces per-state color swaps).
577 pub(crate) state_layer: Option<ContentFn>,
578 /// Dip to [`crate::tokens::PRESS_SCALE`] while pressed.
579 pub(crate) press_scale: bool,
580 /// Recolor the focus ring (and swapped border) to the danger hue.
581 pub(crate) invalid: bool,
582 pub(crate) transition: Option<Transition>,
583}
584
585impl<Msg> Element<Msg> {
586 #[track_caller]
587 fn new(kind: Kind) -> Self {
588 Self {
589 kind,
590 style: Style::default(),
591 children: Vec::new(),
592 key: None,
593 source: std::panic::Location::caller(),
594 stack: false,
595 focusable: false,
596 autofocus: false,
597 stick_bottom: false,
598 cursor: None,
599 disabled: false,
600 on_click: None,
601 on_double_click: None,
602 on_right_click: None,
603 on_hover: None,
604 on_key: None,
605 on_drag: None,
606 on_drag_end: None,
607 on_swipe: None,
608 on_input: None,
609 on_close: None,
610 on_file_drop: None,
611 drag_source: None,
612 on_drop: None,
613 overlay: None,
614 spin: None,
615 keyframes: None,
616 virtual_rows: None,
617 responsive: None,
618 semantics: None,
619 access_value: None,
620 live: false,
621 selectable: false,
622 enter: None,
623 exit: None,
624 animate_layout: false,
625 on_type_ahead: None,
626 label: None,
627 themed: None,
628 hover_style: None,
629 active_style: None,
630 focus_style: None,
631 state_layer: None,
632 press_scale: false,
633 invalid: false,
634 transition: None,
635 }
636 }
637
638 /// The element's style (read access for tests and tooling).
639 pub fn style(&self) -> &Style {
640 &self.style
641 }
642
643 /// Appends children. Anything convertible to an element works, so kit
644 /// widget builders drop in next to `text()`/`div()` trees.
645 pub fn children<M>(mut self, children: impl crate::IntoChildren<Msg, M>) -> Self {
646 self.children.extend(children.into_children());
647 self
648 }
649
650 /// Appends one child.
651 pub fn child(mut self, child: impl Into<Element<Msg>>) -> Self {
652 self.children.push(child.into());
653 self
654 }
655
656 /// Sets a user key for stable identity across reorders.
657 pub fn id(mut self, key: &str) -> Self {
658 self.key = Some(key.to_owned());
659 self
660 }
661
662 /// Emits this message when the element is clicked.
663 pub fn on_click(mut self, msg: Msg) -> Self {
664 self.on_click = Some(msg);
665 self.focusable = true;
666 if self.cursor.is_none() {
667 self.cursor = Some(Cursor::Pointer);
668 }
669 self
670 }
671
672 /// Style overlay applied while hovered.
673 pub fn hover(mut self, f: impl Fn(Style) -> Style + 'static) -> Self {
674 self.hover_style = Some(Box::new(move |_, s| f(s)));
675 self
676 }
677
678 /// Theme-aware hover overlay (used by kit widgets, which have no theme
679 /// in scope at build time).
680 pub fn hover_themed(mut self, f: impl Fn(&Theme, Style) -> Style + 'static) -> Self {
681 self.hover_style = Some(Box::new(f));
682 self
683 }
684
685 /// Style overlay applied while pressed.
686 pub fn active(mut self, f: impl Fn(Style) -> Style + 'static) -> Self {
687 self.active_style = Some(Box::new(move |_, s| f(s)));
688 self
689 }
690
691 /// Theme-aware active overlay.
692 pub fn active_themed(mut self, f: impl Fn(&Theme, Style) -> Style + 'static) -> Self {
693 self.active_style = Some(Box::new(f));
694 self
695 }
696
697 /// Style overlay applied while focused.
698 pub fn focus(mut self, f: impl Fn(Style) -> Style + 'static) -> Self {
699 self.focus_style = Some(Box::new(move |_, s| f(s)));
700 self
701 }
702
703 /// Theme-aware focus overlay.
704 pub fn focus_themed(mut self, f: impl Fn(&Theme, Style) -> Style + 'static) -> Self {
705 self.focus_style = Some(Box::new(f));
706 self
707 }
708
709 /// Drives the uniform Material state layer for this control. `content`
710 /// resolves the color drawn *on* the control (its label/icon color); a
711 /// translucent veil of it is laid over the container on hover, keyboard
712 /// focus, press, and drag at the [`crate::tokens::STATE_LAYER`] opacities —
713 /// one recipe instead of per-state color swaps. A disabled control fades
714 /// its container toward the resting surface. The veil animates through the
715 /// element's transition like any fill change.
716 pub fn state_layer(mut self, content: impl Fn(&Theme) -> Color + 'static) -> Self {
717 self.state_layer = Some(Box::new(content));
718 self
719 }
720
721 /// Dips the control to [`crate::tokens::PRESS_SCALE`] while pressed
722 /// (pointer down) — a tactile shrink that animates and never disturbs
723 /// layout or hit-testing.
724 pub fn press_scale(mut self) -> Self {
725 self.press_scale = true;
726 self
727 }
728
729 /// Marks the control invalid: its keyboard focus ring and swapped border
730 /// recolor to the danger hue (shadcn's `aria-invalid` ring).
731 pub fn invalid(mut self, invalid: bool) -> Self {
732 self.invalid = invalid;
733 self
734 }
735
736 /// Theme-deferred base styling, applied during style resolution. This is
737 /// how kit widgets route every color through tokens without a theme in
738 /// scope: `view()` has no theme parameter.
739 pub fn themed(mut self, f: impl Fn(&Theme, Style) -> Style + 'static) -> Self {
740 self.themed = Some(match self.themed.take() {
741 Some(prev) => Box::new(move |t, s| f(t, prev(t, s))),
742 None => Box::new(f),
743 });
744 self
745 }
746
747 /// Emits this message when the element is clicked twice within 400ms
748 /// (the single-click message fires for both clicks too).
749 pub fn on_double_click(mut self, msg: Msg) -> Self {
750 self.on_double_click = Some(msg);
751 self
752 }
753
754 /// Emits this message on a right-button press over the element (the
755 /// context-menu gesture; fires on press, like macOS).
756 pub fn on_right_click(mut self, msg: Msg) -> Self {
757 self.on_right_click = Some(msg);
758 self
759 }
760
761 /// Emits this message when the pointer enters the element.
762 pub fn on_hover(mut self, msg: Msg) -> Self {
763 self.on_hover = Some(msg);
764 self
765 }
766
767 /// Maps key presses (while focused) to messages.
768 pub fn on_key(mut self, f: impl Fn(&KeyInput) -> Option<Msg> + 'static) -> Self {
769 self.on_key = Some(Box::new(f));
770 self.focusable = true;
771 self
772 }
773
774 /// Maps pointer presses and captured drags to messages. The callback
775 /// receives the pointer position as fractions (0..=1) of the element
776 /// rect on both axes.
777 pub fn on_drag(mut self, f: impl Fn(f32, f32) -> Option<Msg> + 'static) -> Self {
778 self.on_drag = Some(Box::new(f));
779 self
780 }
781
782 /// Emits a message once when a captured drag (see [`Self::on_drag`])
783 /// ends on pointer release. It fires only when this element actually
784 /// captured the press as a drag, so a plain click on a different
785 /// (click-only) element never triggers it. Pairs with `on_drag` to
786 /// model gesture lifecycles — e.g. committing a column resize.
787 pub fn on_drag_end(mut self, msg: Msg) -> Self {
788 self.on_drag_end = Some(msg);
789 self
790 }
791
792 /// Recognizes a swipe (flick) on this element: a press, a quick drag past a
793 /// small threshold, and release fire the closure with the dominant
794 /// [`SwipeDir`]. Good for carousels, dismissible cards, and back gestures —
795 /// the element captures the press, so it works without `on_drag`.
796 pub fn on_swipe(mut self, f: impl Fn(SwipeDir) -> Msg + 'static) -> Self {
797 self.on_swipe = Some(Box::new(f));
798 self
799 }
800
801 /// Maps each text edit of an input element to a message.
802 pub fn on_input(mut self, f: impl Fn(&str) -> Msg + 'static) -> Self {
803 self.on_input = Some(Box::new(f));
804 self
805 }
806
807 /// Maps an OS file dropped onto this element to a message (delivered
808 /// at the last pointer position; with no hit, the first handler in the
809 /// tree receives it). The OS sends one event per dropped file.
810 pub fn on_file_drop(mut self, f: impl Fn(&std::path::Path) -> Msg + 'static) -> Self {
811 self.on_file_drop = Some(Box::new(f));
812 self
813 }
814
815 /// Marks this element as an internal drag source carrying a string
816 /// payload: pressing on it starts a drag, and releasing over an
817 /// element with [`Self::on_drop`] delivers the payload there.
818 pub fn drag_source(mut self, payload: impl Into<String>) -> Self {
819 self.drag_source = Some(payload.into());
820 self
821 }
822
823 /// Receives internal drag payloads released over this element; return
824 /// `None` to reject. Style the drag with `.active(..)` on the source.
825 pub fn on_drop(mut self, f: impl Fn(&str) -> Option<Msg> + 'static) -> Self {
826 self.on_drop = Some(Box::new(f));
827 self
828 }
829
830 /// Marks this element as an overlay child of its parent (the anchor).
831 pub fn overlay(mut self, overlay: Overlay) -> Self {
832 self.overlay = Some(overlay);
833 self
834 }
835
836 /// Emitted when an app-driven overlay is dismissed (outside click, Esc).
837 pub fn on_close(mut self, msg: Msg) -> Self {
838 self.on_close = Some(msg);
839 self
840 }
841
842 /// Rotates the element's painted content continuously (one turn per
843 /// `period_ms`). Only path elements rotate; used by spinners.
844 pub fn spin(mut self, period_ms: f32) -> Self {
845 self.spin = Some(period_ms);
846 self
847 }
848
849 /// Optically overshoots a path icon ([`crate::optical::CIRCLE_OVERSHOOT`]):
850 /// scales the drawn path up about the viewbox center so a round or pointed
851 /// glyph reads the same visual size as square-edged neighbors. No-op on
852 /// non-path elements; off by default (existing icons are unchanged).
853 pub fn optical_overshoot(mut self) -> Self {
854 if let Kind::Path(p) = &mut self.kind {
855 p.optical.overshoot = true;
856 }
857 self
858 }
859
860 /// Centers a path icon on its visual mass ([`crate::optical::centroid`])
861 /// rather than its bounding box — the play-triangle correction. No-op on
862 /// non-path elements; off by default.
863 pub fn optical_center(mut self) -> Self {
864 if let Kind::Path(p) = &mut self.kind {
865 p.optical.center = true;
866 }
867 self
868 }
869
870 /// Attaches a looping [`Keyframes`](crate::style::Keyframes) timeline,
871 /// sampled from the frame clock after `themed`, interaction variants,
872 /// and transitions resolve. Reduced motion pins the first stop.
873 pub fn keyframes(mut self, keyframes: crate::style::Keyframes) -> Self {
874 self.keyframes = Some(keyframes);
875 self
876 }
877
878 /// The most rows a virtualized container will address. The fixed-height
879 /// window (`virtual_rows`) is O(1) in `count` — window math only — but
880 /// the variable-height index (`virtual_rows_variable`) builds a
881 /// `heights`/`prefix` `Vec<f32>` per row (`HeightIndex::ensure`), so an
882 /// unclamped hostile `count` (`usize::MAX`) attempts a
883 /// capacity-overflow-panicking allocation before any window is computed.
884 /// Ten million rows is far beyond any in-memory dataset a real app would
885 /// virtualize in one list, so the ceiling never affects realistic usage
886 /// while keeping worst-case allocation in the tens-of-MB range.
887 const MAX_VIRTUAL_ROWS: usize = 10_000_000;
888
889 /// Virtualizes this container's rows: `builder(i)` is called only for
890 /// the rows inside the scrolled-into-view window (plus overscan), with
891 /// spacers standing in for the rest, so a 100k-row list builds a
892 /// screenful of nodes per frame. Rows are forced to `row_height` and
893 /// keyed by index. Pair with `.scroll_y()` and a stable `.id(..)` (the
894 /// kit's `virtual_list` does both). Overlays inside virtual rows are
895 /// not supported.
896 pub fn virtual_rows(
897 mut self,
898 count: usize,
899 row_height: f32,
900 builder: impl Fn(usize) -> Element<Msg> + 'static,
901 ) -> Self {
902 self.virtual_rows = Some(VirtualData {
903 count: count.min(Self::MAX_VIRTUAL_ROWS),
904 row_height,
905 variable: false,
906 builder: std::rc::Rc::new(builder),
907 });
908 self
909 }
910
911 /// Like [`Self::virtual_rows`], but rows size themselves:
912 /// `estimated_height` positions unmaterialized rows, and real
913 /// heights are measured as rows appear (offsets self-correct over
914 /// the next frame). Constraints: no overlays inside rows.
915 pub fn virtual_rows_variable(
916 mut self,
917 count: usize,
918 estimated_height: f32,
919 builder: impl Fn(usize) -> Element<Msg> + 'static,
920 ) -> Self {
921 self.virtual_rows = Some(VirtualData {
922 count: count.min(Self::MAX_VIRTUAL_ROWS),
923 row_height: estimated_height,
924 variable: true,
925 builder: std::rc::Rc::new(builder),
926 });
927 self
928 }
929
930 /// Maps the focused element's accumulated type-ahead buffer to a
931 /// message: printable keystrokes append (1s window between them,
932 /// Escape clears), and the handler sees the whole buffer — "che"
933 /// jumps a select to "Cherry" instead of cycling C-entries.
934 pub fn on_type_ahead(mut self, f: impl Fn(&str) -> Option<Msg> + 'static) -> Self {
935 self.on_type_ahead = Some(Box::new(f));
936 self
937 }
938
939 /// Animates the element in when it first appears (a fade from
940 /// opacity 0 through the given transition — give stateful entries a
941 /// stable `.id` so reorders don't retrigger it). Pair with
942 /// [`Self::exit`] to also animate removal.
943 pub fn enter(mut self, transition: crate::style::Transition) -> Self {
944 self.enter = Some(crate::style::Transition {
945 opacity: true,
946 ..transition
947 });
948 self
949 }
950
951 /// Animates the element *out* when it is removed from the tree: a
952 /// paint-only snapshot ("ghost") is left in place and fades to transparent
953 /// over `transition`, then dropped (the counterpart of [`Self::enter`]).
954 /// Give stateful entries a stable [`Self::id`] so removal is detected by
955 /// identity, not list position. Inert under reduced motion (removal is
956 /// immediate). Use [`Self::exit_to`] to also scale or slide the ghost.
957 pub fn exit(mut self, transition: crate::style::Transition) -> Self {
958 self.exit = Some(ExitAnim {
959 transition: crate::style::Transition {
960 opacity: true,
961 ..transition
962 },
963 opacity_to: 0.0,
964 scale_to: 1.0,
965 translate_to: (0.0, 0.0),
966 });
967 self
968 }
969
970 /// Like [`Self::exit`], but the leaving ghost animates toward an explicit
971 /// `opacity`, `scale` (about its center), and `(dx, dy)` translation over
972 /// the standard exit timing (a brisk accelerate-eased
973 /// [`MotionDuration::exit_ms`](crate::tokens::MotionDuration::exit_ms)).
974 /// For example `exit_to(0.0, 0.96, 0.0, 8.0)` fades a toast out while it
975 /// shrinks slightly and drops away.
976 pub fn exit_to(mut self, opacity: f32, scale: f32, dx: f32, dy: f32) -> Self {
977 self.exit = Some(ExitAnim {
978 transition: crate::style::Transition::all()
979 .duration_ms(crate::tokens::MotionDuration::Base.exit_ms())
980 .easing(crate::tokens::EASE_EXIT),
981 opacity_to: opacity,
982 scale_to: scale,
983 translate_to: (dx, dy),
984 });
985 self
986 }
987
988 /// Animates this element's *position* when layout moves it between frames
989 /// (FLIP / shared-element). Its measured rect is compared with the
990 /// previous frame's; when the center moves, the element is painted
991 /// starting at the old position and springs to the new one, so reordering
992 /// a list (or a resized sibling pushing it) glides instead of jumping.
993 /// Pair with a stable [`Self::id`] — without one, reordering changes the
994 /// `WidgetId`, the previous position is lost under the new identity, and
995 /// the slide never fires. Composes with any static or animated
996 /// `translate`. Inert under reduced motion (the element snaps).
997 ///
998 /// With no explicit [`Self::transition`]/[`Self::enter`] the slide rides an
999 /// implicit spatial spring; a declared transition wins and drives the slide
1000 /// at its own timing instead. Either way that transition is the element's
1001 /// general one, so an incidental style change animates through it too
1002 /// (colors clamp; only position springs here).
1003 pub fn animate_layout(mut self) -> Self {
1004 self.animate_layout = true;
1005 self
1006 }
1007
1008 /// Makes static text selectable: drag (or double/triple-click)
1009 /// selects, Cmd/Ctrl+C copies. For text and rich-text elements.
1010 pub fn selectable(mut self) -> Self {
1011 self.selectable = true;
1012 self
1013 }
1014
1015 /// Marks a live region: assistive technology announces content
1016 /// changes inside it without focus moving there (status lines,
1017 /// toasts — the kit's toast stack sets this itself).
1018 pub fn live(mut self) -> Self {
1019 self.live = true;
1020 self
1021 }
1022
1023 /// Sets the accessible role and state projected into the accessibility
1024 /// tree. Text, image, and input leaves project their role automatically;
1025 /// use this to set one on a custom element, or to override the default.
1026 pub fn semantics(mut self, semantics: Semantics) -> Self {
1027 self.semantics = Some(semantics);
1028 self
1029 }
1030
1031 /// Sets the accessible name announced by screen readers (text leaves
1032 /// use their content automatically).
1033 pub fn label(mut self, label: impl Into<String>) -> Self {
1034 self.label = Some(label.into());
1035 self
1036 }
1037
1038 /// Sets the accessible value (ARIA `valuetext`) for a non-input control —
1039 /// e.g. a spinbutton's formatted "$5.00" or a meter's "75%". Text inputs
1040 /// derive their value from the edited text; this overrides it.
1041 pub fn value(mut self, value: impl Into<String>) -> Self {
1042 self.access_value = Some(value.into());
1043 self
1044 }
1045
1046 /// Disables interaction: handlers and variants stop applying.
1047 pub fn disabled(mut self, disabled: bool) -> Self {
1048 self.disabled = disabled;
1049 self
1050 }
1051
1052 /// Declares which properties animate between style states.
1053 pub fn transition(mut self, t: Transition) -> Self {
1054 self.transition = Some(t);
1055 self
1056 }
1057
1058 /// Marks the element keyboard-focusable.
1059 pub fn focusable(mut self, focusable: bool) -> Self {
1060 self.focusable = focusable;
1061 self
1062 }
1063
1064 /// Focuses this element when it newly appears in the tree (opening a
1065 /// modal focuses its input). It does not steal focus while it stays
1066 /// mounted, and refocuses when it disappears and reappears. Give at
1067 /// most one element autofocus per view state.
1068 pub fn autofocus(mut self) -> Self {
1069 self.autofocus = true;
1070 self.focusable = true;
1071 self
1072 }
1073
1074 /// Sets the hover cursor.
1075 pub fn cursor(mut self, cursor: Cursor) -> Self {
1076 self.cursor = Some(cursor);
1077 self
1078 }
1079
1080 // ----- style delegation: every fluent `Style` method, on the element -----
1081
1082 /// Padding on all edges.
1083 pub fn p(mut self, v: f32) -> Self {
1084 self.style = self.style.p(v);
1085 self
1086 }
1087
1088 /// Horizontal padding.
1089 pub fn px(mut self, v: f32) -> Self {
1090 self.style = self.style.px(v);
1091 self
1092 }
1093
1094 /// Vertical padding.
1095 pub fn py(mut self, v: f32) -> Self {
1096 self.style = self.style.py(v);
1097 self
1098 }
1099
1100 /// Top padding.
1101 pub fn pt(mut self, v: f32) -> Self {
1102 self.style = self.style.pt(v);
1103 self
1104 }
1105
1106 /// Right padding.
1107 pub fn pr(mut self, v: f32) -> Self {
1108 self.style = self.style.pr(v);
1109 self
1110 }
1111
1112 /// Bottom padding.
1113 pub fn pb(mut self, v: f32) -> Self {
1114 self.style = self.style.pb(v);
1115 self
1116 }
1117
1118 /// Left padding.
1119 pub fn pl(mut self, v: f32) -> Self {
1120 self.style = self.style.pl(v);
1121 self
1122 }
1123
1124 /// Margin on all edges.
1125 pub fn m(mut self, v: f32) -> Self {
1126 self.style = self.style.m(v);
1127 self
1128 }
1129
1130 /// Horizontal margin.
1131 pub fn mx(mut self, v: f32) -> Self {
1132 self.style = self.style.mx(v);
1133 self
1134 }
1135
1136 /// Vertical margin.
1137 pub fn my(mut self, v: f32) -> Self {
1138 self.style = self.style.my(v);
1139 self
1140 }
1141
1142 /// Top margin.
1143 pub fn mt(mut self, v: f32) -> Self {
1144 self.style = self.style.mt(v);
1145 self
1146 }
1147
1148 /// Right margin.
1149 pub fn mr(mut self, v: f32) -> Self {
1150 self.style = self.style.mr(v);
1151 self
1152 }
1153
1154 /// Bottom margin.
1155 pub fn mb(mut self, v: f32) -> Self {
1156 self.style = self.style.mb(v);
1157 self
1158 }
1159
1160 /// Left margin.
1161 pub fn ml(mut self, v: f32) -> Self {
1162 self.style = self.style.ml(v);
1163 self
1164 }
1165
1166 /// Gap between children.
1167 pub fn gap(mut self, v: f32) -> Self {
1168 self.style = self.style.gap(v);
1169 self
1170 }
1171
1172 /// Preferred width (raw `f32` = logical px).
1173 pub fn w(mut self, v: impl Into<Length>) -> Self {
1174 self.style = self.style.w(v);
1175 self
1176 }
1177
1178 /// Preferred height.
1179 pub fn h(mut self, v: impl Into<Length>) -> Self {
1180 self.style = self.style.h(v);
1181 self
1182 }
1183
1184 /// Minimum width.
1185 pub fn min_w(mut self, v: impl Into<Length>) -> Self {
1186 self.style = self.style.min_w(v);
1187 self
1188 }
1189
1190 /// Maximum width.
1191 pub fn max_w(mut self, v: impl Into<Length>) -> Self {
1192 self.style = self.style.max_w(v);
1193 self
1194 }
1195
1196 /// Minimum height.
1197 pub fn min_h(mut self, v: impl Into<Length>) -> Self {
1198 self.style = self.style.min_h(v);
1199 self
1200 }
1201
1202 /// Maximum height.
1203 pub fn max_h(mut self, v: impl Into<Length>) -> Self {
1204 self.style = self.style.max_h(v);
1205 self
1206 }
1207
1208 /// Caps this element's width at a reading measure of `chars` characters
1209 /// (a `ch`-based `max-width`); see [`Style::measure`](crate::Style::measure).
1210 pub fn measure(mut self, chars: f32) -> Self {
1211 self.style = self.style.measure(chars);
1212 self
1213 }
1214
1215 /// Preferred width in `ch` units (see [`Length::Ch`](crate::Length::Ch)).
1216 pub fn w_ch(mut self, chars: f32) -> Self {
1217 self.style = self.style.w_ch(chars);
1218 self
1219 }
1220
1221 /// Minimum width in `ch` units.
1222 pub fn min_w_ch(mut self, chars: f32) -> Self {
1223 self.style = self.style.min_w_ch(chars);
1224 self
1225 }
1226
1227 /// Maximum width in `ch` units (alias of [`Element::measure`]).
1228 pub fn max_w_ch(mut self, chars: f32) -> Self {
1229 self.style = self.style.max_w_ch(chars);
1230 self
1231 }
1232
1233 /// Width 100%.
1234 pub fn w_full(mut self) -> Self {
1235 self.style = self.style.w_full();
1236 self
1237 }
1238
1239 /// Height 100%.
1240 pub fn h_full(mut self) -> Self {
1241 self.style = self.style.h_full();
1242 self
1243 }
1244
1245 /// Flex grow 1.
1246 pub fn grow(mut self) -> Self {
1247 self.style = self.style.grow();
1248 self
1249 }
1250
1251 /// Flex shrink 0.
1252 pub fn shrink0(mut self) -> Self {
1253 self.style = self.style.shrink0();
1254 self
1255 }
1256
1257 /// Align children to the cross-axis start.
1258 pub fn items_start(mut self) -> Self {
1259 self.style = self.style.items_start();
1260 self
1261 }
1262
1263 /// Center children on the cross axis.
1264 pub fn items_center(mut self) -> Self {
1265 self.style = self.style.items_center();
1266 self
1267 }
1268
1269 /// Align children to the cross-axis end.
1270 pub fn items_end(mut self) -> Self {
1271 self.style = self.style.items_end();
1272 self
1273 }
1274
1275 /// Align children on their first text baseline (rows only).
1276 pub fn items_baseline(mut self) -> Self {
1277 self.style = self.style.items_baseline();
1278 self
1279 }
1280
1281 /// Override the parent's cross-axis alignment for this element alone,
1282 /// hugging its content at the cross-axis start instead of stretching.
1283 pub fn self_start(mut self) -> Self {
1284 self.style = self.style.self_start();
1285 self
1286 }
1287
1288 /// Override the parent's cross-axis alignment for this element alone,
1289 /// centering it on the cross axis.
1290 pub fn self_center(mut self) -> Self {
1291 self.style = self.style.self_center();
1292 self
1293 }
1294
1295 /// Override the parent's cross-axis alignment for this element alone,
1296 /// packing it toward the cross-axis end.
1297 pub fn self_end(mut self) -> Self {
1298 self.style = self.style.self_end();
1299 self
1300 }
1301
1302 /// Override the parent's cross-axis alignment for this element alone,
1303 /// stretching it to fill the cross axis.
1304 pub fn self_stretch(mut self) -> Self {
1305 self.style = self.style.self_stretch();
1306 self
1307 }
1308
1309 /// Pack children toward the main-axis start.
1310 pub fn justify_start(mut self) -> Self {
1311 self.style = self.style.justify_start();
1312 self
1313 }
1314
1315 /// Center children on the main axis.
1316 pub fn justify_center(mut self) -> Self {
1317 self.style = self.style.justify_center();
1318 self
1319 }
1320
1321 /// Pack children toward the main-axis end.
1322 pub fn justify_end(mut self) -> Self {
1323 self.style = self.style.justify_end();
1324 self
1325 }
1326
1327 /// Distribute children with space between.
1328 pub fn justify_between(mut self) -> Self {
1329 self.style = self.style.justify_between();
1330 self
1331 }
1332
1333 /// Allow flex children to wrap.
1334 pub fn wrap(mut self) -> Self {
1335 self.style = self.style.wrap();
1336 self
1337 }
1338
1339 /// Position absolutely against the nearest relative ancestor.
1340 pub fn absolute(mut self) -> Self {
1341 self.style = self.style.absolute();
1342 self
1343 }
1344
1345 /// Offset from the top.
1346 pub fn top(mut self, v: f32) -> Self {
1347 self.style = self.style.top(v);
1348 self
1349 }
1350
1351 /// Offset from the right.
1352 pub fn right(mut self, v: f32) -> Self {
1353 self.style = self.style.right(v);
1354 self
1355 }
1356
1357 /// Offset from the bottom.
1358 pub fn bottom(mut self, v: f32) -> Self {
1359 self.style = self.style.bottom(v);
1360 self
1361 }
1362
1363 /// Offset from the left.
1364 pub fn left(mut self, v: f32) -> Self {
1365 self.style = self.style.left(v);
1366 self
1367 }
1368
1369 /// Clip children to the bounds.
1370 pub fn overflow_hidden(mut self) -> Self {
1371 self.style = self.style.overflow_hidden();
1372 self
1373 }
1374
1375 /// Vertical scrolling with clipped content.
1376 pub fn scroll_y(mut self) -> Self {
1377 self.style = self.style.scroll_y();
1378 self
1379 }
1380
1381 /// Horizontal scrolling with clipped content.
1382 pub fn scroll_x(mut self) -> Self {
1383 self.style = self.style.scroll_x();
1384 self
1385 }
1386
1387 /// Scrolling on both axes with clipped content.
1388 pub fn scroll_xy(mut self) -> Self {
1389 self.style = self.style.scroll_xy();
1390 self
1391 }
1392
1393 /// Sticks `offset` px below the scroll viewport's top edge (`position: sticky`).
1394 pub fn sticky_top(mut self, offset: f32) -> Self {
1395 self.style = self.style.sticky_top(offset);
1396 self
1397 }
1398
1399 /// Sticks `offset` px above the scroll viewport's bottom edge.
1400 pub fn sticky_bottom(mut self, offset: f32) -> Self {
1401 self.style = self.style.sticky_bottom(offset);
1402 self
1403 }
1404
1405 /// Sticks `offset` px right of the scroll viewport's left edge.
1406 pub fn sticky_left(mut self, offset: f32) -> Self {
1407 self.style = self.style.sticky_left(offset);
1408 self
1409 }
1410
1411 /// Sticks `offset` px left of the scroll viewport's right edge.
1412 pub fn sticky_right(mut self, offset: f32) -> Self {
1413 self.style = self.style.sticky_right(offset);
1414 self
1415 }
1416
1417 /// Keeps a scroll container pinned to its bottom edge while content
1418 /// grows — until the user scrolls away, and again once they return to
1419 /// the bottom (the chat/log pattern). Starts at the bottom.
1420 pub fn stick_to_bottom(mut self) -> Self {
1421 self.stick_bottom = true;
1422 self
1423 }
1424
1425 /// Background fill.
1426 pub fn bg(mut self, paint: impl Into<Paint>) -> Self {
1427 self.style = self.style.bg(paint);
1428 self
1429 }
1430
1431 /// Uniform border (a stroke on the element's edge).
1432 pub fn border(mut self, width: f32, color: Color) -> Self {
1433 self.style = self.style.border(width, color);
1434 self
1435 }
1436
1437 /// A border stroke on just the top edge — a straight hairline (square
1438 /// corners) for ruled layouts, no manual divider child needed. See
1439 /// [`Style::border_top`](crate::Style::border_top).
1440 pub fn border_top(mut self, width: f32, color: Color) -> Self {
1441 self.style = self.style.border_top(width, color);
1442 self
1443 }
1444
1445 /// A border stroke on just the right edge. See
1446 /// [`Style::border_right`](crate::Style::border_right).
1447 pub fn border_right(mut self, width: f32, color: Color) -> Self {
1448 self.style = self.style.border_right(width, color);
1449 self
1450 }
1451
1452 /// A border stroke on just the bottom edge — a header/row rule. See
1453 /// [`Style::border_bottom`](crate::Style::border_bottom).
1454 pub fn border_bottom(mut self, width: f32, color: Color) -> Self {
1455 self.style = self.style.border_bottom(width, color);
1456 self
1457 }
1458
1459 /// A border stroke on just the left edge — an accent rail. See
1460 /// [`Style::border_left`](crate::Style::border_left).
1461 pub fn border_left(mut self, width: f32, color: Color) -> Self {
1462 self.style = self.style.border_left(width, color);
1463 self
1464 }
1465
1466 /// A crisp `width`-px ring just outside the box, hugging the corner radius
1467 /// (see [`Style::ring`](crate::Style::ring)) — the "ring, not border" look:
1468 /// outside the element, zero layout cost, ideal for selection rings and
1469 /// sub-pixel hairlines.
1470 pub fn ring(mut self, width: f32, color: Color) -> Self {
1471 self.style = self.style.ring(width, color);
1472 self
1473 }
1474
1475 /// The same corner radius on all corners.
1476 pub fn rounded(mut self, r: f32) -> Self {
1477 self.style = self.style.rounded(r);
1478 self
1479 }
1480
1481 /// Fully-rounded corners.
1482 pub fn rounded_full(mut self) -> Self {
1483 self.style = self.style.rounded_full();
1484 self
1485 }
1486
1487 /// Rounds the top two corners only.
1488 pub fn rounded_t(mut self, r: f32) -> Self {
1489 self.style = self.style.rounded_t(r);
1490 self
1491 }
1492
1493 /// Rounds the bottom two corners only.
1494 pub fn rounded_b(mut self, r: f32) -> Self {
1495 self.style = self.style.rounded_b(r);
1496 self
1497 }
1498
1499 /// Rounds the left two corners only.
1500 pub fn rounded_l(mut self, r: f32) -> Self {
1501 self.style = self.style.rounded_l(r);
1502 self
1503 }
1504
1505 /// Rounds the right two corners only.
1506 pub fn rounded_r(mut self, r: f32) -> Self {
1507 self.style = self.style.rounded_r(r);
1508 self
1509 }
1510
1511 /// Sets each corner radius independently (top-left, top-right,
1512 /// bottom-right, bottom-left).
1513 pub fn corners(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
1514 self.style = self.style.corners(tl, tr, br, bl);
1515 self
1516 }
1517
1518 /// Paint-time translation in logical px (never affects layout). Animatable.
1519 pub fn translate(mut self, x: f32, y: f32) -> Self {
1520 self.style = self.style.translate(x, y);
1521 self
1522 }
1523
1524 /// Paint-time rotation in degrees about the element center. Animatable.
1525 pub fn rotate(mut self, degrees: f32) -> Self {
1526 self.style = self.style.rotate(degrees);
1527 self
1528 }
1529
1530 /// Paint-time skew in degrees `(x, y)` about the element center. Animatable.
1531 pub fn skew(mut self, x_degrees: f32, y_degrees: f32) -> Self {
1532 self.style = self.style.skew(x_degrees, y_degrees);
1533 self
1534 }
1535
1536 /// Non-uniform paint-time scale `(x, y)`, composed with the uniform
1537 /// press scale. Never disturbs layout; animatable.
1538 pub fn scale_xy(mut self, x: f32, y: f32) -> Self {
1539 self.style = self.style.scale_xy(x, y);
1540 self
1541 }
1542
1543 /// The pivot for this element's paint-time transforms, as a fraction of
1544 /// its rect (CSS `transform-origin`): `(0, 0)` top-left, `(0.5, 0.5)`
1545 /// center (the default).
1546 pub fn transform_origin(mut self, fx: f32, fy: f32) -> Self {
1547 self.style = self.style.transform_origin(fx, fy);
1548 self
1549 }
1550
1551 /// Continuous-curvature corner smoothing, `0.0..=1.0` (see
1552 /// [`Style::corner_smoothing`](crate::Style::corner_smoothing)). `0.0`
1553 /// (default) keeps exact circular arcs; higher values blend toward a
1554 /// fuller squircle.
1555 pub fn corner_smoothing(mut self, s: f32) -> Self {
1556 self.style = self.style.corner_smoothing(s);
1557 self
1558 }
1559
1560 /// A themed shadow elevation token.
1561 pub fn shadow(mut self, token: ShadowToken) -> Self {
1562 self.style = self.style.shadow(token);
1563 self
1564 }
1565
1566 /// Applies a [`Surface`](crate::Surface) material — the kit ergonomic. The
1567 /// fill, border, radius, shadow, and highlight resolve at theme-resolution
1568 /// time (so it needs no theme in `view()`), replacing the
1569 /// `.rounded(..).shadow(..).themed(|t, s| s.bg(..).border(..))` combo. Call
1570 /// it once as the element's material; chain a `.themed` after it to tweak a
1571 /// single property.
1572 pub fn surface(self, role: crate::surface::Surface) -> Self {
1573 self.themed(move |t, s| {
1574 let mut s = role.bundle().apply(t, s).rounded(role.radius_px(&t.radius));
1575 // Flat elevation drops the shadow on resting cards (border + tone
1576 // carry separation); floating roles always keep theirs.
1577 if t.elevation == crate::Elevation::Flat
1578 && matches!(
1579 role,
1580 crate::surface::Surface::Card | crate::surface::Surface::Raised
1581 )
1582 {
1583 s.shadow_token = None;
1584 }
1585 s
1586 })
1587 }
1588
1589 /// Subtree opacity.
1590 pub fn opacity(mut self, v: f32) -> Self {
1591 self.style = self.style.opacity(v);
1592 self
1593 }
1594
1595 /// Draw progress for path elements (0 = nothing, 1 = full path).
1596 pub fn trim(mut self, v: f32) -> Self {
1597 self.style = self.style.trim(v);
1598 self
1599 }
1600
1601 /// Frosted-glass backdrop blur in logical px: blurs the content *behind*
1602 /// this translucent element (see [`Style::backdrop_blur`](crate::Style::backdrop_blur)).
1603 /// [`Surface::Glass`](crate::Surface::Glass) sets this for you; reach for
1604 /// this builder to tune the radius or frost a custom translucent pane.
1605 pub fn backdrop_blur(mut self, radius: f32) -> Self {
1606 self.style = self.style.backdrop_blur(radius);
1607 self
1608 }
1609
1610 /// A foreground [`ElementFilter`](crate::ElementFilter) on this element's
1611 /// own content (blur / brightness / saturate).
1612 pub fn element_filter(mut self, filter: crate::ElementFilter) -> Self {
1613 self.style = self.style.element_filter(filter);
1614 self
1615 }
1616
1617 /// A luminous specular edge rim — the Liquid Glass perimeter light — on this
1618 /// element. [`Surface::Glass`](crate::Surface::Glass) sets it for you; reach
1619 /// for this builder to put the rim on a custom translucent pane. See
1620 /// [`SpecularEdge`](crate::SpecularEdge) (and [`SpecularEdge::glass`]).
1621 pub fn specular_edge(mut self, edge: crate::style::SpecularEdge) -> Self {
1622 self.style = self.style.specular_edge(edge);
1623 self
1624 }
1625
1626 /// A directional body sheen — the raking glass light — across this element's
1627 /// face. See [`Sheen`](crate::Sheen) (and [`Sheen::glass`]).
1628 pub fn sheen(mut self, sheen: crate::style::Sheen) -> Self {
1629 self.style = self.style.sheen(sheen);
1630 self
1631 }
1632
1633 /// Backdrop-adaptive vibrancy — shift the glass tint's lightness by the mean
1634 /// luminance of the frosted backdrop behind it (headless-only). See
1635 /// [`AdaptiveTint`](crate::AdaptiveTint) (and [`AdaptiveTint::glass`]).
1636 pub fn adaptive_tint(mut self, adaptive: crate::style::AdaptiveTint) -> Self {
1637 self.style = self.style.adaptive_tint(adaptive);
1638 self
1639 }
1640
1641 /// Text size.
1642 pub fn size(mut self, size: TextSize) -> Self {
1643 self.style = self.style.size(size);
1644 self
1645 }
1646
1647 /// Free-form text size in logical px (editorial display sizes).
1648 pub fn size_px(mut self, px: f32) -> Self {
1649 self.style = self.style.size_px(px);
1650 self
1651 }
1652
1653 /// Letter spacing in em (tracked-out eyebrows, small caps).
1654 pub fn tracking(mut self, em: f32) -> Self {
1655 self.style = self.style.tracking(em);
1656 self
1657 }
1658
1659 /// Line height as a multiple of the font size.
1660 pub fn leading(mut self, multiple: f32) -> Self {
1661 self.style = self.style.leading(multiple);
1662 self
1663 }
1664
1665 /// Tabular (fixed-width) numerals (`tnum`) — digits align in columns. For
1666 /// tables, timers, charts, and numeric data.
1667 pub fn tabular(mut self) -> Self {
1668 self.style = self.style.tabular();
1669 self
1670 }
1671
1672 /// Proportional numerals — individually spaced for prose (`pnum`).
1673 pub fn proportional_nums(mut self) -> Self {
1674 self.style = self.style.proportional_nums();
1675 self
1676 }
1677
1678 /// Old-style / text figures (`onum`): ascending and descending digits
1679 /// that sit naturally in serif prose.
1680 pub fn oldstyle_nums(mut self) -> Self {
1681 self.style = self.style.oldstyle_nums();
1682 self
1683 }
1684
1685 /// Lining figures (`lnum`): uniform cap-height digits for data and UI.
1686 pub fn lining_nums(mut self) -> Self {
1687 self.style = self.style.lining_nums();
1688 self
1689 }
1690
1691 /// Render lowercase letters as small capitals (`smcp`).
1692 pub fn small_caps(mut self) -> Self {
1693 self.style = self.style.small_caps();
1694 self
1695 }
1696
1697 /// Enable or disable standard ligatures (`liga`); most fonts default on.
1698 pub fn ligatures(mut self, on: bool) -> Self {
1699 self.style = self.style.ligatures(on);
1700 self
1701 }
1702
1703 /// Common fractions (`frac`): `1/2` becomes a single fraction glyph.
1704 pub fn fractions(mut self) -> Self {
1705 self.style = self.style.fractions();
1706 self
1707 }
1708
1709 /// Font family role (Sans, Mono, or a registered Display/Serif face).
1710 pub fn family(mut self, family: crate::tokens::FamilyRole) -> Self {
1711 self.style = self.style.family(family);
1712 self
1713 }
1714
1715 /// Font weight.
1716 pub fn weight(mut self, weight: Weight) -> Self {
1717 self.style = self.style.weight(weight);
1718 self
1719 }
1720
1721 /// Text color.
1722 pub fn color(mut self, color: Color) -> Self {
1723 self.style = self.style.color(color);
1724 self
1725 }
1726
1727 /// Mono family role.
1728 pub fn mono(mut self) -> Self {
1729 self.style = self.style.mono();
1730 self
1731 }
1732
1733 /// Truncate to one line with an ellipsis.
1734 pub fn truncate(mut self) -> Self {
1735 self.style = self.style.truncate();
1736 self
1737 }
1738
1739 /// Horizontal text alignment.
1740 pub fn text_align(mut self, align: TextAlign) -> Self {
1741 self.style = self.style.text_align(align);
1742 self
1743 }
1744
1745 /// Balance line lengths ([`TextWrap`](crate::TextWrap)`::Balance`) — for
1746 /// headings and titles.
1747 pub fn balance(mut self) -> Self {
1748 self.style = self.style.balance();
1749 self
1750 }
1751
1752 /// Avoid a stranded last word ([`TextWrap`](crate::TextWrap)`::Pretty`) —
1753 /// best-effort for paragraphs.
1754 pub fn pretty(mut self) -> Self {
1755 self.style = self.style.pretty();
1756 self
1757 }
1758
1759 /// Sets the line-breaking mode explicitly ([`TextWrap`](crate::TextWrap)).
1760 pub fn text_wrap(mut self, wrap: TextWrap) -> Self {
1761 self.style = self.style.text_wrap(wrap);
1762 self
1763 }
1764
1765 /// Sets optical sizing explicitly
1766 /// ([`OpticalSizing`](crate::OpticalSizing)) — how a variable font's
1767 /// `opsz` axis is driven.
1768 pub fn optical(mut self, optical: crate::style::OpticalSizing) -> Self {
1769 self.style = self.style.optical(optical);
1770 self
1771 }
1772
1773 /// Tracks the `opsz` axis to the rendered size
1774 /// ([`OpticalSizing::Auto`](crate::OpticalSizing::Auto)) — small text gets
1775 /// the text-optical master, large sizes the display master. A no-op on
1776 /// static faces (the embedded Inter / mono).
1777 pub fn optical_auto(mut self) -> Self {
1778 self.style = self.style.optical_auto();
1779 self
1780 }
1781}
1782
1783/// A plain container box (flex row by default, like taffy).
1784#[track_caller]
1785pub fn div<Msg>() -> Element<Msg> {
1786 Element::new(Kind::Box)
1787}
1788
1789/// A flex row.
1790#[track_caller]
1791pub fn row<Msg>() -> Element<Msg> {
1792 Element::new(Kind::Box)
1793}
1794
1795/// A flex column.
1796#[track_caller]
1797pub fn col<Msg>() -> Element<Msg> {
1798 let mut el = Element::new(Kind::Box);
1799 el.style.direction = crate::style::Direction::Column;
1800 el
1801}
1802
1803/// A z-stack: children occupy the same rect and paint in order.
1804#[track_caller]
1805pub fn stack<Msg>() -> Element<Msg> {
1806 let mut el = Element::new(Kind::Box);
1807 el.style.display = crate::style::Display::Grid;
1808 el.stack = true;
1809 el
1810}
1811
1812/// A paragraph of styled runs: spans wrap together as one layout, and
1813/// each [`span`] may override weight, color, size, family, or italic.
1814#[track_caller]
1815pub fn rich_text<Msg>(spans: impl IntoIterator<Item = Span>) -> Element<Msg> {
1816 Element::new(Kind::Rich(spans.into_iter().collect()))
1817}
1818
1819/// A text run.
1820#[track_caller]
1821pub fn text<Msg>(content: impl Into<String>) -> Element<Msg> {
1822 Element::new(Kind::Text(content.into()))
1823}
1824
1825/// Flexible empty space (flex grow 1).
1826#[track_caller]
1827pub fn spacer<Msg>() -> Element<Msg> {
1828 Element::new(Kind::Box).grow()
1829}
1830
1831/// A horizontal hairline rule in `border_subtle`, 1px tall and full width.
1832/// For a vertical rule, override with `.w(1.0)` and `.h_full()`.
1833#[track_caller]
1834pub fn divider<Msg>() -> Element<Msg> {
1835 Element::new(Kind::Divider).w_full().h(1.0).shrink0()
1836}
1837
1838/// A container that chooses its own layout from its measured size — a
1839/// **container query**, the counterpart of window-size
1840/// [`App::view_at`](crate::App::view_at). `f(available)` receives this
1841/// container's own content size in logical px *from the previous frame* (the
1842/// layout the motion system already records) and returns the element to lay
1843/// out in its place.
1844///
1845/// It is **one frame deferred**: the very first frame has no measured size, so
1846/// it builds at the hint `(0.0, 0.0)` — the "smallest" branch — and converges
1847/// on the next frame; a later resize re-converges one frame after it lands.
1848/// Reach for [`responsive_hinted`] to seed a first-frame size and skip that
1849/// flash.
1850///
1851/// The returned element is **transparent**: it replaces the `responsive(..)`
1852/// wrapper entirely, so style the element you return, not this call. Two rules
1853/// keep the feedback loop well-behaved:
1854/// - **Stable identity.** Give the wrapper a stable [`Element::id`] if its
1855/// position among its siblings can change, so the query keys off a stable
1856/// [`WidgetId`](crate::WidgetId) across frames (a fixed tree position is
1857/// already stable).
1858/// - **Monotone in width, and never self-wrapping.** `f` should not return
1859/// content that shrinks the container back below a threshold a wider size
1860/// crossed (otherwise it can flip every frame at a boundary — prefer
1861/// width-driven breakpoints on a parent-sized container). Nest a finer
1862/// `responsive(..)` as a *child* of the returned element, never as its root:
1863/// a closure that directly returns another `responsive(..)` chains under the
1864/// same id, and is expanded only a bounded number of times before being
1865/// flattened to empty (it degrades, it never overflows the stack).
1866///
1867/// Like a virtualized list, the generated subtree is not part of the *declared*
1868/// tree, so an [`overlay`](Element::overlay) inside it is silently skipped (the
1869/// overlay machinery indexes the declared tree). Mount overlays outside the
1870/// `responsive` wrapper.
1871#[track_caller]
1872pub fn responsive<Msg>(f: impl Fn((f32, f32)) -> Element<Msg> + 'static) -> Element<Msg> {
1873 responsive_hinted((0.0, 0.0), f)
1874}
1875
1876/// [`responsive`] with an explicit first-frame size: `f(hint)` builds the
1877/// initial frame (before any measurement exists), removing the one-frame
1878/// "smallest branch" flash when the rough size is known up front. See
1879/// [`responsive`] for the convergence model and the identity / monotonicity
1880/// caveats.
1881#[track_caller]
1882pub fn responsive_hinted<Msg>(
1883 hint: (f32, f32),
1884 f: impl Fn((f32, f32)) -> Element<Msg> + 'static,
1885) -> Element<Msg> {
1886 let mut el = Element::new(Kind::Box);
1887 el.responsive = Some(ResponsiveData {
1888 hint,
1889 f: std::rc::Rc::new(f),
1890 });
1891 el
1892}
1893
1894/// A bare single-line text input leaf. Most apps want the styled
1895/// `fenestra_kit` `text_input` instead; this is the primitive it wraps.
1896/// Focusable, shows the text I-beam, and emits `on_input` per edit.
1897#[track_caller]
1898pub fn raw_input<Msg>(value: impl Into<String>, placeholder: impl Into<String>) -> Element<Msg> {
1899 Element::new(Kind::Input(InputData {
1900 value: value.into(),
1901 placeholder: placeholder.into(),
1902 multiline: false,
1903 }))
1904 .focusable(true)
1905 .cursor(Cursor::Text)
1906}
1907
1908/// A bare multiline text area leaf: text wraps to the element width, Enter
1909/// inserts a newline, arrows move by line, and the measured height grows
1910/// with the wrapped content (constrain it with `.min_h`/`.max_h` plus an
1911/// outer scroll container). Most apps want the styled `fenestra_kit`
1912/// `text_area` instead; this is the primitive it wraps.
1913#[track_caller]
1914pub fn raw_text_area<Msg>(
1915 value: impl Into<String>,
1916 placeholder: impl Into<String>,
1917) -> Element<Msg> {
1918 Element::new(Kind::Input(InputData {
1919 value: value.into(),
1920 placeholder: placeholder.into(),
1921 multiline: true,
1922 }))
1923 .focusable(true)
1924 .cursor(Cursor::Text)
1925}
1926
1927/// Package already-decoded straight-alpha RGBA8 pixels (row-major, 4 bytes per
1928/// pixel) into a shareable [`ImageData`]: the buffer becomes an atomically
1929/// reference-counted blob, so cloning the result and handing it to
1930/// [`image_from_data`] shares the one allocation instead of re-decoding or
1931/// copying — the reuse path a decoded-image cache wants. If `pixels` holds
1932/// fewer than `width * height` complete rows, the payload shrinks to the rows
1933/// provided (its `height` reflects that), matching [`image_rgba8`].
1934#[must_use]
1935pub fn image_payload(width: u32, height: u32, mut pixels: Vec<u8>) -> ImageData {
1936 let row = width as usize * 4;
1937 let rows = pixels
1938 .len()
1939 .checked_div(row)
1940 .unwrap_or(0)
1941 .min(height as usize);
1942 pixels.truncate(row * rows);
1943 #[expect(clippy::cast_possible_truncation, reason = "rows <= height: u32")]
1944 let height = rows as u32;
1945 ImageData {
1946 image: peniko::ImageData {
1947 data: pixels.into(),
1948 format: peniko::ImageFormat::Rgba8,
1949 alpha_type: peniko::ImageAlphaType::Alpha,
1950 width,
1951 height,
1952 },
1953 }
1954}
1955
1956/// An image leaf rendering an already-decoded [`ImageData`] payload (from
1957/// [`image_payload`]), sized to it. Building many elements or frames from
1958/// clones of one payload shares a single reference-counted pixel blob, so a
1959/// decoded-image cache never re-decodes or re-copies. See [`image_rgba8`] for
1960/// the owned-`Vec` convenience wrapper.
1961#[must_use]
1962pub fn image_from_data<Msg>(data: ImageData) -> Element<Msg> {
1963 let (width, height) = (data.image.width, data.image.height);
1964 #[expect(clippy::cast_precision_loss, reason = "image sizes fit in f32")]
1965 Element::new(Kind::Image(data))
1966 .w(width as f32)
1967 .h(height as f32)
1968 .shrink0()
1969}
1970
1971/// An image leaf showing straight-alpha RGBA8 pixels (row-major, 4 bytes
1972/// per pixel). Sized to the image by default, stretched when styled
1973/// otherwise, and painted clipped to the corner radius — so
1974/// `.rounded_full()` crops a square source into a round avatar. If `pixels`
1975/// holds fewer than `width * height` complete rows, the element shrinks to
1976/// the rows actually provided instead of panicking.
1977#[track_caller]
1978pub fn image_rgba8<Msg>(width: u32, height: u32, pixels: Vec<u8>) -> Element<Msg> {
1979 image_from_data(image_payload(width, height, pixels))
1980}
1981
1982/// A vector path drawn in `viewbox` coordinates and scaled to the element
1983/// rect (sized to the viewbox by default). `stroke` is a width in viewbox
1984/// units; `None` fills instead. Painted in the resolved text color.
1985#[track_caller]
1986pub fn path<Msg>(bez: kurbo::BezPath, viewbox: (f64, f64), stroke: Option<f64>) -> Element<Msg> {
1987 #[expect(clippy::cast_possible_truncation, reason = "viewbox sizes are small")]
1988 Element::new(Kind::Path(PathData {
1989 path: std::sync::Arc::new(bez),
1990 viewbox,
1991 stroke,
1992 optical: OpticalCorrection::default(),
1993 }))
1994 .w(viewbox.0 as f32)
1995 .h(viewbox.1 as f32)
1996 .shrink0()
1997}
1998
1999impl<Msg: 'static> Element<Msg> {
2000 /// Converts every message this subtree can emit with `f`, so a
2001 /// component written around its own message type drops into any parent:
2002 /// the Elm composition tool.
2003 ///
2004 /// ```
2005 /// use fenestra_core::{Element, div, text};
2006 ///
2007 /// #[derive(Clone)]
2008 /// enum CardMsg { Open }
2009 /// #[derive(Clone)]
2010 /// enum AppMsg { Card(usize, CardMsg) }
2011 ///
2012 /// fn card() -> Element<CardMsg> {
2013 /// div().on_click(CardMsg::Open).child(text("open"))
2014 /// }
2015 ///
2016 /// let el: Element<AppMsg> = card().map(|m| AppMsg::Card(0, m));
2017 /// ```
2018 pub fn map<B: 'static>(self, f: impl Fn(Msg) -> B + Clone + 'static) -> Element<B> {
2019 Element {
2020 kind: self.kind,
2021 style: self.style,
2022 children: self
2023 .children
2024 .into_iter()
2025 .map(|c| c.map(f.clone()))
2026 .collect(),
2027 key: self.key,
2028 source: self.source,
2029 stack: self.stack,
2030 focusable: self.focusable,
2031 autofocus: self.autofocus,
2032 stick_bottom: self.stick_bottom,
2033 cursor: self.cursor,
2034 disabled: self.disabled,
2035 on_click: self.on_click.map(&f),
2036 on_double_click: self.on_double_click.map(&f),
2037 on_right_click: self.on_right_click.map(&f),
2038 on_hover: self.on_hover.map(&f),
2039 on_key: self.on_key.map(|k| {
2040 let f = f.clone();
2041 Box::new(move |key: &KeyInput| k(key).map(&f)) as KeyFn<B>
2042 }),
2043 on_type_ahead: self.on_type_ahead.map(|k| {
2044 let f = f.clone();
2045 Box::new(move |buffer: &str| k(buffer).map(&f)) as TypeAheadFn<B>
2046 }),
2047 on_drag: self.on_drag.map(|d| {
2048 let f = f.clone();
2049 Box::new(move |x: f32, y: f32| d(x, y).map(&f)) as DragFn<B>
2050 }),
2051 on_drag_end: self.on_drag_end.map(&f),
2052 on_swipe: self.on_swipe.map(|s| {
2053 let f = f.clone();
2054 Box::new(move |d: SwipeDir| f(s(d))) as SwipeFn<B>
2055 }),
2056 on_input: self.on_input.map(|i| {
2057 let f = f.clone();
2058 Box::new(move |s: &str| f(i(s))) as InputFn<B>
2059 }),
2060 on_close: self.on_close.map(&f),
2061 on_file_drop: self.on_file_drop.map(|d| {
2062 let f = f.clone();
2063 Box::new(move |p: &std::path::Path| f(d(p))) as FileDropFn<B>
2064 }),
2065 drag_source: self.drag_source,
2066 on_drop: self.on_drop.map(|d| {
2067 let f = f.clone();
2068 Box::new(move |payload: &str| d(payload).map(&f)) as DropFn<B>
2069 }),
2070 overlay: self.overlay,
2071 spin: self.spin,
2072 keyframes: self.keyframes,
2073 virtual_rows: self.virtual_rows.map(|v| {
2074 let f = f.clone();
2075 let builder = v.builder;
2076 VirtualData {
2077 count: v.count,
2078 row_height: v.row_height,
2079 variable: v.variable,
2080 builder: std::rc::Rc::new(move |i| builder(i).map(f.clone())),
2081 }
2082 }),
2083 responsive: self.responsive.map(|r| {
2084 let f = f.clone();
2085 let inner = r.f;
2086 ResponsiveData {
2087 hint: r.hint,
2088 f: std::rc::Rc::new(move |sz| inner(sz).map(f.clone())),
2089 }
2090 }),
2091 semantics: self.semantics,
2092 access_value: self.access_value,
2093 live: self.live,
2094 selectable: self.selectable,
2095 enter: self.enter,
2096 exit: self.exit,
2097 animate_layout: self.animate_layout,
2098 label: self.label,
2099 themed: self.themed,
2100 hover_style: self.hover_style,
2101 active_style: self.active_style,
2102 focus_style: self.focus_style,
2103 state_layer: self.state_layer,
2104 press_scale: self.press_scale,
2105 invalid: self.invalid,
2106 transition: self.transition,
2107 }
2108 }
2109}
2110
2111impl<Msg> Element<Msg> {
2112 /// Grid template columns (switches display to grid). Accepts plain
2113 /// [`Track`](crate::style::Track)s or full
2114 /// [`GridTemplate`](crate::style::GridTemplate) entries (e.g. `repeat(...)`).
2115 pub fn grid_cols<T: Into<crate::style::GridTemplate>>(
2116 mut self,
2117 tracks: impl IntoIterator<Item = T>,
2118 ) -> Self {
2119 self.style = self.style.grid_cols(tracks);
2120 self
2121 }
2122
2123 /// Grid template rows (switches display to grid). Accepts plain
2124 /// [`Track`](crate::style::Track)s or full
2125 /// [`GridTemplate`](crate::style::GridTemplate) entries (e.g. `repeat(...)`).
2126 pub fn grid_rows<T: Into<crate::style::GridTemplate>>(
2127 mut self,
2128 tracks: impl IntoIterator<Item = T>,
2129 ) -> Self {
2130 self.style = self.style.grid_rows(tracks);
2131 self
2132 }
2133
2134 /// Places this element at a 1-based grid column, spanning `span` tracks.
2135 pub fn grid_col(mut self, start: i16, span: u16) -> Self {
2136 self.style = self.style.grid_col(start, span);
2137 self
2138 }
2139
2140 /// Places this element at a 1-based grid row, spanning `span` tracks.
2141 pub fn grid_row(mut self, start: i16, span: u16) -> Self {
2142 self.style = self.style.grid_row(start, span);
2143 self
2144 }
2145
2146 /// `grid-template-areas` (CSS): each row is a string of whitespace-separated
2147 /// area names, `.` for an empty cell. Place children with
2148 /// [`Element::grid_area`]. Implies a grid of `auto` tracks matching the area
2149 /// shape when no explicit tracks are given.
2150 pub fn grid_template_areas<R: AsRef<str>>(mut self, rows: impl IntoIterator<Item = R>) -> Self {
2151 self.style = self.style.grid_template_areas(rows);
2152 self
2153 }
2154
2155 /// Places this element in a named grid area (CSS `grid-area`).
2156 pub fn grid_area(mut self, name: impl Into<String>) -> Self {
2157 self.style = self.style.grid_area(name);
2158 self
2159 }
2160
2161 /// Places this element's columns between two named grid lines
2162 /// (CSS `grid-column: start / end`).
2163 pub fn grid_col_lines(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
2164 self.style = self.style.grid_col_lines(start, end);
2165 self
2166 }
2167
2168 /// Places this element's rows between two named grid lines
2169 /// (CSS `grid-row: start / end`).
2170 pub fn grid_row_lines(mut self, start: impl Into<String>, end: impl Into<String>) -> Self {
2171 self.style = self.style.grid_row_lines(start, end);
2172 self
2173 }
2174
2175 /// Names the column grid lines positionally: the i-th name labels the
2176 /// (i+1)-th line. Reference them from [`Element::grid_col_lines`].
2177 pub fn grid_col_names<S: Into<String>>(mut self, names: impl IntoIterator<Item = S>) -> Self {
2178 self.style = self.style.grid_col_names(names);
2179 self
2180 }
2181
2182 /// Names the row grid lines positionally: the i-th name labels the (i+1)-th
2183 /// line. Reference them from [`Element::grid_row_lines`].
2184 pub fn grid_row_names<S: Into<String>>(mut self, names: impl IntoIterator<Item = S>) -> Self {
2185 self.style = self.style.grid_row_names(names);
2186 self
2187 }
2188}
2189
2190#[cfg(test)]
2191mod surface_tests {
2192 use super::div;
2193 use crate::surface::Surface;
2194
2195 #[test]
2196 fn element_surface_defers_to_resolution() {
2197 // The fill is installed by the deferred `themed`, not the base style,
2198 // so `.surface(..)` needs no theme at build time.
2199 let el = div::<()>().surface(Surface::Card);
2200 assert!(el.style().fill.is_none());
2201 }
2202}