Skip to main content

fret_ui_kit/
ui_builder.rs

1use crate::Corners4;
2use crate::{
3    ChromeRefinement, ColorRef, Edges4, Items, Justify, LayoutRefinement, LengthRefinement,
4    MarginEdge, MetricRef, Radius, SignedMetricRef, Space,
5};
6use fret_core::{
7    FontId, FontWeight, Px, SemanticsRole, TextLineHeightPolicy, TextOverflow, TextWrap,
8};
9use fret_ui::element::{AnyElement, CrossAlign, ScrollAxis, SemanticsDecoration, TextInkOverflow};
10use fret_ui::scroll::ScrollHandle;
11use fret_ui::{ElementContext, ElementContextAccess, UiHost};
12use std::panic::Location;
13use std::sync::Arc;
14
15/// Aggregated authoring patch applied by `UiBuilder`.
16///
17/// This is an ecosystem-only authoring surface (see ADR 0160). It intentionally composes:
18/// - control chrome patches (`ChromeRefinement`)
19/// - layout-affecting patches (`LayoutRefinement`)
20#[derive(Debug, Clone, Default)]
21pub struct UiPatch {
22    pub chrome: ChromeRefinement,
23    pub layout: LayoutRefinement,
24}
25
26impl UiPatch {
27    pub fn merge(mut self, other: UiPatch) -> Self {
28        self.chrome = self.chrome.merge(other.chrome);
29        self.layout = self.layout.merge(other.layout);
30        self
31    }
32}
33
34/// A type that opts into the `ui()` builder surface by accepting a `UiPatch`.
35///
36/// This is intentionally an ecosystem-only authoring surface (see ADR 0160).
37pub trait UiPatchTarget: Sized {
38    /// Applies an aggregated authoring patch (chrome + layout) and returns the refined value.
39    ///
40    /// Most types will merge the relevant parts of the patch into their internal refinement
41    /// structs (or ignore fields they don't support).
42    fn apply_ui_patch(self, patch: UiPatch) -> Self;
43}
44
45/// Marker trait enabling `UiBuilder` chrome/styling methods for a `UiPatchTarget`.
46pub trait UiSupportsChrome {}
47
48/// Marker trait enabling `UiBuilder` layout methods for a `UiPatchTarget`.
49pub trait UiSupportsLayout {}
50
51/// Unified public conversion contract for reusable component authoring.
52///
53/// This trait keeps `.into_element(cx)` as the one public landing operation whether the value is:
54///
55/// - already-landed raw elements, or
56/// - host-bound (for example late builders that capture `H`-typed closures internally).
57pub trait IntoUiElement<H: UiHost>: Sized {
58    #[track_caller]
59    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement;
60}
61
62/// Explicit landing helpers for surfaces that only expose [`ElementContextAccess`] rather than a
63/// raw `&mut ElementContext<...>`.
64pub trait IntoUiElementInExt<H: UiHost>: IntoUiElement<H> + Sized {
65    #[track_caller]
66    fn into_element_in<'a, Cx>(self, cx: &mut Cx) -> AnyElement
67    where
68        Cx: ElementContextAccess<'a, H>,
69        H: 'a,
70    {
71        self.into_element(cx.elements())
72    }
73}
74
75impl<H: UiHost, T> IntoUiElementInExt<H> for T where T: IntoUiElement<H> {}
76
77impl<H: UiHost> IntoUiElement<H> for AnyElement {
78    #[track_caller]
79    fn into_element(self, _cx: &mut ElementContext<'_, H>) -> AnyElement {
80        self
81    }
82}
83
84/// The main fluent authoring surface: `value.ui().px_2().w_full().into_element(cx)`.
85#[derive(Debug, Clone)]
86pub struct UiBuilder<T> {
87    inner: T,
88    patch: UiPatch,
89    semantics: Option<SemanticsDecoration>,
90    key_context: Option<Arc<str>>,
91}
92
93impl<T> UiBuilder<T> {
94    pub fn new(inner: T) -> Self {
95        Self {
96            inner,
97            patch: UiPatch::default(),
98            semantics: None,
99            key_context: None,
100        }
101    }
102
103    pub fn semantics(mut self, decoration: SemanticsDecoration) -> Self {
104        self.semantics = Some(match self.semantics.take() {
105            Some(existing) => existing.merge(decoration),
106            None => decoration,
107        });
108        self
109    }
110
111    pub fn test_id(self, test_id: impl Into<Arc<str>>) -> Self {
112        self.semantics(SemanticsDecoration::default().test_id(test_id))
113    }
114
115    pub fn a11y_role(self, role: SemanticsRole) -> Self {
116        self.semantics(SemanticsDecoration::default().role(role))
117    }
118
119    pub fn role(self, role: SemanticsRole) -> Self {
120        self.a11y_role(role)
121    }
122
123    pub fn a11y_label(self, label: impl Into<Arc<str>>) -> Self {
124        self.semantics(SemanticsDecoration::default().label(label))
125    }
126
127    pub fn key_context(mut self, key_context: impl Into<Arc<str>>) -> Self {
128        self.key_context = Some(key_context.into());
129        self
130    }
131
132    pub fn style(mut self, style: ChromeRefinement) -> Self
133    where
134        T: UiSupportsChrome,
135    {
136        self.patch.chrome = self.patch.chrome.merge(style);
137        self
138    }
139
140    pub fn layout(mut self, layout: LayoutRefinement) -> Self
141    where
142        T: UiSupportsLayout,
143    {
144        self.patch.layout = self.patch.layout.merge(layout);
145        self
146    }
147
148    pub fn style_with(self, f: impl FnOnce(ChromeRefinement) -> ChromeRefinement) -> Self
149    where
150        T: UiSupportsChrome,
151    {
152        self.style(f(ChromeRefinement::default()))
153    }
154
155    pub fn layout_with(self, f: impl FnOnce(LayoutRefinement) -> LayoutRefinement) -> Self
156    where
157        T: UiSupportsLayout,
158    {
159        self.layout(f(LayoutRefinement::default()))
160    }
161}
162
163macro_rules! forward_style_noargs {
164    ($($name:ident),+ $(,)?) => {
165        $(
166            pub fn $name(self) -> Self {
167                self.style_with(|c| c.$name())
168            }
169        )+
170    };
171}
172
173macro_rules! forward_layout_noargs {
174    ($($name:ident),+ $(,)?) => {
175        $(
176            pub fn $name(self) -> Self {
177                self.layout_with(|l| l.$name())
178            }
179        )+
180    };
181}
182
183impl UiBuilder<crate::ui::TextBox> {
184    /// Enables or disables drag-to-select + `edit.copy` for this text element.
185    ///
186    /// Notes:
187    /// - Keep this off by default for most UI labels to avoid gesture conflicts with pressable
188    ///   rows/buttons. Prefer a dedicated copy button in interactive surfaces.
189    pub fn selectable(mut self, selectable: bool) -> Self {
190        self.inner.selectable = selectable;
191        self
192    }
193
194    /// Convenience helper for `selectable(true)`.
195    pub fn selectable_on(self) -> Self {
196        self.selectable(true)
197    }
198
199    /// Convenience helper for `selectable(false)`.
200    pub fn selectable_off(self) -> Self {
201        self.selectable(false)
202    }
203
204    pub fn text_xs(mut self) -> Self {
205        self.inner.preset = crate::ui::TextPreset::Xs;
206        self.inner.wrap = TextWrap::Word;
207        self
208    }
209
210    pub fn text_sm(mut self) -> Self {
211        self.inner.preset = crate::ui::TextPreset::Sm;
212        self.inner.wrap = TextWrap::Word;
213        self
214    }
215
216    pub fn text_base(mut self) -> Self {
217        self.inner.preset = crate::ui::TextPreset::Base;
218        self.inner.wrap = TextWrap::Word;
219        self
220    }
221
222    pub fn text_prose(mut self) -> Self {
223        self.inner.preset = crate::ui::TextPreset::Prose;
224        self.inner.wrap = TextWrap::Word;
225        self
226    }
227
228    pub fn font_weight(mut self, weight: FontWeight) -> Self {
229        self.inner.weight_override = Some(weight);
230        self
231    }
232
233    pub fn font(mut self, font: FontId) -> Self {
234        self.inner.font_override = Some(font);
235        self
236    }
237
238    pub fn font_feature(mut self, tag: impl Into<String>, value: u32) -> Self {
239        self.inner
240            .features_override
241            .push(fret_core::TextFontFeatureSetting {
242                tag: tag.into().into(),
243                value,
244            });
245        self
246    }
247
248    pub fn font_axis(mut self, tag: impl Into<String>, value: f32) -> Self {
249        self.inner
250            .axes_override
251            .push(fret_core::TextFontAxisSetting {
252                tag: tag.into().into(),
253                value,
254            });
255        self
256    }
257
258    /// Enables OpenType tabular numbers (`font-variant-numeric: tabular-nums`).
259    pub fn tabular_nums(self) -> Self {
260        self.font_feature("tnum", 1)
261    }
262
263    /// Enables OpenType proportional numbers (`font-variant-numeric: proportional-nums`).
264    pub fn proportional_nums(self) -> Self {
265        self.font_feature("pnum", 1)
266    }
267
268    /// Enables OpenType lining numbers (`font-variant-numeric: lining-nums`).
269    pub fn lining_nums(self) -> Self {
270        self.font_feature("lnum", 1)
271    }
272
273    /// Enables OpenType oldstyle numbers (`font-variant-numeric: oldstyle-nums`).
274    pub fn oldstyle_nums(self) -> Self {
275        self.font_feature("onum", 1)
276    }
277
278    /// Enables OpenType slashed zero (`font-variant-numeric: slashed-zero`).
279    pub fn slashed_zero(self) -> Self {
280        self.font_feature("zero", 1)
281    }
282
283    /// Enables OpenType ordinal forms (`font-variant-numeric: ordinal`).
284    pub fn ordinal(self) -> Self {
285        self.font_feature("ordn", 1)
286    }
287
288    /// Enables OpenType diagonal fractions (`font-variant-numeric: diagonal-fractions`).
289    pub fn diagonal_fractions(self) -> Self {
290        self.font_feature("frac", 1)
291    }
292
293    /// Enables OpenType stacked fractions (`font-variant-numeric: stacked-fractions`).
294    pub fn stacked_fractions(self) -> Self {
295        self.font_feature("afrc", 1)
296    }
297
298    pub fn font_ui(self) -> Self {
299        self.font(FontId::ui())
300    }
301
302    pub fn font_monospace(self) -> Self {
303        self.font(FontId::monospace())
304    }
305
306    pub fn font_normal(self) -> Self {
307        self.font_weight(FontWeight::NORMAL)
308    }
309
310    pub fn font_medium(self) -> Self {
311        self.font_weight(FontWeight::MEDIUM)
312    }
313
314    pub fn font_semibold(self) -> Self {
315        self.font_weight(FontWeight::SEMIBOLD)
316    }
317
318    pub fn font_bold(self) -> Self {
319        self.font_weight(FontWeight::BOLD)
320    }
321
322    pub fn text_color(mut self, color: ColorRef) -> Self {
323        self.inner.color_override = Some(color);
324        self
325    }
326
327    pub fn text_size_px(mut self, size: Px) -> Self {
328        self.inner.size_override = Some(size);
329        self
330    }
331
332    pub fn line_height_px(mut self, height: Px) -> Self {
333        self.inner.line_height_override = Some(height);
334        if self.inner.line_height_policy_override.is_none() {
335            self.inner.line_height_policy_override = Some(TextLineHeightPolicy::FixedFromStyle);
336        }
337        self
338    }
339
340    pub fn line_height_em(mut self, line_height_em: f32) -> Self {
341        self.inner.line_height_em_override = Some(line_height_em);
342        if self.inner.line_height_policy_override.is_none() {
343            self.inner.line_height_policy_override = Some(TextLineHeightPolicy::FixedFromStyle);
344        }
345        self
346    }
347
348    pub fn line_height_preset(mut self, preset: crate::ui::TextLineHeightPreset) -> Self {
349        self.inner.line_height_em_override = Some(preset.em());
350        self.inner.line_height_policy_override = Some(TextLineHeightPolicy::FixedFromStyle);
351        self
352    }
353
354    pub fn line_height_policy(mut self, policy: TextLineHeightPolicy) -> Self {
355        self.inner.line_height_policy_override = Some(policy);
356        self
357    }
358
359    pub fn control(self) -> Self {
360        self.line_height_policy(TextLineHeightPolicy::FixedFromStyle)
361    }
362
363    pub fn content(self) -> Self {
364        self.line_height_policy(TextLineHeightPolicy::ExpandToFit)
365    }
366
367    /// Configures a fixed line box by setting both `line_height_px(height)` and `h_px(height)`.
368    ///
369    /// This is a pragmatic escape hatch for fixed-height controls (tabs, pills, buttons) where
370    /// centering by glyph bounds can read as slightly bottom-heavy. A fixed line box allows the
371    /// text widget to apply CSS/GPUI-like "half-leading" baseline placement.
372    pub fn fixed_line_box_px(mut self, height: Px) -> Self {
373        self.inner.line_height_policy_override = Some(TextLineHeightPolicy::FixedFromStyle);
374        self.line_height_px(height).h_px(height)
375    }
376
377    pub fn letter_spacing_em(mut self, letter_spacing_em: f32) -> Self {
378        self.inner.letter_spacing_em_override = Some(letter_spacing_em);
379        self
380    }
381
382    pub fn wrap(mut self, wrap: TextWrap) -> Self {
383        self.inner.wrap = wrap;
384        self
385    }
386
387    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
388        self.inner.overflow = overflow;
389        self
390    }
391
392    pub fn ink_overflow(mut self, ink_overflow: TextInkOverflow) -> Self {
393        self.inner.ink_overflow_override = Some(ink_overflow);
394        self
395    }
396
397    pub fn auto_pad_ink_overflow(self) -> Self {
398        self.ink_overflow(TextInkOverflow::AutoPad)
399    }
400
401    pub fn text_align(mut self, align: fret_core::TextAlign) -> Self {
402        self.inner.align = align;
403        self
404    }
405
406    pub fn nowrap(self) -> Self {
407        self.wrap(TextWrap::None).overflow(TextOverflow::Clip)
408    }
409
410    pub fn truncate(self) -> Self {
411        self.wrap(TextWrap::None).overflow(TextOverflow::Ellipsis)
412    }
413
414    /// Sets `TextWrap::WordBreak` and clips overflow.
415    ///
416    /// This matches Tailwind's `break-words` intent (prevent horizontal overflow by allowing
417    /// breaks inside long tokens such as URLs, paths, and identifiers).
418    pub fn break_words(self) -> Self {
419        self.wrap(TextWrap::WordBreak).overflow(TextOverflow::Clip)
420    }
421
422    /// Enables balanced line breaking (Tailwind `text-balance`).
423    pub fn text_balance(self) -> Self {
424        self.wrap(TextWrap::Balance)
425    }
426
427    /// Opt into "bounds-as-line-box" baseline placement for fixed-height controls.
428    ///
429    /// This is intended for single-line labels that should look vertically centered inside a
430    /// container whose height is larger than the natural line height.
431    pub fn line_box_in_bounds(mut self) -> Self {
432        self.inner.vertical_placement_override =
433            Some(fret_core::TextVerticalPlacement::BoundsAsLineBox);
434        self
435    }
436}
437
438impl UiBuilder<crate::ui::RawTextBox> {
439    pub fn text_color(mut self, color: ColorRef) -> Self {
440        self.inner.color_override = Some(color);
441        self
442    }
443
444    pub fn wrap(mut self, wrap: TextWrap) -> Self {
445        self.inner.wrap = wrap;
446        self
447    }
448
449    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
450        self.inner.overflow = overflow;
451        self
452    }
453
454    pub fn ink_overflow(mut self, ink_overflow: TextInkOverflow) -> Self {
455        self.inner.ink_overflow_override = Some(ink_overflow);
456        self
457    }
458
459    pub fn auto_pad_ink_overflow(self) -> Self {
460        self.ink_overflow(TextInkOverflow::AutoPad)
461    }
462
463    pub fn text_align(mut self, align: fret_core::TextAlign) -> Self {
464        self.inner.align = align;
465        self
466    }
467
468    pub fn nowrap(self) -> Self {
469        self.wrap(TextWrap::None).overflow(TextOverflow::Clip)
470    }
471
472    pub fn truncate(self) -> Self {
473        self.wrap(TextWrap::None).overflow(TextOverflow::Ellipsis)
474    }
475
476    /// Sets `TextWrap::WordBreak` and clips overflow.
477    ///
478    /// This matches Tailwind's `break-words` intent (prevent horizontal overflow by allowing
479    /// breaks inside long tokens such as URLs, paths, and identifiers).
480    pub fn break_words(self) -> Self {
481        self.wrap(TextWrap::WordBreak).overflow(TextOverflow::Clip)
482    }
483
484    /// Enables balanced line breaking (Tailwind `text-balance`).
485    pub fn text_balance(self) -> Self {
486        self.wrap(TextWrap::Balance)
487    }
488}
489
490impl UiBuilder<crate::ui::RichTextBox> {
491    pub fn text_style(mut self, style: fret_core::TextStyle) -> Self {
492        self.inner.style_override = Some(style);
493        self
494    }
495
496    pub fn text_color(mut self, color: ColorRef) -> Self {
497        self.inner.color_override = Some(color);
498        self
499    }
500
501    pub fn wrap(mut self, wrap: TextWrap) -> Self {
502        self.inner.wrap = wrap;
503        self
504    }
505
506    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
507        self.inner.overflow = overflow;
508        self
509    }
510
511    pub fn ink_overflow(mut self, ink_overflow: TextInkOverflow) -> Self {
512        self.inner.ink_overflow_override = Some(ink_overflow);
513        self
514    }
515
516    pub fn auto_pad_ink_overflow(self) -> Self {
517        self.ink_overflow(TextInkOverflow::AutoPad)
518    }
519
520    pub fn text_align(mut self, align: fret_core::TextAlign) -> Self {
521        self.inner.align = align;
522        self
523    }
524
525    pub fn nowrap(self) -> Self {
526        self.wrap(TextWrap::None).overflow(TextOverflow::Clip)
527    }
528
529    pub fn truncate(self) -> Self {
530        self.wrap(TextWrap::None).overflow(TextOverflow::Ellipsis)
531    }
532
533    pub fn break_words(self) -> Self {
534        self.wrap(TextWrap::WordBreak).overflow(TextOverflow::Clip)
535    }
536
537    pub fn text_balance(self) -> Self {
538        self.wrap(TextWrap::Balance)
539    }
540}
541
542impl<T: UiSupportsChrome> UiBuilder<T> {
543    pub fn paddings(self, paddings: impl Into<Edges4<MetricRef>>) -> Self {
544        self.style_with(|mut c| {
545            let Edges4 {
546                top,
547                right,
548                bottom,
549                left,
550            } = paddings.into();
551            let mut padding = c.padding.unwrap_or_default();
552            padding.top = Some(top);
553            padding.right = Some(right);
554            padding.bottom = Some(bottom);
555            padding.left = Some(left);
556            c.padding = Some(padding);
557            c
558        })
559    }
560
561    pub fn paddings_fraction(self, paddings: impl Into<Edges4<f32>>) -> Self {
562        self.style_with(|mut c| {
563            let Edges4 {
564                top,
565                right,
566                bottom,
567                left,
568            } = paddings.into();
569            let mut padding = c.padding_length.unwrap_or_default();
570            padding.top = Some(LengthRefinement::Fraction(top));
571            padding.right = Some(LengthRefinement::Fraction(right));
572            padding.bottom = Some(LengthRefinement::Fraction(bottom));
573            padding.left = Some(LengthRefinement::Fraction(left));
574            c.padding_length = Some(padding);
575            c
576        })
577    }
578
579    pub fn paddings_percent(self, paddings: impl Into<Edges4<f32>>) -> Self {
580        let Edges4 {
581            top,
582            right,
583            bottom,
584            left,
585        } = paddings.into();
586        self.paddings_fraction(Edges4 {
587            top: top / 100.0,
588            right: right / 100.0,
589            bottom: bottom / 100.0,
590            left: left / 100.0,
591        })
592    }
593
594    pub fn padding(self, padding: impl Into<MetricRef>) -> Self {
595        self.paddings(Edges4::all(padding.into()))
596    }
597
598    pub fn padding_fraction(self, fraction: f32) -> Self {
599        self.paddings_fraction(Edges4::all(fraction))
600    }
601
602    pub fn padding_percent(self, percent: f32) -> Self {
603        self.padding_fraction(percent / 100.0)
604    }
605
606    pub fn padding_px(self, px: Px) -> Self {
607        self.padding(px)
608    }
609
610    pub fn padding_space(self, space: Space) -> Self {
611        self.padding(space)
612    }
613
614    pub fn focused_border(self) -> Self {
615        self.style_with(ChromeRefinement::focused_border)
616    }
617
618    pub fn corner_radii(self, radii: impl Into<Corners4<MetricRef>>) -> Self {
619        self.style_with(|c| c.corner_radii(radii))
620    }
621
622    pub fn rounded_tl(self, radius: Radius) -> Self {
623        self.style_with(|c| c.rounded_tl(radius))
624    }
625
626    pub fn rounded_tr(self, radius: Radius) -> Self {
627        self.style_with(|c| c.rounded_tr(radius))
628    }
629
630    pub fn rounded_br(self, radius: Radius) -> Self {
631        self.style_with(|c| c.rounded_br(radius))
632    }
633
634    pub fn rounded_bl(self, radius: Radius) -> Self {
635        self.style_with(|c| c.rounded_bl(radius))
636    }
637
638    pub fn shadow_none(self) -> Self {
639        self.style_with(ChromeRefinement::shadow_none)
640    }
641
642    pub fn shadow_xs(self) -> Self {
643        self.style_with(ChromeRefinement::shadow_xs)
644    }
645
646    pub fn shadow_sm(self) -> Self {
647        self.style_with(ChromeRefinement::shadow_sm)
648    }
649
650    pub fn shadow_md(self) -> Self {
651        self.style_with(ChromeRefinement::shadow_md)
652    }
653
654    pub fn shadow_lg(self) -> Self {
655        self.style_with(ChromeRefinement::shadow_lg)
656    }
657
658    pub fn shadow_xl(self) -> Self {
659        self.style_with(ChromeRefinement::shadow_xl)
660    }
661
662    pub fn debug_border(self, color: ColorRef) -> Self {
663        self.style_with(|c| c.debug_border(color))
664    }
665
666    pub fn debug_border_primary(self) -> Self {
667        self.style_with(ChromeRefinement::debug_border_primary)
668    }
669
670    pub fn debug_border_destructive(self) -> Self {
671        self.style_with(ChromeRefinement::debug_border_destructive)
672    }
673
674    pub fn debug_border_ring(self) -> Self {
675        self.style_with(ChromeRefinement::debug_border_ring)
676    }
677
678    pub fn px(self, space: Space) -> Self {
679        self.style_with(|c| c.px(space))
680    }
681
682    pub fn py(self, space: Space) -> Self {
683        self.style_with(|c| c.py(space))
684    }
685
686    pub fn p(self, space: Space) -> Self {
687        self.style_with(|c| c.p(space))
688    }
689
690    pub fn pt(self, space: Space) -> Self {
691        self.style_with(|c| c.pt(space))
692    }
693
694    pub fn pr(self, space: Space) -> Self {
695        self.style_with(|c| c.pr(space))
696    }
697
698    pub fn pb(self, space: Space) -> Self {
699        self.style_with(|c| c.pb(space))
700    }
701
702    pub fn pl(self, space: Space) -> Self {
703        self.style_with(|c| c.pl(space))
704    }
705
706    pub fn rounded(self, radius: Radius) -> Self {
707        self.style_with(|c| c.rounded(radius))
708    }
709
710    pub fn border_width(self, width: impl Into<MetricRef>) -> Self {
711        self.style_with(|c| c.border_width(width))
712    }
713
714    pub fn radius(self, radius: impl Into<MetricRef>) -> Self {
715        self.style_with(|c| c.radius(radius))
716    }
717
718    pub fn bg(self, color: ColorRef) -> Self {
719        self.style_with(|c| c.bg(color))
720    }
721
722    pub fn border_color(self, color: ColorRef) -> Self {
723        self.style_with(|c| c.border_color(color))
724    }
725
726    pub fn text_color(self, color: ColorRef) -> Self {
727        self.style_with(|c| c.text_color(color))
728    }
729
730    forward_style_noargs!(
731        px_0, px_1, px_0p5, px_1p5, px_2, px_2p5, px_3, px_4, py_0, py_1, py_0p5, py_1p5, py_2,
732        py_2p5, py_3, py_4, p_0, p_1, p_0p5, p_1p5, p_2, p_2p5, p_3, p_4, rounded_md, border_1,
733    );
734}
735
736impl<T: UiSupportsLayout> UiBuilder<T> {
737    pub fn insets(self, insets: impl Into<Edges4<SignedMetricRef>>) -> Self {
738        self.layout_with(|mut l| {
739            let Edges4 {
740                top,
741                right,
742                bottom,
743                left,
744            } = insets.into();
745            let mut inset = l.inset.unwrap_or_default();
746            inset.top = Some(crate::style::InsetEdgeRefinement::Px(top));
747            inset.right = Some(crate::style::InsetEdgeRefinement::Px(right));
748            inset.bottom = Some(crate::style::InsetEdgeRefinement::Px(bottom));
749            inset.left = Some(crate::style::InsetEdgeRefinement::Px(left));
750            l.inset = Some(inset);
751            l
752        })
753    }
754
755    pub fn margins(self, margins: impl Into<Edges4<MarginEdge>>) -> Self {
756        self.layout_with(|mut l| {
757            let Edges4 {
758                top,
759                right,
760                bottom,
761                left,
762            } = margins.into();
763            let mut margin = l.margin.unwrap_or_default();
764            margin.top = Some(top.into());
765            margin.right = Some(right.into());
766            margin.bottom = Some(bottom.into());
767            margin.left = Some(left.into());
768            l.margin = Some(margin);
769            l
770        })
771    }
772
773    pub fn aspect_ratio(self, ratio: f32) -> Self {
774        self.layout_with(|l| l.aspect_ratio(ratio))
775    }
776
777    pub fn inset(self, space: Space) -> Self {
778        self.layout_with(|l| l.inset(space))
779    }
780
781    pub fn inset_px(self, px: Px) -> Self {
782        self.layout_with(|l| l.inset_px(px))
783    }
784
785    pub fn top(self, space: Space) -> Self {
786        self.layout_with(|l| l.top(space))
787    }
788
789    pub fn top_px(self, px: Px) -> Self {
790        self.layout_with(|l| l.top_px(px))
791    }
792
793    pub fn top_neg(self, space: Space) -> Self {
794        self.layout_with(|l| l.top_neg(space))
795    }
796
797    pub fn right(self, space: Space) -> Self {
798        self.layout_with(|l| l.right(space))
799    }
800
801    pub fn right_px(self, px: Px) -> Self {
802        self.layout_with(|l| l.right_px(px))
803    }
804
805    pub fn right_neg(self, space: Space) -> Self {
806        self.layout_with(|l| l.right_neg(space))
807    }
808
809    pub fn bottom(self, space: Space) -> Self {
810        self.layout_with(|l| l.bottom(space))
811    }
812
813    pub fn bottom_px(self, px: Px) -> Self {
814        self.layout_with(|l| l.bottom_px(px))
815    }
816
817    pub fn bottom_neg(self, space: Space) -> Self {
818        self.layout_with(|l| l.bottom_neg(space))
819    }
820
821    pub fn left(self, space: Space) -> Self {
822        self.layout_with(|l| l.left(space))
823    }
824
825    pub fn left_px(self, px: Px) -> Self {
826        self.layout_with(|l| l.left_px(px))
827    }
828
829    pub fn left_neg(self, space: Space) -> Self {
830        self.layout_with(|l| l.left_neg(space))
831    }
832
833    pub fn m(self, space: Space) -> Self {
834        self.layout_with(|l| l.m(space))
835    }
836
837    pub fn m_px(self, px: Px) -> Self {
838        self.layout_with(|l| l.m_px(px))
839    }
840
841    pub fn m_neg(self, space: Space) -> Self {
842        self.layout_with(|l| l.m_neg(space))
843    }
844
845    pub fn mx(self, space: Space) -> Self {
846        self.layout_with(|l| l.mx(space))
847    }
848
849    pub fn mx_px(self, px: Px) -> Self {
850        self.layout_with(|l| l.mx_px(px))
851    }
852
853    pub fn mx_neg(self, space: Space) -> Self {
854        self.layout_with(|l| l.mx_neg(space))
855    }
856
857    pub fn my(self, space: Space) -> Self {
858        self.layout_with(|l| l.my(space))
859    }
860
861    pub fn my_px(self, px: Px) -> Self {
862        self.layout_with(|l| l.my_px(px))
863    }
864
865    pub fn my_neg(self, space: Space) -> Self {
866        self.layout_with(|l| l.my_neg(space))
867    }
868
869    pub fn mt(self, space: Space) -> Self {
870        self.layout_with(|l| l.mt(space))
871    }
872
873    pub fn mt_px(self, px: Px) -> Self {
874        self.layout_with(|l| l.mt_px(px))
875    }
876
877    pub fn mt_neg(self, space: Space) -> Self {
878        self.layout_with(|l| l.mt_neg(space))
879    }
880
881    pub fn mr(self, space: Space) -> Self {
882        self.layout_with(|l| l.mr(space))
883    }
884
885    pub fn mr_px(self, px: Px) -> Self {
886        self.layout_with(|l| l.mr_px(px))
887    }
888
889    pub fn mr_neg(self, space: Space) -> Self {
890        self.layout_with(|l| l.mr_neg(space))
891    }
892
893    pub fn mb(self, space: Space) -> Self {
894        self.layout_with(|l| l.mb(space))
895    }
896
897    pub fn mb_px(self, px: Px) -> Self {
898        self.layout_with(|l| l.mb_px(px))
899    }
900
901    pub fn mb_neg(self, space: Space) -> Self {
902        self.layout_with(|l| l.mb_neg(space))
903    }
904
905    pub fn ml(self, space: Space) -> Self {
906        self.layout_with(|l| l.ml(space))
907    }
908
909    pub fn ml_px(self, px: Px) -> Self {
910        self.layout_with(|l| l.ml_px(px))
911    }
912
913    pub fn ml_neg(self, space: Space) -> Self {
914        self.layout_with(|l| l.ml_neg(space))
915    }
916
917    pub fn min_w(self, width: impl Into<MetricRef>) -> Self {
918        self.layout_with(|l| l.min_w(width))
919    }
920
921    pub fn min_w_space(self, width: Space) -> Self {
922        self.layout_with(|l| l.min_w_space(width))
923    }
924
925    pub fn min_h(self, height: impl Into<MetricRef>) -> Self {
926        self.layout_with(|l| l.min_h(height))
927    }
928
929    pub fn min_h_space(self, height: Space) -> Self {
930        self.layout_with(|l| l.min_h_space(height))
931    }
932
933    pub fn w(self, width: LengthRefinement) -> Self {
934        self.layout_with(|l| l.w(width))
935    }
936
937    pub fn h(self, height: LengthRefinement) -> Self {
938        self.layout_with(|l| l.h(height))
939    }
940
941    pub fn w_px(self, width: impl Into<MetricRef>) -> Self {
942        self.layout_with(|l| l.w_px(width))
943    }
944
945    pub fn w_space(self, width: Space) -> Self {
946        self.layout_with(|l| l.w_space(width))
947    }
948
949    pub fn h_px(self, height: impl Into<MetricRef>) -> Self {
950        self.layout_with(|l| l.h_px(height))
951    }
952
953    pub fn h_space(self, height: Space) -> Self {
954        self.layout_with(|l| l.h_space(height))
955    }
956
957    pub fn max_w(self, width: impl Into<MetricRef>) -> Self {
958        self.layout_with(|l| l.max_w(width))
959    }
960
961    pub fn max_w_space(self, width: Space) -> Self {
962        self.layout_with(|l| l.max_w_space(width))
963    }
964
965    pub fn max_h(self, height: impl Into<MetricRef>) -> Self {
966        self.layout_with(|l| l.max_h(height))
967    }
968
969    pub fn max_h_space(self, height: Space) -> Self {
970        self.layout_with(|l| l.max_h_space(height))
971    }
972
973    pub fn basis(self, basis: LengthRefinement) -> Self {
974        self.layout_with(|l| l.basis(basis))
975    }
976
977    pub fn basis_px(self, basis: impl Into<MetricRef>) -> Self {
978        self.layout_with(|l| l.basis_px(basis))
979    }
980
981    pub fn flex_grow(self, grow: f32) -> Self {
982        self.layout_with(|l| l.flex_grow(grow))
983    }
984
985    pub fn flex_shrink(self, shrink: f32) -> Self {
986        self.layout_with(|l| l.flex_shrink(shrink))
987    }
988
989    pub fn align_self(self, align: CrossAlign) -> Self {
990        self.layout_with(|l| l.align_self(align))
991    }
992
993    pub fn justify_self(self, align: CrossAlign) -> Self {
994        self.layout_with(|l| l.justify_self(align))
995    }
996
997    forward_layout_noargs!(
998        relative,
999        absolute,
1000        overflow_hidden,
1001        overflow_visible,
1002        overflow_x_hidden,
1003        overflow_y_hidden,
1004        m_auto,
1005        mx_auto,
1006        my_auto,
1007        mt_auto,
1008        mr_auto,
1009        mb_auto,
1010        ml_auto,
1011        min_w_0,
1012        w_full,
1013        h_full,
1014        size_full,
1015        basis_0,
1016        flex_shrink_0,
1017        flex_1,
1018        flex_none,
1019        self_start,
1020        self_center,
1021        self_end,
1022        self_stretch,
1023        justify_self_start,
1024        justify_self_center,
1025        justify_self_end,
1026        justify_self_stretch,
1027        w_0,
1028        h_0,
1029        min_h_0,
1030        max_w_0,
1031        max_h_0,
1032        w_0p5,
1033        h_0p5,
1034        min_w_0p5,
1035        min_h_0p5,
1036        max_w_0p5,
1037        max_h_0p5,
1038        w_1,
1039        h_1,
1040        min_w_1,
1041        min_h_1,
1042        max_w_1,
1043        max_h_1,
1044        w_1p5,
1045        h_1p5,
1046        min_w_1p5,
1047        min_h_1p5,
1048        max_w_1p5,
1049        max_h_1p5,
1050        w_2,
1051        h_2,
1052        min_w_2,
1053        min_h_2,
1054        max_w_2,
1055        max_h_2,
1056        w_2p5,
1057        h_2p5,
1058        min_w_2p5,
1059        min_h_2p5,
1060        max_w_2p5,
1061        max_h_2p5,
1062        w_3,
1063        h_3,
1064        min_w_3,
1065        min_h_3,
1066        max_w_3,
1067        max_h_3,
1068        w_3p5,
1069        h_3p5,
1070        min_w_3p5,
1071        min_h_3p5,
1072        max_w_3p5,
1073        max_h_3p5,
1074        w_4,
1075        h_4,
1076        min_w_4,
1077        min_h_4,
1078        max_w_4,
1079        max_h_4,
1080        w_5,
1081        h_5,
1082        min_w_5,
1083        min_h_5,
1084        max_w_5,
1085        max_h_5,
1086        w_6,
1087        h_6,
1088        min_w_6,
1089        min_h_6,
1090        max_w_6,
1091        max_h_6,
1092        w_8,
1093        h_8,
1094        min_w_8,
1095        min_h_8,
1096        max_w_8,
1097        max_h_8,
1098        w_10,
1099        h_10,
1100        min_w_10,
1101        min_h_10,
1102        max_w_10,
1103        max_h_10,
1104        w_11,
1105        h_11,
1106        min_w_11,
1107        min_h_11,
1108        max_w_11,
1109        max_h_11,
1110    );
1111}
1112
1113impl<T: UiPatchTarget> UiBuilder<T> {
1114    pub fn build(self) -> T {
1115        self.inner.apply_ui_patch(self.patch)
1116    }
1117}
1118
1119impl<H, F> UiBuilder<crate::ui::FlexBox<H, F>> {
1120    pub fn gap(mut self, gap: impl Into<MetricRef>) -> Self {
1121        self.inner.gap = gap.into();
1122        self.inner.gap_length = None;
1123        self
1124    }
1125
1126    pub fn gap_px(self, gap: Px) -> Self {
1127        self.gap(gap)
1128    }
1129
1130    pub fn gap_metric(self, gap: MetricRef) -> Self {
1131        self.gap(gap)
1132    }
1133
1134    pub fn gap_fraction(mut self, fraction: f32) -> Self {
1135        self.inner.gap_length = Some(LengthRefinement::Fraction(fraction));
1136        self
1137    }
1138
1139    pub fn gap_percent(self, percent: f32) -> Self {
1140        self.gap_fraction(percent / 100.0)
1141    }
1142
1143    pub fn gap_full(mut self) -> Self {
1144        self.inner.gap_length = Some(LengthRefinement::Fill);
1145        self
1146    }
1147
1148    pub fn justify(mut self, justify: Justify) -> Self {
1149        self.inner.justify = justify;
1150        self
1151    }
1152
1153    pub fn justify_start(self) -> Self {
1154        self.justify(Justify::Start)
1155    }
1156
1157    pub fn justify_center(self) -> Self {
1158        self.justify(Justify::Center)
1159    }
1160
1161    pub fn justify_end(self) -> Self {
1162        self.justify(Justify::End)
1163    }
1164
1165    pub fn justify_between(self) -> Self {
1166        self.justify(Justify::Between)
1167    }
1168
1169    pub fn items(mut self, items: Items) -> Self {
1170        self.inner.items = items;
1171        self
1172    }
1173
1174    pub fn items_start(self) -> Self {
1175        self.items(Items::Start)
1176    }
1177
1178    pub fn items_center(self) -> Self {
1179        self.items(Items::Center)
1180    }
1181
1182    pub fn items_end(self) -> Self {
1183        self.items(Items::End)
1184    }
1185
1186    pub fn items_stretch(self) -> Self {
1187        self.items(Items::Stretch)
1188    }
1189
1190    pub fn wrap(mut self) -> Self {
1191        self.inner.wrap = true;
1192        self
1193    }
1194
1195    pub fn no_wrap(mut self) -> Self {
1196        self.inner.wrap = false;
1197        self
1198    }
1199}
1200
1201impl<H, B> UiBuilder<crate::ui::FlexBoxBuild<H, B>> {
1202    pub fn gap(mut self, gap: impl Into<MetricRef>) -> Self {
1203        self.inner.gap = gap.into();
1204        self.inner.gap_length = None;
1205        self
1206    }
1207
1208    pub fn gap_px(self, gap: Px) -> Self {
1209        self.gap(gap)
1210    }
1211
1212    pub fn gap_metric(self, gap: MetricRef) -> Self {
1213        self.gap(gap)
1214    }
1215
1216    pub fn gap_fraction(mut self, fraction: f32) -> Self {
1217        self.inner.gap_length = Some(LengthRefinement::Fraction(fraction));
1218        self
1219    }
1220
1221    pub fn gap_percent(self, percent: f32) -> Self {
1222        self.gap_fraction(percent / 100.0)
1223    }
1224
1225    pub fn gap_full(mut self) -> Self {
1226        self.inner.gap_length = Some(LengthRefinement::Fill);
1227        self
1228    }
1229
1230    pub fn justify(mut self, justify: Justify) -> Self {
1231        self.inner.justify = justify;
1232        self
1233    }
1234
1235    pub fn justify_start(self) -> Self {
1236        self.justify(Justify::Start)
1237    }
1238
1239    pub fn justify_center(self) -> Self {
1240        self.justify(Justify::Center)
1241    }
1242
1243    pub fn justify_end(self) -> Self {
1244        self.justify(Justify::End)
1245    }
1246
1247    pub fn justify_between(self) -> Self {
1248        self.justify(Justify::Between)
1249    }
1250
1251    pub fn items(mut self, items: Items) -> Self {
1252        self.inner.items = items;
1253        self
1254    }
1255
1256    pub fn items_start(self) -> Self {
1257        self.items(Items::Start)
1258    }
1259
1260    pub fn items_center(self) -> Self {
1261        self.items(Items::Center)
1262    }
1263
1264    pub fn items_end(self) -> Self {
1265        self.items(Items::End)
1266    }
1267
1268    pub fn items_stretch(self) -> Self {
1269        self.items(Items::Stretch)
1270    }
1271
1272    pub fn wrap(mut self) -> Self {
1273        self.inner.wrap = true;
1274        self
1275    }
1276
1277    pub fn no_wrap(mut self) -> Self {
1278        self.inner.wrap = false;
1279        self
1280    }
1281}
1282
1283impl<H, F> UiBuilder<crate::ui::ScrollAreaBox<H, F>> {
1284    pub fn axis(mut self, axis: ScrollAxis) -> Self {
1285        self.inner.axis = axis;
1286        self
1287    }
1288
1289    pub fn show_scrollbar_x(mut self, show: bool) -> Self {
1290        self.inner.show_scrollbar_x = show;
1291        self
1292    }
1293
1294    pub fn show_scrollbar_y(mut self, show: bool) -> Self {
1295        self.inner.show_scrollbar_y = show;
1296        self
1297    }
1298
1299    pub fn show_scrollbars(self, x: bool, y: bool) -> Self {
1300        self.show_scrollbar_x(x).show_scrollbar_y(y)
1301    }
1302
1303    pub fn handle(mut self, handle: ScrollHandle) -> Self {
1304        self.inner.handle = Some(handle);
1305        self
1306    }
1307}
1308
1309impl<H, B> UiBuilder<crate::ui::ScrollAreaBoxBuild<H, B>> {
1310    pub fn axis(mut self, axis: ScrollAxis) -> Self {
1311        self.inner.axis = axis;
1312        self
1313    }
1314
1315    pub fn show_scrollbar_x(mut self, show: bool) -> Self {
1316        self.inner.show_scrollbar_x = show;
1317        self
1318    }
1319
1320    pub fn show_scrollbar_y(mut self, show: bool) -> Self {
1321        self.inner.show_scrollbar_y = show;
1322        self
1323    }
1324
1325    pub fn show_scrollbars(self, x: bool, y: bool) -> Self {
1326        self.show_scrollbar_x(x).show_scrollbar_y(y)
1327    }
1328
1329    pub fn handle(mut self, handle: ScrollHandle) -> Self {
1330        self.inner.handle = Some(handle);
1331        self
1332    }
1333}
1334
1335#[track_caller]
1336fn render_ui_builder_target_into_element<H: UiHost, T>(
1337    value: T,
1338    cx: &mut ElementContext<'_, H>,
1339) -> AnyElement
1340where
1341    T: IntoUiElement<H>,
1342{
1343    IntoUiElement::into_element(value, cx)
1344}
1345
1346#[track_caller]
1347fn finalize_ui_builder_element<H: UiHost, T: UiPatchTarget>(
1348    builder: UiBuilder<T>,
1349    cx: &mut ElementContext<'_, H>,
1350    render: impl FnOnce(T, &mut ElementContext<'_, H>) -> AnyElement,
1351) -> AnyElement {
1352    let UiBuilder {
1353        inner,
1354        patch,
1355        semantics,
1356        key_context,
1357    } = builder;
1358    let builder = UiBuilder {
1359        inner,
1360        patch,
1361        semantics: None,
1362        key_context: None,
1363    };
1364    let el = render(builder.build(), cx);
1365    let el = match semantics {
1366        Some(decoration) => el.attach_semantics(decoration),
1367        None => el,
1368    };
1369    match key_context {
1370        Some(key_context) => el.key_context(key_context),
1371        None => el,
1372    }
1373}
1374
1375impl<H: UiHost, T> IntoUiElement<H> for UiBuilder<T>
1376where
1377    T: UiPatchTarget + IntoUiElement<H>,
1378{
1379    #[track_caller]
1380    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1381        let loc = Location::caller();
1382        finalize_ui_builder_element(self, cx, move |built, cx| {
1383            cx.scope_at(loc, |cx| render_ui_builder_target_into_element(built, cx))
1384        })
1385    }
1386}
1387
1388impl<T: UiPatchTarget> UiBuilder<T> {
1389    #[track_caller]
1390    pub fn into_element<H: UiHost>(self, cx: &mut ElementContext<'_, H>) -> AnyElement
1391    where
1392        T: IntoUiElement<H>,
1393    {
1394        let loc = Location::caller();
1395        finalize_ui_builder_element(self, cx, move |built, cx| {
1396            cx.scope_at(loc, |cx| render_ui_builder_target_into_element(built, cx))
1397        })
1398    }
1399}
1400
1401impl<H: UiHost, B> IntoUiElement<H> for UiBuilder<crate::ui::ContainerPropsBoxBuild<H, B>>
1402where
1403    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1404{
1405    #[track_caller]
1406    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1407        UiBuilder::<crate::ui::ContainerPropsBoxBuild<H, B>>::into_element(self, cx)
1408    }
1409}
1410
1411impl<H: UiHost, B> UiBuilder<crate::ui::ContainerPropsBoxBuild<H, B>>
1412where
1413    B: FnOnce(&mut ElementContext<'_, H>, &mut Vec<AnyElement>),
1414{
1415    #[track_caller]
1416    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1417        let UiBuilder {
1418            inner,
1419            patch: _,
1420            semantics,
1421            key_context,
1422        } = self;
1423        let el = inner.into_element(cx);
1424        let el = match semantics {
1425            Some(decoration) => el.attach_semantics(decoration),
1426            None => el,
1427        };
1428        match key_context {
1429            Some(key_context) => el.key_context(key_context),
1430            None => el,
1431        }
1432    }
1433}
1434
1435impl<H: UiHost, F, I> IntoUiElement<H> for UiBuilder<crate::ui::ContainerPropsBox<H, F>>
1436where
1437    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1438    I: IntoIterator,
1439    I::Item: crate::IntoUiElement<H>,
1440{
1441    #[track_caller]
1442    fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1443        UiBuilder::<crate::ui::ContainerPropsBox<H, F>>::into_element(self, cx)
1444    }
1445}
1446
1447impl<H: UiHost, F, I> UiBuilder<crate::ui::ContainerPropsBox<H, F>>
1448where
1449    F: FnOnce(&mut ElementContext<'_, H>) -> I,
1450    I: IntoIterator,
1451    I::Item: crate::IntoUiElement<H>,
1452{
1453    #[track_caller]
1454    pub fn into_element(self, cx: &mut ElementContext<'_, H>) -> AnyElement {
1455        let UiBuilder {
1456            inner,
1457            patch: _,
1458            semantics,
1459            key_context,
1460        } = self;
1461        let el = inner.into_element(cx);
1462        let el = match semantics {
1463            Some(decoration) => el.attach_semantics(decoration),
1464            None => el,
1465        };
1466        match key_context {
1467            Some(key_context) => el.key_context(key_context),
1468            None => el,
1469        }
1470    }
1471}
1472
1473/// Extension trait providing the `ui()` entrypoint for types that opt into `UiPatchTarget`.
1474///
1475/// Most of the `ui::*` helpers already return a `UiBuilder<T>`. This trait is primarily useful for:
1476/// - custom patch targets (your own boxes/components),
1477/// - values constructed via inherent constructors that return `T` (not `UiBuilder<T>`).
1478///
1479/// ```
1480/// use fret_ui_kit::{ChromeRefinement, LayoutRefinement, UiExt, UiPatch, UiPatchTarget, UiSupportsChrome, UiSupportsLayout};
1481///
1482/// #[derive(Debug, Default, Clone)]
1483/// struct MyBox {
1484///     chrome: ChromeRefinement,
1485///     layout: LayoutRefinement,
1486/// }
1487///
1488/// impl UiPatchTarget for MyBox {
1489///     fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1490///         self.chrome = self.chrome.merge(patch.chrome);
1491///         self.layout = self.layout.merge(patch.layout);
1492///         self
1493///     }
1494/// }
1495///
1496/// impl UiSupportsChrome for MyBox {}
1497/// impl UiSupportsLayout for MyBox {}
1498///
1499/// let _refined = MyBox::default().ui().px_2().w_full().build();
1500/// ```
1501pub trait UiExt: UiPatchTarget + Sized {
1502    fn ui(self) -> UiBuilder<Self> {
1503        UiBuilder::new(self)
1504    }
1505}
1506
1507impl<T: UiPatchTarget> UiExt for T {}
1508
1509#[cfg(test)]
1510mod tests {
1511    use super::*;
1512    use crate::{LengthRefinement, MetricRef};
1513    use fret_core::Axis;
1514    use fret_core::Color;
1515    use fret_core::Px;
1516
1517    #[derive(Debug, Default, Clone)]
1518    struct Dummy {
1519        chrome: ChromeRefinement,
1520        layout: LayoutRefinement,
1521    }
1522
1523    impl UiPatchTarget for Dummy {
1524        fn apply_ui_patch(mut self, patch: UiPatch) -> Self {
1525            self.chrome = self.chrome.merge(patch.chrome);
1526            self.layout = self.layout.merge(patch.layout);
1527            self
1528        }
1529    }
1530
1531    impl UiSupportsChrome for Dummy {}
1532    impl UiSupportsLayout for Dummy {}
1533
1534    #[test]
1535    fn ui_builder_merges_chrome_and_layout() {
1536        let dummy = Dummy::default()
1537            .ui()
1538            .px_3()
1539            .py_2()
1540            .border_1()
1541            .rounded_md()
1542            .w_full()
1543            .build();
1544
1545        let padding = dummy.chrome.padding.expect("expected padding refinement");
1546        match padding.left {
1547            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N3.token_key()),
1548            _ => panic!("expected left padding token"),
1549        }
1550        match padding.top {
1551            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N2.token_key()),
1552            _ => panic!("expected top padding token"),
1553        }
1554
1555        assert!(dummy.chrome.border_width.is_some());
1556        assert!(dummy.chrome.radius.is_some());
1557
1558        let size = dummy.layout.size.expect("expected size refinement");
1559        match size.width {
1560            Some(LengthRefinement::Fill) => {}
1561            other => panic!("expected width Fill, got {other:?}"),
1562        }
1563        assert!(size.min_width.is_none());
1564        assert!(size.min_height.is_none());
1565    }
1566
1567    #[test]
1568    fn ui_builder_allows_px_and_space_mix() {
1569        let dummy = Dummy::default()
1570            .ui()
1571            .style_with(|mut c| {
1572                c.min_height = Some(Px(40.0).into());
1573                c
1574            })
1575            .build();
1576        assert!(dummy.chrome.min_height.is_some());
1577    }
1578
1579    #[test]
1580    fn ui_builder_edges4_helpers_write_fields() {
1581        let dummy = Dummy::default()
1582            .ui()
1583            .paddings(Edges4::trbl(Space::N1, Space::N2, Space::N3, Space::N4))
1584            .margins(Edges4::trbl(
1585                MarginEdge::auto(),
1586                Space::N2.into(),
1587                Space::N3.into(),
1588                Space::N4.into(),
1589            ))
1590            .insets(Edges4::all(Space::N1).neg())
1591            .focused_border()
1592            .corner_radii(Corners4::tltrbrbl(
1593                Radius::Sm,
1594                Radius::Md,
1595                Radius::Lg,
1596                Radius::Full,
1597            ))
1598            .rounded_tl(Radius::Lg)
1599            .shadow_md()
1600            .debug_border_primary()
1601            .debug_border_destructive()
1602            .build();
1603
1604        let padding = dummy.chrome.padding.expect("expected padding refinement");
1605        match padding.top {
1606            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N1.token_key()),
1607            other => panic!("expected top padding token, got {other:?}"),
1608        }
1609        match padding.right {
1610            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N2.token_key()),
1611            other => panic!("expected right padding token, got {other:?}"),
1612        }
1613        match padding.bottom {
1614            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N3.token_key()),
1615            other => panic!("expected bottom padding token, got {other:?}"),
1616        }
1617        match padding.left {
1618            Some(MetricRef::Token { key, .. }) => assert_eq!(key, Space::N4.token_key()),
1619            other => panic!("expected left padding token, got {other:?}"),
1620        }
1621
1622        let margin = dummy.layout.margin.expect("expected margin refinement");
1623        assert!(matches!(
1624            margin.top,
1625            Some(crate::style::MarginEdgeRefinement::Auto)
1626        ));
1627        match margin.right {
1628            Some(crate::style::MarginEdgeRefinement::Px(SignedMetricRef::Pos(
1629                MetricRef::Token { key, .. },
1630            ))) => assert_eq!(key, Space::N2.token_key()),
1631            other => panic!("expected right margin token, got {other:?}"),
1632        }
1633
1634        let inset = dummy.layout.inset.expect("expected inset refinement");
1635        match inset.left {
1636            Some(crate::style::InsetEdgeRefinement::Px(SignedMetricRef::Neg(
1637                MetricRef::Token { key, .. },
1638            ))) => {
1639                assert_eq!(key, Space::N1.token_key())
1640            }
1641            other => panic!("expected left inset negative token, got {other:?}"),
1642        }
1643
1644        match dummy.chrome.border_color {
1645            Some(ColorRef::Token { key, .. }) => assert_eq!(key, "destructive"),
1646            other => panic!("expected debug_border_destructive to set border_color, got {other:?}"),
1647        }
1648
1649        assert_eq!(dummy.chrome.shadow, Some(crate::style::ShadowPreset::Md));
1650
1651        let radii = dummy
1652            .chrome
1653            .corner_radii
1654            .expect("expected corner radii refinement");
1655        match radii.top_left {
1656            Some(MetricRef::Token { key, .. }) => assert_eq!(key, "component.radius.lg"),
1657            other => panic!("expected top_left token radius, got {other:?}"),
1658        }
1659    }
1660
1661    #[test]
1662    fn ui_builder_forwards_full_vocabulary_smoke() {
1663        let _ = Dummy::default()
1664            .ui()
1665            // ChromeRefinement
1666            .paddings(Edges4::all(Space::N1))
1667            .px(Space::N1)
1668            .py(Space::N2)
1669            .p(Space::N3)
1670            .pt(Space::N0p5)
1671            .pr(Space::N1p5)
1672            .pb(Space::N2p5)
1673            .pl(Space::N3p5)
1674            .rounded(Radius::Full)
1675            .bg(ColorRef::Color(Color {
1676                r: 0.1,
1677                g: 0.2,
1678                b: 0.3,
1679                a: 1.0,
1680            }))
1681            .border_color(ColorRef::Color(Color {
1682                r: 0.3,
1683                g: 0.2,
1684                b: 0.1,
1685                a: 1.0,
1686            }))
1687            .text_color(ColorRef::Color(Color {
1688                r: 0.9,
1689                g: 0.9,
1690                b: 0.9,
1691                a: 1.0,
1692            }))
1693            .px_0()
1694            .px_1()
1695            .px_0p5()
1696            .px_1p5()
1697            .px_2()
1698            .px_2p5()
1699            .px_3()
1700            .px_4()
1701            .py_0()
1702            .py_1()
1703            .py_0p5()
1704            .py_1p5()
1705            .py_2()
1706            .py_2p5()
1707            .py_3()
1708            .py_4()
1709            .p_0()
1710            .p_1()
1711            .p_0p5()
1712            .p_1p5()
1713            .p_2()
1714            .p_2p5()
1715            .p_3()
1716            .p_4()
1717            .rounded_md()
1718            .border_1()
1719            // LayoutRefinement
1720            .aspect_ratio(1.0)
1721            .relative()
1722            .absolute()
1723            .overflow_hidden()
1724            .overflow_visible()
1725            .overflow_x_hidden()
1726            .overflow_y_hidden()
1727            .margins(Edges4::all(Space::N1))
1728            .insets(Edges4::all(Space::N1))
1729            .inset(Space::N2)
1730            .top(Space::N3)
1731            .top_neg(Space::N3)
1732            .right(Space::N3)
1733            .right_neg(Space::N3)
1734            .bottom(Space::N3)
1735            .bottom_neg(Space::N3)
1736            .left(Space::N3)
1737            .left_neg(Space::N3)
1738            .m(Space::N2)
1739            .m_neg(Space::N2)
1740            .m_auto()
1741            .mx(Space::N2)
1742            .mx_neg(Space::N2)
1743            .mx_auto()
1744            .my(Space::N2)
1745            .my_neg(Space::N2)
1746            .my_auto()
1747            .mt(Space::N2)
1748            .mt_neg(Space::N2)
1749            .mt_auto()
1750            .mr(Space::N2)
1751            .mr_neg(Space::N2)
1752            .mr_auto()
1753            .mb(Space::N2)
1754            .mb_neg(Space::N2)
1755            .mb_auto()
1756            .ml(Space::N2)
1757            .ml_neg(Space::N2)
1758            .ml_auto()
1759            .min_w(Px(10.0))
1760            .min_w_space(Space::N1)
1761            .min_h(Px(10.0))
1762            .min_h_space(Space::N1)
1763            .min_w_0()
1764            .w(LengthRefinement::Fill)
1765            .h(LengthRefinement::Auto)
1766            .w_px(Px(10.0))
1767            .w_space(Space::N10)
1768            .h_px(Px(11.0))
1769            .h_space(Space::N11)
1770            .w_full()
1771            .h_full()
1772            .size_full()
1773            .max_w(Px(10.0))
1774            .max_w_space(Space::N1)
1775            .max_h(Px(10.0))
1776            .max_h_space(Space::N1)
1777            .basis(LengthRefinement::Auto)
1778            .basis_px(Px(10.0))
1779            .basis_0()
1780            .flex_grow(1.0)
1781            .flex_shrink(1.0)
1782            .flex_shrink_0()
1783            .flex_1()
1784            .flex_none()
1785            // LayoutRefinement shorthands
1786            .w_0()
1787            .h_0()
1788            .min_h_0()
1789            .max_w_0()
1790            .max_h_0()
1791            .w_0p5()
1792            .h_0p5()
1793            .min_w_0p5()
1794            .min_h_0p5()
1795            .max_w_0p5()
1796            .max_h_0p5()
1797            .w_1()
1798            .h_1()
1799            .min_w_1()
1800            .min_h_1()
1801            .max_w_1()
1802            .max_h_1()
1803            .w_1p5()
1804            .h_1p5()
1805            .min_w_1p5()
1806            .min_h_1p5()
1807            .max_w_1p5()
1808            .max_h_1p5()
1809            .w_2()
1810            .h_2()
1811            .min_w_2()
1812            .min_h_2()
1813            .max_w_2()
1814            .max_h_2()
1815            .w_2p5()
1816            .h_2p5()
1817            .min_w_2p5()
1818            .min_h_2p5()
1819            .max_w_2p5()
1820            .max_h_2p5()
1821            .w_3()
1822            .h_3()
1823            .min_w_3()
1824            .min_h_3()
1825            .max_w_3()
1826            .max_h_3()
1827            .w_3p5()
1828            .h_3p5()
1829            .min_w_3p5()
1830            .min_h_3p5()
1831            .max_w_3p5()
1832            .max_h_3p5()
1833            .w_4()
1834            .h_4()
1835            .min_w_4()
1836            .min_h_4()
1837            .max_w_4()
1838            .max_h_4()
1839            .w_5()
1840            .h_5()
1841            .min_w_5()
1842            .min_h_5()
1843            .max_w_5()
1844            .max_h_5()
1845            .w_6()
1846            .h_6()
1847            .min_w_6()
1848            .min_h_6()
1849            .max_w_6()
1850            .max_h_6()
1851            .w_8()
1852            .h_8()
1853            .min_w_8()
1854            .min_h_8()
1855            .max_w_8()
1856            .max_h_8()
1857            .w_10()
1858            .h_10()
1859            .min_w_10()
1860            .min_h_10()
1861            .max_w_10()
1862            .max_h_10()
1863            .w_11()
1864            .h_11()
1865            .min_w_11()
1866            .min_h_11()
1867            .max_w_11()
1868            .max_h_11()
1869            .build();
1870    }
1871
1872    #[test]
1873    fn ui_flex_box_builder_records_gap_and_alignment() {
1874        let flex = crate::ui::FlexBox::<(), ()>::new(Axis::Horizontal, ())
1875            .ui()
1876            .gap(Space::N2)
1877            .justify_between()
1878            .items_center()
1879            .wrap()
1880            .build();
1881
1882        assert!(matches!(flex.gap, MetricRef::Token { key, .. } if key == Space::N2.token_key()));
1883        assert_eq!(flex.justify, Justify::Between);
1884        assert_eq!(flex.items, Items::Center);
1885        assert!(flex.wrap);
1886    }
1887}