Skip to main content

fret_ui_kit/
ui.rs

1use std::hash::Hash;
2use std::marker::PhantomData;
3use std::panic::Location;
4use std::sync::Arc;
5
6pub use crate::children;
7
8use smallvec::SmallVec;
9
10use fret_core::{
11    AttributedText, Axis, Edges, EffectChain, EffectMode, FontId, FontWeight, Px, TextAlign,
12    TextOverflow, TextSpan, TextStyle, TextWrap,
13};
14use fret_ui::element::{
15    AnyElement, ContainerProps, EffectLayerProps, Elements, FlexProps, HoverRegionProps,
16    InsetStyle, LayoutStyle, Length, Overflow, PositionStyle, ScrollAxis, ScrollProps,
17    ScrollbarAxis, ScrollbarProps, ScrollbarStyle, SelectableTextProps, SizeStyle, StackProps,
18    StyledTextProps, TextProps,
19};
20use fret_ui::scroll::ScrollHandle;
21use fret_ui::{ElementContext, ElementContextAccess, Theme, UiHost};
22
23use crate::declarative::style as decl_style;
24use crate::declarative::text as decl_text;
25use crate::{
26    ChromeRefinement, IntoUiElement, Items, Justify, LayoutRefinement, LengthRefinement, MetricRef,
27    Space, UiBuilder, UiPatch, UiPatchTarget, UiSupportsChrome, UiSupportsLayout,
28};
29
30fn collect_ui_children<'a, H: UiHost + 'a, Cx, I>(cx: &mut Cx, iter: I) -> SmallVec<[AnyElement; 8]>
31where
32    Cx: ElementContextAccess<'a, H>,
33    I: IntoIterator,
34    I::Item: IntoUiElement<H>,
35{
36    let mut out: SmallVec<[AnyElement; 8]> = SmallVec::new();
37    for child in iter {
38        out.push(crate::land_child(cx, child));
39    }
40    out
41}
42
43fn flex_root_needs_fill_height(direction: Axis, layout: &LayoutRefinement) -> bool {
44    fn metric_ref_is_zero(metric: &crate::MetricRef) -> bool {
45        match metric {
46            crate::MetricRef::Px(px) => px.0.abs() <= f32::EPSILON,
47            crate::MetricRef::Token { key, fallback } => {
48                *key == crate::Space::N0.token_key()
49                    || matches!(fallback, crate::style::MetricFallback::Px(px) if px.0.abs() <= f32::EPSILON)
50            }
51        }
52    }
53
54    fn min_max_height_requests_fill(length: &crate::LengthRefinement) -> bool {
55        match length {
56            crate::LengthRefinement::Auto => false,
57            crate::LengthRefinement::Fill | crate::LengthRefinement::Fraction(_) => true,
58            // `min_h_0()` / `max_h_0()` are escape hatches for shrink/clamp behavior; they should
59            // not turn an otherwise auto-height stack into a fill-height flex root.
60            crate::LengthRefinement::Px(metric) => !metric_ref_is_zero(metric),
61        }
62    }
63
64    let has_height_constraint = layout.size.as_ref().is_some_and(|size| {
65        matches!(
66            size.height,
67            Some(LengthRefinement::Px(_) | LengthRefinement::Fraction(_) | LengthRefinement::Fill)
68        ) || size
69            .min_height
70            .as_ref()
71            .is_some_and(min_max_height_requests_fill)
72            || size
73                .max_height
74                .as_ref()
75                .is_some_and(min_max_height_requests_fill)
76    });
77    if has_height_constraint {
78        return true;
79    }
80
81    matches!(direction, Axis::Vertical)
82        && layout.flex_item.as_ref().is_some_and(|flex| {
83            flex.grow.is_some_and(|grow| grow > 0.0)
84                || matches!(
85                    flex.basis,
86                    Some(
87                        LengthRefinement::Px(_)
88                            | LengthRefinement::Fraction(_)
89                            | LengthRefinement::Fill
90                    )
91                )
92        })
93}
94
95fn apply_inner_flex_root_width_constraints(
96    theme: &Theme,
97    layout: &LayoutRefinement,
98    force_width_fill: bool,
99    flex_props: &mut FlexProps,
100) {
101    let resolved_layout = decl_style::layout_style(theme, layout.clone());
102    let size = layout.size.as_ref();
103
104    if size.and_then(|size| size.width.as_ref()).is_some() {
105        flex_props.layout.size.width = resolved_layout.size.width;
106    } else if force_width_fill {
107        flex_props.layout.size.width = Length::Fill;
108    }
109
110    if size.and_then(|size| size.min_width.as_ref()).is_some() {
111        flex_props.layout.size.min_width = resolved_layout.size.min_width;
112    }
113
114    if size.and_then(|size| size.max_width.as_ref()).is_some() {
115        flex_props.layout.size.max_width = resolved_layout.size.max_width;
116    }
117}
118
119/// Late-lands a single typed child into `Ui` / `Elements`.
120///
121/// This is the narrow default-path helper for render roots or wrapper closures that only need to
122/// return one already-typed child without spelling `ui::children![cx; child].into()`.
123#[track_caller]
124pub fn single<'a, H: UiHost + 'a, Cx, T>(cx: &mut Cx, child: T) -> Elements
125where
126    Cx: ElementContextAccess<'a, H>,
127    T: IntoUiElement<H>,
128{
129    Elements::from(crate::land_child(cx, child))
130}
131
132/// Extension helpers for `*_build` child sinks.
133///
134/// These helpers make builder-first layout code read more like direct composition without falling
135/// back to `ui::children!` only to convert a heterogeneous list into `AnyElement` values.
136pub trait UiElementSinkExt {
137    fn push_ui<'a, H: UiHost + 'a, Cx, T>(&mut self, cx: &mut Cx, child: T)
138    where
139        Cx: ElementContextAccess<'a, H>,
140        T: IntoUiElement<H>;
141
142    fn extend_ui<'a, H: UiHost + 'a, Cx, I>(&mut self, cx: &mut Cx, children: I)
143    where
144        Cx: ElementContextAccess<'a, H>,
145        I: IntoIterator,
146        I::Item: IntoUiElement<H>;
147}
148
149impl UiElementSinkExt for Vec<AnyElement> {
150    fn push_ui<'a, H: UiHost + 'a, Cx, T>(&mut self, cx: &mut Cx, child: T)
151    where
152        Cx: ElementContextAccess<'a, H>,
153        T: IntoUiElement<H>,
154    {
155        self.push(crate::land_child(cx, child));
156    }
157
158    fn extend_ui<'a, H: UiHost + 'a, Cx, I>(&mut self, cx: &mut Cx, children: I)
159    where
160        Cx: ElementContextAccess<'a, H>,
161        I: IntoIterator,
162        I::Item: IntoUiElement<H>,
163    {
164        for child in children {
165            self.push_ui(cx, child);
166        }
167    }
168}
169
170fn resolve_text_align_for_direction(
171    align: TextAlign,
172    direction: crate::primitives::direction::LayoutDirection,
173) -> TextAlign {
174    use crate::primitives::direction::LayoutDirection;
175
176    match (align, direction) {
177        (TextAlign::Start, LayoutDirection::Rtl) => TextAlign::End,
178        (TextAlign::End, LayoutDirection::Rtl) => TextAlign::Start,
179        _ => align,
180    }
181}
182
183/// A patchable flex layout constructor for authoring ergonomics.
184///
185/// This is an ecosystem-only helper intended to reduce runtime-props boilerplate in layout-only
186/// code while keeping layering rules intact (no policy in `crates/fret-ui`).
187#[derive(Debug, Clone)]
188pub struct FlexBox<H, F> {
189    pub(crate) chrome: ChromeRefinement,
190    pub(crate) layout: LayoutRefinement,
191    pub(crate) direction: Axis,
192    pub(crate) force_width_fill: bool,
193    pub(crate) gap: MetricRef,
194    pub(crate) gap_length: Option<LengthRefinement>,
195    pub(crate) justify: Justify,
196    pub(crate) items: Items,
197    pub(crate) wrap: bool,
198    pub(crate) children: Option<F>,
199    pub(crate) _phantom: PhantomData<fn() -> H>,
200}
201
202/// Variant of [`FlexBox`] that collects children into a sink to avoid iterator borrow pitfalls.
203#[derive(Debug)]
204pub struct FlexBoxBuild<H, B> {
205    pub(crate) chrome: ChromeRefinement,
206    pub(crate) layout: LayoutRefinement,
207    pub(crate) direction: Axis,
208    pub(crate) force_width_fill: bool,
209    pub(crate) gap: MetricRef,
210    pub(crate) gap_length: Option<LengthRefinement>,
211    pub(crate) justify: Justify,
212    pub(crate) items: Items,
213    pub(crate) wrap: bool,
214    pub(crate) build: Option<B>,
215    pub(crate) _phantom: PhantomData<fn() -> H>,
216}
217
218impl<H, F> FlexBox<H, F> {
219    pub fn new(direction: Axis, children: F) -> Self {
220        let items = match direction {
221            Axis::Horizontal => Items::Center,
222            Axis::Vertical => Items::Stretch,
223        };
224        Self {
225            chrome: ChromeRefinement::default(),
226            layout: LayoutRefinement::default(),
227            direction,
228            force_width_fill: true,
229            gap: MetricRef::space(Space::N0),
230            gap_length: None,
231            justify: Justify::Start,
232            items,
233            wrap: false,
234            children: Some(children),
235            _phantom: PhantomData,
236        }
237    }
238}
239
240impl<H, B> FlexBoxBuild<H, B> {
241    pub fn new(direction: Axis, build: B) -> Self {
242        let items = match direction {
243            Axis::Horizontal => Items::Center,
244            Axis::Vertical => Items::Stretch,
245        };
246        Self {
247            chrome: ChromeRefinement::default(),
248            layout: LayoutRefinement::default(),
249            direction,
250            force_width_fill: true,
251            gap: MetricRef::space(Space::N0),
252            gap_length: None,
253            justify: Justify::Start,
254            items,
255            wrap: false,
256            build: Some(build),
257            _phantom: PhantomData,
258        }
259    }
260}
261
262impl<H, F> UiPatchTarget for FlexBox<H, F> {
263    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
264        self.chrome = self.chrome.merge(patch.chrome);
265        self.layout = self.layout.merge(patch.layout);
266        self
267    }
268}
269
270impl<H, F> UiSupportsChrome for FlexBox<H, F> {}
271impl<H, F> UiSupportsLayout for FlexBox<H, F> {}
272
273impl<H, B> UiPatchTarget for FlexBoxBuild<H, B> {
274    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
275        self.chrome = self.chrome.merge(patch.chrome);
276        self.layout = self.layout.merge(patch.layout);
277        self
278    }
279}
280
281impl<H, B> UiSupportsChrome for FlexBoxBuild<H, B> {}
282impl<H, B> UiSupportsLayout for FlexBoxBuild<H, B> {}
283
284impl<H: UiHost, F, I> FlexBox<H, F>
285where
286    F: FnOnce(&mut ElementContext<'_, H>) -> I,
287    I: IntoIterator,
288    I::Item: IntoUiElement<H>,
289{
290    #[track_caller]
291    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
292        let theme = Theme::global(&*cx.app);
293        let needs_fill_height = flex_root_needs_fill_height(self.direction, &self.layout);
294        let layout = self.layout;
295        let container = decl_style::container_props(theme, self.chrome, layout.clone());
296
297        let gap = self.gap_length.as_ref().and_then(|l| match l {
298            LengthRefinement::Auto => None,
299            LengthRefinement::Px(m) => Some(fret_ui::element::SpacingLength::Px(m.resolve(theme))),
300            LengthRefinement::Fraction(f) => Some(fret_ui::element::SpacingLength::Fraction(*f)),
301            LengthRefinement::Fill => Some(fret_ui::element::SpacingLength::Fill),
302        });
303        let gap =
304            gap.unwrap_or_else(|| fret_ui::element::SpacingLength::Px(self.gap.resolve(theme)));
305        let mut flex_props = FlexProps {
306            direction: self.direction,
307            gap,
308            padding: Edges::all(Px(0.0)).into(),
309            justify: self.justify.to_main_align(),
310            align: self.items.to_cross_align(),
311            wrap: self.wrap,
312            ..Default::default()
313        };
314        apply_inner_flex_root_width_constraints(
315            theme,
316            &layout,
317            self.force_width_fill,
318            &mut flex_props,
319        );
320        if needs_fill_height {
321            flex_props.layout.size.height = Length::Fill;
322        }
323
324        let children = self.children.expect("expected flex children closure");
325        cx.container(container, move |cx| {
326            vec![cx.flex(flex_props, move |cx| {
327                let children = children(cx);
328                collect_ui_children(cx, children)
329            })]
330        })
331    }
332}
333
334impl<H: UiHost, B> FlexBoxBuild<H, B>
335where
336    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
337{
338    #[track_caller]
339    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
340        let theme = Theme::global(&*cx.app);
341        let needs_fill_height = flex_root_needs_fill_height(self.direction, &self.layout);
342        let layout = self.layout;
343        let container = decl_style::container_props(theme, self.chrome, layout.clone());
344
345        let gap = self.gap_length.as_ref().and_then(|l| match l {
346            LengthRefinement::Auto => None,
347            LengthRefinement::Px(m) => Some(fret_ui::element::SpacingLength::Px(m.resolve(theme))),
348            LengthRefinement::Fraction(f) => Some(fret_ui::element::SpacingLength::Fraction(*f)),
349            LengthRefinement::Fill => Some(fret_ui::element::SpacingLength::Fill),
350        });
351        let gap =
352            gap.unwrap_or_else(|| fret_ui::element::SpacingLength::Px(self.gap.resolve(theme)));
353        let mut flex_props = FlexProps {
354            direction: self.direction,
355            gap,
356            padding: Edges::all(Px(0.0)).into(),
357            justify: self.justify.to_main_align(),
358            align: self.items.to_cross_align(),
359            wrap: self.wrap,
360            ..Default::default()
361        };
362        apply_inner_flex_root_width_constraints(
363            theme,
364            &layout,
365            self.force_width_fill,
366            &mut flex_props,
367        );
368        if needs_fill_height {
369            flex_props.layout.size.height = Length::Fill;
370        }
371
372        let build = self.build.expect("expected flex build closure");
373        cx.container(container, move |cx| {
374            vec![cx.flex(flex_props, move |cx| {
375                let mut out = Vec::new();
376                build(cx, &mut out);
377                out
378            })]
379        })
380    }
381}
382
383impl<H: UiHost, F, I> IntoUiElement<H> for FlexBox<H, F>
384where
385    F: FnOnce(&mut ElementContext<'_, H>) -> I,
386    I: IntoIterator,
387    I::Item: IntoUiElement<H>,
388{
389    #[track_caller]
390    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
391        FlexBox::<H, F>::into_element(self, cx)
392    }
393}
394
395impl<H: UiHost, B> IntoUiElement<H> for FlexBoxBuild<H, B>
396where
397    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
398{
399    #[track_caller]
400    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
401        FlexBoxBuild::<H, B>::into_element(self, cx)
402    }
403}
404
405/// Returns a patchable horizontal flex layout builder.
406///
407/// Usage:
408/// - `ui::h_flex(|cx| vec![...]).gap(Space::N2).px_2().into_element(cx)`
409pub fn h_flex<H: UiHost, F, I>(children: F) -> UiBuilder<FlexBox<H, F>>
410where
411    F: FnOnce(&mut ElementContext<'_, H>) -> I,
412    I: IntoIterator,
413    I::Item: IntoUiElement<H>,
414{
415    UiBuilder::new(FlexBox::new(Axis::Horizontal, children))
416}
417
418/// Returns a patchable horizontal flex layout builder that does **not** force `width: fill`.
419///
420/// Use this when you want the row to shrink-wrap its contents (or to avoid inflating child hit
421/// boxes due to a fill-width flex root).
422pub fn h_row<H: UiHost, F, I>(children: F) -> UiBuilder<FlexBox<H, F>>
423where
424    F: FnOnce(&mut ElementContext<'_, H>) -> I,
425    I: IntoIterator,
426    I::Item: IntoUiElement<H>,
427{
428    let mut flex = FlexBox::new(Axis::Horizontal, children);
429    flex.force_width_fill = false;
430    UiBuilder::new(flex)
431}
432
433/// Variant of [`h_flex`] that avoids iterator borrow pitfalls by collecting into a sink.
434///
435/// Use this when the natural authoring form is an iterator that captures `&mut cx` (e.g.
436/// `items.iter().map(|it| cx.keyed(...))`), which cannot be returned directly.
437pub fn h_flex_build<H: UiHost, B>(build: B) -> UiBuilder<FlexBoxBuild<H, B>>
438where
439    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
440{
441    UiBuilder::new(FlexBoxBuild::new(Axis::Horizontal, build))
442}
443
444/// Variant of [`h_row`] that avoids iterator borrow pitfalls by collecting into a sink.
445pub fn h_row_build<H: UiHost, B>(build: B) -> UiBuilder<FlexBoxBuild<H, B>>
446where
447    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
448{
449    let mut flex = FlexBoxBuild::new(Axis::Horizontal, build);
450    flex.force_width_fill = false;
451    UiBuilder::new(flex)
452}
453
454/// Returns a patchable vertical flex layout builder.
455pub fn v_flex<H: UiHost, F, I>(children: F) -> UiBuilder<FlexBox<H, F>>
456where
457    F: FnOnce(&mut ElementContext<'_, H>) -> I,
458    I: IntoIterator,
459    I::Item: IntoUiElement<H>,
460{
461    UiBuilder::new(FlexBox::new(Axis::Vertical, children))
462}
463
464/// Returns a patchable vertical flex layout builder that does **not** force `width: fill`.
465///
466/// Use this when you want the column to shrink-wrap its contents (or to avoid inflating child hit
467/// boxes due to a fill-width flex root).
468pub fn v_stack<H: UiHost, F, I>(children: F) -> UiBuilder<FlexBox<H, F>>
469where
470    F: FnOnce(&mut ElementContext<'_, H>) -> I,
471    I: IntoIterator,
472    I::Item: IntoUiElement<H>,
473{
474    let mut flex = FlexBox::new(Axis::Vertical, children);
475    flex.force_width_fill = false;
476    UiBuilder::new(flex)
477}
478
479/// Variant of [`v_flex`] that avoids iterator borrow pitfalls by collecting into a sink.
480///
481/// Use this when the natural authoring form is an iterator that captures `&mut cx` (e.g.
482/// `items.iter().map(|it| cx.keyed(...))`), which cannot be returned directly.
483pub fn v_flex_build<H: UiHost, B>(build: B) -> UiBuilder<FlexBoxBuild<H, B>>
484where
485    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
486{
487    UiBuilder::new(FlexBoxBuild::new(Axis::Vertical, build))
488}
489
490/// Variant of [`v_stack`] that avoids iterator borrow pitfalls by collecting into a sink.
491pub fn v_stack_build<H: UiHost, B>(build: B) -> UiBuilder<FlexBoxBuild<H, B>>
492where
493    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
494{
495    let mut flex = FlexBoxBuild::new(Axis::Vertical, build);
496    flex.force_width_fill = false;
497    UiBuilder::new(flex)
498}
499
500/// A patchable container constructor for authoring ergonomics.
501///
502/// This is intended to be the default “box” layout node in the fluent authoring surface.
503#[derive(Debug, Clone)]
504pub struct ContainerBox<H, F> {
505    pub(crate) chrome: ChromeRefinement,
506    pub(crate) layout: LayoutRefinement,
507    pub(crate) children: Option<F>,
508    pub(crate) _phantom: PhantomData<fn() -> H>,
509}
510
511/// Variant of [`ContainerBox`] that collects children into a sink to avoid iterator borrow pitfalls.
512#[derive(Debug)]
513pub struct ContainerBoxBuild<H, B> {
514    pub(crate) chrome: ChromeRefinement,
515    pub(crate) layout: LayoutRefinement,
516    pub(crate) build: Option<B>,
517    pub(crate) _phantom: PhantomData<fn() -> H>,
518}
519
520/// A raw-container variant that preserves caller-provided [`ContainerProps`] while still allowing
521/// builder-first child authoring to land at the last possible moment.
522#[derive(Debug, Clone)]
523pub struct ContainerPropsBox<H, F> {
524    pub(crate) props: ContainerProps,
525    pub(crate) children: Option<F>,
526    pub(crate) _phantom: PhantomData<fn() -> H>,
527}
528
529/// Sink-based variant of [`ContainerPropsBox`] for iterator-heavy or borrow-sensitive child flows.
530#[derive(Debug)]
531pub struct ContainerPropsBoxBuild<H, B> {
532    pub(crate) props: ContainerProps,
533    pub(crate) build: Option<B>,
534    pub(crate) _phantom: PhantomData<fn() -> H>,
535}
536
537impl<H, F> ContainerBox<H, F> {
538    pub fn new(children: F) -> Self {
539        Self {
540            chrome: ChromeRefinement::default(),
541            layout: LayoutRefinement::default(),
542            children: Some(children),
543            _phantom: PhantomData,
544        }
545    }
546}
547
548impl<H, B> ContainerBoxBuild<H, B> {
549    pub fn new(build: B) -> Self {
550        Self {
551            chrome: ChromeRefinement::default(),
552            layout: LayoutRefinement::default(),
553            build: Some(build),
554            _phantom: PhantomData,
555        }
556    }
557}
558
559impl<H, F> ContainerPropsBox<H, F> {
560    pub fn new(props: ContainerProps, children: F) -> Self {
561        Self {
562            props,
563            children: Some(children),
564            _phantom: PhantomData,
565        }
566    }
567}
568
569impl<H, B> ContainerPropsBoxBuild<H, B> {
570    pub fn new(props: ContainerProps, build: B) -> Self {
571        Self {
572            props,
573            build: Some(build),
574            _phantom: PhantomData,
575        }
576    }
577}
578
579impl<H, F> UiPatchTarget for ContainerBox<H, F> {
580    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
581        self.chrome = self.chrome.merge(patch.chrome);
582        self.layout = self.layout.merge(patch.layout);
583        self
584    }
585}
586
587impl<H, F> UiSupportsChrome for ContainerBox<H, F> {}
588impl<H, F> UiSupportsLayout for ContainerBox<H, F> {}
589
590impl<H, B> UiPatchTarget for ContainerBoxBuild<H, B> {
591    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
592        self.chrome = self.chrome.merge(patch.chrome);
593        self.layout = self.layout.merge(patch.layout);
594        self
595    }
596}
597
598impl<H, B> UiSupportsChrome for ContainerBoxBuild<H, B> {}
599impl<H, B> UiSupportsLayout for ContainerBoxBuild<H, B> {}
600
601impl<H: UiHost, F, I> ContainerBox<H, F>
602where
603    F: FnOnce(&mut ElementContext<'_, H>) -> I,
604    I: IntoIterator,
605    I::Item: IntoUiElement<H>,
606{
607    #[track_caller]
608    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
609        let theme = Theme::global(&*cx.app);
610        let container = decl_style::container_props(theme, self.chrome, self.layout);
611        let children = self.children.expect("expected container children closure");
612        cx.container(container, move |cx| {
613            let children = children(cx);
614            collect_ui_children(cx, children)
615        })
616    }
617}
618
619impl<H: UiHost, B> ContainerBoxBuild<H, B>
620where
621    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
622{
623    #[track_caller]
624    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
625        let theme = Theme::global(&*cx.app);
626        let container = decl_style::container_props(theme, self.chrome, self.layout);
627        let build = self.build.expect("expected container build closure");
628        cx.container(container, move |cx| {
629            let mut out = Vec::new();
630            build(cx, &mut out);
631            out
632        })
633    }
634}
635
636impl<H: UiHost, F, I> ContainerPropsBox<H, F>
637where
638    F: FnOnce(&mut ElementContext<'_, H>) -> I,
639    I: IntoIterator,
640    I::Item: IntoUiElement<H>,
641{
642    #[track_caller]
643    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
644        let props = self.props;
645        let children = self
646            .children
647            .expect("expected container-props children closure");
648        cx.container(props, move |cx| {
649            let children = children(cx);
650            collect_ui_children(cx, children)
651        })
652    }
653}
654
655impl<H: UiHost, B> ContainerPropsBoxBuild<H, B>
656where
657    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
658{
659    #[track_caller]
660    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
661        let props = self.props;
662        let build = self.build.expect("expected container-props build closure");
663        cx.container(props, move |cx| {
664            let mut out = Vec::new();
665            build(cx, &mut out);
666            out
667        })
668    }
669}
670
671impl<H: UiHost, F, I> IntoUiElement<H> for ContainerBox<H, F>
672where
673    F: FnOnce(&mut ElementContext<'_, H>) -> I,
674    I: IntoIterator,
675    I::Item: IntoUiElement<H>,
676{
677    #[track_caller]
678    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
679        ContainerBox::<H, F>::into_element(self, cx)
680    }
681}
682
683impl<H: UiHost, B> IntoUiElement<H> for ContainerBoxBuild<H, B>
684where
685    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
686{
687    #[track_caller]
688    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
689        ContainerBoxBuild::<H, B>::into_element(self, cx)
690    }
691}
692
693impl<H: UiHost, F, I> IntoUiElement<H> for ContainerPropsBox<H, F>
694where
695    F: FnOnce(&mut ElementContext<'_, H>) -> I,
696    I: IntoIterator,
697    I::Item: IntoUiElement<H>,
698{
699    #[track_caller]
700    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
701        ContainerPropsBox::<H, F>::into_element(self, cx)
702    }
703}
704
705impl<H: UiHost, B> IntoUiElement<H> for ContainerPropsBoxBuild<H, B>
706where
707    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
708{
709    #[track_caller]
710    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
711        ContainerPropsBoxBuild::<H, B>::into_element(self, cx)
712    }
713}
714
715/// Returns a patchable container builder.
716///
717/// Usage:
718/// - `ui::container(|cx| vec![...]).px_2().into_element(cx)`
719pub fn container<H: UiHost, F, I>(children: F) -> UiBuilder<ContainerBox<H, F>>
720where
721    F: FnOnce(&mut ElementContext<'_, H>) -> I,
722    I: IntoIterator,
723    I::Item: IntoUiElement<H>,
724{
725    UiBuilder::new(ContainerBox::new(children))
726}
727
728/// Variant of [`container`] that avoids iterator borrow pitfalls by collecting into a sink.
729pub fn container_build<H: UiHost, B>(build: B) -> UiBuilder<ContainerBoxBuild<H, B>>
730where
731    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
732{
733    UiBuilder::new(ContainerBoxBuild::new(build))
734}
735
736/// Returns a raw `ContainerProps` root that still keeps child authoring on the late-landing path.
737pub fn container_props<H: UiHost, F, I>(
738    props: ContainerProps,
739    children: F,
740) -> UiBuilder<ContainerPropsBox<H, F>>
741where
742    F: FnOnce(&mut ElementContext<'_, H>) -> I,
743    I: IntoIterator,
744    I::Item: IntoUiElement<H>,
745{
746    UiBuilder::new(ContainerPropsBox::new(props, children))
747}
748
749/// Sink-based variant of [`container_props`] for iterator-heavy or borrow-sensitive child flows.
750pub fn container_props_build<H: UiHost, B>(
751    props: ContainerProps,
752    build: B,
753) -> UiBuilder<ContainerPropsBoxBuild<H, B>>
754where
755    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
756{
757    UiBuilder::new(ContainerPropsBoxBuild::new(props, build))
758}
759
760/// A patchable scroll area constructor for authoring ergonomics.
761///
762/// This is a thin wrapper over the runtime `Scroll` + `Scrollbar` elements with sensible defaults.
763#[derive(Debug, Clone)]
764pub struct ScrollAreaBox<H, F> {
765    pub(crate) chrome: ChromeRefinement,
766    pub(crate) layout: LayoutRefinement,
767    pub(crate) axis: ScrollAxis,
768    pub(crate) show_scrollbar_x: bool,
769    pub(crate) show_scrollbar_y: bool,
770    pub(crate) handle: Option<ScrollHandle>,
771    pub(crate) children: Option<F>,
772    pub(crate) _phantom: PhantomData<fn() -> H>,
773}
774
775/// Variant of [`ScrollAreaBox`] that collects children into a sink to avoid iterator borrow pitfalls.
776#[derive(Debug)]
777pub struct ScrollAreaBoxBuild<H, B> {
778    pub(crate) chrome: ChromeRefinement,
779    pub(crate) layout: LayoutRefinement,
780    pub(crate) axis: ScrollAxis,
781    pub(crate) show_scrollbar_x: bool,
782    pub(crate) show_scrollbar_y: bool,
783    pub(crate) handle: Option<ScrollHandle>,
784    pub(crate) build: Option<B>,
785    pub(crate) _phantom: PhantomData<fn() -> H>,
786}
787
788impl<H, F> ScrollAreaBox<H, F> {
789    pub fn new(children: F) -> Self {
790        Self {
791            chrome: ChromeRefinement::default(),
792            layout: LayoutRefinement::default(),
793            axis: ScrollAxis::Y,
794            show_scrollbar_x: false,
795            show_scrollbar_y: true,
796            handle: None,
797            children: Some(children),
798            _phantom: PhantomData,
799        }
800    }
801}
802
803impl<H, B> ScrollAreaBoxBuild<H, B> {
804    pub fn new(build: B) -> Self {
805        Self {
806            chrome: ChromeRefinement::default(),
807            layout: LayoutRefinement::default(),
808            axis: ScrollAxis::Y,
809            show_scrollbar_x: false,
810            show_scrollbar_y: true,
811            handle: None,
812            build: Some(build),
813            _phantom: PhantomData,
814        }
815    }
816}
817
818impl<H, F> UiPatchTarget for ScrollAreaBox<H, F> {
819    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
820        self.chrome = self.chrome.merge(patch.chrome);
821        self.layout = self.layout.merge(patch.layout);
822        self
823    }
824}
825
826impl<H, F> UiSupportsChrome for ScrollAreaBox<H, F> {}
827impl<H, F> UiSupportsLayout for ScrollAreaBox<H, F> {}
828
829impl<H, B> UiPatchTarget for ScrollAreaBoxBuild<H, B> {
830    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
831        self.chrome = self.chrome.merge(patch.chrome);
832        self.layout = self.layout.merge(patch.layout);
833        self
834    }
835}
836
837impl<H, B> UiSupportsChrome for ScrollAreaBoxBuild<H, B> {}
838impl<H, B> UiSupportsLayout for ScrollAreaBoxBuild<H, B> {}
839
840impl<H: UiHost, F, I> ScrollAreaBox<H, F>
841where
842    F: FnOnce(&mut ElementContext<'_, H>) -> I,
843    I: IntoIterator,
844    I::Item: IntoUiElement<H>,
845{
846    #[track_caller]
847    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
848        let (container, scrollbar_w, thumb, thumb_hover, corner_bg) = {
849            let theme = Theme::global(&*cx.app);
850            let container = decl_style::container_props(theme, self.chrome, self.layout);
851
852            let scrollbar_w = theme.metric_token("metric.scrollbar.width");
853            let thumb = theme.color_token("scrollbar.thumb.background");
854            let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
855            let corner_bg = theme
856                .color_by_key("scrollbar.corner.background")
857                .or_else(|| theme.color_by_key("scrollbar.track.background"))
858                .unwrap_or(fret_core::Color::TRANSPARENT);
859            (container, scrollbar_w, thumb, thumb_hover, corner_bg)
860        };
861
862        let axis = self.axis;
863        let show_scrollbar_x = self.show_scrollbar_x;
864        let show_scrollbar_y = self.show_scrollbar_y;
865        let provided_handle = self.handle;
866        let children = self.children.expect("expected scroll children closure");
867
868        cx.container(container, move |cx| {
869            let handle = cx.slot_state(ScrollHandle::default, |h| {
870                if let Some(handle) = provided_handle.clone() {
871                    *h = handle;
872                }
873                h.clone()
874            });
875
876            let mut scroll_layout = LayoutStyle::default();
877            scroll_layout.size.width = Length::Fill;
878            scroll_layout.size.height = Length::Fill;
879            scroll_layout.overflow = Overflow::Clip;
880
881            let scroll = cx.scroll(
882                ScrollProps {
883                    layout: scroll_layout,
884                    axis,
885                    scroll_handle: Some(handle.clone()),
886                    ..Default::default()
887                },
888                move |cx| {
889                    let children = children(cx);
890                    collect_ui_children(cx, children)
891                },
892            );
893
894            let scroll_id = scroll.id;
895            let mut out = vec![scroll];
896
897            if show_scrollbar_y {
898                let scrollbar_layout = LayoutStyle {
899                    position: PositionStyle::Absolute,
900                    inset: InsetStyle {
901                        top: Some(Px(0.0)).into(),
902                        right: Some(Px(0.0)).into(),
903                        bottom: Some(if show_scrollbar_x {
904                            scrollbar_w
905                        } else {
906                            Px(0.0)
907                        })
908                        .into(),
909                        left: None.into(),
910                    },
911                    size: SizeStyle {
912                        width: Length::Px(scrollbar_w),
913                        ..Default::default()
914                    },
915                    ..Default::default()
916                };
917
918                out.push(cx.scrollbar(ScrollbarProps {
919                    layout: scrollbar_layout,
920                    axis: ScrollbarAxis::Vertical,
921                    scroll_target: Some(scroll_id),
922                    scroll_handle: handle.clone(),
923                    style: ScrollbarStyle {
924                        thumb,
925                        thumb_hover,
926                        ..Default::default()
927                    },
928                }));
929            }
930
931            if show_scrollbar_x {
932                let scrollbar_layout = LayoutStyle {
933                    position: PositionStyle::Absolute,
934                    inset: InsetStyle {
935                        top: None.into(),
936                        right: Some(if show_scrollbar_y {
937                            scrollbar_w
938                        } else {
939                            Px(0.0)
940                        })
941                        .into(),
942                        bottom: Some(Px(0.0)).into(),
943                        left: Some(Px(0.0)).into(),
944                    },
945                    size: SizeStyle {
946                        height: Length::Px(scrollbar_w),
947                        ..Default::default()
948                    },
949                    ..Default::default()
950                };
951
952                out.push(cx.scrollbar(ScrollbarProps {
953                    layout: scrollbar_layout,
954                    axis: ScrollbarAxis::Horizontal,
955                    scroll_target: Some(scroll_id),
956                    scroll_handle: handle.clone(),
957                    style: ScrollbarStyle {
958                        thumb,
959                        thumb_hover,
960                        ..Default::default()
961                    },
962                }));
963            }
964
965            if show_scrollbar_x && show_scrollbar_y {
966                let corner_layout = LayoutStyle {
967                    position: PositionStyle::Absolute,
968                    inset: InsetStyle {
969                        top: None.into(),
970                        right: Some(Px(0.0)).into(),
971                        bottom: Some(Px(0.0)).into(),
972                        left: None.into(),
973                    },
974                    size: SizeStyle {
975                        width: Length::Px(scrollbar_w),
976                        height: Length::Px(scrollbar_w),
977                        ..Default::default()
978                    },
979                    ..Default::default()
980                };
981                out.push(cx.container(
982                    ContainerProps {
983                        layout: corner_layout,
984                        background: Some(corner_bg),
985                        ..Default::default()
986                    },
987                    |_cx| [],
988                ));
989            }
990
991            out
992        })
993    }
994}
995
996impl<H: UiHost, B> ScrollAreaBoxBuild<H, B>
997where
998    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
999{
1000    #[track_caller]
1001    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1002        let (container, scrollbar_w, thumb, thumb_hover, corner_bg) = {
1003            let theme = Theme::global(&*cx.app);
1004            let container = decl_style::container_props(theme, self.chrome, self.layout);
1005            let scrollbar_w = theme.metric_token("metric.scrollbar.width");
1006            let thumb = theme.color_token("scrollbar.thumb.background");
1007            let thumb_hover = theme.color_token("scrollbar.thumb.hover.background");
1008            let corner_bg = theme.color_token("scrollbar.track.background");
1009            (container, scrollbar_w, thumb, thumb_hover, corner_bg)
1010        };
1011
1012        let axis = self.axis;
1013        let show_scrollbar_x = self.show_scrollbar_x;
1014        let show_scrollbar_y = self.show_scrollbar_y;
1015        let provided_handle = self.handle;
1016        let build = self.build.expect("expected scroll area build closure");
1017
1018        cx.container(container, move |cx| {
1019            let handle = cx.slot_state(ScrollHandle::default, |h| {
1020                if let Some(handle) = provided_handle.clone() {
1021                    *h = handle;
1022                }
1023                h.clone()
1024            });
1025
1026            let mut scroll_layout = LayoutStyle::default();
1027            scroll_layout.size.width = Length::Fill;
1028            scroll_layout.size.height = Length::Fill;
1029            scroll_layout.overflow = Overflow::Clip;
1030
1031            let scroll = cx.scroll(
1032                ScrollProps {
1033                    layout: scroll_layout,
1034                    axis,
1035                    scroll_handle: Some(handle.clone()),
1036                    ..Default::default()
1037                },
1038                move |cx| {
1039                    let mut out = Vec::new();
1040                    build(cx, &mut out);
1041                    out
1042                },
1043            );
1044
1045            let scroll_id = scroll.id;
1046            let mut out = vec![scroll];
1047
1048            if show_scrollbar_y {
1049                let scrollbar_layout = LayoutStyle {
1050                    position: PositionStyle::Absolute,
1051                    inset: InsetStyle {
1052                        top: Some(Px(0.0)).into(),
1053                        right: Some(Px(0.0)).into(),
1054                        bottom: Some(if show_scrollbar_x {
1055                            scrollbar_w
1056                        } else {
1057                            Px(0.0)
1058                        })
1059                        .into(),
1060                        left: None.into(),
1061                    },
1062                    size: SizeStyle {
1063                        width: Length::Px(scrollbar_w),
1064                        ..Default::default()
1065                    },
1066                    ..Default::default()
1067                };
1068
1069                out.push(cx.scrollbar(ScrollbarProps {
1070                    layout: scrollbar_layout,
1071                    axis: ScrollbarAxis::Vertical,
1072                    scroll_target: Some(scroll_id),
1073                    scroll_handle: handle.clone(),
1074                    style: ScrollbarStyle {
1075                        thumb,
1076                        thumb_hover,
1077                        ..Default::default()
1078                    },
1079                }));
1080            }
1081
1082            if show_scrollbar_x {
1083                let scrollbar_layout = LayoutStyle {
1084                    position: PositionStyle::Absolute,
1085                    inset: InsetStyle {
1086                        top: None.into(),
1087                        right: Some(if show_scrollbar_y {
1088                            scrollbar_w
1089                        } else {
1090                            Px(0.0)
1091                        })
1092                        .into(),
1093                        bottom: Some(Px(0.0)).into(),
1094                        left: Some(Px(0.0)).into(),
1095                    },
1096                    size: SizeStyle {
1097                        height: Length::Px(scrollbar_w),
1098                        ..Default::default()
1099                    },
1100                    ..Default::default()
1101                };
1102
1103                out.push(cx.scrollbar(ScrollbarProps {
1104                    layout: scrollbar_layout,
1105                    axis: ScrollbarAxis::Horizontal,
1106                    scroll_target: Some(scroll_id),
1107                    scroll_handle: handle.clone(),
1108                    style: ScrollbarStyle {
1109                        thumb,
1110                        thumb_hover,
1111                        ..Default::default()
1112                    },
1113                }));
1114            }
1115
1116            if show_scrollbar_x && show_scrollbar_y {
1117                let corner_layout = LayoutStyle {
1118                    position: PositionStyle::Absolute,
1119                    inset: InsetStyle {
1120                        top: None.into(),
1121                        right: Some(Px(0.0)).into(),
1122                        bottom: Some(Px(0.0)).into(),
1123                        left: None.into(),
1124                    },
1125                    size: SizeStyle {
1126                        width: Length::Px(scrollbar_w),
1127                        height: Length::Px(scrollbar_w),
1128                        ..Default::default()
1129                    },
1130                    ..Default::default()
1131                };
1132                out.push(cx.container(
1133                    ContainerProps {
1134                        layout: corner_layout,
1135                        background: Some(corner_bg),
1136                        ..Default::default()
1137                    },
1138                    |_cx| [],
1139                ));
1140            }
1141
1142            out
1143        })
1144    }
1145}
1146
1147impl<H: UiHost, F, I> IntoUiElement<H> for ScrollAreaBox<H, F>
1148where
1149    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1150    I: IntoIterator,
1151    I::Item: IntoUiElement<H>,
1152{
1153    #[track_caller]
1154    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1155        ScrollAreaBox::<H, F>::into_element(self, cx)
1156    }
1157}
1158
1159impl<H: UiHost, B> IntoUiElement<H> for ScrollAreaBoxBuild<H, B>
1160where
1161    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1162{
1163    #[track_caller]
1164    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1165        ScrollAreaBoxBuild::<H, B>::into_element(self, cx)
1166    }
1167}
1168
1169/// Returns a patchable scroll area builder.
1170///
1171/// Defaults:
1172/// - axis: vertical
1173/// - scrollbar: Y on, X off
1174pub fn scroll_area<H: UiHost, F, I>(children: F) -> UiBuilder<ScrollAreaBox<H, F>>
1175where
1176    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1177    I: IntoIterator,
1178    I::Item: IntoUiElement<H>,
1179{
1180    UiBuilder::new(ScrollAreaBox::new(children))
1181}
1182
1183/// Variant of [`scroll_area`] that avoids iterator borrow pitfalls by collecting into a sink.
1184pub fn scroll_area_build<H: UiHost, B>(build: B) -> UiBuilder<ScrollAreaBoxBuild<H, B>>
1185where
1186    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1187{
1188    UiBuilder::new(ScrollAreaBoxBuild::new(build))
1189}
1190
1191/// A patchable stack layout constructor for authoring ergonomics.
1192///
1193/// The runtime `Stack` element is a positioned-container style layout: children can be absolutely
1194/// positioned, and non-absolute children are laid out against the same bounds.
1195#[derive(Debug, Clone)]
1196pub struct StackBox<H, F> {
1197    pub(crate) chrome: ChromeRefinement,
1198    pub(crate) layout: LayoutRefinement,
1199    pub(crate) children: Option<F>,
1200    pub(crate) _phantom: PhantomData<fn() -> H>,
1201}
1202
1203impl<H, F> StackBox<H, F> {
1204    pub fn new(children: F) -> Self {
1205        Self {
1206            chrome: ChromeRefinement::default(),
1207            layout: LayoutRefinement::default(),
1208            children: Some(children),
1209            _phantom: PhantomData,
1210        }
1211    }
1212}
1213
1214impl<H, F> UiPatchTarget for StackBox<H, F> {
1215    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1216        self.chrome = self.chrome.merge(patch.chrome);
1217        self.layout = self.layout.merge(patch.layout);
1218        self
1219    }
1220}
1221
1222impl<H, F> UiSupportsChrome for StackBox<H, F> {}
1223impl<H, F> UiSupportsLayout for StackBox<H, F> {}
1224
1225impl<H: UiHost, F, I> StackBox<H, F>
1226where
1227    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1228    I: IntoIterator,
1229    I::Item: IntoUiElement<H>,
1230{
1231    #[track_caller]
1232    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1233        let theme = Theme::global(&*cx.app);
1234        let container = decl_style::container_props(theme, self.chrome, self.layout);
1235        let children = self.children.expect("expected stack children closure");
1236
1237        cx.container(container, move |cx| {
1238            vec![cx.stack_props(StackProps::default(), move |cx| {
1239                let children = children(cx);
1240                collect_ui_children(cx, children)
1241            })]
1242        })
1243    }
1244}
1245
1246impl<H: UiHost, F, I> IntoUiElement<H> for StackBox<H, F>
1247where
1248    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1249    I: IntoIterator,
1250    I::Item: IntoUiElement<H>,
1251{
1252    #[track_caller]
1253    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1254        StackBox::<H, F>::into_element(self, cx)
1255    }
1256}
1257
1258/// Returns a patchable stack layout builder.
1259///
1260/// Usage:
1261/// - `ui::stack(|cx| vec![...]).inset(Space::N2).into_element(cx)`
1262pub fn stack<H: UiHost, F, I>(children: F) -> UiBuilder<StackBox<H, F>>
1263where
1264    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1265    I: IntoIterator,
1266    I::Item: IntoUiElement<H>,
1267{
1268    UiBuilder::new(StackBox::new(children))
1269}
1270
1271/// A keyed identity wrapper that keeps the original `cx.keyed(...)` callsite stable across
1272/// builder-first / late-landing authoring paths.
1273#[derive(Debug, Clone)]
1274pub struct KeyedBox<H, K, F> {
1275    pub(crate) callsite: &'static Location<'static>,
1276    pub(crate) key: Option<K>,
1277    pub(crate) child: Option<F>,
1278    pub(crate) _phantom: PhantomData<fn() -> H>,
1279}
1280
1281impl<H, K, F> KeyedBox<H, K, F> {
1282    fn new(callsite: &'static Location<'static>, key: K, child: F) -> Self {
1283        Self {
1284            callsite,
1285            key: Some(key),
1286            child: Some(child),
1287            _phantom: PhantomData,
1288        }
1289    }
1290}
1291
1292impl<H, K, F> UiPatchTarget for KeyedBox<H, K, F> {
1293    fn apply_ui_patch(self, _patch: UiPatch) -> Self {
1294        self
1295    }
1296}
1297
1298impl<H: UiHost, K: Hash, F, T> KeyedBox<H, K, F>
1299where
1300    F: FnOnce(&mut ElementContext<'_, H>) -> T,
1301    T: IntoUiElement<H>,
1302{
1303    #[track_caller]
1304    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1305        let Self {
1306            callsite,
1307            key,
1308            child,
1309            _phantom: _,
1310        } = self;
1311        let key = key.expect("keyed box key already taken");
1312        let child = child.expect("keyed box child already taken");
1313        cx.keyed_at(callsite, key, |cx| child(cx).into_element(cx))
1314    }
1315}
1316
1317impl<H: UiHost, K: Hash, F, T> IntoUiElement<H> for KeyedBox<H, K, F>
1318where
1319    F: FnOnce(&mut ElementContext<'_, H>) -> T,
1320    T: IntoUiElement<H>,
1321{
1322    #[track_caller]
1323    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1324        KeyedBox::<H, K, F>::into_element(self, cx)
1325    }
1326}
1327
1328/// Returns an identity-preserving keyed wrapper for a single child subtree.
1329///
1330/// Prefer this over raw `cx.keyed(...)` inside `*_build(|cx, out| ...)` sinks when you want to
1331/// stay on the builder-first path and avoid materializing `AnyElement` early.
1332#[track_caller]
1333pub fn keyed<H: UiHost, K: Hash, F, T>(key: K, child: F) -> UiBuilder<KeyedBox<H, K, F>>
1334where
1335    F: FnOnce(&mut ElementContext<'_, H>) -> T,
1336    T: IntoUiElement<H>,
1337{
1338    UiBuilder::new(KeyedBox::new(Location::caller(), key, child))
1339}
1340
1341/// Collects a keyed dynamic child list without forcing callers onto `*_build(|cx, out| ...)`.
1342///
1343/// This is the preferred authoring helper when a layout closure naturally wants to return
1344/// a conditional `Vec<AnyElement>`: empty-state content can still use `ui::children![cx; ...]`,
1345/// while the non-empty branch keeps stable keyed identity without open-coding
1346/// `for item in ... { out.push_ui(cx, ui::keyed(...)) }`.
1347///
1348/// Use [`for_each_keyed_with_cx`] when the per-row builder itself needs the keyed child scope.
1349#[track_caller]
1350pub fn for_each_keyed<H: UiHost, I, KF, BF, K, T>(
1351    cx: &mut ElementContext<'_, H>,
1352    items: I,
1353    key_of: KF,
1354    mut build: BF,
1355) -> Vec<AnyElement>
1356where
1357    I: IntoIterator,
1358    KF: FnMut(&I::Item) -> K,
1359    BF: FnMut(I::Item) -> T,
1360    K: Hash,
1361    T: IntoUiElement<H>,
1362{
1363    for_each_keyed_with_cx(cx, items, key_of, |_cx, item| build(item))
1364}
1365
1366/// Collects a keyed dynamic child list while exposing the keyed child scope to each row builder.
1367///
1368/// Prefer this when each row needs its own keyed `cx` for row-local state, `cx.text(...)`,
1369/// nested local models, or other child-scope work that should happen inside the keyed boundary.
1370#[track_caller]
1371pub fn for_each_keyed_with_cx<H: UiHost, I, KF, BF, K, T>(
1372    cx: &mut ElementContext<'_, H>,
1373    items: I,
1374    mut key_of: KF,
1375    mut build: BF,
1376) -> Vec<AnyElement>
1377where
1378    I: IntoIterator,
1379    KF: FnMut(&I::Item) -> K,
1380    BF: FnMut(&mut ElementContext<'_, H>, I::Item) -> T,
1381    K: Hash,
1382    T: IntoUiElement<H>,
1383{
1384    let callsite = Location::caller();
1385    let mut out = Vec::new();
1386
1387    for item in items {
1388        let key = key_of(&item);
1389        let build = &mut build;
1390        out.push(cx.keyed_at(callsite, key, |cx| build(cx, item).into_element(cx)));
1391    }
1392
1393    out
1394}
1395
1396#[derive(Debug, Clone)]
1397pub struct EffectLayerBox<H, F> {
1398    pub(crate) props: EffectLayerProps,
1399    pub(crate) layout: LayoutRefinement,
1400    pub(crate) children: Option<F>,
1401    pub(crate) _phantom: PhantomData<fn() -> H>,
1402}
1403
1404#[derive(Debug)]
1405pub struct EffectLayerBoxBuild<H, B> {
1406    pub(crate) props: EffectLayerProps,
1407    pub(crate) layout: LayoutRefinement,
1408    pub(crate) build: Option<B>,
1409    pub(crate) _phantom: PhantomData<fn() -> H>,
1410}
1411
1412impl<H, F> EffectLayerBox<H, F> {
1413    pub fn new(props: EffectLayerProps, children: F) -> Self {
1414        Self {
1415            props,
1416            layout: LayoutRefinement::default(),
1417            children: Some(children),
1418            _phantom: PhantomData,
1419        }
1420    }
1421
1422    pub fn refine_layout(mut self, layout: LayoutRefinement) -> Self {
1423        self.layout = self.layout.merge(layout);
1424        self
1425    }
1426}
1427
1428impl<H, B> EffectLayerBoxBuild<H, B> {
1429    pub fn new(props: EffectLayerProps, build: B) -> Self {
1430        Self {
1431            props,
1432            layout: LayoutRefinement::default(),
1433            build: Some(build),
1434            _phantom: PhantomData,
1435        }
1436    }
1437
1438    pub fn refine_layout(mut self, layout: LayoutRefinement) -> Self {
1439        self.layout = self.layout.merge(layout);
1440        self
1441    }
1442}
1443
1444impl<H, F> UiPatchTarget for EffectLayerBox<H, F> {
1445    fn apply_ui_patch(self, patch: UiPatch) -> Self {
1446        self.refine_layout(patch.layout)
1447    }
1448}
1449
1450impl<H, B> UiPatchTarget for EffectLayerBoxBuild<H, B> {
1451    fn apply_ui_patch(self, patch: UiPatch) -> Self {
1452        self.refine_layout(patch.layout)
1453    }
1454}
1455
1456impl<H, F> UiSupportsLayout for EffectLayerBox<H, F> {}
1457impl<H, B> UiSupportsLayout for EffectLayerBoxBuild<H, B> {}
1458
1459impl<H: UiHost, F, I> EffectLayerBox<H, F>
1460where
1461    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1462    I: IntoIterator,
1463    I::Item: IntoUiElement<H>,
1464{
1465    #[track_caller]
1466    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1467        let theme = Theme::global(&*cx.app);
1468        let mut props = self.props;
1469        decl_style::apply_layout_refinement(theme, self.layout, &mut props.layout);
1470        let children = self
1471            .children
1472            .expect("expected effect layer children closure");
1473        cx.effect_layer_props(props, move |cx| {
1474            let children = children(cx);
1475            collect_ui_children(cx, children)
1476        })
1477    }
1478}
1479
1480impl<H: UiHost, B> EffectLayerBoxBuild<H, B>
1481where
1482    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1483{
1484    #[track_caller]
1485    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1486        let theme = Theme::global(&*cx.app);
1487        let mut props = self.props;
1488        decl_style::apply_layout_refinement(theme, self.layout, &mut props.layout);
1489        let build = self.build.expect("expected effect layer build closure");
1490        cx.effect_layer_props(props, move |cx| {
1491            let mut out = Vec::new();
1492            build(cx, &mut out);
1493            out
1494        })
1495    }
1496}
1497
1498impl<H: UiHost, F, I> IntoUiElement<H> for EffectLayerBox<H, F>
1499where
1500    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1501    I: IntoIterator,
1502    I::Item: IntoUiElement<H>,
1503{
1504    #[track_caller]
1505    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1506        EffectLayerBox::<H, F>::into_element(self, cx)
1507    }
1508}
1509
1510impl<H: UiHost, B> IntoUiElement<H> for EffectLayerBoxBuild<H, B>
1511where
1512    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1513{
1514    #[track_caller]
1515    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1516        EffectLayerBoxBuild::<H, B>::into_element(self, cx)
1517    }
1518}
1519
1520/// Returns a patchable effect-layer builder.
1521///
1522/// Usage:
1523/// - `ui::effect_layer(EffectMode::FilterContent, chain, |_cx| [child]).w_full().into_element(cx)`
1524pub fn effect_layer<H: UiHost, F, I>(
1525    mode: EffectMode,
1526    chain: EffectChain,
1527    children: F,
1528) -> UiBuilder<EffectLayerBox<H, F>>
1529where
1530    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1531    I: IntoIterator,
1532    I::Item: IntoUiElement<H>,
1533{
1534    effect_layer_props(
1535        EffectLayerProps {
1536            mode,
1537            chain,
1538            ..Default::default()
1539        },
1540        children,
1541    )
1542}
1543
1544/// Returns a patchable effect-layer builder with explicit props.
1545pub fn effect_layer_props<H: UiHost, F, I>(
1546    props: EffectLayerProps,
1547    children: F,
1548) -> UiBuilder<EffectLayerBox<H, F>>
1549where
1550    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1551    I: IntoIterator,
1552    I::Item: IntoUiElement<H>,
1553{
1554    UiBuilder::new(EffectLayerBox::new(props, children))
1555}
1556
1557/// Variant of [`effect_layer`] that avoids iterator borrow pitfalls by collecting into a sink.
1558pub fn effect_layer_build<H: UiHost, B>(
1559    mode: EffectMode,
1560    chain: EffectChain,
1561    build: B,
1562) -> UiBuilder<EffectLayerBoxBuild<H, B>>
1563where
1564    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1565{
1566    effect_layer_props_build(
1567        EffectLayerProps {
1568            mode,
1569            chain,
1570            ..Default::default()
1571        },
1572        build,
1573    )
1574}
1575
1576/// Variant of [`effect_layer_props`] that collects children into a sink.
1577pub fn effect_layer_props_build<H: UiHost, B>(
1578    props: EffectLayerProps,
1579    build: B,
1580) -> UiBuilder<EffectLayerBoxBuild<H, B>>
1581where
1582    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1583{
1584    UiBuilder::new(EffectLayerBoxBuild::new(props, build))
1585}
1586
1587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1588pub enum TextPreset {
1589    Xs,
1590    Sm,
1591    Base,
1592    Prose,
1593    Label,
1594}
1595
1596#[derive(Debug, Clone, Copy, PartialEq)]
1597pub enum TextLineHeightPreset {
1598    Compact,
1599    Standard,
1600    Comfortable,
1601    Custom(f32),
1602}
1603
1604impl TextLineHeightPreset {
1605    pub fn em(self) -> f32 {
1606        match self {
1607            Self::Compact => 1.2,
1608            Self::Standard => 1.3,
1609            Self::Comfortable => 1.618,
1610            Self::Custom(v) => v,
1611        }
1612    }
1613}
1614
1615/// A patchable text constructor for authoring ergonomics.
1616///
1617/// This is intentionally small: it supports layout patching and a minimal text refinement surface
1618/// (size preset, weight, color, wrap/overflow).
1619#[derive(Debug, Clone)]
1620pub struct TextBox {
1621    pub(crate) layout: LayoutRefinement,
1622    pub(crate) text: Arc<str>,
1623    pub(crate) preset: TextPreset,
1624    pub(crate) selectable: bool,
1625    pub(crate) font_override: Option<FontId>,
1626    pub(crate) features_override: Vec<fret_core::TextFontFeatureSetting>,
1627    pub(crate) axes_override: Vec<fret_core::TextFontAxisSetting>,
1628    pub(crate) size_override: Option<Px>,
1629    pub(crate) line_height_override: Option<Px>,
1630    pub(crate) line_height_em_override: Option<f32>,
1631    pub(crate) line_height_policy_override: Option<fret_core::TextLineHeightPolicy>,
1632    pub(crate) ink_overflow_override: Option<fret_ui::element::TextInkOverflow>,
1633    pub(crate) weight_override: Option<FontWeight>,
1634    pub(crate) letter_spacing_em_override: Option<f32>,
1635    pub(crate) color_override: Option<crate::ColorRef>,
1636    pub(crate) wrap: TextWrap,
1637    pub(crate) overflow: TextOverflow,
1638    pub(crate) align: TextAlign,
1639    pub(crate) vertical_placement_override: Option<fret_core::TextVerticalPlacement>,
1640}
1641
1642impl TextBox {
1643    pub fn new(text: impl Into<Arc<str>>, preset: TextPreset) -> Self {
1644        let wrap = match preset {
1645            TextPreset::Label => TextWrap::None,
1646            TextPreset::Xs | TextPreset::Sm | TextPreset::Base | TextPreset::Prose => {
1647                TextWrap::Word
1648            }
1649        };
1650
1651        Self {
1652            layout: LayoutRefinement::default(),
1653            text: text.into(),
1654            preset,
1655            selectable: false,
1656            font_override: None,
1657            features_override: Vec::new(),
1658            axes_override: Vec::new(),
1659            size_override: None,
1660            line_height_override: None,
1661            line_height_em_override: None,
1662            line_height_policy_override: None,
1663            ink_overflow_override: None,
1664            weight_override: None,
1665            letter_spacing_em_override: None,
1666            color_override: None,
1667            wrap,
1668            overflow: TextOverflow::Clip,
1669            align: TextAlign::Start,
1670            vertical_placement_override: None,
1671        }
1672    }
1673}
1674
1675impl UiPatchTarget for TextBox {
1676    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1677        self.layout = self.layout.merge(patch.layout);
1678        self
1679    }
1680}
1681
1682impl UiSupportsLayout for TextBox {}
1683
1684impl<H: UiHost> IntoUiElement<H> for TextBox {
1685    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1686        let TextBox {
1687            layout: layout_refinement,
1688            text,
1689            preset,
1690            font_override,
1691            features_override,
1692            axes_override,
1693            size_override,
1694            line_height_override,
1695            line_height_em_override,
1696            line_height_policy_override,
1697            ink_overflow_override,
1698            weight_override,
1699            letter_spacing_em_override,
1700            color_override,
1701            wrap,
1702            overflow,
1703            align,
1704            vertical_placement_override,
1705            selectable,
1706        } = self;
1707
1708        let direction = crate::primitives::direction::use_direction_in_scope(cx, None);
1709        let align = resolve_text_align_for_direction(align, direction);
1710
1711        let (mut style, mut layout, default_label_line_height, resolved_color) = {
1712            let theme = Theme::global(&*cx.app);
1713
1714            let (style, label_line_height) = match preset {
1715                TextPreset::Xs => (decl_text::text_xs_style(theme), None),
1716                TextPreset::Sm => (decl_text::text_sm_style(theme), None),
1717                TextPreset::Base => (decl_text::text_base_style(theme), None),
1718                TextPreset::Prose => (decl_text::text_prose_style(theme), None),
1719                TextPreset::Label => {
1720                    let (style, line_height) = decl_text::label_style(theme);
1721                    (style, Some(line_height))
1722                }
1723            };
1724
1725            let layout = decl_style::layout_style(theme, layout_refinement);
1726
1727            let resolved_color = color_override.as_ref().map(|c| c.resolve(theme));
1728
1729            (style, layout, label_line_height, resolved_color)
1730        };
1731
1732        if let Some(font) = font_override {
1733            style.font = font;
1734        }
1735        if !features_override.is_empty() {
1736            style.features.extend(features_override);
1737        }
1738        if !axes_override.is_empty() {
1739            style.axes.extend(axes_override);
1740        }
1741        if let Some(size) = size_override {
1742            style.size = size;
1743        }
1744        if let Some(height) = line_height_override {
1745            style.line_height = Some(height);
1746        }
1747        if let Some(line_height_em) = line_height_em_override {
1748            style.line_height_em = Some(line_height_em);
1749        }
1750        if let Some(weight) = weight_override {
1751            style.weight = weight;
1752        }
1753        if let Some(letter_spacing_em) = letter_spacing_em_override {
1754            style.letter_spacing_em = Some(letter_spacing_em);
1755        }
1756        if let Some(line_height_policy) = line_height_policy_override {
1757            style.line_height_policy = line_height_policy;
1758        }
1759        if let Some(vertical_placement) = vertical_placement_override {
1760            style.vertical_placement = vertical_placement;
1761        }
1762
1763        // `TextPreset::Label` defaults to single-line text (Tailwind/shadcn `leading-none` label),
1764        // so we fix the line box height by default. If the caller explicitly enables wrapping,
1765        // keep the height auto so multi-line labels can expand without overlap.
1766        if preset == TextPreset::Label
1767            && wrap == TextWrap::None
1768            && matches!(layout.size.height, Length::Auto)
1769        {
1770            let line_height = line_height_override
1771                .or(default_label_line_height)
1772                .unwrap_or(Px(0.0));
1773            layout.size.height = Length::Px(line_height);
1774        }
1775
1776        if selectable {
1777            let spans: Arc<[TextSpan]> = Arc::from([TextSpan::new(text.len())]);
1778            let rich = AttributedText::new(text, spans);
1779            cx.selectable_text_props(SelectableTextProps {
1780                layout,
1781                rich,
1782                style: Some(style),
1783                color: resolved_color,
1784                wrap,
1785                overflow,
1786                align,
1787                ink_overflow: ink_overflow_override.unwrap_or_default(),
1788                interactive_spans: Arc::from([]),
1789            })
1790        } else {
1791            cx.text_props(TextProps {
1792                layout,
1793                text,
1794                style: Some(style),
1795                color: resolved_color,
1796                wrap,
1797                overflow,
1798                align,
1799                ink_overflow: ink_overflow_override.unwrap_or_default(),
1800            })
1801        }
1802    }
1803}
1804
1805/// Returns a patchable text builder (shadcn-aligned defaults).
1806///
1807/// Usage:
1808/// - `ui::text("Hello").text_sm().font_medium().into_element(cx)`
1809pub fn text(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1810    UiBuilder::new(TextBox::new(text, TextPreset::Sm))
1811}
1812
1813/// Returns a patchable block text builder (full-width; shadcn-aligned defaults).
1814///
1815/// Use this for paragraph-like text that should wrap against the available inner width of its
1816/// containing block.
1817pub fn text_block(content: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1818    text(content).w_full()
1819}
1820
1821/// Returns a patchable selectable text builder (drag-to-select + `edit.copy`).
1822///
1823/// Prefer this for read-only values (paths/IDs/snippets) and documentation-like content.
1824/// Avoid using it inside pressable/clickable rows: it intentionally captures left-drag selection
1825/// gestures and stops propagation (use a dedicated copy button instead).
1826pub fn selectable_text(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1827    crate::ui::text(text).selectable_on()
1828}
1829
1830/// Returns a patchable selectable block text builder (full-width; drag-to-select + `edit.copy`).
1831pub fn selectable_text_block(content: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1832    selectable_text(content).w_full()
1833}
1834
1835/// Returns a patchable label builder (single-line, medium weight).
1836pub fn label(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1837    UiBuilder::new(TextBox::new(text, TextPreset::Label))
1838}
1839
1840/// A patchable unstyled text builder matching `TextProps::new(...)` defaults.
1841#[derive(Debug, Clone)]
1842pub struct RawTextBox {
1843    pub(crate) layout: LayoutRefinement,
1844    pub(crate) text: Arc<str>,
1845    pub(crate) color_override: Option<crate::ColorRef>,
1846    pub(crate) wrap: TextWrap,
1847    pub(crate) overflow: TextOverflow,
1848    pub(crate) align: TextAlign,
1849    pub(crate) ink_overflow_override: Option<fret_ui::element::TextInkOverflow>,
1850}
1851
1852impl RawTextBox {
1853    pub fn new(text: impl Into<Arc<str>>) -> Self {
1854        Self {
1855            layout: LayoutRefinement::default(),
1856            text: text.into(),
1857            color_override: None,
1858            wrap: TextWrap::Word,
1859            overflow: TextOverflow::Clip,
1860            align: TextAlign::Start,
1861            ink_overflow_override: None,
1862        }
1863    }
1864}
1865
1866/// A patchable attributed-text builder matching `StyledTextProps::new(...)` defaults.
1867#[derive(Debug, Clone)]
1868pub struct RichTextBox {
1869    pub(crate) layout: LayoutRefinement,
1870    pub(crate) rich: AttributedText,
1871    pub(crate) style_override: Option<TextStyle>,
1872    pub(crate) color_override: Option<crate::ColorRef>,
1873    pub(crate) wrap: TextWrap,
1874    pub(crate) overflow: TextOverflow,
1875    pub(crate) align: TextAlign,
1876    pub(crate) ink_overflow_override: Option<fret_ui::element::TextInkOverflow>,
1877}
1878
1879impl RichTextBox {
1880    pub fn new(rich: AttributedText) -> Self {
1881        Self {
1882            layout: LayoutRefinement::default(),
1883            rich,
1884            style_override: None,
1885            color_override: None,
1886            wrap: TextWrap::Word,
1887            overflow: TextOverflow::Clip,
1888            align: TextAlign::Start,
1889            ink_overflow_override: None,
1890        }
1891    }
1892}
1893
1894impl UiPatchTarget for RichTextBox {
1895    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1896        self.layout = self.layout.merge(patch.layout);
1897        self
1898    }
1899}
1900
1901impl UiSupportsLayout for RichTextBox {}
1902
1903impl<H: UiHost> IntoUiElement<H> for RichTextBox {
1904    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1905        let RichTextBox {
1906            layout: layout_refinement,
1907            rich,
1908            style_override,
1909            color_override,
1910            wrap,
1911            overflow,
1912            align,
1913            ink_overflow_override,
1914        } = self;
1915
1916        let direction = crate::primitives::direction::use_direction_in_scope(cx, None);
1917        let align = resolve_text_align_for_direction(align, direction);
1918
1919        let (layout, color) = {
1920            let theme = Theme::global(&*cx.app);
1921            let layout = decl_style::layout_style(theme, layout_refinement);
1922            let color = color_override.as_ref().map(|c| c.resolve(theme));
1923            (layout, color)
1924        };
1925
1926        cx.styled_text_props(StyledTextProps {
1927            layout,
1928            rich,
1929            style: style_override,
1930            color,
1931            wrap,
1932            overflow,
1933            align,
1934            ink_overflow: ink_overflow_override.unwrap_or_default(),
1935        })
1936    }
1937}
1938
1939/// Returns a patchable attributed-text builder matching `StyledTextProps::new(...)` defaults.
1940pub fn rich_text(rich: AttributedText) -> UiBuilder<RichTextBox> {
1941    UiBuilder::new(RichTextBox::new(rich))
1942}
1943
1944/// A patchable hover-region builder for app-facing interaction shells.
1945#[derive(Debug)]
1946pub struct HoverRegionBox<H, F> {
1947    pub(crate) layout: LayoutRefinement,
1948    pub(crate) children: Option<F>,
1949    pub(crate) _phantom: PhantomData<fn() -> H>,
1950}
1951
1952impl<H, F> HoverRegionBox<H, F> {
1953    pub fn new(children: F) -> Self {
1954        Self {
1955            layout: LayoutRefinement::default(),
1956            children: Some(children),
1957            _phantom: PhantomData,
1958        }
1959    }
1960}
1961
1962impl<H, F> UiPatchTarget for HoverRegionBox<H, F> {
1963    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1964        self.layout = self.layout.merge(patch.layout);
1965        self
1966    }
1967}
1968
1969impl<H, F> UiSupportsLayout for HoverRegionBox<H, F> {}
1970
1971impl<H: UiHost, F, I> IntoUiElement<H> for HoverRegionBox<H, F>
1972where
1973    F: FnOnce(&mut ElementContext<'_, H>, bool) -> I,
1974    I: IntoIterator,
1975    I::Item: IntoUiElement<H>,
1976{
1977    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1978        let layout = {
1979            let theme = Theme::global(&*cx.app);
1980            decl_style::layout_style(theme, self.layout)
1981        };
1982        let children = self
1983            .children
1984            .expect("HoverRegionBox::into_element called more than once");
1985
1986        cx.hover_region(HoverRegionProps { layout }, move |cx, hovered| {
1987            let built = children(cx, hovered);
1988            let mut out: SmallVec<[AnyElement; 8]> = SmallVec::new();
1989            for child in built {
1990                out.push(crate::land_child(cx, child));
1991            }
1992            out
1993        })
1994    }
1995}
1996
1997/// Returns a patchable hover-region builder.
1998pub fn hover_region<H: UiHost, F, I>(children: F) -> UiBuilder<HoverRegionBox<H, F>>
1999where
2000    F: FnOnce(&mut ElementContext<'_, H>, bool) -> I,
2001    I: IntoIterator,
2002    I::Item: IntoUiElement<H>,
2003{
2004    UiBuilder::new(HoverRegionBox::new(children))
2005}
2006
2007impl UiPatchTarget for RawTextBox {
2008    fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
2009        self.layout = self.layout.merge(patch.layout);
2010        self
2011    }
2012}
2013
2014impl UiSupportsLayout for RawTextBox {}
2015
2016impl<H: UiHost> IntoUiElement<H> for RawTextBox {
2017    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
2018        let RawTextBox {
2019            layout: layout_refinement,
2020            text,
2021            color_override,
2022            wrap,
2023            overflow,
2024            align,
2025            ink_overflow_override,
2026        } = self;
2027
2028        let direction = crate::primitives::direction::use_direction_in_scope(cx, None);
2029        let align = resolve_text_align_for_direction(align, direction);
2030
2031        let (layout, color) = {
2032            let theme = Theme::global(&*cx.app);
2033            let layout = decl_style::layout_style(theme, layout_refinement);
2034            let color = color_override.as_ref().map(|c| c.resolve(theme));
2035            (layout, color)
2036        };
2037
2038        cx.text_props(TextProps {
2039            layout,
2040            text,
2041            style: None,
2042            color,
2043            wrap,
2044            overflow,
2045            align,
2046            ink_overflow: ink_overflow_override.unwrap_or_default(),
2047        })
2048    }
2049}
2050
2051/// Returns a patchable unstyled text builder matching `TextProps::new(...)` defaults.
2052pub fn raw_text(text: impl Into<Arc<str>>) -> UiBuilder<RawTextBox> {
2053    UiBuilder::new(RawTextBox::new(text))
2054}
2055
2056#[cfg(test)]
2057mod tests {
2058    use super::*;
2059    use crate::UiExt;
2060    use crate::{LengthRefinement, MetricRef};
2061    use fret_app::App;
2062    use fret_core::SemanticsRole;
2063    use fret_core::{AppWindowId, Point, Rect, Size};
2064    use fret_ui::element::{ElementKind, Length};
2065
2066    #[test]
2067    fn text_align_start_end_flip_under_rtl() {
2068        use crate::primitives::direction::LayoutDirection;
2069
2070        assert_eq!(
2071            resolve_text_align_for_direction(TextAlign::Start, LayoutDirection::Ltr),
2072            TextAlign::Start
2073        );
2074        assert_eq!(
2075            resolve_text_align_for_direction(TextAlign::Start, LayoutDirection::Rtl),
2076            TextAlign::End
2077        );
2078        assert_eq!(
2079            resolve_text_align_for_direction(TextAlign::End, LayoutDirection::Rtl),
2080            TextAlign::Start
2081        );
2082        assert_eq!(
2083            resolve_text_align_for_direction(TextAlign::Center, LayoutDirection::Rtl),
2084            TextAlign::Center
2085        );
2086    }
2087
2088    // Compile-only: ensure `ui::*` layout constructors accept `IntoUiElement<H>` children
2089    // (e.g. `UiBuilder<TextBox>`) without requiring call-site `.into_element(cx)`.
2090    #[allow(dead_code)]
2091    fn h_flex_accepts_ui_builder_children<H: UiHost>(cx: &mut ElementContext<'_, H>) -> AnyElement {
2092        h_flex(|_cx| [text("a"), text("b")])
2093            .gap(Space::N2)
2094            .into_element(cx)
2095    }
2096
2097    #[allow(dead_code)]
2098    fn hover_region_accepts_ui_builder_children<H: UiHost>(
2099        cx: &mut ElementContext<'_, H>,
2100    ) -> AnyElement {
2101        hover_region(|_cx, hovered| [text(if hovered { "hovered" } else { "idle" }).truncate()])
2102            .w_full()
2103            .into_element(cx)
2104    }
2105
2106    #[allow(dead_code)]
2107    fn rich_text_builder_lands_without_raw_styled_text_props<H: UiHost>(
2108        cx: &mut ElementContext<'_, H>,
2109        rich: AttributedText,
2110    ) -> AnyElement {
2111        rich_text(rich).truncate().w_full().into_element(cx)
2112    }
2113
2114    // Compile-only: ensure `ui::children!` accepts nested layout builders without requiring an
2115    // explicit `.into_element(cx)` cliff at heterogeneous child boundaries.
2116    #[allow(dead_code)]
2117    fn children_macro_accepts_nested_layout_builders<H: UiHost>(
2118        cx: &mut ElementContext<'_, H>,
2119    ) -> AnyElement {
2120        h_flex(|cx| {
2121            children![cx;
2122                v_flex(|cx| children![cx; text("a"), text("b")]).gap(Space::N1),
2123                container(|cx| children![cx; text("c")]).p_1(),
2124            ]
2125        })
2126        .gap(Space::N2)
2127        .into_element(cx)
2128    }
2129
2130    // Compile-only: ensure effect-layer roots accept late builder children without forcing the
2131    // child subtree to materialize before the effect boundary.
2132    #[allow(dead_code)]
2133    fn effect_layer_accepts_ui_builder_children<H: UiHost>(
2134        cx: &mut ElementContext<'_, H>,
2135    ) -> AnyElement {
2136        effect_layer(EffectMode::FilterContent, EffectChain::EMPTY, |_cx| {
2137            [container(|_cx| [text("effect child")]).w_full().h_full()]
2138        })
2139        .w_full()
2140        .into_element(cx)
2141    }
2142
2143    // Compile-only: ensure keyed late-landing helpers preserve builder-first child authoring
2144    // without falling back to raw `cx.keyed(...)` + eager `AnyElement` materialization.
2145    #[allow(dead_code)]
2146    fn keyed_accepts_ui_builder_children<H: UiHost>(cx: &mut ElementContext<'_, H>) -> AnyElement {
2147        v_flex_build(|cx, out| {
2148            out.push_ui(cx, keyed("row-1", |_cx| text("row").test_id("row")));
2149        })
2150        .test_id("rows")
2151        .into_element(cx)
2152    }
2153
2154    // Compile-only: ensure keyed list helpers can stay on the ordinary `v_flex(|cx| ..)` lane
2155    // without falling back to sink-based `v_flex_build(...)` authoring.
2156    #[allow(dead_code)]
2157    fn for_each_keyed_accepts_ui_builder_children<H: UiHost>(
2158        cx: &mut ElementContext<'_, H>,
2159    ) -> AnyElement {
2160        let rows = [("row-1", "Alpha"), ("row-2", "Beta")];
2161
2162        v_flex(|cx| for_each_keyed(cx, rows, |(id, _label)| *id, |(_id, label)| text(label)))
2163            .test_id("rows")
2164            .into_element(cx)
2165    }
2166
2167    // Compile-only: ensure keyed list helpers can also hand row builders the inner keyed scope
2168    // when the row content needs to be assembled inside that boundary.
2169    #[allow(dead_code)]
2170    fn for_each_keyed_with_cx_accepts_row_local_scope<H: UiHost>(
2171        cx: &mut ElementContext<'_, H>,
2172    ) -> AnyElement {
2173        let rows = [("row-1", "Alpha"), ("row-2", "Beta")];
2174
2175        v_flex(|cx| {
2176            for_each_keyed_with_cx(
2177                cx,
2178                rows,
2179                |(id, _label)| *id,
2180                |_cx, (_id, label)| container(move |cx| [cx.text(label)]).test_id(label),
2181            )
2182        })
2183        .test_id("rows")
2184        .into_element(cx)
2185    }
2186
2187    // Compile-only: ensure a single typed child can be late-landed into `Ui` / `Elements`
2188    // without spelling `ui::children![cx; child].into()` at the call site.
2189    #[allow(dead_code)]
2190    fn single_accepts_typed_child_roots<H: UiHost>(
2191        cx: &mut ElementContext<'_, H>,
2192    ) -> fret_ui::element::Elements {
2193        single(
2194            cx,
2195            container(|_cx| [text("root child")])
2196                .w_full()
2197                .test_id("single-root"),
2198        )
2199    }
2200
2201    // Compile-only: ensure low-level raw `ContainerProps` roots can still keep children on the
2202    // builder-first path without forcing eager landing before the host container boundary.
2203    #[allow(dead_code)]
2204    fn container_props_accepts_ui_builder_children<H: UiHost>(
2205        cx: &mut ElementContext<'_, H>,
2206    ) -> AnyElement {
2207        let mut layout = LayoutStyle::default();
2208        layout.size.width = Length::Fill;
2209        container_props(
2210            ContainerProps {
2211                layout,
2212                ..Default::default()
2213            },
2214            |_cx| {
2215                [h_flex(|_cx| [text("row"), text("meta")])
2216                    .gap(Space::N2)
2217                    .w_full()]
2218            },
2219        )
2220        .test_id("container-props")
2221        .into_element(cx)
2222    }
2223
2224    // Compile-only: ensure the sink-based raw-container variant stays on the same child pipeline.
2225    #[allow(dead_code)]
2226    fn container_props_build_accepts_ui_builder_children<H: UiHost>(
2227        cx: &mut ElementContext<'_, H>,
2228    ) -> AnyElement {
2229        let mut layout = LayoutStyle::default();
2230        layout.size.width = Length::Fill;
2231        container_props_build(
2232            ContainerProps {
2233                layout,
2234                ..Default::default()
2235            },
2236            |cx, out| {
2237                out.push_ui(cx, text("row"));
2238            },
2239        )
2240        .test_id("container-props-build")
2241        .into_element(cx)
2242    }
2243
2244    // Compile-only: ensure layout constructor roots can be decorated on the builder path without
2245    // early landing, mirroring common cookbook usage (`test_id`, role).
2246    #[allow(dead_code)]
2247    fn h_flex_root_accepts_semantics_decorators<H: UiHost>(
2248        cx: &mut ElementContext<'_, H>,
2249    ) -> AnyElement {
2250        h_flex(|_cx| [text("a"), text("b")])
2251            .test_id("root")
2252            .a11y_role(SemanticsRole::Group)
2253            .into_element(cx)
2254    }
2255
2256    // Compile-only: ensure public-trait semantics decorators can be applied before
2257    // `into_element(cx)` (so callsites can avoid "decorate-only" early landing).
2258    #[allow(dead_code)]
2259    fn h_flex_accepts_decorated_children<H: UiHost>(cx: &mut ElementContext<'_, H>) -> AnyElement {
2260        h_flex(|_cx| [text("a").test_id("a"), text("b").test_id("b")])
2261            .gap(Space::N2)
2262            .into_element(cx)
2263    }
2264
2265    #[test]
2266    fn container_box_accepts_ui_patches() {
2267        let container = ContainerBox::<(), ()>::new(())
2268            .ui()
2269            .p_1()
2270            .w(LengthRefinement::Fill)
2271            .build();
2272
2273        let padding = container
2274            .chrome
2275            .padding
2276            .expect("expected padding refinement");
2277        assert!(matches!(padding.left, Some(MetricRef::Token { .. })));
2278        assert!(container.layout.size.is_some());
2279    }
2280
2281    #[test]
2282    fn text_box_supports_layout_and_text_refinements() {
2283        let text = TextBox::new("hello", TextPreset::Sm)
2284            .ui()
2285            .w(LengthRefinement::Fill)
2286            .font_bold()
2287            .build();
2288
2289        assert!(text.layout.size.is_some());
2290        assert_eq!(text.weight_override, Some(FontWeight::BOLD));
2291    }
2292
2293    #[test]
2294    fn stack_box_accepts_ui_patches() {
2295        let stack = StackBox::<(), ()>::new(())
2296            .ui()
2297            .p_1()
2298            .w(LengthRefinement::Fill)
2299            .build();
2300
2301        let padding = stack.chrome.padding.expect("expected padding refinement");
2302        assert!(matches!(padding.left, Some(MetricRef::Token { .. })));
2303        assert!(stack.layout.size.is_some());
2304    }
2305
2306    #[test]
2307    fn text_box_selectable_renders_selectable_text_element() {
2308        let window = AppWindowId::default();
2309        let mut app = App::new();
2310        let bounds = Rect::new(
2311            Point::new(Px(0.0), Px(0.0)),
2312            Size::new(Px(400.0), Px(300.0)),
2313        );
2314
2315        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2316            let el = text("hello").selectable_on().into_element(cx);
2317            assert!(
2318                matches!(el.kind, ElementKind::SelectableText(_)),
2319                "expected ui::text(...).selectable_on() to render a SelectableText element"
2320            );
2321        });
2322    }
2323
2324    #[test]
2325    fn text_inherits_current_color_when_available() {
2326        let window = AppWindowId::default();
2327        let mut app = App::new();
2328        let bounds = Rect::new(
2329            Point::new(Px(0.0), Px(0.0)),
2330            Size::new(Px(400.0), Px(300.0)),
2331        );
2332
2333        let expected = fret_core::Color {
2334            r: 0.25,
2335            g: 0.5,
2336            b: 0.75,
2337            a: 1.0,
2338        };
2339
2340        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2341            let mut els = crate::declarative::current_color::scope_children(
2342                cx,
2343                crate::ColorRef::Color(expected),
2344                |cx| [text("hello").into_element(cx)],
2345            );
2346
2347            let child = els.pop().expect("expected a child element");
2348            assert_eq!(
2349                child.inherited_foreground,
2350                Some(expected),
2351                "expected current_color::scope_children(...) to stamp inherited foreground on the existing root"
2352            );
2353            let ElementKind::Text(props) = child.kind else {
2354                panic!("expected Text element");
2355            };
2356            assert_eq!(
2357                props.color, None,
2358                "expected text to keep color late-bound for inherited foreground paint resolution"
2359            );
2360        });
2361    }
2362
2363    #[test]
2364    fn rich_text_builder_renders_styled_text_element() {
2365        let window = AppWindowId::default();
2366        let mut app = App::new();
2367        let bounds = Rect::new(
2368            Point::new(Px(0.0), Px(0.0)),
2369            Size::new(Px(400.0), Px(300.0)),
2370        );
2371
2372        let rich = AttributedText::new(
2373            Arc::<str>::from("hello"),
2374            [TextSpan {
2375                len: 5,
2376                shaping: Default::default(),
2377                paint: Default::default(),
2378            }],
2379        );
2380
2381        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2382            let el = rich_text(rich.clone()).truncate().w_full().into_element(cx);
2383            let ElementKind::StyledText(props) = el.kind else {
2384                panic!("expected ui::rich_text(...) to render a StyledText element");
2385            };
2386            assert_eq!(props.wrap, TextWrap::None);
2387            assert_eq!(props.overflow, TextOverflow::Ellipsis);
2388        });
2389    }
2390
2391    #[test]
2392    fn hover_region_builder_renders_hover_region_element() {
2393        let window = AppWindowId::default();
2394        let mut app = App::new();
2395        let bounds = Rect::new(
2396            Point::new(Px(0.0), Px(0.0)),
2397            Size::new(Px(400.0), Px(300.0)),
2398        );
2399
2400        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2401            let el = hover_region(|_cx, hovered| [text(if hovered { "hovered" } else { "idle" })])
2402                .w_full()
2403                .into_element(cx);
2404            assert!(
2405                matches!(el.kind, ElementKind::HoverRegion(_)),
2406                "expected ui::hover_region(...) to render a HoverRegion element"
2407            );
2408            assert_eq!(el.children.len(), 1, "expected a single hover-region child");
2409            assert!(
2410                matches!(el.children[0].kind, ElementKind::Text(_)),
2411                "expected ui::hover_region(...) child to late-land text"
2412            );
2413        });
2414    }
2415
2416    #[test]
2417    fn flex_box_height_constraints_propagate_fill_height_to_inner_flex_root() {
2418        let window = AppWindowId::default();
2419        let mut app = App::new();
2420        let bounds = Rect::new(
2421            Point::new(Px(0.0), Px(0.0)),
2422            Size::new(Px(400.0), Px(300.0)),
2423        );
2424
2425        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2426            let el = v_flex(|_cx| [text("hello")])
2427                .min_h(Px(100.0))
2428                .max_h(Px(100.0))
2429                .into_element(cx);
2430
2431            let inner = match &el.kind {
2432                ElementKind::Container(props) => {
2433                    assert_eq!(props.layout.size.min_height, Some(Length::Px(Px(100.0))));
2434                    assert_eq!(props.layout.size.max_height, Some(Length::Px(Px(100.0))));
2435                    el.children
2436                        .first()
2437                        .expect("flex box container should wrap an inner flex root")
2438                }
2439                other => panic!("expected outer container wrapper, got {other:?}"),
2440            };
2441
2442            match &inner.kind {
2443                ElementKind::Flex(props) => {
2444                    assert!(
2445                        matches!(props.layout.size.height, Length::Fill),
2446                        "inner flex root should fill the constrained outer wrapper height"
2447                    );
2448                }
2449                other => panic!("expected inner flex root, got {other:?}"),
2450            }
2451        });
2452    }
2453
2454    #[test]
2455    fn stack_box_min_h_0_keeps_inner_flex_root_auto_height() {
2456        let window = AppWindowId::default();
2457        let mut app = App::new();
2458        let bounds = Rect::new(
2459            Point::new(Px(0.0), Px(0.0)),
2460            Size::new(Px(400.0), Px(300.0)),
2461        );
2462
2463        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2464            let el = v_stack(|_cx| [text("hello")]).min_h_0().into_element(cx);
2465
2466            let inner = match &el.kind {
2467                ElementKind::Container(props) => {
2468                    assert_eq!(props.layout.size.min_height, Some(Length::Px(Px(0.0))));
2469                    el.children
2470                        .first()
2471                        .expect("stack box container should wrap an inner flex root")
2472                }
2473                other => panic!("expected outer container wrapper, got {other:?}"),
2474            };
2475
2476            match &inner.kind {
2477                ElementKind::Flex(props) => {
2478                    assert_eq!(
2479                        props.layout.size.height,
2480                        Length::Auto,
2481                        "min_h_0 should not force the inner stack root to fill available height"
2482                    );
2483                }
2484                other => panic!("expected inner flex root, got {other:?}"),
2485            }
2486        });
2487    }
2488
2489    #[test]
2490    fn h_row_width_constraints_propagate_to_inner_flex_root() {
2491        let window = AppWindowId::default();
2492        let mut app = App::new();
2493        let bounds = Rect::new(
2494            Point::new(Px(0.0), Px(0.0)),
2495            Size::new(Px(400.0), Px(300.0)),
2496        );
2497
2498        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2499            let el = h_row(|_cx| [text("hello")])
2500                .w_full()
2501                .min_w_0()
2502                .max_w(Px(280.0))
2503                .into_element(cx);
2504
2505            let inner = match &el.kind {
2506                ElementKind::Container(props) => {
2507                    assert_eq!(props.layout.size.width, Length::Fill);
2508                    assert_eq!(props.layout.size.min_width, Some(Length::Px(Px(0.0))));
2509                    assert_eq!(props.layout.size.max_width, Some(Length::Px(Px(280.0))));
2510                    el.children
2511                        .first()
2512                        .expect("flex box container should wrap an inner flex root")
2513                }
2514                other => panic!("expected outer container wrapper, got {other:?}"),
2515            };
2516
2517            match &inner.kind {
2518                ElementKind::Flex(props) => {
2519                    assert_eq!(
2520                        props.layout.size.width,
2521                        Length::Fill,
2522                        "expected explicit width constraints on h_row(...) to land on the inner row root"
2523                    );
2524                    assert_eq!(
2525                        props.layout.size.min_width,
2526                        Some(Length::Px(Px(0.0))),
2527                        "expected min_w_0 on h_row(...) to land on the inner row root"
2528                    );
2529                    assert_eq!(
2530                        props.layout.size.max_width,
2531                        Some(Length::Px(Px(280.0))),
2532                        "expected max_w on h_row(...) to land on the inner row root"
2533                    );
2534                }
2535                other => panic!("expected inner flex root, got {other:?}"),
2536            }
2537        });
2538    }
2539
2540    #[test]
2541    fn h_flex_explicit_width_overrides_default_fill_on_inner_flex_root() {
2542        let window = AppWindowId::default();
2543        let mut app = App::new();
2544        let bounds = Rect::new(
2545            Point::new(Px(0.0), Px(0.0)),
2546            Size::new(Px(400.0), Px(300.0)),
2547        );
2548
2549        fret_ui::elements::with_element_cx(&mut app, window, bounds, "test", |cx| {
2550            let el = h_flex(|_cx| [text("hello")])
2551                .layout(LayoutRefinement::default().w_auto().min_w_0())
2552                .into_element(cx);
2553
2554            let inner = match &el.kind {
2555                ElementKind::Container(props) => {
2556                    assert_eq!(props.layout.size.width, Length::Auto);
2557                    assert_eq!(props.layout.size.min_width, Some(Length::Px(Px(0.0))));
2558                    el.children
2559                        .first()
2560                        .expect("flex box container should wrap an inner flex root")
2561                }
2562                other => panic!("expected outer container wrapper, got {other:?}"),
2563            };
2564
2565            match &inner.kind {
2566                ElementKind::Flex(props) => {
2567                    assert_eq!(
2568                        props.layout.size.width,
2569                        Length::Auto,
2570                        "expected explicit w_auto() to override the default fill-width inner flex root"
2571                    );
2572                    assert_eq!(
2573                        props.layout.size.min_width,
2574                        Some(Length::Px(Px(0.0))),
2575                        "expected min_w_0 to land on the inner flex root even when explicit width overrides the default fill contract"
2576                    );
2577                }
2578                other => panic!("expected inner flex root, got {other:?}"),
2579            }
2580        });
2581    }
2582}