1use frus_core::{Alignment, Rect, Scene, Size};
17use frus_layout::{Dimension, Style};
18
19use crate::interaction::Status;
20use crate::theme::Theme;
21use crate::widget::{FillAxes, Widget};
22
23pub struct LimitedBox<Msg = crate::callback::Callback> {
37 max: (Option<f32>, Option<f32>),
38 children: Vec<Box<dyn Widget<Msg>>>,
39}
40
41impl<Msg> LimitedBox<Msg> {
42 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
44 Self {
45 max: (None, None),
46 children: vec![Box::new(child)],
47 }
48 }
49
50 pub fn max_width(mut self, max: f32) -> Self {
52 self.max.0 = Some(max.max(0.0));
53 self
54 }
55
56 pub fn max_height(mut self, max: f32) -> Self {
58 self.max.1 = Some(max.max(0.0));
59 self
60 }
61}
62
63impl<Msg: Clone> Widget<Msg> for LimitedBox<Msg> {
64 fn style(&self) -> Style {
65 Style::default()
66 }
67
68 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
69 &self.children
70 }
71
72 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
73
74 fn on_click(&self) -> Option<Msg> {
75 None
76 }
77
78 fn constraints_transform(&self) -> Option<ConstraintsTransform> {
79 let rule = |max: Option<f32>| match max {
80 Some(max) => AxisConstraint::Limited(max),
81 None => AxisConstraint::Loose,
82 };
83 Some(ConstraintsTransform {
84 width: rule(self.max.0),
85 height: rule(self.max.1),
86 alignment: Alignment::TOP_LEFT,
87 report: false,
88 })
89 }
90
91 fn debug_name(&self) -> &'static str {
92 "LimitedBox"
93 }
94}
95
96pub struct SizedBox<Msg = crate::callback::Callback> {
102 width: Dimension,
103 height: Dimension,
104 fills: FillAxes,
106 children: Vec<Box<dyn Widget<Msg>>>,
107}
108
109impl<Msg> SizedBox<Msg> {
110 pub fn empty() -> Self {
112 Self {
113 width: Dimension::Auto,
114 height: Dimension::Auto,
115 fills: FillAxes::NONE,
116 children: Vec::new(),
117 }
118 }
119
120 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
122 let mut boxed = Self::empty();
123 boxed.children.push(Box::new(child));
124 boxed
125 }
126
127 pub fn square(side: f32, child: impl Widget<Msg> + 'static) -> Self {
129 Self::new(child).width(side).height(side)
130 }
131
132 pub fn expand(child: impl Widget<Msg> + 'static) -> Self {
140 let mut boxed = Self::new(child);
141 boxed.fills = FillAxes::BOTH;
142 boxed
143 }
144
145 pub fn shrink(child: impl Widget<Msg> + 'static) -> Self {
149 Self::new(child)
150 }
151
152 pub fn width(self, width: f32) -> Self {
154 self.width_dimension(Dimension::Length(width))
155 }
156
157 pub fn height(self, height: f32) -> Self {
159 self.height_dimension(Dimension::Length(height))
160 }
161
162 pub fn width_fraction(self, fraction: f32) -> Self {
164 self.width_dimension(Dimension::Percent(fraction))
165 }
166
167 pub fn height_fraction(self, fraction: f32) -> Self {
169 self.height_dimension(Dimension::Percent(fraction))
170 }
171
172 fn width_dimension(mut self, width: Dimension) -> Self {
173 self.width = width;
174 self
175 }
176
177 fn height_dimension(mut self, height: Dimension) -> Self {
178 self.height = height;
179 self
180 }
181
182 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
184 self.children.clear();
185 self.children.push(Box::new(child));
186 self
187 }
188}
189
190impl<Msg: Clone> Widget<Msg> for SizedBox<Msg> {
191 fn style(&self) -> Style {
192 let base = Style {
193 width: self.width,
194 height: self.height,
195 ..Default::default()
196 };
197 match self.fills == FillAxes::BOTH {
200 true => Style {
201 overlap: true,
202 align: frus_layout::Align::Stretch,
203 ..base
204 },
205 false => base,
206 }
207 }
208
209 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
210 &self.children
211 }
212
213 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
214 }
216
217 fn on_click(&self) -> Option<Msg> {
218 None
219 }
220
221 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
225 self.fills
226 }
227}
228
229pub struct ConstrainedBox<Msg = crate::callback::Callback> {
236 min_width: Dimension,
237 max_width: Dimension,
238 min_height: Dimension,
239 max_height: Dimension,
240 children: Vec<Box<dyn Widget<Msg>>>,
241}
242
243impl<Msg> ConstrainedBox<Msg> {
244 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
246 Self {
247 min_width: Dimension::Auto,
248 max_width: Dimension::Auto,
249 min_height: Dimension::Auto,
250 max_height: Dimension::Auto,
251 children: vec![Box::new(child)],
252 }
253 }
254
255 pub fn new_boxed(child: Box<dyn Widget<Msg>>) -> Self {
258 Self {
259 min_width: Dimension::Auto,
260 max_width: Dimension::Auto,
261 min_height: Dimension::Auto,
262 max_height: Dimension::Auto,
263 children: vec![child],
264 }
265 }
266
267 pub fn tight(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
269 Self::new(child)
270 .min_width(width)
271 .max_width(width)
272 .min_height(height)
273 .max_height(height)
274 }
275
276 pub fn loose(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
278 Self::new(child).max_width(width).max_height(height)
279 }
280
281 pub fn min_width(mut self, width: f32) -> Self {
283 self.min_width = Dimension::Length(width);
284 self
285 }
286
287 pub fn max_width(mut self, width: f32) -> Self {
289 self.max_width = Dimension::Length(width);
290 self
291 }
292
293 pub fn min_height(mut self, height: f32) -> Self {
295 self.min_height = Dimension::Length(height);
296 self
297 }
298
299 pub fn max_height(mut self, height: f32) -> Self {
301 self.max_height = Dimension::Length(height);
302 self
303 }
304}
305
306impl<Msg: Clone> Widget<Msg> for ConstrainedBox<Msg> {
307 fn style(&self) -> Style {
308 Style {
309 min_width: self.min_width,
310 max_width: self.max_width,
311 min_height: self.min_height,
312 max_height: self.max_height,
313 ..Default::default()
314 }
315 }
316
317 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
318 &self.children
319 }
320
321 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
322
323 fn on_click(&self) -> Option<Msg> {
324 None
325 }
326}
327
328#[derive(Copy, Clone, Debug, PartialEq, Eq)]
331pub enum IntrinsicAxis {
332 Width,
334 Height,
336}
337
338pub struct Intrinsic<Msg = crate::callback::Callback> {
348 axis: IntrinsicAxis,
349 step: Option<f32>,
350 children: Vec<Box<dyn Widget<Msg>>>,
351}
352
353pub type IntrinsicWidth<Msg> = Intrinsic<Msg>;
355pub type IntrinsicHeight<Msg> = Intrinsic<Msg>;
357
358impl<Msg> Intrinsic<Msg> {
359 pub fn width(child: impl Widget<Msg> + 'static) -> Self {
361 Self {
362 axis: IntrinsicAxis::Width,
363 step: None,
364 children: vec![Box::new(child)],
365 }
366 }
367
368 pub fn height(child: impl Widget<Msg> + 'static) -> Self {
370 Self {
371 axis: IntrinsicAxis::Height,
372 step: None,
373 children: vec![Box::new(child)],
374 }
375 }
376
377 pub fn step(mut self, step: f32) -> Self {
382 self.step = (step > 0.0).then_some(step);
383 self
384 }
385
386 pub fn axis(&self) -> IntrinsicAxis {
388 self.axis
389 }
390
391 pub fn quantise(&self, extent: f32) -> f32 {
393 match self.step {
394 Some(step) => (extent / step).ceil() * step,
395 None => extent,
396 }
397 }
398}
399
400impl<Msg: Clone> Widget<Msg> for Intrinsic<Msg> {
401 fn style(&self) -> Style {
402 Style::default()
405 }
406
407 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
408 &self.children
409 }
410
411 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
412
413 fn on_click(&self) -> Option<Msg> {
414 None
415 }
416
417 fn intrinsic(&self) -> Option<(IntrinsicAxis, Option<f32>)> {
418 Some((self.axis, self.step))
419 }
420}
421
422#[derive(Copy, Clone, Debug, PartialEq)]
424pub struct Overflow {
425 pub width: Option<f32>,
427 pub height: Option<f32>,
429 pub unconstrained: bool,
432 pub natural_height: bool,
436 pub alignment: Alignment,
438}
439
440pub struct OverflowBox<Msg = crate::callback::Callback> {
453 overflow: Overflow,
454 children: Vec<Box<dyn Widget<Msg>>>,
455}
456
457impl<Msg> OverflowBox<Msg> {
458 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
461 Self {
462 overflow: Overflow {
463 width: None,
464 height: None,
465 unconstrained: false,
466 natural_height: false,
467 alignment: Alignment::CENTER,
468 },
469 children: vec![Box::new(child)],
470 }
471 }
472
473 pub fn unconstrained(child: impl Widget<Msg> + 'static) -> Self {
476 let mut boxed = Self::new(child);
477 boxed.overflow.unconstrained = true;
478 boxed
479 }
480
481 pub fn width(mut self, width: f32) -> Self {
483 self.overflow.width = Some(width);
484 self
485 }
486
487 pub fn height(mut self, height: f32) -> Self {
489 self.overflow.height = Some(height);
490 self
491 }
492
493 pub fn natural_height(mut self) -> Self {
496 self.overflow.natural_height = true;
497 self
498 }
499
500 pub fn alignment(mut self, alignment: Alignment) -> Self {
502 self.overflow.alignment = alignment;
503 self
504 }
505}
506
507impl<Msg: Clone> Widget<Msg> for OverflowBox<Msg> {
508 fn style(&self) -> Style {
509 Style {
514 width: Dimension::Percent(1.0),
515 height: Dimension::Percent(1.0),
516 ..Default::default()
517 }
518 }
519
520 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
521 &self.children
522 }
523
524 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
525
526 fn on_click(&self) -> Option<Msg> {
527 None
528 }
529
530 fn overflow_box(&self) -> Option<Overflow> {
531 Some(self.overflow)
532 }
533}
534
535impl Overflow {
536 pub(crate) fn child_size(&self, own: Size) -> Size {
539 Size::new(
540 self.width.unwrap_or(own.width),
541 self.height.unwrap_or(own.height),
542 )
543 }
544
545 pub(crate) fn origin(&self, own: Rect, child: Size) -> (f32, f32) {
547 (
548 own.x + (own.width - child.width) * self.alignment.fraction_x(),
549 own.y + (own.height - child.height) * self.alignment.fraction_y(),
550 )
551 }
552}
553
554pub struct SizedOverflowBox<Msg = crate::callback::Callback> {
568 width: f32,
569 height: f32,
570 overflow: Overflow,
571 children: Vec<Box<dyn Widget<Msg>>>,
572}
573
574impl<Msg> SizedOverflowBox<Msg> {
575 pub fn new(width: f32, height: f32, child: impl Widget<Msg> + 'static) -> Self {
578 Self {
579 width,
580 height,
581 overflow: Overflow {
582 width: None,
583 height: None,
584 unconstrained: true,
585 natural_height: false,
586 alignment: Alignment::CENTER,
587 },
588 children: vec![Box::new(child)],
589 }
590 }
591
592 pub fn child_size(mut self, width: f32, height: f32) -> Self {
594 self.overflow.unconstrained = false;
595 self.overflow.width = Some(width);
596 self.overflow.height = Some(height);
597 self
598 }
599
600 pub fn alignment(mut self, alignment: Alignment) -> Self {
602 self.overflow.alignment = alignment;
603 self
604 }
605}
606
607impl<Msg: Clone> Widget<Msg> for SizedOverflowBox<Msg> {
608 fn style(&self) -> Style {
609 Style {
610 width: Dimension::Length(self.width),
611 height: Dimension::Length(self.height),
612 ..Default::default()
613 }
614 }
615
616 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
617 &self.children
618 }
619
620 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
621
622 fn on_click(&self) -> Option<Msg> {
623 None
624 }
625
626 fn overflow_box(&self) -> Option<Overflow> {
627 Some(self.overflow)
628 }
629
630 fn debug_name(&self) -> &'static str {
631 "SizedOverflowBox"
632 }
633}
634
635#[derive(Clone, Copy, Debug, PartialEq)]
637pub enum AxisConstraint {
638 AsGiven,
640 Unbounded,
643 Fixed(f32),
645 Loose,
649 Limited(f32),
652}
653
654#[derive(Clone, Copy, Debug)]
670pub struct ConstraintsTransform {
671 pub width: AxisConstraint,
673 pub height: AxisConstraint,
675 pub alignment: Alignment,
677 pub report: bool,
681}
682
683impl ConstraintsTransform {
684 pub(crate) fn layout_hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
688 use std::hash::Hash;
689 for axis in [self.width, self.height] {
690 match axis {
691 AxisConstraint::AsGiven => 0u8.hash(hasher),
692 AxisConstraint::Unbounded => 1u8.hash(hasher),
693 AxisConstraint::Fixed(v) => {
694 2u8.hash(hasher);
695 v.to_bits().hash(hasher);
696 }
697 AxisConstraint::Loose => 3u8.hash(hasher),
698 AxisConstraint::Limited(v) => {
699 4u8.hash(hasher);
700 v.to_bits().hash(hasher);
701 }
702 }
703 }
704 self.alignment.x.to_bits().hash(hasher);
705 self.alignment.y.to_bits().hash(hasher);
706 }
707
708 pub fn new() -> Self {
710 Self {
711 width: AxisConstraint::AsGiven,
712 height: AxisConstraint::AsGiven,
713 alignment: Alignment::CENTER,
714 report: true,
715 }
716 }
717
718 pub fn width(mut self, width: AxisConstraint) -> Self {
720 self.width = width;
721 self
722 }
723
724 pub fn height(mut self, height: AxisConstraint) -> Self {
726 self.height = height;
727 self
728 }
729
730 pub fn alignment(mut self, alignment: Alignment) -> Self {
732 self.alignment = alignment;
733 self
734 }
735
736 pub fn report(mut self, report: bool) -> Self {
739 self.report = report;
740 self
741 }
742}
743
744impl Default for ConstraintsTransform {
745 fn default() -> Self {
746 Self::new()
747 }
748}
749
750impl AxisConstraint {
751 pub(crate) fn offered(self, offer: Option<f32>) -> (f32, bool, bool) {
756 match self {
757 AxisConstraint::Unbounded => (0.0, true, false),
758 AxisConstraint::Fixed(extent) => (extent.max(0.0), false, true),
759 AxisConstraint::AsGiven => match offer {
760 Some(extent) => (extent.max(0.0), false, true),
761 None => (0.0, true, false),
762 },
763 AxisConstraint::Loose => match offer {
764 Some(extent) => (extent.max(0.0), false, false),
765 None => (0.0, true, false),
766 },
767 AxisConstraint::Limited(most) => match offer {
768 Some(extent) => (extent.max(0.0), false, false),
769 None => (most.max(0.0), false, false),
770 },
771 }
772 }
773
774 pub(crate) fn at(self, own: f32) -> (f32, bool, bool) {
786 match self {
787 AxisConstraint::Unbounded => (0.0, true, false),
788 AxisConstraint::Fixed(extent) => (extent.max(0.0), false, true),
789 AxisConstraint::AsGiven => (own.max(0.0), false, true),
790 AxisConstraint::Loose | AxisConstraint::Limited(_) => (own.max(0.0), false, false),
791 }
792 }
793}
794
795pub struct ConstraintsTransformBox<Msg = crate::callback::Callback> {
813 transform: ConstraintsTransform,
814 children: Vec<Box<dyn Widget<Msg>>>,
815}
816
817impl<Msg> ConstraintsTransformBox<Msg> {
818 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
820 Self {
821 transform: ConstraintsTransform::new(),
822 children: vec![Box::new(child)],
823 }
824 }
825
826 pub fn width(mut self, width: AxisConstraint) -> Self {
828 self.transform.width = width;
829 self
830 }
831
832 pub fn height(mut self, height: AxisConstraint) -> Self {
834 self.transform.height = height;
835 self
836 }
837
838 pub fn alignment(mut self, alignment: Alignment) -> Self {
840 self.transform.alignment = alignment;
841 self
842 }
843
844 pub fn report(mut self, report: bool) -> Self {
846 self.transform.report = report;
847 self
848 }
849
850 pub fn with(child: impl Widget<Msg> + 'static, transform: ConstraintsTransform) -> Self {
852 Self {
853 transform,
854 children: vec![Box::new(child)],
855 }
856 }
857}
858
859impl<Msg: Clone> Widget<Msg> for ConstraintsTransformBox<Msg> {
860 fn style(&self) -> Style {
861 Style::default()
862 }
863
864 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
865 &self.children
866 }
867
868 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
869
870 fn on_click(&self) -> Option<Msg> {
871 None
872 }
873
874 fn constraints_transform(&self) -> Option<ConstraintsTransform> {
875 Some(self.transform)
876 }
877
878 fn debug_name(&self) -> &'static str {
879 "ConstraintsTransformBox"
880 }
881}
882
883pub struct UnconstrainedBox;
905
906impl UnconstrainedBox {
907 #[allow(clippy::new_ret_no_self)]
909 pub fn new<Msg: Clone + 'static>(
910 child: impl Widget<Msg> + 'static,
911 ) -> ConstraintsTransformBox<Msg> {
912 ConstraintsTransformBox::new(child)
913 .width(AxisConstraint::Unbounded)
914 .height(AxisConstraint::Unbounded)
915 }
916
917 pub fn axis<Msg: Clone + 'static>(
919 child: impl Widget<Msg> + 'static,
920 axis: crate::scroll::Axis,
921 ) -> ConstraintsTransformBox<Msg> {
922 let freed = |free: bool| match free {
923 true => AxisConstraint::Unbounded,
924 false => AxisConstraint::AsGiven,
925 };
926 ConstraintsTransformBox::new(child)
927 .width(freed(axis.free_x()))
928 .height(freed(axis.free_y()))
929 }
930}
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::{build_ui, Container, Flex, InspectorNode, Runtime, Text};
935 use frus_core::{Color, Primitive};
936
937 const RED: Color = Color {
938 r: 1.0,
939 g: 0.0,
940 b: 0.0,
941 a: 1.0,
942 };
943
944 fn filler() -> Container<()> {
948 Container::new().flex(1.0).color(RED)
949 }
950
951 fn red_box<W: Widget<()> + 'static>(root: W, available: Size) -> Rect {
953 let runtime = Runtime::default();
954 let ui = build_ui(&root, available, &runtime, &Theme::dark());
955 ui.scene()
956 .primitives()
957 .iter()
958 .find_map(|p| match p {
959 Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
960 Some(*rect)
961 }
962 _ => None,
963 })
964 .expect("the red box")
965 }
966
967 #[test]
968 fn a_ceiling_stops_a_box_that_would_fill() {
969 let root = Flex::<()>::column()
971 .width(400.0)
972 .child(ConstrainedBox::new(filler().height(40.0)).max_width(120.0));
973 let rect = red_box(root, Size::new(400.0, 200.0));
974 assert!((rect.width - 120.0).abs() < 0.5, "capped: {rect:?}");
975 }
976
977 #[test]
978 fn a_floor_stops_a_box_that_would_hug() {
979 let root = Flex::<()>::row()
982 .width(400.0)
983 .height(60.0)
984 .child(ConstrainedBox::new(filler()).min_width(90.0))
985 .child(Container::new().width(10.0).height(10.0));
989 let rect = red_box(root, Size::new(400.0, 200.0));
990 assert!((rect.width - 90.0).abs() < 0.5, "floored: {rect:?}");
991 }
992
993 #[test]
994 fn a_tight_box_is_that_size_whatever_is_inside_it() {
995 let root = Flex::<()>::column()
996 .width(400.0)
997 .child(ConstrainedBox::tight(75.0, 33.0, filler()));
998 let rect = red_box(root, Size::new(400.0, 200.0));
999 assert!(
1000 (rect.width - 75.0).abs() < 0.5 && (rect.height - 33.0).abs() < 0.5,
1001 "tight: {rect:?}"
1002 );
1003 }
1004
1005 #[test]
1006 fn a_sized_box_that_expands_takes_everything_on_offer() {
1007 let root = Flex::<()>::column()
1008 .width(400.0)
1009 .height(200.0)
1010 .child(SizedBox::expand(filler()));
1011 let rect = red_box(root, Size::new(400.0, 200.0));
1012 assert!(
1013 (rect.width - 400.0).abs() < 0.5 && (rect.height - 200.0).abs() < 0.5,
1014 "expanded: {rect:?}"
1015 );
1016 }
1017
1018 #[test]
1019 fn a_sized_box_is_the_size_it_was_given() {
1020 let root = Flex::<()>::column()
1021 .width(400.0)
1022 .child(SizedBox::square(64.0, filler()));
1023 let rect = red_box(root, Size::new(400.0, 200.0));
1024 assert!(
1025 (rect.width - 64.0).abs() < 0.5 && (rect.height - 64.0).abs() < 0.5,
1026 "square: {rect:?}"
1027 );
1028 }
1029
1030 #[test]
1031 fn an_intrinsic_width_is_what_the_content_wants_not_what_the_column_offers() {
1032 let root = Flex::<()>::column().width(400.0).child(Intrinsic::width(
1035 Flex::<()>::column()
1036 .child(Text::new("a short line"))
1037 .child(Container::new().height(10.0).color(RED)),
1038 ));
1039 let rect = red_box(root, Size::new(400.0, 200.0));
1040 assert!(
1041 rect.width > 1.0 && rect.width < 380.0,
1042 "the content's width, not the column's: {rect:?}"
1043 );
1044 }
1045
1046 #[test]
1047 fn an_intrinsic_width_rounds_up_to_its_step() {
1048 let step = Intrinsic::<()>::width(Container::new()).step(20.0);
1049 assert_eq!(step.quantise(1.0), 20.0);
1050 assert_eq!(step.quantise(20.0), 20.0);
1051 assert_eq!(step.quantise(21.0), 40.0);
1052 let plain = Intrinsic::<()>::width(Container::new());
1054 assert_eq!(plain.quantise(21.0), 21.0);
1055 }
1056
1057 #[test]
1058 fn an_overflowing_child_is_bigger_than_its_box_and_centred_on_it() {
1059 let root = Flex::<()>::column().width(400.0).child(
1061 SizedBox::empty()
1062 .width(40.0)
1063 .height(40.0)
1064 .child(OverflowBox::new(filler()).width(100.0).height(100.0)),
1065 );
1066 let rect = red_box(root, Size::new(400.0, 200.0));
1067 assert!(
1068 (rect.width - 100.0).abs() < 0.5 && (rect.height - 100.0).abs() < 0.5,
1069 "the child keeps its own size: {rect:?}"
1070 );
1071 assert!(
1072 rect.x < -29.0 && rect.x > -31.0,
1073 "centred, spilling: {rect:?}"
1074 );
1075 }
1076
1077 #[test]
1078 fn an_overflow_box_can_anchor_the_spill_to_one_corner() {
1079 let root = Flex::<()>::column().width(400.0).child(
1080 SizedBox::empty().width(40.0).height(40.0).child(
1081 OverflowBox::new(filler())
1082 .width(100.0)
1083 .height(100.0)
1084 .alignment(Alignment::TOP_LEFT),
1085 ),
1086 );
1087 let rect = red_box(root, Size::new(400.0, 200.0));
1088 assert!(
1089 rect.x.abs() < 0.5 && rect.y.abs() < 0.5,
1090 "anchored top-left, spilling right and down: {rect:?}"
1091 );
1092 }
1093
1094 #[test]
1095 fn the_box_around_an_overflowing_child_does_not_grow() {
1096 let root = Flex::<()>::column()
1099 .width(400.0)
1100 .child(
1101 SizedBox::empty().width(40.0).height(40.0).child(
1102 OverflowBox::new(Container::new().flex(1.0))
1103 .width(100.0)
1104 .height(100.0),
1105 ),
1106 )
1107 .child(Container::new().width(10.0).height(10.0).color(RED));
1108 let rect = red_box(root, Size::new(400.0, 300.0));
1109 assert!((rect.y - 40.0).abs() < 0.5, "the neighbour moved: {rect:?}");
1110 }
1111
1112 #[test]
1113 fn an_unconstrained_child_is_the_size_it_asks_for() {
1114 let root = Flex::<()>::column().width(400.0).child(
1116 SizedBox::empty()
1117 .width(20.0)
1118 .height(20.0)
1119 .child(OverflowBox::unconstrained(
1120 Container::new().width(90.0).height(30.0).color(RED),
1121 )),
1122 );
1123 let rect = red_box(root, Size::new(400.0, 200.0));
1124 assert!(
1125 (rect.width - 90.0).abs() < 0.5 && (rect.height - 30.0).abs() < 0.5,
1126 "unconstrained: {rect:?}"
1127 );
1128 }
1129
1130 fn inspected(root: &dyn Widget<()>, available: Size) -> (crate::Ui<()>, Vec<InspectorNode>) {
1132 crate::build_ui_inspected(root, available, &Runtime::default(), &Theme::dark())
1133 }
1134
1135 fn box_named(nodes: &[InspectorNode], name: &str) -> Rect {
1138 nodes
1139 .iter()
1140 .find(|n| n.name == name)
1141 .unwrap_or_else(|| panic!("no {name} in {}", crate::dump_tree(nodes)))
1142 .rect
1143 }
1144
1145 fn red(width: f32, height: f32) -> Container<()> {
1147 Container::new().width(width).height(height).color(RED)
1148 }
1149
1150 fn narrow_column(width: f32) -> Flex<()> {
1153 Flex::<()>::column()
1154 .width(width)
1155 .align(frus_layout::Align::Start)
1156 }
1157
1158 #[test]
1159 fn a_child_that_fits_gives_the_unconstrained_box_its_own_size() {
1160 let root = narrow_column(400.0).child(UnconstrainedBox::new(red(90.0, 30.0)));
1161 let (ui, nodes) = inspected(&root, Size::new(400.0, 200.0));
1162 let own = box_named(&nodes, "ConstraintsTransformBox");
1163 assert!(
1164 (own.width - 90.0).abs() < 0.5 && (own.height - 30.0).abs() < 0.5,
1165 "the box is the child's size: {own:?}"
1166 );
1167 assert!(ui.overflows().is_empty(), "nothing ran past anything");
1168 }
1169
1170 #[test]
1174 fn an_unconstrained_child_that_does_not_fit_overflows_and_says_so() {
1175 let root = narrow_column(100.0).child(UnconstrainedBox::new(red(300.0, 20.0)));
1176 let (ui, nodes) = inspected(&root, Size::new(100.0, 200.0));
1177 let child = red_box_in(&ui);
1178 assert!(
1179 (child.width - 300.0).abs() < 0.5,
1180 "the child kept its size: {child:?}"
1181 );
1182 let own = box_named(&nodes, "ConstraintsTransformBox");
1183 assert!(
1184 (own.width - 100.0).abs() < 0.5,
1185 "the box kept the room it was offered: {own:?}"
1186 );
1187 let spills = ui.overflows();
1190 let mut sides: Vec<_> = spills.iter().map(|o| (o.side, o.amount)).collect();
1191 sides.sort_by_key(|(side, _)| format!("{side:?}"));
1192 assert_eq!(sides.len(), 2, "two edges: {spills:?}");
1193 assert_eq!(sides[0].0, frus_layout::Side::Left);
1194 assert_eq!(sides[1].0, frus_layout::Side::Right);
1195 assert!(
1196 sides.iter().all(|(_, amount)| (amount - 100.0).abs() < 0.5),
1197 "a hundred either side: {spills:?}"
1198 );
1199 }
1200
1201 #[test]
1204 fn freeing_one_axis_leaves_the_other_one_alone() {
1205 let root = narrow_column(100.0).child(UnconstrainedBox::axis(
1206 Container::new().height(200.0).color(RED),
1207 crate::scroll::Axis::Vertical,
1208 ));
1209 let (ui, _) = inspected(&root, Size::new(100.0, 80.0));
1210 let child = red_box_in(&ui);
1211 assert!(
1212 (child.width - 100.0).abs() < 0.5,
1213 "held to the column: {child:?}"
1214 );
1215 assert!(
1216 (child.height - 200.0).abs() < 0.5,
1217 "and as tall as it asked: {child:?}"
1218 );
1219 }
1220
1221 #[test]
1225 fn a_transform_can_give_the_child_a_room_of_its_own() {
1226 let root = narrow_column(200.0).child(
1227 ConstraintsTransformBox::new(Container::new().height(20.0).color(RED))
1228 .width(AxisConstraint::Fixed(100.0)),
1229 );
1230 let (ui, nodes) = inspected(&root, Size::new(200.0, 200.0));
1231 let child = red_box_in(&ui);
1232 assert!(
1233 (child.width - 100.0).abs() < 0.5,
1234 "the child was laid out in the room it was given: {child:?}"
1235 );
1236 assert!((box_named(&nodes, "ConstraintsTransformBox").width - 100.0).abs() < 0.5);
1237 assert!(ui.overflows().is_empty(), "half of it fits in all of it");
1238 }
1239
1240 #[test]
1241 fn a_sized_overflow_box_reports_one_size_and_lays_its_child_out_at_another() {
1242 let root = Flex::<()>::column()
1243 .width(400.0)
1244 .child(SizedOverflowBox::new(40.0, 40.0, red(100.0, 100.0)));
1245 let (ui, nodes) = inspected(&root, Size::new(400.0, 300.0));
1246 let own = box_named(&nodes, "SizedOverflowBox");
1247 assert!(
1248 (own.width - 40.0).abs() < 0.5 && (own.height - 40.0).abs() < 0.5,
1249 "the box is the size it stated: {own:?}"
1250 );
1251 let child = red_box_in(&ui);
1252 assert!(
1253 (child.width - 100.0).abs() < 0.5,
1254 "the child is not: {child:?}"
1255 );
1256 assert!(
1257 (child.x + 30.0).abs() < 0.5 && (child.y + 30.0).abs() < 0.5,
1258 "centred on the box it hangs out of: {child:?}"
1259 );
1260 }
1261
1262 fn red_box_in(ui: &crate::Ui<()>) -> Rect {
1264 ui.scene()
1265 .primitives()
1266 .iter()
1267 .find_map(|p| match p {
1268 Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
1269 Some(*rect)
1270 }
1271 _ => None,
1272 })
1273 .expect("the red box")
1274 }
1275
1276 fn expanding() -> Container<()> {
1279 Container::new()
1280 .color(RED)
1281 .child(crate::Center::new(SizedBox::empty()))
1282 }
1283
1284 #[test]
1287 fn a_limited_box_caps_a_child_where_the_room_has_no_end() {
1288 let root = crate::SingleChildScrollView::<()>::new()
1289 .width(200.0)
1290 .height(300.0)
1291 .child(Flex::column().child(LimitedBox::new(expanding()).max_height(120.0)));
1292 let rect = red_box(root, Size::new(200.0, 300.0));
1293 assert!((rect.height - 120.0).abs() < 0.5, "{rect:?}");
1294 assert!((rect.width - 200.0).abs() < 0.5, "{rect:?}");
1295 }
1296
1297 #[test]
1300 fn a_limited_box_leaves_a_bounded_room_alone() {
1301 let rect = red_box(
1302 LimitedBox::new(expanding()).max_height(120.0),
1303 Size::new(200.0, 300.0),
1304 );
1305 assert!((rect.height - 300.0).abs() < 0.5, "{rect:?}");
1306 }
1307
1308 #[test]
1310 fn a_limited_box_does_not_grow_a_child_that_hugs() {
1311 let root = crate::SingleChildScrollView::<()>::new()
1312 .width(200.0)
1313 .height(300.0)
1314 .child(
1315 Flex::column().child(
1316 LimitedBox::new(Container::new().width(40.0).height(20.0).color(RED))
1317 .max_height(120.0),
1318 ),
1319 );
1320 let rect = red_box(root, Size::new(200.0, 300.0));
1321 assert!((rect.height - 20.0).abs() < 0.5, "{rect:?}");
1322 assert!((rect.width - 40.0).abs() < 0.5, "{rect:?}");
1323 }
1324
1325 #[test]
1328 fn a_limited_box_caps_across_too() {
1329 let root = crate::SingleChildScrollView::<()>::new()
1330 .axis(crate::scroll::Axis::Horizontal)
1331 .width(300.0)
1332 .height(100.0)
1333 .child(Flex::row().child(LimitedBox::new(expanding()).max_width(90.0)));
1334 let rect = red_box(root, Size::new(300.0, 100.0));
1335 assert!((rect.width - 90.0).abs() < 0.5, "{rect:?}");
1336 }
1337
1338 #[test]
1341 fn a_paragraph_wraps_at_the_limit() {
1342 let words = "one two three four five six seven eight nine ten eleven twelve";
1343 let root = crate::SingleChildScrollView::<()>::new()
1344 .axis(crate::scroll::Axis::Horizontal)
1345 .width(300.0)
1346 .height(200.0)
1347 .child(
1348 Flex::row().child(
1349 LimitedBox::new(Container::new().color(RED).child(Text::new(words)))
1350 .max_width(120.0),
1351 ),
1352 );
1353 let rect = red_box(root, Size::new(300.0, 200.0));
1354 assert!((rect.width - 120.0).abs() < 0.5, "{rect:?}");
1355 assert!(rect.height > 50.0, "several lines: {rect:?}");
1356 }
1357
1358 #[test]
1362 fn a_sized_box_that_expands_is_as_wide_as_a_scroll() {
1363 let root = crate::SingleChildScrollView::<()>::new()
1364 .width(200.0)
1365 .height(300.0)
1366 .child(
1367 Flex::column().child(SizedBox::expand(Container::new().color(RED)).height(50.0)),
1368 );
1369 let rect = red_box(root, Size::new(200.0, 300.0));
1370 assert!((rect.width - 200.0).abs() < 0.5, "{rect:?}");
1371 assert!((rect.height - 50.0).abs() < 0.5, "{rect:?}");
1372 }
1373
1374 #[test]
1377 fn a_sized_box_that_expands_fills_a_page_and_shares_a_line() {
1378 let rect = red_box(SizedBox::expand(filler()), Size::new(200.0, 300.0));
1379 assert!(
1380 (rect.width - 200.0).abs() < 0.5 && (rect.height - 300.0).abs() < 0.5,
1381 "{rect:?}"
1382 );
1383 let row = Flex::<()>::row()
1384 .width(300.0)
1385 .height(100.0)
1386 .child(Text::new("label").no_wrap())
1387 .child(SizedBox::expand(Container::new().width(30.0).color(RED)));
1388 let rect = red_box(row, Size::new(300.0, 100.0));
1389 assert!((rect.width - 30.0).abs() < 0.5, "{rect:?}");
1390 assert!((rect.height - 100.0).abs() < 0.5, "{rect:?}");
1391 assert!(rect.x > 20.0, "after the label: {rect:?}");
1392 }
1393}