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 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#[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
132pub 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#[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#[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
405pub 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
418pub 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
433pub 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
444pub 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
454pub 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
464pub 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
479pub 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
490pub 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#[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#[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#[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#[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
715pub 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
728pub 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
736pub 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
749pub 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#[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#[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
1169pub 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
1183pub 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#[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
1258pub 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#[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#[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#[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#[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
1520pub 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
1544pub 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
1557pub 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
1576pub 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#[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 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
1805pub fn text(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1810 UiBuilder::new(TextBox::new(text, TextPreset::Sm))
1811}
1812
1813pub fn text_block(content: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1818 text(content).w_full()
1819}
1820
1821pub fn selectable_text(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1827 crate::ui::text(text).selectable_on()
1828}
1829
1830pub fn selectable_text_block(content: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1832 selectable_text(content).w_full()
1833}
1834
1835pub fn label(text: impl Into<Arc<str>>) -> UiBuilder<TextBox> {
1837 UiBuilder::new(TextBox::new(text, TextPreset::Label))
1838}
1839
1840#[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#[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
1939pub fn rich_text(rich: AttributedText) -> UiBuilder<RichTextBox> {
1941 UiBuilder::new(RichTextBox::new(rich))
1942}
1943
1944#[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
1997pub 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
2051pub 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}