1use alloc::{collections::BTreeSet, vec, vec::Vec};
4use nami::{Computed, Signal, SignalExt};
5use waterui_core::{
6 AnyView, IntoSignalF32, View,
7 layout::{LayoutInvalidationCallback, StretchAxis},
8};
9
10use crate::{
11 Layout, PlacedSubview, Point, ProposalSize, Rect, Size, SubView, SubviewPlacement,
12 ViewDimensions,
13 container::FixedContainer,
14 stack::{
15 Alignment, HorizontalAlignment, VerticalAlignment,
16 distribute::{LineAnchor, container_line},
17 },
18};
19
20#[derive(Debug, Clone, Default)]
22pub struct FrameLayout {
23 min_width: Option<Computed<f32>>,
24 ideal_width: Option<Computed<f32>>,
25 max_width: Option<Computed<f32>>,
26 min_height: Option<Computed<f32>>,
27 ideal_height: Option<Computed<f32>>,
28 max_height: Option<Computed<f32>>,
29 alignment: Alignment,
30}
31
32#[derive(Clone, Copy)]
33struct ResolvedFrameLayout {
34 min_width: Option<f32>,
35 ideal_width: Option<f32>,
36 max_width: Option<f32>,
37 min_height: Option<f32>,
38 ideal_height: Option<f32>,
39 max_height: Option<f32>,
40}
41
42impl FrameLayout {
43 fn resolved(&self) -> ResolvedFrameLayout {
44 ResolvedFrameLayout {
45 min_width: self.min_width.as_ref().map(Signal::get),
46 ideal_width: self.ideal_width.as_ref().map(Signal::get),
47 max_width: self.max_width.as_ref().map(Signal::get),
48 min_height: self.min_height.as_ref().map(Signal::get),
49 ideal_height: self.ideal_height.as_ref().map(Signal::get),
50 max_height: self.max_height.as_ref().map(Signal::get),
51 }
52 }
53}
54
55impl ResolvedFrameLayout {
56 fn resolve(&self, proposal: ProposalSize, child: Size) -> Size {
58 Size::new(
59 frame_resolved_axis(
60 proposal.width,
61 child.width,
62 self.min_width,
63 self.ideal_width,
64 self.max_width,
65 ),
66 frame_resolved_axis(
67 proposal.height,
68 child.height,
69 self.min_height,
70 self.ideal_height,
71 self.max_height,
72 ),
73 )
74 }
75
76 fn placement_proposal(&self, bounds: Rect, proposal: ProposalSize) -> ProposalSize {
79 self.child_proposal(ProposalSize {
80 width: if self.min_width.is_some() || self.max_width.is_some() {
81 Some(bounds.width())
82 } else {
83 proposal.width
84 },
85 height: if self.min_height.is_some() || self.max_height.is_some() {
86 Some(bounds.height())
87 } else {
88 proposal.height
89 },
90 })
91 }
92
93 fn child_proposal(&self, proposal: ProposalSize) -> ProposalSize {
97 ProposalSize {
98 width: frame_child_proposal_axis(
99 proposal.width,
100 self.min_width,
101 self.ideal_width,
102 self.max_width,
103 ),
104 height: frame_child_proposal_axis(
105 proposal.height,
106 self.min_height,
107 self.ideal_height,
108 self.max_height,
109 ),
110 }
111 }
112}
113
114impl Layout for FrameLayout {
115 fn stretch_axis(&self, _children: &[StretchAxis]) -> StretchAxis {
120 let resolved = self.resolved();
121 let horizontal = resolved.max_width.is_some_and(f32::is_infinite);
122 let vertical = resolved.max_height.is_some_and(f32::is_infinite);
123 match (horizontal, vertical) {
124 (true, true) => StretchAxis::Both,
125 (true, false) => StretchAxis::Horizontal,
126 (false, true) => StretchAxis::Vertical,
127 (false, false) => StretchAxis::None,
128 }
129 }
130
131 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
132 let resolved = self.resolved();
133 let child_proposal = resolved.child_proposal(proposal);
138 let Some(child) = children.first() else {
139 return resolved.resolve(proposal, Size::zero());
140 };
141
142 let first = resolved.resolve(proposal, child.measure(child_proposal).size);
147
148 let placement = resolved.placement_proposal(Rect::from_size(first), proposal);
154 if placement == child_proposal {
155 first
156 } else {
157 resolved.resolve(proposal, child.measure(placement).size)
158 }
159 }
160
161 fn place(
162 &self,
163 bounds: Rect,
164 proposal: ProposalSize,
165 children: &[&dyn SubView],
166 ) -> Vec<SubviewPlacement> {
167 if children.is_empty() {
168 return vec![];
169 }
170
171 let resolved = self.resolved();
172 let child_proposal = resolved.placement_proposal(bounds, proposal);
175
176 let child_dimensions = children
177 .first()
178 .map_or(ViewDimensions::new(Size::zero()), |c| {
179 c.measure(child_proposal)
180 });
181 let child_size = child_dimensions.size;
182
183 let child_width = if child_size.width.is_infinite() {
185 bounds.width()
186 } else {
187 child_size.width
188 };
189
190 let child_height = if child_size.height.is_infinite() {
191 bounds.height()
192 } else {
193 child_size.height
194 };
195
196 let final_child_size = Size::new(child_width, child_height);
197 let mut adjusted_dimensions = child_dimensions;
198 adjusted_dimensions.size = final_child_size;
199
200 let horizontal = self.alignment.horizontal();
203 let guide_x = adjusted_dimensions.horizontal(horizontal);
204 let child_x = bounds.x()
205 + container_line(
206 bounds.width(),
207 LineAnchor::horizontal(horizontal),
208 guide_x,
209 final_child_size.width - guide_x,
210 )
211 - guide_x;
212
213 let vertical = self.alignment.vertical();
214 let guide_y = adjusted_dimensions.vertical(vertical);
215 let child_y = bounds.y()
216 + container_line(
217 bounds.height(),
218 LineAnchor::vertical(vertical),
219 guide_y,
220 final_child_size.height - guide_y,
221 )
222 - guide_y;
223
224 vec![SubviewPlacement::new(
225 Rect::new(Point::new(child_x, child_y), final_child_size),
226 child_proposal,
227 )]
228 }
229
230 fn explicit_horizontal(
231 &self,
232 alignment: HorizontalAlignment,
233 _bounds: Rect,
234 children: &[PlacedSubview<'_>],
235 ) -> Option<f32> {
236 children
237 .first()
238 .and_then(|child| child.explicit_horizontal(alignment))
239 }
240
241 fn explicit_vertical(
242 &self,
243 alignment: VerticalAlignment,
244 _bounds: Rect,
245 children: &[PlacedSubview<'_>],
246 ) -> Option<f32> {
247 children
248 .first()
249 .and_then(|child| child.explicit_vertical(alignment))
250 }
251
252 fn watch_invalidation(
253 &self,
254 invalidate: LayoutInvalidationCallback,
255 ) -> Vec<nami::watcher::BoxWatcherGuard> {
256 let signals = [
257 self.min_width.as_ref(),
258 self.ideal_width.as_ref(),
259 self.max_width.as_ref(),
260 self.min_height.as_ref(),
261 self.ideal_height.as_ref(),
262 self.max_height.as_ref(),
263 ];
264 let mut identities = BTreeSet::new();
265 let mut guards = Vec::new();
266 for signal in signals.into_iter().flatten() {
267 if signal
268 .identity()
269 .is_some_and(|identity| !identities.insert(identity))
270 {
271 continue;
272 }
273 let invalidate = invalidate.clone();
274 guards.push(signal.watch(move |_| invalidate()));
275 }
276 guards
277 }
278}
279
280#[inline]
281fn clamp_frame_axis(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
282 value
283 .max(min.unwrap_or(f32::NEG_INFINITY))
284 .min(max.unwrap_or(f32::INFINITY))
285}
286
287#[inline]
299fn frame_child_proposal_axis(
300 parent_proposal: Option<f32>,
301 min: Option<f32>,
302 ideal: Option<f32>,
303 max: Option<f32>,
304) -> Option<f32> {
305 parent_proposal
306 .or(ideal)
307 .map(|value| clamp_frame_axis(value, min, max))
308}
309
310#[inline]
337fn frame_resolved_axis(
338 parent_proposal: Option<f32>,
339 child_size: f32,
340 min: Option<f32>,
341 ideal: Option<f32>,
342 max: Option<f32>,
343) -> f32 {
344 let content = if max.is_some() && parent_proposal != Some(0.0) {
345 parent_proposal.or(ideal).unwrap_or(child_size)
346 } else {
347 child_size
348 };
349 clamp_frame_axis(content, min, max)
350}
351
352#[derive(Debug)]
357pub struct Frame {
358 layout: FrameLayout,
359 content: AnyView,
360}
361
362impl Frame {
363 #[must_use]
369 pub fn new(content: impl View) -> Self {
370 Self {
371 layout: FrameLayout::default(),
372 content: AnyView::new(content),
373 }
374 }
375
376 #[must_use]
383 pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
384 self.layout.alignment = alignment.into();
385 self
386 }
387
388 crate::alignment::two_dimensional_alignment_methods!();
389
390 #[must_use]
396 pub fn width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
397 let width = width.into_signal_f32().computed();
398 self.layout.min_width = Some(width.clone());
399 self.layout.ideal_width = Some(width.clone());
400 self.layout.max_width = Some(width);
401 self
402 }
403
404 #[must_use]
406 pub fn height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
407 let height = height.into_signal_f32().computed();
408 self.layout.min_height = Some(height.clone());
409 self.layout.ideal_height = Some(height.clone());
410 self.layout.max_height = Some(height);
411 self
412 }
413
414 #[must_use]
420 pub fn ideal_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
421 self.layout.ideal_width = Some(width.into_signal_f32().computed());
422 self
423 }
424
425 #[must_use]
429 pub fn ideal_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
430 self.layout.ideal_height = Some(height.into_signal_f32().computed());
431 self
432 }
433
434 #[must_use]
436 pub fn min_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
437 self.layout.min_width = Some(width.into_signal_f32().computed());
438 self
439 }
440
441 #[must_use]
443 pub fn max_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
444 self.layout.max_width = Some(width.into_signal_f32().computed());
445 self
446 }
447
448 #[must_use]
450 pub fn min_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
451 self.layout.min_height = Some(height.into_signal_f32().computed());
452 self
453 }
454
455 #[must_use]
457 pub fn max_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
458 self.layout.max_height = Some(height.into_signal_f32().computed());
459 self
460 }
461}
462
463impl View for Frame {
464 fn body(self, _env: &waterui_core::Environment) -> impl View {
465 FixedContainer::new(self.layout, vec![self.content])
467 }
468
469 fn stretch_axis(&self) -> StretchAxis {
472 self.layout.stretch_axis(&[self.content.stretch_axis()])
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::StretchAxis;
480 use alloc::rc::Rc;
481 use core::cell::Cell;
482 use nami::{SignalExt, binding};
483
484 #[test]
485 fn constrained_axes_select_their_owned_region_during_placement() {
486 for (minimum, maximum, ideal, expected) in [
487 (Some(28.0), Some(28.0), 0.0, 28.0),
488 (Some(100.0), None, 0.0, 100.0),
489 (None, Some(60.0), 200.0, 60.0),
490 ] {
491 let layout = FrameLayout {
492 min_width: minimum.map(Computed::constant),
493 max_width: maximum.map(Computed::constant),
494 ..Default::default()
495 };
496 let child = RecordingSubView::new(FillingSubView {
497 intrinsic: Size::new(ideal, 10.0),
498 });
499 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
500 assert_eq!(size, Size::new(expected, 10.0));
501 let placements =
502 layout.place(Rect::from_size(size), ProposalSize::UNSPECIFIED, &[&child]);
503 assert_eq!(
504 placements[0].proposal,
505 ProposalSize::new(Some(expected), None)
506 );
507 assert_eq!(*placements[0].frame.size(), Size::new(expected, 10.0));
508 }
509 }
510
511 struct FramedChild<C> {
512 layout: FrameLayout,
513 child: C,
514 }
515
516 impl<C: SubView> SubView for FramedChild<C> {
517 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
518 ViewDimensions::new(self.layout.size_that_fits(proposal, &[&self.child]))
519 }
520 fn stretch_axis(&self) -> StretchAxis {
521 self.layout.stretch_axis(&[self.child.stretch_axis()])
522 }
523 fn priority(&self) -> i32 {
524 0
525 }
526 }
527
528 struct WrappingChild;
529
530 impl SubView for WrappingChild {
531 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
532 let width = proposal.width.unwrap_or(200.0).clamp(10.0, 200.0);
533 ViewDimensions::new(Size::new(width, (200.0 / width).ceil() * 10.0))
534 }
535 fn stretch_axis(&self) -> StretchAxis {
536 StretchAxis::None
537 }
538 fn priority(&self) -> i32 {
539 0
540 }
541 }
542
543 #[test]
544 fn finite_stack_offers_remeasure_capped_wrapping_children() {
545 let child = FramedChild {
546 layout: FrameLayout {
547 max_width: Some(Computed::constant(60.0)),
548 ..Default::default()
549 },
550 child: WrappingChild,
551 };
552 let stack = crate::stack::HStackLayout::default();
553 assert_eq!(
554 stack.size_that_fits(ProposalSize::new(Some(240.0), None), &[&child]),
555 Size::new(60.0, 40.0)
556 );
557 }
558
559 #[test]
560 fn finite_maximum_frames_grow_only_to_their_reported_limit() {
561 let child = FramedChild {
562 layout: FrameLayout {
563 max_width: Some(Computed::constant(100.0)),
564 ..Default::default()
565 },
566 child: FillingSubView {
567 intrinsic: Size::new(20.0, 10.0),
568 },
569 };
570 let stack = crate::stack::HStackLayout::default();
571 for (offer, width) in [(80.0, 80.0), (240.0, 100.0)] {
572 assert_eq!(
573 stack.size_that_fits(ProposalSize::new(Some(offer), None), &[&child]),
574 Size::new(width, 10.0)
575 );
576 }
577 }
578
579 #[test]
580 fn a_max_only_frame_reports_the_child_floor_on_a_min_size_query() {
581 let layout = FrameLayout {
587 max_width: Some(Computed::constant(f32::INFINITY)),
588 ..Default::default()
589 };
590 let child = MockSubView {
591 size: Size::new(220.0, 40.0),
592 };
593
594 let size = layout.size_that_fits(ProposalSize::ZERO, &[&child]);
595 assert_extent(size.width, 220.0, "the frame's minimum width");
596
597 let size = layout.size_that_fits(ProposalSize::new(Some(500.0), None), &[&child]);
599 assert_extent(size.width, 500.0, "the frame's offered width");
600 }
601
602 #[test]
603 fn a_bounded_maximum_clamps_the_reported_floor() {
604 let layout = FrameLayout {
608 max_width: Some(Computed::constant(100.0)),
609 ..Default::default()
610 };
611 let child = MockSubView {
612 size: Size::new(220.0, 40.0),
613 };
614
615 let size = layout.size_that_fits(ProposalSize::ZERO, &[&child]);
616 assert_extent(size.width, 100.0, "the frame's clamped minimum width");
617 }
618
619 struct MockSubView {
620 size: Size,
621 }
622
623 impl SubView for MockSubView {
624 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
625 ViewDimensions::new(self.size)
626 }
627 fn stretch_axis(&self) -> StretchAxis {
628 StretchAxis::None
629 }
630 fn priority(&self) -> i32 {
631 0
632 }
633 }
634
635 struct FillingSubView {
639 intrinsic: Size,
640 }
641
642 impl SubView for FillingSubView {
643 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
644 ViewDimensions::new(Size::new(
645 proposal.width.unwrap_or(self.intrinsic.width),
646 proposal.height.unwrap_or(self.intrinsic.height),
647 ))
648 }
649 fn stretch_axis(&self) -> StretchAxis {
650 StretchAxis::Both
651 }
652 fn priority(&self) -> i32 {
653 0
654 }
655 }
656
657 struct RecordingSubView<C> {
659 inner: C,
660 proposal: Cell<Option<ProposalSize>>,
661 }
662
663 impl<C> RecordingSubView<C> {
664 fn new(inner: C) -> Self {
665 Self {
666 inner,
667 proposal: Cell::new(None),
668 }
669 }
670
671 fn proposal(&self) -> ProposalSize {
672 self.proposal
673 .get()
674 .expect("the frame never measured its child")
675 }
676 }
677
678 impl<C: SubView> SubView for RecordingSubView<C> {
679 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
680 self.proposal.set(Some(proposal));
681 self.inner.measure(proposal)
682 }
683 fn stretch_axis(&self) -> StretchAxis {
684 self.inner.stretch_axis()
685 }
686 fn priority(&self) -> i32 {
687 self.inner.priority()
688 }
689 }
690
691 fn assert_extent(actual: f32, expected: f32, what: &str) {
692 assert!(
693 (actual - expected).abs() < f32::EPSILON,
694 "expected {what} to be {expected}, got {actual}"
695 );
696 }
697
698 #[test]
699 fn an_ideal_frame_still_adopts_a_rigid_child_size() {
700 let layout = FrameLayout {
705 ideal_width: Some(Computed::constant(100.0)),
706 ideal_height: Some(Computed::constant(50.0)),
707 ..Default::default()
708 };
709
710 let child = RecordingSubView::new(MockSubView {
711 size: Size::new(30.0, 20.0),
712 });
713
714 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
715
716 assert_eq!(
717 child.proposal(),
718 ProposalSize::new(Some(100.0), Some(50.0)),
719 "the ideal should answer the dimensions the parent left unspecified"
720 );
721 assert_extent(size.width, 30.0, "the frame's width");
722 assert_extent(size.height, 20.0, "the frame's height");
723 }
724
725 #[test]
726 fn an_ideal_answers_only_the_dimensions_the_parent_left_unspecified() {
727 let layout = FrameLayout {
734 ideal_width: Some(Computed::constant(24.0)),
735 ideal_height: Some(Computed::constant(24.0)),
736 ..Default::default()
737 };
738 let proposal = ProposalSize::new(Some(48.0), Some(36.0));
739
740 let resizable = RecordingSubView::new(FillingSubView {
741 intrinsic: Size::new(24.0, 24.0),
742 });
743 let filled = layout.size_that_fits(proposal, &[&resizable]);
744 assert_eq!(
745 resizable.proposal(),
746 proposal,
747 "the child should hear the parent's proposal, not the ideal"
748 );
749 assert_extent(filled.width, 48.0, "a resizable child's frame width");
750 assert_extent(filled.height, 36.0, "a resizable child's frame height");
751
752 let placements = layout.place(
755 Rect::new(Point::new(0.0, 0.0), filled),
756 proposal,
757 &[&resizable],
758 );
759 assert_eq!(
760 resizable.proposal(),
761 proposal,
762 "placement should re-propose what measurement proposed"
763 );
764 assert_extent(
765 placements[0].frame.width(),
766 48.0,
767 "the resizable child's width",
768 );
769 assert_extent(
770 placements[0].frame.height(),
771 36.0,
772 "the resizable child's height",
773 );
774
775 let rigid = RecordingSubView::new(MockSubView {
776 size: Size::new(24.0, 24.0),
777 });
778 let hugged = layout.size_that_fits(proposal, &[&rigid]);
779 assert_eq!(
780 rigid.proposal(),
781 proposal,
782 "a rigid child hears the proposal too; it just declines it"
783 );
784 assert_extent(hugged.width, 24.0, "a rigid child's frame width");
785 assert_extent(hugged.height, 24.0, "a rigid child's frame height");
786 }
787
788 #[test]
789 fn a_rigid_child_does_not_grow_into_a_larger_proposal() {
790 let layout = FrameLayout {
794 ideal_width: Some(Computed::constant(24.0)),
795 ..Default::default()
796 };
797
798 let child = MockSubView {
799 size: Size::new(24.0, 24.0),
800 };
801
802 let size = layout.size_that_fits(ProposalSize::new(Some(400.0), None), &[&child]);
803
804 assert_extent(size.width, 24.0, "the frame's width");
805 }
806
807 #[test]
808 fn test_frame_alignment_bounded_proposal() {
809 let layout = FrameLayout {
810 alignment: Alignment::BottomTrailing,
811 ..Default::default()
812 };
813
814 let mut child = MockSubView {
815 size: Size::new(30.0, 20.0),
816 };
817 let children: Vec<&dyn SubView> = vec![&mut child];
818
819 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
820 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
821 let placements = layout.place(bounds, proposal, &children);
822
823 assert!((placements[0].frame.x() - 70.0).abs() < f32::EPSILON); assert!((placements[0].frame.y() - 80.0).abs() < f32::EPSILON); }
827
828 #[test]
829 fn test_fixed_width_resists_zero_min_query() {
830 let layout = FrameLayout {
831 min_width: Some(Computed::constant(120.0)),
832 ideal_width: Some(Computed::constant(120.0)),
833 max_width: Some(Computed::constant(120.0)),
834 ..Default::default()
835 };
836
837 let mut child = MockSubView {
838 size: Size::new(30.0, 20.0),
839 };
840 let children: Vec<&dyn SubView> = vec![&mut child];
841
842 let size = layout.size_that_fits(ProposalSize::ZERO, &children);
843 assert!((size.width - 120.0).abs() < f32::EPSILON);
844 }
845
846 #[test]
847 fn test_min_width_clamps_zero_min_query() {
848 let layout = FrameLayout {
849 min_width: Some(Computed::constant(64.0)),
850 ..Default::default()
851 };
852
853 let mut child = MockSubView {
854 size: Size::new(10.0, 10.0),
855 };
856 let children: Vec<&dyn SubView> = vec![&mut child];
857
858 let size = layout.size_that_fits(ProposalSize::ZERO, &children);
859 assert!((size.width - 64.0).abs() < f32::EPSILON);
860 }
861
862 #[test]
863 fn reactive_width_invalidates_and_updates_measurement() {
864 let width = binding(80.0_f32);
865 let signal = width.computed();
866 let layout = FrameLayout {
867 min_width: Some(signal.clone()),
868 ideal_width: Some(signal.clone()),
869 max_width: Some(signal),
870 ..Default::default()
871 };
872 let invalidations = Rc::new(Cell::new(0));
873 let callback_invalidations = Rc::clone(&invalidations);
874 let _guards = layout.watch_invalidation(Rc::new(move || {
875 callback_invalidations.set(callback_invalidations.get() + 1);
876 }));
877
878 width.set(120.0);
879 assert_eq!(invalidations.get(), 1);
880
881 let child = MockSubView {
882 size: Size::new(10.0, 10.0),
883 };
884 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
885 assert_eq!(size.width, 120.0);
886 }
887
888 #[test]
889 fn a_minimum_alone_does_not_make_the_frame_greedy() {
890 let layout = FrameLayout {
893 min_width: Some(Computed::constant(50.0)),
894 ..Default::default()
895 };
896
897 let child = MockSubView {
898 size: Size::new(30.0, 20.0),
899 };
900 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
901
902 assert!(
903 (size.width - 50.0).abs() < f32::EPSILON,
904 "expected the frame to stay at its minimum, got {}",
905 size.width
906 );
907 }
908
909 #[test]
910 fn a_bounded_maximum_fills_up_to_that_maximum() {
911 let layout = FrameLayout {
913 max_width: Some(Computed::constant(100.0)),
914 ..Default::default()
915 };
916
917 let child = MockSubView {
918 size: Size::new(30.0, 20.0),
919 };
920 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
921
922 assert!(
923 (size.width - 100.0).abs() < f32::EPSILON,
924 "expected the frame to fill to its maximum, got {}",
925 size.width
926 );
927 }
928
929 #[test]
930 fn an_infinite_maximum_takes_the_whole_offer() {
931 let layout = FrameLayout {
932 max_width: Some(Computed::constant(f32::INFINITY)),
933 ..Default::default()
934 };
935
936 let child = MockSubView {
937 size: Size::new(30.0, 20.0),
938 };
939 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
940
941 assert!(
942 (size.width - 300.0).abs() < f32::EPSILON,
943 "expected the frame to fill the proposal, got {}",
944 size.width
945 );
946 }
947
948 #[test]
949 fn an_ideal_size_is_what_an_unconstrained_parent_gets() {
950 let layout = FrameLayout {
951 ideal_width: Some(Computed::constant(120.0)),
952 ..Default::default()
953 };
954
955 let child = FillingSubView {
956 intrinsic: Size::new(30.0, 20.0),
957 };
958
959 let unconstrained = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
962 assert_extent(
963 unconstrained.width,
964 120.0,
965 "the unconstrained frame's width",
966 );
967
968 let proposed = layout.size_that_fits(ProposalSize::new(Some(300.0), Some(90.0)), &[&child]);
972 assert_extent(proposed.width, 300.0, "the proposed frame's width");
973 assert_extent(proposed.height, 90.0, "the proposed frame's height");
974 }
975
976 #[test]
977 fn an_ideal_does_not_cap_the_child_once_the_frame_has_grown() {
978 let layout = FrameLayout {
983 ideal_width: Some(Computed::constant(100.0)),
984 ideal_height: Some(Computed::constant(50.0)),
985 max_width: Some(Computed::constant(f32::INFINITY)),
986 max_height: Some(Computed::constant(f32::INFINITY)),
987 ..Default::default()
988 };
989
990 let child = RecordingSubView::new(FillingSubView {
991 intrinsic: Size::new(30.0, 20.0),
992 });
993
994 let size = layout.size_that_fits(ProposalSize::new(Some(320.0), Some(180.0)), &[&child]);
995 assert_extent(size.width, 320.0, "the frame's width");
996 assert_extent(size.height, 180.0, "the frame's height");
997
998 let proposal = ProposalSize::new(Some(320.0), Some(180.0));
999 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(320.0, 180.0));
1000 let placements = layout.place(bounds, proposal, &[&child]);
1001
1002 assert_eq!(
1003 child.proposal(),
1004 ProposalSize::new(Some(320.0), Some(180.0)),
1005 "the child should be re-proposed what it was measured with"
1006 );
1007 assert_extent(placements[0].frame.width(), 320.0, "the child's width");
1008 assert_extent(placements[0].frame.height(), 180.0, "the child's height");
1009 }
1010
1011 #[test]
1012 fn an_unconstrained_frame_takes_its_ideal_and_hands_that_to_the_child() {
1013 let layout = FrameLayout {
1016 ideal_width: Some(Computed::constant(100.0)),
1017 ideal_height: Some(Computed::constant(50.0)),
1018 ..Default::default()
1019 };
1020
1021 let child = RecordingSubView::new(FillingSubView {
1022 intrinsic: Size::new(30.0, 20.0),
1023 });
1024
1025 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
1026 assert_extent(size.width, 100.0, "the frame's width");
1027 assert_extent(size.height, 50.0, "the frame's height");
1028
1029 let placements = layout.place(
1030 Rect::new(Point::new(0.0, 0.0), size),
1031 ProposalSize::UNSPECIFIED,
1032 &[&child],
1033 );
1034 assert_eq!(
1035 child.proposal(),
1036 ProposalSize::new(Some(100.0), Some(50.0)),
1037 "the child should be proposed the ideal the frame resolved to"
1038 );
1039 assert_extent(placements[0].frame.width(), 100.0, "the child's width");
1040 assert_extent(placements[0].frame.height(), 50.0, "the child's height");
1041 }
1042
1043 #[test]
1044 fn placement_holds_the_frames_own_minimum_and_maximum_bounded_proposal() {
1045 let layout = FrameLayout {
1049 min_width: Some(Computed::constant(120.0)),
1050 max_height: Some(Computed::constant(40.0)),
1051 ..Default::default()
1052 };
1053
1054 let child = RecordingSubView::new(FillingSubView {
1055 intrinsic: Size::new(30.0, 20.0),
1056 });
1057
1058 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(80.0, 300.0));
1059 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
1060 layout.place(bounds, proposal, &[&child]);
1061
1062 assert_eq!(
1063 child.proposal(),
1064 ProposalSize::new(Some(120.0), Some(40.0)),
1065 "the child's proposal should be the offer clamped by the frame"
1066 );
1067 }
1068
1069 #[test]
1070 fn an_ideal_that_is_also_the_maximum_is_a_natural_size_that_only_shrinks() {
1071 let layout = FrameLayout {
1078 ideal_width: Some(Computed::constant(24.0)),
1079 ideal_height: Some(Computed::constant(24.0)),
1080 max_width: Some(Computed::constant(24.0)),
1081 max_height: Some(Computed::constant(24.0)),
1082 ..Default::default()
1083 };
1084 let scene = || {
1085 RecordingSubView::new(FillingSubView {
1086 intrinsic: Size::zero(),
1087 })
1088 };
1089
1090 let child = scene();
1092 let natural = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
1093 assert_extent(natural.width, 24.0, "the unproposed width");
1094 assert_extent(natural.height, 24.0, "the unproposed height");
1095
1096 let child = scene();
1099 let in_a_row = layout.size_that_fits(ProposalSize::new(None, Some(44.0)), &[&child]);
1100 assert_eq!(
1101 child.proposal(),
1102 ProposalSize::new(Some(24.0), Some(24.0)),
1103 "the scene should never be offered more than the natural size"
1104 );
1105 assert_extent(in_a_row.width, 24.0, "the width in a taller row");
1106 assert_extent(in_a_row.height, 24.0, "the height in a taller row");
1107
1108 let child = scene();
1110 let resized = layout.size_that_fits(ProposalSize::new(Some(8.0), Some(10.0)), &[&child]);
1111 assert_eq!(
1112 child.proposal(),
1113 ProposalSize::new(Some(8.0), Some(10.0)),
1114 "a smaller box should reach the scene unaltered"
1115 );
1116 assert_extent(resized.width, 8.0, "the resized width");
1117 assert_extent(resized.height, 10.0, "the resized height");
1118 }
1119
1120 #[test]
1121 fn a_filling_frame_reproposes_its_offer_to_a_rigid_child() {
1122 let layout = FrameLayout {
1127 max_width: Some(Computed::constant(f32::INFINITY)),
1128 ..Default::default()
1129 };
1130
1131 let child = RecordingSubView::new(MockSubView {
1132 size: Size::new(30.0, 20.0),
1133 });
1134
1135 let proposal = ProposalSize::new(Some(320.0), Some(180.0));
1136 let size = layout.size_that_fits(proposal, &[&child]);
1137 assert_extent(size.width, 320.0, "the frame's width");
1138 assert_extent(size.height, 20.0, "the frame's height");
1139
1140 let placements = layout.place(Rect::new(Point::new(0.0, 0.0), size), proposal, &[&child]);
1141 assert_eq!(
1142 child.proposal(),
1143 ProposalSize::new(Some(320.0), Some(180.0)),
1144 "a rigid child is re-proposed the frame's own measurement proposal"
1145 );
1146 assert_extent(placements[0].frame.width(), 30.0, "the child's width");
1147 assert_extent(placements[0].frame.x(), 145.0, "the child's x");
1148 }
1149}