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, ViewDimensions,
12 container::FixedContainer,
13 stack::{Alignment, HorizontalAlignment, VerticalAlignment},
14};
15
16#[derive(Debug, Clone, Default)]
18pub struct FrameLayout {
19 min_width: Option<Computed<f32>>,
20 ideal_width: Option<Computed<f32>>,
21 max_width: Option<Computed<f32>>,
22 min_height: Option<Computed<f32>>,
23 ideal_height: Option<Computed<f32>>,
24 max_height: Option<Computed<f32>>,
25 alignment: Alignment,
26}
27
28#[derive(Clone, Copy)]
29struct ResolvedFrameLayout {
30 min_width: Option<f32>,
31 ideal_width: Option<f32>,
32 max_width: Option<f32>,
33 min_height: Option<f32>,
34 ideal_height: Option<f32>,
35 max_height: Option<f32>,
36}
37
38impl FrameLayout {
39 fn resolved(&self) -> ResolvedFrameLayout {
40 ResolvedFrameLayout {
41 min_width: self.min_width.as_ref().map(Signal::get),
42 ideal_width: self.ideal_width.as_ref().map(Signal::get),
43 max_width: self.max_width.as_ref().map(Signal::get),
44 min_height: self.min_height.as_ref().map(Signal::get),
45 ideal_height: self.ideal_height.as_ref().map(Signal::get),
46 max_height: self.max_height.as_ref().map(Signal::get),
47 }
48 }
49}
50
51impl Layout for FrameLayout {
52 fn stretch_axis(&self, _children: &[StretchAxis]) -> StretchAxis {
57 let resolved = self.resolved();
58 let horizontal = resolved.max_width.is_some_and(f32::is_infinite);
59 let vertical = resolved.max_height.is_some_and(f32::is_infinite);
60 match (horizontal, vertical) {
61 (true, true) => StretchAxis::Both,
62 (true, false) => StretchAxis::Horizontal,
63 (false, true) => StretchAxis::Vertical,
64 (false, false) => StretchAxis::None,
65 }
66 }
67
68 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
69 let resolved = self.resolved();
70 let child_proposal = ProposalSize {
75 width: frame_child_proposal_axis(
76 proposal.width,
77 resolved.min_width,
78 resolved.ideal_width,
79 resolved.max_width,
80 ),
81 height: frame_child_proposal_axis(
82 proposal.height,
83 resolved.min_height,
84 resolved.ideal_height,
85 resolved.max_height,
86 ),
87 };
88
89 let child_dimensions = children
91 .first()
92 .map_or(ViewDimensions::new(Size::zero()), |c| {
93 c.measure(child_proposal)
94 });
95 let child_size = child_dimensions.size;
96
97 let final_width = frame_resolved_axis(
103 proposal.width,
104 child_size.width,
105 resolved.min_width,
106 resolved.ideal_width,
107 resolved.max_width,
108 );
109 let final_height = frame_resolved_axis(
110 proposal.height,
111 child_size.height,
112 resolved.min_height,
113 resolved.ideal_height,
114 resolved.max_height,
115 );
116
117 Size::new(final_width, final_height)
118 }
119
120 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
121 if children.is_empty() {
122 return vec![];
123 }
124
125 let resolved = self.resolved();
126 let child_proposal = ProposalSize {
138 width: Some(clamp_frame_axis(
139 bounds.width(),
140 resolved.min_width,
141 resolved.max_width,
142 )),
143 height: Some(clamp_frame_axis(
144 bounds.height(),
145 resolved.min_height,
146 resolved.max_height,
147 )),
148 };
149
150 let child_dimensions = children
151 .first()
152 .map_or(ViewDimensions::new(Size::zero()), |c| {
153 c.measure(child_proposal)
154 });
155 let child_size = child_dimensions.size;
156
157 let child_width = if child_size.width.is_infinite() {
159 bounds.width()
160 } else {
161 child_size.width
162 };
163
164 let child_height = if child_size.height.is_infinite() {
165 bounds.height()
166 } else {
167 child_size.height
168 };
169
170 let final_child_size = Size::new(child_width, child_height);
171 let mut adjusted_dimensions = child_dimensions;
172 adjusted_dimensions.size = final_child_size;
173
174 let horizontal = self.alignment.horizontal();
176 let horizontal_target = if horizontal == HorizontalAlignment::Leading {
177 0.0
178 } else if horizontal == HorizontalAlignment::Trailing {
179 bounds.width()
180 } else if horizontal == HorizontalAlignment::Center {
181 bounds.width() * 0.5
182 } else {
183 adjusted_dimensions
184 .horizontal(horizontal)
185 .clamp(0.0, final_child_size.width)
186 };
187 let child_x = bounds.x() + horizontal_target
188 - adjusted_dimensions
189 .horizontal(horizontal)
190 .clamp(0.0, final_child_size.width);
191
192 let vertical = self.alignment.vertical();
193 let vertical_target = if vertical == VerticalAlignment::Top {
194 0.0
195 } else if vertical == VerticalAlignment::Bottom {
196 bounds.height()
197 } else if vertical == VerticalAlignment::Center {
198 bounds.height() * 0.5
199 } else {
200 adjusted_dimensions
201 .vertical(vertical)
202 .clamp(0.0, final_child_size.height)
203 };
204 let child_y = bounds.y() + vertical_target
205 - adjusted_dimensions
206 .vertical(vertical)
207 .clamp(0.0, final_child_size.height);
208
209 vec![Rect::new(Point::new(child_x, child_y), final_child_size)]
210 }
211
212 fn explicit_horizontal(
213 &self,
214 alignment: HorizontalAlignment,
215 _bounds: Rect,
216 children: &[PlacedSubview<'_>],
217 ) -> Option<f32> {
218 children
219 .first()
220 .and_then(|child| child.explicit_horizontal(alignment))
221 }
222
223 fn explicit_vertical(
224 &self,
225 alignment: VerticalAlignment,
226 _bounds: Rect,
227 children: &[PlacedSubview<'_>],
228 ) -> Option<f32> {
229 children
230 .first()
231 .and_then(|child| child.explicit_vertical(alignment))
232 }
233
234 fn watch_invalidation(
235 &self,
236 invalidate: LayoutInvalidationCallback,
237 ) -> Vec<nami::watcher::BoxWatcherGuard> {
238 let signals = [
239 self.min_width.as_ref(),
240 self.ideal_width.as_ref(),
241 self.max_width.as_ref(),
242 self.min_height.as_ref(),
243 self.ideal_height.as_ref(),
244 self.max_height.as_ref(),
245 ];
246 let mut identities = BTreeSet::new();
247 let mut guards = Vec::new();
248 for signal in signals.into_iter().flatten() {
249 if signal
250 .identity()
251 .is_some_and(|identity| !identities.insert(identity))
252 {
253 continue;
254 }
255 let invalidate = invalidate.clone();
256 guards.push(signal.watch(move |_| invalidate()));
257 }
258 guards
259 }
260}
261
262#[inline]
263fn clamp_frame_axis(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
264 value
265 .max(min.unwrap_or(f32::NEG_INFINITY))
266 .min(max.unwrap_or(f32::INFINITY))
267}
268
269#[inline]
281fn frame_child_proposal_axis(
282 parent_proposal: Option<f32>,
283 min: Option<f32>,
284 ideal: Option<f32>,
285 max: Option<f32>,
286) -> Option<f32> {
287 parent_proposal
288 .or(ideal)
289 .map(|value| clamp_frame_axis(value, min, max))
290}
291
292#[inline]
312fn frame_resolved_axis(
313 parent_proposal: Option<f32>,
314 child_size: f32,
315 min: Option<f32>,
316 ideal: Option<f32>,
317 max: Option<f32>,
318) -> f32 {
319 let content = if max.is_some() {
320 parent_proposal.or(ideal).unwrap_or(child_size)
321 } else {
322 child_size
323 };
324 clamp_frame_axis(content, min, max)
325}
326
327#[derive(Debug)]
332pub struct Frame {
333 layout: FrameLayout,
334 content: AnyView,
335}
336
337impl Frame {
338 #[must_use]
344 pub fn new(content: impl View) -> Self {
345 Self {
346 layout: FrameLayout::default(),
347 content: AnyView::new(content),
348 }
349 }
350
351 #[must_use]
356 pub const fn alignment(mut self, alignment: Alignment) -> Self {
357 self.layout.alignment = alignment;
358 self
359 }
360
361 #[must_use]
367 pub fn width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
368 let width = width.into_signal_f32().computed();
369 self.layout.min_width = Some(width.clone());
370 self.layout.ideal_width = Some(width.clone());
371 self.layout.max_width = Some(width);
372 self
373 }
374
375 #[must_use]
377 pub fn height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
378 let height = height.into_signal_f32().computed();
379 self.layout.min_height = Some(height.clone());
380 self.layout.ideal_height = Some(height.clone());
381 self.layout.max_height = Some(height);
382 self
383 }
384
385 #[must_use]
391 pub fn ideal_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
392 self.layout.ideal_width = Some(width.into_signal_f32().computed());
393 self
394 }
395
396 #[must_use]
400 pub fn ideal_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
401 self.layout.ideal_height = Some(height.into_signal_f32().computed());
402 self
403 }
404
405 #[must_use]
407 pub fn min_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
408 self.layout.min_width = Some(width.into_signal_f32().computed());
409 self
410 }
411
412 #[must_use]
414 pub fn max_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
415 self.layout.max_width = Some(width.into_signal_f32().computed());
416 self
417 }
418
419 #[must_use]
421 pub fn min_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
422 self.layout.min_height = Some(height.into_signal_f32().computed());
423 self
424 }
425
426 #[must_use]
428 pub fn max_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
429 self.layout.max_height = Some(height.into_signal_f32().computed());
430 self
431 }
432}
433
434impl View for Frame {
435 fn body(self, _env: &waterui_core::Environment) -> impl View {
436 FixedContainer::new(self.layout, vec![self.content])
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::StretchAxis;
445 use alloc::rc::Rc;
446 use core::cell::Cell;
447 use nami::{SignalExt, binding};
448
449 struct MockSubView {
450 size: Size,
451 }
452
453 impl SubView for MockSubView {
454 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
455 ViewDimensions::new(self.size)
456 }
457 fn stretch_axis(&self) -> StretchAxis {
458 StretchAxis::None
459 }
460 fn priority(&self) -> i32 {
461 0
462 }
463 }
464
465 struct FillingSubView {
469 intrinsic: Size,
470 }
471
472 impl SubView for FillingSubView {
473 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
474 ViewDimensions::new(Size::new(
475 proposal.width.unwrap_or(self.intrinsic.width),
476 proposal.height.unwrap_or(self.intrinsic.height),
477 ))
478 }
479 fn stretch_axis(&self) -> StretchAxis {
480 StretchAxis::Both
481 }
482 fn priority(&self) -> i32 {
483 0
484 }
485 }
486
487 struct RecordingSubView<C> {
489 inner: C,
490 proposal: Cell<Option<ProposalSize>>,
491 }
492
493 impl<C> RecordingSubView<C> {
494 fn new(inner: C) -> Self {
495 Self {
496 inner,
497 proposal: Cell::new(None),
498 }
499 }
500
501 fn proposal(&self) -> ProposalSize {
502 self.proposal
503 .get()
504 .expect("the frame never measured its child")
505 }
506 }
507
508 impl<C: SubView> SubView for RecordingSubView<C> {
509 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
510 self.proposal.set(Some(proposal));
511 self.inner.measure(proposal)
512 }
513 fn stretch_axis(&self) -> StretchAxis {
514 self.inner.stretch_axis()
515 }
516 fn priority(&self) -> i32 {
517 self.inner.priority()
518 }
519 }
520
521 fn assert_extent(actual: f32, expected: f32, what: &str) {
522 assert!(
523 (actual - expected).abs() < f32::EPSILON,
524 "expected {what} to be {expected}, got {actual}"
525 );
526 }
527
528 #[test]
529 fn an_ideal_frame_still_adopts_a_rigid_child_size() {
530 let layout = FrameLayout {
535 ideal_width: Some(Computed::constant(100.0)),
536 ideal_height: Some(Computed::constant(50.0)),
537 ..Default::default()
538 };
539
540 let child = RecordingSubView::new(MockSubView {
541 size: Size::new(30.0, 20.0),
542 });
543
544 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
545
546 assert_eq!(
547 child.proposal(),
548 ProposalSize::new(Some(100.0), Some(50.0)),
549 "the ideal should answer the dimensions the parent left unspecified"
550 );
551 assert_extent(size.width, 30.0, "the frame's width");
552 assert_extent(size.height, 20.0, "the frame's height");
553 }
554
555 #[test]
556 fn an_ideal_answers_only_the_dimensions_the_parent_left_unspecified() {
557 let layout = FrameLayout {
564 ideal_width: Some(Computed::constant(24.0)),
565 ideal_height: Some(Computed::constant(24.0)),
566 ..Default::default()
567 };
568 let proposal = ProposalSize::new(Some(48.0), Some(36.0));
569
570 let resizable = RecordingSubView::new(FillingSubView {
571 intrinsic: Size::new(24.0, 24.0),
572 });
573 let filled = layout.size_that_fits(proposal, &[&resizable]);
574 assert_eq!(
575 resizable.proposal(),
576 proposal,
577 "the child should hear the parent's proposal, not the ideal"
578 );
579 assert_extent(filled.width, 48.0, "a resizable child's frame width");
580 assert_extent(filled.height, 36.0, "a resizable child's frame height");
581
582 let rects = layout.place(Rect::new(Point::new(0.0, 0.0), filled), &[&resizable]);
586 assert_eq!(
587 resizable.proposal(),
588 proposal,
589 "placement should re-propose the extent the child was measured at"
590 );
591 assert_extent(rects[0].width(), 48.0, "the resizable child's width");
592 assert_extent(rects[0].height(), 36.0, "the resizable child's height");
593
594 let rigid = RecordingSubView::new(MockSubView {
595 size: Size::new(24.0, 24.0),
596 });
597 let hugged = layout.size_that_fits(proposal, &[&rigid]);
598 assert_eq!(
599 rigid.proposal(),
600 proposal,
601 "a rigid child hears the proposal too; it just declines it"
602 );
603 assert_extent(hugged.width, 24.0, "a rigid child's frame width");
604 assert_extent(hugged.height, 24.0, "a rigid child's frame height");
605 }
606
607 #[test]
608 fn a_rigid_child_does_not_grow_into_a_larger_proposal() {
609 let layout = FrameLayout {
613 ideal_width: Some(Computed::constant(24.0)),
614 ..Default::default()
615 };
616
617 let child = MockSubView {
618 size: Size::new(24.0, 24.0),
619 };
620
621 let size = layout.size_that_fits(ProposalSize::new(Some(400.0), None), &[&child]);
622
623 assert_extent(size.width, 24.0, "the frame's width");
624 }
625
626 #[test]
627 fn test_frame_alignment() {
628 let layout = FrameLayout {
629 alignment: Alignment::BottomTrailing,
630 ..Default::default()
631 };
632
633 let mut child = MockSubView {
634 size: Size::new(30.0, 20.0),
635 };
636 let children: Vec<&dyn SubView> = vec![&mut child];
637
638 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
639 let rects = layout.place(bounds, &children);
640
641 assert!((rects[0].x() - 70.0).abs() < f32::EPSILON); assert!((rects[0].y() - 80.0).abs() < f32::EPSILON); }
645
646 #[test]
647 fn test_fixed_width_resists_zero_min_query() {
648 let layout = FrameLayout {
649 min_width: Some(Computed::constant(120.0)),
650 ideal_width: Some(Computed::constant(120.0)),
651 max_width: Some(Computed::constant(120.0)),
652 ..Default::default()
653 };
654
655 let mut child = MockSubView {
656 size: Size::new(30.0, 20.0),
657 };
658 let children: Vec<&dyn SubView> = vec![&mut child];
659
660 let size = layout.size_that_fits(ProposalSize::ZERO, &children);
661 assert!((size.width - 120.0).abs() < f32::EPSILON);
662 }
663
664 #[test]
665 fn test_min_width_clamps_zero_min_query() {
666 let layout = FrameLayout {
667 min_width: Some(Computed::constant(64.0)),
668 ..Default::default()
669 };
670
671 let mut child = MockSubView {
672 size: Size::new(10.0, 10.0),
673 };
674 let children: Vec<&dyn SubView> = vec![&mut child];
675
676 let size = layout.size_that_fits(ProposalSize::ZERO, &children);
677 assert!((size.width - 64.0).abs() < f32::EPSILON);
678 }
679
680 #[test]
681 fn reactive_width_invalidates_and_updates_measurement() {
682 let width = binding(80.0_f32);
683 let signal = width.computed();
684 let layout = FrameLayout {
685 min_width: Some(signal.clone()),
686 ideal_width: Some(signal.clone()),
687 max_width: Some(signal),
688 ..Default::default()
689 };
690 let invalidations = Rc::new(Cell::new(0));
691 let callback_invalidations = Rc::clone(&invalidations);
692 let _guards = layout.watch_invalidation(Rc::new(move || {
693 callback_invalidations.set(callback_invalidations.get() + 1);
694 }));
695
696 width.set(120.0);
697 assert_eq!(invalidations.get(), 1);
698
699 let child = MockSubView {
700 size: Size::new(10.0, 10.0),
701 };
702 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
703 assert_eq!(size.width, 120.0);
704 }
705
706 #[test]
707 fn a_minimum_alone_does_not_make_the_frame_greedy() {
708 let layout = FrameLayout {
711 min_width: Some(Computed::constant(50.0)),
712 ..Default::default()
713 };
714
715 let child = MockSubView {
716 size: Size::new(30.0, 20.0),
717 };
718 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
719
720 assert!(
721 (size.width - 50.0).abs() < f32::EPSILON,
722 "expected the frame to stay at its minimum, got {}",
723 size.width
724 );
725 }
726
727 #[test]
728 fn a_bounded_maximum_fills_up_to_that_maximum() {
729 let layout = FrameLayout {
731 max_width: Some(Computed::constant(100.0)),
732 ..Default::default()
733 };
734
735 let child = MockSubView {
736 size: Size::new(30.0, 20.0),
737 };
738 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
739
740 assert!(
741 (size.width - 100.0).abs() < f32::EPSILON,
742 "expected the frame to fill to its maximum, got {}",
743 size.width
744 );
745 }
746
747 #[test]
748 fn an_infinite_maximum_takes_the_whole_offer() {
749 let layout = FrameLayout {
750 max_width: Some(Computed::constant(f32::INFINITY)),
751 ..Default::default()
752 };
753
754 let child = MockSubView {
755 size: Size::new(30.0, 20.0),
756 };
757 let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
758
759 assert!(
760 (size.width - 300.0).abs() < f32::EPSILON,
761 "expected the frame to fill the proposal, got {}",
762 size.width
763 );
764 }
765
766 #[test]
767 fn an_ideal_size_is_what_an_unconstrained_parent_gets() {
768 let layout = FrameLayout {
769 ideal_width: Some(Computed::constant(120.0)),
770 ..Default::default()
771 };
772
773 let child = FillingSubView {
774 intrinsic: Size::new(30.0, 20.0),
775 };
776
777 let unconstrained = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
780 assert_extent(
781 unconstrained.width,
782 120.0,
783 "the unconstrained frame's width",
784 );
785
786 let proposed = layout.size_that_fits(ProposalSize::new(Some(300.0), Some(90.0)), &[&child]);
790 assert_extent(proposed.width, 300.0, "the proposed frame's width");
791 assert_extent(proposed.height, 90.0, "the proposed frame's height");
792 }
793
794 #[test]
795 fn an_ideal_does_not_cap_the_child_once_the_frame_has_grown() {
796 let layout = FrameLayout {
801 ideal_width: Some(Computed::constant(100.0)),
802 ideal_height: Some(Computed::constant(50.0)),
803 max_width: Some(Computed::constant(f32::INFINITY)),
804 max_height: Some(Computed::constant(f32::INFINITY)),
805 ..Default::default()
806 };
807
808 let child = RecordingSubView::new(FillingSubView {
809 intrinsic: Size::new(30.0, 20.0),
810 });
811
812 let size = layout.size_that_fits(ProposalSize::new(Some(320.0), Some(180.0)), &[&child]);
813 assert_extent(size.width, 320.0, "the frame's width");
814 assert_extent(size.height, 180.0, "the frame's height");
815
816 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(320.0, 180.0));
817 let rects = layout.place(bounds, &[&child]);
818
819 assert_eq!(
820 child.proposal(),
821 ProposalSize::new(Some(320.0), Some(180.0)),
822 "the child should be proposed the frame's resolved bounds"
823 );
824 assert_extent(rects[0].width(), 320.0, "the child's width");
825 assert_extent(rects[0].height(), 180.0, "the child's height");
826 }
827
828 #[test]
829 fn an_unconstrained_frame_takes_its_ideal_and_hands_that_to_the_child() {
830 let layout = FrameLayout {
833 ideal_width: Some(Computed::constant(100.0)),
834 ideal_height: Some(Computed::constant(50.0)),
835 ..Default::default()
836 };
837
838 let child = RecordingSubView::new(FillingSubView {
839 intrinsic: Size::new(30.0, 20.0),
840 });
841
842 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
843 assert_extent(size.width, 100.0, "the frame's width");
844 assert_extent(size.height, 50.0, "the frame's height");
845
846 let rects = layout.place(Rect::new(Point::new(0.0, 0.0), size), &[&child]);
847 assert_eq!(
848 child.proposal(),
849 ProposalSize::new(Some(100.0), Some(50.0)),
850 "the child should be proposed the ideal the frame resolved to"
851 );
852 assert_extent(rects[0].width(), 100.0, "the child's width");
853 assert_extent(rects[0].height(), 50.0, "the child's height");
854 }
855
856 #[test]
857 fn placement_holds_the_frames_own_minimum_and_maximum() {
858 let layout = FrameLayout {
862 min_width: Some(Computed::constant(120.0)),
863 max_height: Some(Computed::constant(40.0)),
864 ..Default::default()
865 };
866
867 let child = RecordingSubView::new(FillingSubView {
868 intrinsic: Size::new(30.0, 20.0),
869 });
870
871 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(80.0, 300.0));
872 layout.place(bounds, &[&child]);
873
874 assert_eq!(
875 child.proposal(),
876 ProposalSize::new(Some(120.0), Some(40.0)),
877 "the child's proposal should be the bounds clamped by the frame"
878 );
879 }
880
881 #[test]
882 fn an_ideal_that_is_also_the_maximum_is_a_natural_size_that_only_shrinks() {
883 let layout = FrameLayout {
890 ideal_width: Some(Computed::constant(24.0)),
891 ideal_height: Some(Computed::constant(24.0)),
892 max_width: Some(Computed::constant(24.0)),
893 max_height: Some(Computed::constant(24.0)),
894 ..Default::default()
895 };
896 let scene = || {
897 RecordingSubView::new(FillingSubView {
898 intrinsic: Size::zero(),
899 })
900 };
901
902 let child = scene();
904 let natural = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
905 assert_extent(natural.width, 24.0, "the unproposed width");
906 assert_extent(natural.height, 24.0, "the unproposed height");
907
908 let child = scene();
911 let in_a_row = layout.size_that_fits(ProposalSize::new(None, Some(44.0)), &[&child]);
912 assert_eq!(
913 child.proposal(),
914 ProposalSize::new(Some(24.0), Some(24.0)),
915 "the scene should never be offered more than the natural size"
916 );
917 assert_extent(in_a_row.width, 24.0, "the width in a taller row");
918 assert_extent(in_a_row.height, 24.0, "the height in a taller row");
919
920 let child = scene();
922 let resized = layout.size_that_fits(ProposalSize::new(Some(8.0), Some(10.0)), &[&child]);
923 assert_eq!(
924 child.proposal(),
925 ProposalSize::new(Some(8.0), Some(10.0)),
926 "a smaller box should reach the scene unaltered"
927 );
928 assert_extent(resized.width, 8.0, "the resized width");
929 assert_extent(resized.height, 10.0, "the resized height");
930 }
931
932 #[test]
933 fn a_filling_frame_offers_its_whole_width_to_a_rigid_child() {
934 let layout = FrameLayout {
938 max_width: Some(Computed::constant(f32::INFINITY)),
939 ..Default::default()
940 };
941
942 let child = RecordingSubView::new(MockSubView {
943 size: Size::new(30.0, 20.0),
944 });
945
946 let size = layout.size_that_fits(ProposalSize::new(Some(320.0), Some(180.0)), &[&child]);
947 assert_extent(size.width, 320.0, "the frame's width");
948 assert_extent(size.height, 20.0, "the frame's height");
949
950 let rects = layout.place(Rect::new(Point::new(0.0, 0.0), size), &[&child]);
951 assert_eq!(
952 child.proposal(),
953 ProposalSize::new(Some(320.0), Some(20.0)),
954 "a rigid child is still offered the frame it sits in"
955 );
956 assert_extent(rects[0].width(), 30.0, "the child's width");
957 assert_extent(rects[0].x(), 145.0, "the child's x");
958 }
959}