use alloc::{collections::BTreeSet, vec, vec::Vec};
use nami::{Computed, Signal, SignalExt};
use waterui_core::{
AnyView, IntoSignalF32, View,
layout::{LayoutInvalidationCallback, StretchAxis},
};
use crate::{
Layout, PlacedSubview, Point, ProposalSize, Rect, Size, SubView, SubviewPlacement,
ViewDimensions,
container::FixedContainer,
stack::{
Alignment, HorizontalAlignment, VerticalAlignment,
distribute::{LineAnchor, container_line},
},
};
#[derive(Debug, Clone, Default)]
pub struct FrameLayout {
min_width: Option<Computed<f32>>,
ideal_width: Option<Computed<f32>>,
max_width: Option<Computed<f32>>,
min_height: Option<Computed<f32>>,
ideal_height: Option<Computed<f32>>,
max_height: Option<Computed<f32>>,
alignment: Alignment,
}
#[derive(Clone, Copy)]
struct ResolvedFrameLayout {
min_width: Option<f32>,
ideal_width: Option<f32>,
max_width: Option<f32>,
min_height: Option<f32>,
ideal_height: Option<f32>,
max_height: Option<f32>,
}
impl FrameLayout {
fn resolved(&self) -> ResolvedFrameLayout {
ResolvedFrameLayout {
min_width: self.min_width.as_ref().map(Signal::get),
ideal_width: self.ideal_width.as_ref().map(Signal::get),
max_width: self.max_width.as_ref().map(Signal::get),
min_height: self.min_height.as_ref().map(Signal::get),
ideal_height: self.ideal_height.as_ref().map(Signal::get),
max_height: self.max_height.as_ref().map(Signal::get),
}
}
}
impl ResolvedFrameLayout {
fn resolve(&self, proposal: ProposalSize, child: Size) -> Size {
Size::new(
frame_resolved_axis(
proposal.width,
child.width,
self.min_width,
self.ideal_width,
self.max_width,
),
frame_resolved_axis(
proposal.height,
child.height,
self.min_height,
self.ideal_height,
self.max_height,
),
)
}
fn placement_proposal(&self, bounds: Rect, proposal: ProposalSize) -> ProposalSize {
self.child_proposal(ProposalSize {
width: if self.min_width.is_some() || self.max_width.is_some() {
Some(bounds.width())
} else {
proposal.width
},
height: if self.min_height.is_some() || self.max_height.is_some() {
Some(bounds.height())
} else {
proposal.height
},
})
}
fn child_proposal(&self, proposal: ProposalSize) -> ProposalSize {
ProposalSize {
width: frame_child_proposal_axis(
proposal.width,
self.min_width,
self.ideal_width,
self.max_width,
),
height: frame_child_proposal_axis(
proposal.height,
self.min_height,
self.ideal_height,
self.max_height,
),
}
}
}
impl Layout for FrameLayout {
fn stretch_axis(&self, _children: &[StretchAxis]) -> StretchAxis {
let resolved = self.resolved();
let horizontal = resolved.max_width.is_some_and(f32::is_infinite);
let vertical = resolved.max_height.is_some_and(f32::is_infinite);
match (horizontal, vertical) {
(true, true) => StretchAxis::Both,
(true, false) => StretchAxis::Horizontal,
(false, true) => StretchAxis::Vertical,
(false, false) => StretchAxis::None,
}
}
fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
let resolved = self.resolved();
let child_proposal = resolved.child_proposal(proposal);
let Some(child) = children.first() else {
return resolved.resolve(proposal, Size::zero());
};
let first = resolved.resolve(proposal, child.measure(child_proposal).size);
let placement = resolved.placement_proposal(Rect::from_size(first), proposal);
if placement == child_proposal {
first
} else {
resolved.resolve(proposal, child.measure(placement).size)
}
}
fn place(
&self,
bounds: Rect,
proposal: ProposalSize,
children: &[&dyn SubView],
) -> Vec<SubviewPlacement> {
if children.is_empty() {
return vec![];
}
let resolved = self.resolved();
let child_proposal = resolved.placement_proposal(bounds, proposal);
let child_dimensions = children
.first()
.map_or(ViewDimensions::new(Size::zero()), |c| {
c.measure(child_proposal)
});
let child_size = child_dimensions.size;
let child_width = if child_size.width.is_infinite() {
bounds.width()
} else {
child_size.width
};
let child_height = if child_size.height.is_infinite() {
bounds.height()
} else {
child_size.height
};
let final_child_size = Size::new(child_width, child_height);
let mut adjusted_dimensions = child_dimensions;
adjusted_dimensions.size = final_child_size;
let horizontal = self.alignment.horizontal();
let guide_x = adjusted_dimensions.horizontal(horizontal);
let child_x = bounds.x()
+ container_line(
bounds.width(),
LineAnchor::horizontal(horizontal),
guide_x,
final_child_size.width - guide_x,
)
- guide_x;
let vertical = self.alignment.vertical();
let guide_y = adjusted_dimensions.vertical(vertical);
let child_y = bounds.y()
+ container_line(
bounds.height(),
LineAnchor::vertical(vertical),
guide_y,
final_child_size.height - guide_y,
)
- guide_y;
vec![SubviewPlacement::new(
Rect::new(Point::new(child_x, child_y), final_child_size),
child_proposal,
)]
}
fn explicit_horizontal(
&self,
alignment: HorizontalAlignment,
_bounds: Rect,
children: &[PlacedSubview<'_>],
) -> Option<f32> {
children
.first()
.and_then(|child| child.explicit_horizontal(alignment))
}
fn explicit_vertical(
&self,
alignment: VerticalAlignment,
_bounds: Rect,
children: &[PlacedSubview<'_>],
) -> Option<f32> {
children
.first()
.and_then(|child| child.explicit_vertical(alignment))
}
fn watch_invalidation(
&self,
invalidate: LayoutInvalidationCallback,
) -> Vec<nami::watcher::BoxWatcherGuard> {
let signals = [
self.min_width.as_ref(),
self.ideal_width.as_ref(),
self.max_width.as_ref(),
self.min_height.as_ref(),
self.ideal_height.as_ref(),
self.max_height.as_ref(),
];
let mut identities = BTreeSet::new();
let mut guards = Vec::new();
for signal in signals.into_iter().flatten() {
if signal
.identity()
.is_some_and(|identity| !identities.insert(identity))
{
continue;
}
let invalidate = invalidate.clone();
guards.push(signal.watch(move |_| invalidate()));
}
guards
}
}
#[inline]
fn clamp_frame_axis(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
value
.max(min.unwrap_or(f32::NEG_INFINITY))
.min(max.unwrap_or(f32::INFINITY))
}
#[inline]
fn frame_child_proposal_axis(
parent_proposal: Option<f32>,
min: Option<f32>,
ideal: Option<f32>,
max: Option<f32>,
) -> Option<f32> {
parent_proposal
.or(ideal)
.map(|value| clamp_frame_axis(value, min, max))
}
#[inline]
fn frame_resolved_axis(
parent_proposal: Option<f32>,
child_size: f32,
min: Option<f32>,
ideal: Option<f32>,
max: Option<f32>,
) -> f32 {
let content = if max.is_some() && parent_proposal != Some(0.0) {
parent_proposal.or(ideal).unwrap_or(child_size)
} else {
child_size
};
clamp_frame_axis(content, min, max)
}
#[derive(Debug)]
pub struct Frame {
layout: FrameLayout,
content: AnyView,
}
impl Frame {
#[must_use]
pub fn new(content: impl View) -> Self {
Self {
layout: FrameLayout::default(),
content: AnyView::new(content),
}
}
#[must_use]
pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
self.layout.alignment = alignment.into();
self
}
crate::alignment::two_dimensional_alignment_methods!();
#[must_use]
pub fn width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
let width = width.into_signal_f32().computed();
self.layout.min_width = Some(width.clone());
self.layout.ideal_width = Some(width.clone());
self.layout.max_width = Some(width);
self
}
#[must_use]
pub fn height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
let height = height.into_signal_f32().computed();
self.layout.min_height = Some(height.clone());
self.layout.ideal_height = Some(height.clone());
self.layout.max_height = Some(height);
self
}
#[must_use]
pub fn ideal_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
self.layout.ideal_width = Some(width.into_signal_f32().computed());
self
}
#[must_use]
pub fn ideal_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
self.layout.ideal_height = Some(height.into_signal_f32().computed());
self
}
#[must_use]
pub fn min_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
self.layout.min_width = Some(width.into_signal_f32().computed());
self
}
#[must_use]
pub fn max_width(mut self, width: impl IntoSignalF32 + 'static) -> Self {
self.layout.max_width = Some(width.into_signal_f32().computed());
self
}
#[must_use]
pub fn min_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
self.layout.min_height = Some(height.into_signal_f32().computed());
self
}
#[must_use]
pub fn max_height(mut self, height: impl IntoSignalF32 + 'static) -> Self {
self.layout.max_height = Some(height.into_signal_f32().computed());
self
}
}
impl View for Frame {
fn body(self, _env: &waterui_core::Environment) -> impl View {
FixedContainer::new(self.layout, vec![self.content])
}
fn stretch_axis(&self) -> StretchAxis {
self.layout.stretch_axis(&[self.content.stretch_axis()])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::StretchAxis;
use alloc::rc::Rc;
use core::cell::Cell;
use nami::{SignalExt, binding};
#[test]
fn constrained_axes_select_their_owned_region_during_placement() {
for (minimum, maximum, ideal, expected) in [
(Some(28.0), Some(28.0), 0.0, 28.0),
(Some(100.0), None, 0.0, 100.0),
(None, Some(60.0), 200.0, 60.0),
] {
let layout = FrameLayout {
min_width: minimum.map(Computed::constant),
max_width: maximum.map(Computed::constant),
..Default::default()
};
let child = RecordingSubView::new(FillingSubView {
intrinsic: Size::new(ideal, 10.0),
});
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_eq!(size, Size::new(expected, 10.0));
let placements =
layout.place(Rect::from_size(size), ProposalSize::UNSPECIFIED, &[&child]);
assert_eq!(
placements[0].proposal,
ProposalSize::new(Some(expected), None)
);
assert_eq!(*placements[0].frame.size(), Size::new(expected, 10.0));
}
}
struct FramedChild<C> {
layout: FrameLayout,
child: C,
}
impl<C: SubView> SubView for FramedChild<C> {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.layout.size_that_fits(proposal, &[&self.child]))
}
fn stretch_axis(&self) -> StretchAxis {
self.layout.stretch_axis(&[self.child.stretch_axis()])
}
fn priority(&self) -> i32 {
0
}
}
struct WrappingChild;
impl SubView for WrappingChild {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let width = proposal.width.unwrap_or(200.0).clamp(10.0, 200.0);
ViewDimensions::new(Size::new(width, (200.0 / width).ceil() * 10.0))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
#[test]
fn finite_stack_offers_remeasure_capped_wrapping_children() {
let child = FramedChild {
layout: FrameLayout {
max_width: Some(Computed::constant(60.0)),
..Default::default()
},
child: WrappingChild,
};
let stack = crate::stack::HStackLayout::default();
assert_eq!(
stack.size_that_fits(ProposalSize::new(Some(240.0), None), &[&child]),
Size::new(60.0, 40.0)
);
}
#[test]
fn finite_maximum_frames_grow_only_to_their_reported_limit() {
let child = FramedChild {
layout: FrameLayout {
max_width: Some(Computed::constant(100.0)),
..Default::default()
},
child: FillingSubView {
intrinsic: Size::new(20.0, 10.0),
},
};
let stack = crate::stack::HStackLayout::default();
for (offer, width) in [(80.0, 80.0), (240.0, 100.0)] {
assert_eq!(
stack.size_that_fits(ProposalSize::new(Some(offer), None), &[&child]),
Size::new(width, 10.0)
);
}
}
#[test]
fn a_max_only_frame_reports_the_child_floor_on_a_min_size_query() {
let layout = FrameLayout {
max_width: Some(Computed::constant(f32::INFINITY)),
..Default::default()
};
let child = MockSubView {
size: Size::new(220.0, 40.0),
};
let size = layout.size_that_fits(ProposalSize::ZERO, &[&child]);
assert_extent(size.width, 220.0, "the frame's minimum width");
let size = layout.size_that_fits(ProposalSize::new(Some(500.0), None), &[&child]);
assert_extent(size.width, 500.0, "the frame's offered width");
}
#[test]
fn a_bounded_maximum_clamps_the_reported_floor() {
let layout = FrameLayout {
max_width: Some(Computed::constant(100.0)),
..Default::default()
};
let child = MockSubView {
size: Size::new(220.0, 40.0),
};
let size = layout.size_that_fits(ProposalSize::ZERO, &[&child]);
assert_extent(size.width, 100.0, "the frame's clamped minimum width");
}
struct MockSubView {
size: Size,
}
impl SubView for MockSubView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.size)
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
struct FillingSubView {
intrinsic: Size,
}
impl SubView for FillingSubView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(Size::new(
proposal.width.unwrap_or(self.intrinsic.width),
proposal.height.unwrap_or(self.intrinsic.height),
))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Both
}
fn priority(&self) -> i32 {
0
}
}
struct RecordingSubView<C> {
inner: C,
proposal: Cell<Option<ProposalSize>>,
}
impl<C> RecordingSubView<C> {
fn new(inner: C) -> Self {
Self {
inner,
proposal: Cell::new(None),
}
}
fn proposal(&self) -> ProposalSize {
self.proposal
.get()
.expect("the frame never measured its child")
}
}
impl<C: SubView> SubView for RecordingSubView<C> {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
self.proposal.set(Some(proposal));
self.inner.measure(proposal)
}
fn stretch_axis(&self) -> StretchAxis {
self.inner.stretch_axis()
}
fn priority(&self) -> i32 {
self.inner.priority()
}
}
fn assert_extent(actual: f32, expected: f32, what: &str) {
assert!(
(actual - expected).abs() < f32::EPSILON,
"expected {what} to be {expected}, got {actual}"
);
}
#[test]
fn an_ideal_frame_still_adopts_a_rigid_child_size() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(100.0)),
ideal_height: Some(Computed::constant(50.0)),
..Default::default()
};
let child = RecordingSubView::new(MockSubView {
size: Size::new(30.0, 20.0),
});
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(100.0), Some(50.0)),
"the ideal should answer the dimensions the parent left unspecified"
);
assert_extent(size.width, 30.0, "the frame's width");
assert_extent(size.height, 20.0, "the frame's height");
}
#[test]
fn an_ideal_answers_only_the_dimensions_the_parent_left_unspecified() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(24.0)),
ideal_height: Some(Computed::constant(24.0)),
..Default::default()
};
let proposal = ProposalSize::new(Some(48.0), Some(36.0));
let resizable = RecordingSubView::new(FillingSubView {
intrinsic: Size::new(24.0, 24.0),
});
let filled = layout.size_that_fits(proposal, &[&resizable]);
assert_eq!(
resizable.proposal(),
proposal,
"the child should hear the parent's proposal, not the ideal"
);
assert_extent(filled.width, 48.0, "a resizable child's frame width");
assert_extent(filled.height, 36.0, "a resizable child's frame height");
let placements = layout.place(
Rect::new(Point::new(0.0, 0.0), filled),
proposal,
&[&resizable],
);
assert_eq!(
resizable.proposal(),
proposal,
"placement should re-propose what measurement proposed"
);
assert_extent(
placements[0].frame.width(),
48.0,
"the resizable child's width",
);
assert_extent(
placements[0].frame.height(),
36.0,
"the resizable child's height",
);
let rigid = RecordingSubView::new(MockSubView {
size: Size::new(24.0, 24.0),
});
let hugged = layout.size_that_fits(proposal, &[&rigid]);
assert_eq!(
rigid.proposal(),
proposal,
"a rigid child hears the proposal too; it just declines it"
);
assert_extent(hugged.width, 24.0, "a rigid child's frame width");
assert_extent(hugged.height, 24.0, "a rigid child's frame height");
}
#[test]
fn a_rigid_child_does_not_grow_into_a_larger_proposal() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(24.0)),
..Default::default()
};
let child = MockSubView {
size: Size::new(24.0, 24.0),
};
let size = layout.size_that_fits(ProposalSize::new(Some(400.0), None), &[&child]);
assert_extent(size.width, 24.0, "the frame's width");
}
#[test]
fn test_frame_alignment_bounded_proposal() {
let layout = FrameLayout {
alignment: Alignment::BottomTrailing,
..Default::default()
};
let mut child = MockSubView {
size: Size::new(30.0, 20.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
let placements = layout.place(bounds, proposal, &children);
assert!((placements[0].frame.x() - 70.0).abs() < f32::EPSILON); assert!((placements[0].frame.y() - 80.0).abs() < f32::EPSILON); }
#[test]
fn test_fixed_width_resists_zero_min_query() {
let layout = FrameLayout {
min_width: Some(Computed::constant(120.0)),
ideal_width: Some(Computed::constant(120.0)),
max_width: Some(Computed::constant(120.0)),
..Default::default()
};
let mut child = MockSubView {
size: Size::new(30.0, 20.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let size = layout.size_that_fits(ProposalSize::ZERO, &children);
assert!((size.width - 120.0).abs() < f32::EPSILON);
}
#[test]
fn test_min_width_clamps_zero_min_query() {
let layout = FrameLayout {
min_width: Some(Computed::constant(64.0)),
..Default::default()
};
let mut child = MockSubView {
size: Size::new(10.0, 10.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let size = layout.size_that_fits(ProposalSize::ZERO, &children);
assert!((size.width - 64.0).abs() < f32::EPSILON);
}
#[test]
fn reactive_width_invalidates_and_updates_measurement() {
let width = binding(80.0_f32);
let signal = width.computed();
let layout = FrameLayout {
min_width: Some(signal.clone()),
ideal_width: Some(signal.clone()),
max_width: Some(signal),
..Default::default()
};
let invalidations = Rc::new(Cell::new(0));
let callback_invalidations = Rc::clone(&invalidations);
let _guards = layout.watch_invalidation(Rc::new(move || {
callback_invalidations.set(callback_invalidations.get() + 1);
}));
width.set(120.0);
assert_eq!(invalidations.get(), 1);
let child = MockSubView {
size: Size::new(10.0, 10.0),
};
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_eq!(size.width, 120.0);
}
#[test]
fn a_minimum_alone_does_not_make_the_frame_greedy() {
let layout = FrameLayout {
min_width: Some(Computed::constant(50.0)),
..Default::default()
};
let child = MockSubView {
size: Size::new(30.0, 20.0),
};
let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
assert!(
(size.width - 50.0).abs() < f32::EPSILON,
"expected the frame to stay at its minimum, got {}",
size.width
);
}
#[test]
fn a_bounded_maximum_fills_up_to_that_maximum() {
let layout = FrameLayout {
max_width: Some(Computed::constant(100.0)),
..Default::default()
};
let child = MockSubView {
size: Size::new(30.0, 20.0),
};
let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
assert!(
(size.width - 100.0).abs() < f32::EPSILON,
"expected the frame to fill to its maximum, got {}",
size.width
);
}
#[test]
fn an_infinite_maximum_takes_the_whole_offer() {
let layout = FrameLayout {
max_width: Some(Computed::constant(f32::INFINITY)),
..Default::default()
};
let child = MockSubView {
size: Size::new(30.0, 20.0),
};
let size = layout.size_that_fits(ProposalSize::new(Some(300.0), None), &[&child]);
assert!(
(size.width - 300.0).abs() < f32::EPSILON,
"expected the frame to fill the proposal, got {}",
size.width
);
}
#[test]
fn an_ideal_size_is_what_an_unconstrained_parent_gets() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(120.0)),
..Default::default()
};
let child = FillingSubView {
intrinsic: Size::new(30.0, 20.0),
};
let unconstrained = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_extent(
unconstrained.width,
120.0,
"the unconstrained frame's width",
);
let proposed = layout.size_that_fits(ProposalSize::new(Some(300.0), Some(90.0)), &[&child]);
assert_extent(proposed.width, 300.0, "the proposed frame's width");
assert_extent(proposed.height, 90.0, "the proposed frame's height");
}
#[test]
fn an_ideal_does_not_cap_the_child_once_the_frame_has_grown() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(100.0)),
ideal_height: Some(Computed::constant(50.0)),
max_width: Some(Computed::constant(f32::INFINITY)),
max_height: Some(Computed::constant(f32::INFINITY)),
..Default::default()
};
let child = RecordingSubView::new(FillingSubView {
intrinsic: Size::new(30.0, 20.0),
});
let size = layout.size_that_fits(ProposalSize::new(Some(320.0), Some(180.0)), &[&child]);
assert_extent(size.width, 320.0, "the frame's width");
assert_extent(size.height, 180.0, "the frame's height");
let proposal = ProposalSize::new(Some(320.0), Some(180.0));
let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(320.0, 180.0));
let placements = layout.place(bounds, proposal, &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(320.0), Some(180.0)),
"the child should be re-proposed what it was measured with"
);
assert_extent(placements[0].frame.width(), 320.0, "the child's width");
assert_extent(placements[0].frame.height(), 180.0, "the child's height");
}
#[test]
fn an_unconstrained_frame_takes_its_ideal_and_hands_that_to_the_child() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(100.0)),
ideal_height: Some(Computed::constant(50.0)),
..Default::default()
};
let child = RecordingSubView::new(FillingSubView {
intrinsic: Size::new(30.0, 20.0),
});
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_extent(size.width, 100.0, "the frame's width");
assert_extent(size.height, 50.0, "the frame's height");
let placements = layout.place(
Rect::new(Point::new(0.0, 0.0), size),
ProposalSize::UNSPECIFIED,
&[&child],
);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(100.0), Some(50.0)),
"the child should be proposed the ideal the frame resolved to"
);
assert_extent(placements[0].frame.width(), 100.0, "the child's width");
assert_extent(placements[0].frame.height(), 50.0, "the child's height");
}
#[test]
fn placement_holds_the_frames_own_minimum_and_maximum_bounded_proposal() {
let layout = FrameLayout {
min_width: Some(Computed::constant(120.0)),
max_height: Some(Computed::constant(40.0)),
..Default::default()
};
let child = RecordingSubView::new(FillingSubView {
intrinsic: Size::new(30.0, 20.0),
});
let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(80.0, 300.0));
let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
layout.place(bounds, proposal, &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(120.0), Some(40.0)),
"the child's proposal should be the offer clamped by the frame"
);
}
#[test]
fn an_ideal_that_is_also_the_maximum_is_a_natural_size_that_only_shrinks() {
let layout = FrameLayout {
ideal_width: Some(Computed::constant(24.0)),
ideal_height: Some(Computed::constant(24.0)),
max_width: Some(Computed::constant(24.0)),
max_height: Some(Computed::constant(24.0)),
..Default::default()
};
let scene = || {
RecordingSubView::new(FillingSubView {
intrinsic: Size::zero(),
})
};
let child = scene();
let natural = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[&child]);
assert_extent(natural.width, 24.0, "the unproposed width");
assert_extent(natural.height, 24.0, "the unproposed height");
let child = scene();
let in_a_row = layout.size_that_fits(ProposalSize::new(None, Some(44.0)), &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(24.0), Some(24.0)),
"the scene should never be offered more than the natural size"
);
assert_extent(in_a_row.width, 24.0, "the width in a taller row");
assert_extent(in_a_row.height, 24.0, "the height in a taller row");
let child = scene();
let resized = layout.size_that_fits(ProposalSize::new(Some(8.0), Some(10.0)), &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(8.0), Some(10.0)),
"a smaller box should reach the scene unaltered"
);
assert_extent(resized.width, 8.0, "the resized width");
assert_extent(resized.height, 10.0, "the resized height");
}
#[test]
fn a_filling_frame_reproposes_its_offer_to_a_rigid_child() {
let layout = FrameLayout {
max_width: Some(Computed::constant(f32::INFINITY)),
..Default::default()
};
let child = RecordingSubView::new(MockSubView {
size: Size::new(30.0, 20.0),
});
let proposal = ProposalSize::new(Some(320.0), Some(180.0));
let size = layout.size_that_fits(proposal, &[&child]);
assert_extent(size.width, 320.0, "the frame's width");
assert_extent(size.height, 20.0, "the frame's height");
let placements = layout.place(Rect::new(Point::new(0.0, 0.0), size), proposal, &[&child]);
assert_eq!(
child.proposal(),
ProposalSize::new(Some(320.0), Some(180.0)),
"a rigid child is re-proposed the frame's own measurement proposal"
);
assert_extent(placements[0].frame.width(), 30.0, "the child's width");
assert_extent(placements[0].frame.x(), 145.0, "the child's x");
}
}