1use alloc::{vec, vec::Vec};
4use nami::{Computed, Signal, SignalExt, collection::Collection};
5use num_traits::ToPrimitive;
6use waterui_core::{
7 AnyView, IntoSignalF32, View, env::with, id::Identifiable, layout::LayoutInvalidationCallback,
8 view::TupleViews, views::ForEach,
9};
10
11use crate::{
12 Layout, LazyContainer, PlacedSubview, Point, ProposalSize, Rect, Size, StretchAxis, SubView,
13 ViewDimensions,
14 container::FixedContainer,
15 stack::{
16 Axis, VerticalAlignment,
17 distribute::{Extent, compress_to_fit, exceeds},
18 stack_stretch_axis,
19 },
20};
21
22#[derive(Debug, Clone)]
62pub struct HStack<C> {
63 layout: HStackLayout,
64 contents: C,
65}
66
67#[derive(Debug, Clone)]
69pub struct HStackLayout {
70 pub alignment: VerticalAlignment,
72 pub spacing: Computed<f32>,
74}
75
76impl Default for HStackLayout {
77 fn default() -> Self {
78 Self {
79 alignment: VerticalAlignment::Center,
80 spacing: Computed::constant(10.0),
81 }
82 }
83}
84
85struct ChildMeasurement {
87 dimensions: ViewDimensions,
88 stretch_axis: StretchAxis,
89}
90
91impl ChildMeasurement {
92 const fn size(&self) -> Size {
93 self.dimensions.size
94 }
95
96 fn vertical_guide(&self, alignment: VerticalAlignment) -> f32 {
97 self.dimensions.vertical(alignment)
98 }
99
100 const fn stretches_main_axis(&self) -> bool {
105 matches!(
106 self.stretch_axis,
107 StretchAxis::Horizontal | StretchAxis::Both | StretchAxis::MainAxis
108 )
109 }
110
111 const fn stretches_cross_axis(&self) -> bool {
115 matches!(
116 self.stretch_axis,
117 StretchAxis::Vertical | StretchAxis::Both | StretchAxis::CrossAxis
118 )
119 }
120}
121
122fn hstack_intrinsic_cross_metrics(
132 measurements: &[ChildMeasurement],
133 alignment: VerticalAlignment,
134) -> (f32, f32) {
135 let mut max_above = 0.0_f32;
136 let mut max_below = 0.0_f32;
137
138 for measurement in measurements
139 .iter()
140 .filter(|measurement| measurement.size().height.is_finite())
141 {
142 let size = measurement.size();
143 let guide = measurement
144 .vertical_guide(alignment)
145 .clamp(0.0, size.height);
146 max_above = max_above.max(guide);
147 max_below = max_below.max((size.height - guide).max(0.0));
148 }
149
150 (max_above, max_below)
151}
152
153fn hstack_container_guide_offset(
154 bounds: Rect,
155 alignment: VerticalAlignment,
156 intrinsic_above: f32,
157) -> f32 {
158 if alignment == VerticalAlignment::Top {
159 0.0
160 } else if alignment == VerticalAlignment::Bottom {
161 bounds.height()
162 } else if alignment == VerticalAlignment::Center {
163 bounds.height() * 0.5
164 } else {
165 intrinsic_above
166 }
167}
168
169fn hstack_stretch_allocations(
170 stretch_indices: &[usize],
171 available_for_children: f32,
172 fixed_width: f32,
173) -> Vec<(usize, f32)> {
174 if stretch_indices.is_empty() {
175 return Vec::new();
176 }
177
178 let width =
179 (available_for_children - fixed_width).max(0.0) / usize_to_f32(stretch_indices.len());
180
181 stretch_indices.iter().map(|&idx| (idx, width)).collect()
182}
183
184fn compress_children(
191 measurements: &mut [ChildMeasurement],
192 children: &[&dyn SubView],
193 compress_indices: &[usize],
194 available: f32,
195 height_proposal: Option<f32>,
196) {
197 if compress_indices.is_empty() {
198 return;
199 }
200
201 let ideal_total: f32 = compress_indices
206 .iter()
207 .map(|&index| measurements[index].size().width)
208 .sum();
209 if !exceeds(ideal_total, available, compress_indices.len()) {
210 return;
211 }
212
213 let extents: Vec<Extent> = compress_indices
214 .iter()
215 .map(|&index| Extent {
216 ideal: measurements[index].size().width,
217 min: children[index]
218 .measure(ProposalSize::new(Some(0.0), height_proposal))
219 .size
220 .width,
221 priority: children[index].priority(),
222 })
223 .collect();
224
225 for (&index, resolved) in compress_indices
226 .iter()
227 .zip(compress_to_fit(&extents, available))
228 {
229 if resolved >= measurements[index].size().width {
230 continue;
231 }
232 let constrained = ProposalSize::new(Some(resolved), height_proposal);
233 measurements[index].dimensions = children[index].measure(constrained);
234 measurements[index].dimensions.size.width = measurements[index].size().width.min(resolved);
235 }
236}
237
238#[allow(clippy::too_many_lines)]
239impl Layout for HStackLayout {
240 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
247 stack_stretch_axis(Axis::Horizontal, children)
248 }
249
250 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
251 if children.is_empty() {
252 return Size::zero();
253 }
254
255 let spacing = self.spacing.get();
256 let total_spacing = if children.len() > 1 {
257 usize_to_f32(children.len() - 1) * spacing
258 } else {
259 0.0
260 };
261
262 let is_min_size_query = proposal.width == Some(0.0);
264 let intrinsic_proposal = if is_min_size_query {
265 ProposalSize::new(Some(0.0), proposal.height)
266 } else {
267 ProposalSize::new(None, proposal.height)
268 };
269 let mut measurements: Vec<ChildMeasurement> = children
270 .iter()
271 .map(|child| ChildMeasurement {
272 dimensions: child.measure(intrinsic_proposal),
273 stretch_axis: child.stretch_axis(),
274 })
275 .collect();
276
277 let has_main_axis_stretch = measurements
279 .iter()
280 .any(ChildMeasurement::stretches_main_axis);
281 let main_axis_stretch_indices: Vec<usize> = measurements
282 .iter()
283 .enumerate()
284 .filter(|(_, m)| m.stretches_main_axis())
285 .map(|(idx, _)| idx)
286 .collect();
287 let main_axis_stretch_count = main_axis_stretch_indices.len();
288
289 let intrinsic_width_all: f32 =
290 measurements.iter().map(|m| m.size().width).sum::<f32>() + total_spacing;
291
292 if is_min_size_query {
293 let (max_above, max_below) =
294 hstack_intrinsic_cross_metrics(&measurements, self.alignment);
295 return Size::new(intrinsic_width_all, max_above + max_below);
296 }
297
298 let intrinsic_width = intrinsic_width_all;
302
303 let final_width = proposal.width.map_or(intrinsic_width, |proposed| {
305 if has_main_axis_stretch {
306 proposed
307 } else {
308 intrinsic_width.min(proposed)
309 }
310 });
311
312 let available_for_children = (final_width - total_spacing).max(0.0);
315
316 let fixed_indices: Vec<usize> = if main_axis_stretch_count > 0 && proposal.width.is_some() {
317 measurements
318 .iter()
319 .enumerate()
320 .filter(|(_, m)| !m.stretches_main_axis())
321 .map(|(idx, _)| idx)
322 .collect()
323 } else {
324 (0..measurements.len()).collect()
325 };
326
327 if proposal.width.is_some() {
328 compress_children(
329 &mut measurements,
330 children,
331 &fixed_indices,
332 available_for_children,
333 proposal.height,
334 );
335 }
336
337 if proposal.width.is_some() && main_axis_stretch_count > 0 {
340 let fixed_width: f32 = measurements
341 .iter()
342 .enumerate()
343 .filter(|(_, m)| !m.stretches_main_axis())
344 .map(|(_, m)| m.size().width)
345 .sum();
346
347 let allocations = hstack_stretch_allocations(
348 &main_axis_stretch_indices,
349 available_for_children,
350 fixed_width,
351 );
352
353 for (idx, stretch_width) in allocations {
354 let constrained_proposal = ProposalSize::new(Some(stretch_width), proposal.height);
355 measurements[idx].dimensions = children[idx].measure(constrained_proposal);
356 measurements[idx].dimensions.size.width =
357 measurements[idx].size().width.max(stretch_width);
358 }
359 }
360
361 let (max_above, max_below) = hstack_intrinsic_cross_metrics(&measurements, self.alignment);
365 let max_height = max_above + max_below;
366
367 Size::new(final_width, max_height)
368 }
369
370 #[allow(clippy::too_many_lines)]
371 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
372 if children.is_empty() {
373 return vec![];
374 }
375
376 let spacing = self.spacing.get();
377 let total_spacing = if children.len() > 1 {
378 usize_to_f32(children.len() - 1) * spacing
379 } else {
380 0.0
381 };
382
383 let available_width = bounds.width() - total_spacing;
384
385 let intrinsic_proposal = ProposalSize::new(None, Some(bounds.height()));
387 let mut measurements: Vec<ChildMeasurement> = children
388 .iter()
389 .map(|child| ChildMeasurement {
390 dimensions: child.measure(intrinsic_proposal),
391 stretch_axis: child.stretch_axis(),
392 })
393 .collect();
394
395 let main_axis_stretch_indices: Vec<usize> = measurements
397 .iter()
398 .enumerate()
399 .filter(|(_, m)| m.stretches_main_axis())
400 .map(|(idx, _)| idx)
401 .collect();
402 let main_axis_stretch_count = main_axis_stretch_indices.len();
403
404 let fixed_indices: Vec<usize> = if main_axis_stretch_count > 0 {
405 measurements
406 .iter()
407 .enumerate()
408 .filter(|(_, m)| !m.stretches_main_axis())
409 .map(|(idx, _)| idx)
410 .collect()
411 } else {
412 (0..measurements.len()).collect()
413 };
414
415 compress_children(
416 &mut measurements,
417 children,
418 &fixed_indices,
419 available_width,
420 Some(bounds.height()),
421 );
422
423 let actual_fixed_width: f32 = measurements
425 .iter()
426 .enumerate()
427 .filter(|(_, m)| !m.stretches_main_axis())
428 .map(|(_, m)| m.size().width)
429 .sum();
430
431 let stretch_allocations = hstack_stretch_allocations(
432 &main_axis_stretch_indices,
433 available_width,
434 actual_fixed_width,
435 );
436
437 if main_axis_stretch_count > 0 {
439 for &(idx, stretch_width) in &stretch_allocations {
440 let constrained_proposal =
441 ProposalSize::new(Some(stretch_width), Some(bounds.height()));
442 measurements[idx].dimensions = children[idx].measure(constrained_proposal);
443 measurements[idx].dimensions.size.width =
444 measurements[idx].size().width.max(stretch_width);
445 }
446 }
447
448 let (intrinsic_above, _intrinsic_below) =
449 hstack_intrinsic_cross_metrics(&measurements, self.alignment);
450 let guide_line =
451 bounds.y() + hstack_container_guide_offset(bounds, self.alignment, intrinsic_above);
452
453 let mut rects = Vec::with_capacity(children.len());
455 let mut current_x = bounds.x();
456
457 for (i, measurement) in measurements.iter().enumerate() {
458 if i > 0 {
459 current_x += spacing;
460 }
461
462 let child_height = if measurement.stretches_cross_axis() {
464 bounds.height()
466 } else if measurement.size().height.is_infinite() {
467 bounds.height()
468 } else {
469 measurement.size().height.min(bounds.height())
470 };
471
472 let child_width = measurement.size().width;
473
474 let mut adjusted_dimensions = measurement.dimensions.clone();
475 adjusted_dimensions.size = Size::new(child_width, child_height);
476
477 let y =
478 if measurement.stretches_cross_axis() || self.alignment == VerticalAlignment::Top {
479 bounds.y()
480 } else if self.alignment == VerticalAlignment::Bottom {
481 bounds.y() + bounds.height() - child_height
482 } else {
483 let guide = adjusted_dimensions
484 .vertical(self.alignment)
485 .clamp(0.0, child_height);
486 guide_line - guide
487 };
488
489 let rect = Rect::new(
490 Point::new(current_x, y),
491 Size::new(child_width, child_height),
492 );
493 rects.push(rect);
494
495 current_x += child_width;
496 }
497
498 rects
499 }
500
501 fn explicit_vertical(
502 &self,
503 alignment: VerticalAlignment,
504 _bounds: Rect,
505 children: &[PlacedSubview<'_>],
506 ) -> Option<f32> {
507 if alignment == VerticalAlignment::LastBaseline {
508 return children
509 .iter()
510 .filter_map(|child| child.explicit_vertical(alignment))
511 .max_by(f32::total_cmp);
512 }
513
514 if alignment == VerticalAlignment::FirstBaseline || alignment == self.alignment {
515 return children
516 .iter()
517 .filter_map(|child| child.explicit_vertical(alignment))
518 .min_by(f32::total_cmp);
519 }
520
521 None
522 }
523
524 fn watch_invalidation(
525 &self,
526 invalidate: LayoutInvalidationCallback,
527 ) -> Vec<nami::watcher::BoxWatcherGuard> {
528 vec![self.spacing.watch(move |_| invalidate())]
529 }
530}
531
532fn usize_to_f32(value: usize) -> f32 {
533 value
534 .to_f32()
535 .expect("HStackLayout: child count must be representable as f32")
536}
537
538impl<C> HStack<(C,)> {
539 pub fn new(alignment: VerticalAlignment, spacing: f32, contents: C) -> Self {
542 Self {
543 layout: HStackLayout {
544 alignment,
545 spacing: Computed::constant(spacing),
546 },
547 contents: (contents,),
548 }
549 }
550}
551
552impl<C> HStack<C> {
553 #[must_use]
555 pub const fn alignment(mut self, alignment: VerticalAlignment) -> Self {
556 self.layout.alignment = alignment;
557 self
558 }
559
560 #[must_use]
565 pub fn spacing(mut self, spacing: impl IntoSignalF32 + 'static) -> Self {
566 self.layout.spacing = spacing.into_signal_f32().computed();
567 self
568 }
569}
570
571crate::stack::impl_stack_for_each!(HStack, HStackLayout);
572
573impl<V> FromIterator<V> for HStack<(Vec<AnyView>,)>
574where
575 V: View,
576{
577 fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
578 let contents = iter.into_iter().map(AnyView::new).collect();
579 Self::new(VerticalAlignment::default(), 10.0, contents)
580 }
581}
582
583pub fn hstack<C>(contents: C) -> HStack<(C,)> {
585 HStack::new(VerticalAlignment::Center, 10.0, contents)
586}
587
588impl<C, F, V> View for HStack<ForEach<C, F, V>>
589where
590 C: Collection + Clone,
591 C::Item: Identifiable,
592 F: 'static + Fn(C::Item) -> V,
593 V: View,
594{
595 fn body(self, _env: &waterui_core::Environment) -> impl View {
596 with(
598 LazyContainer::new(self.layout, self.contents),
599 Axis::Horizontal,
600 )
601 }
602}
603
604impl<C: TupleViews + 'static> View for HStack<(C,)> {
605 fn body(self, _env: &waterui_core::Environment) -> impl View {
606 with(
608 FixedContainer::new(self.layout, self.contents.0),
609 Axis::Horizontal,
610 )
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use crate::tests::CompressibleView;
618
619 struct MockSubView {
620 size: Size,
621 stretch_axis: StretchAxis,
622 }
623
624 impl SubView for MockSubView {
625 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
626 ViewDimensions::new(self.size)
627 }
628 fn stretch_axis(&self) -> StretchAxis {
629 self.stretch_axis
630 }
631 fn priority(&self) -> i32 {
632 0
633 }
634 }
635
636 struct ResponsiveSubView {
637 intrinsic: Size,
638 wrapped_height: f32,
639 wrap_at_or_below: f32,
640 stretch_axis: StretchAxis,
641 }
642
643 impl SubView for ResponsiveSubView {
644 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
645 ViewDimensions::new(match proposal.width {
646 Some(width) if width <= self.wrap_at_or_below => {
647 Size::new(width, self.wrapped_height)
648 }
649 Some(width) => Size::new(width, self.intrinsic.height),
650 None => self.intrinsic,
651 })
652 }
653
654 fn stretch_axis(&self) -> StretchAxis {
655 self.stretch_axis
656 }
657
658 fn priority(&self) -> i32 {
659 0
660 }
661 }
662
663 #[test]
664 fn test_hstack_size_two_children() {
665 let layout = HStackLayout {
666 alignment: VerticalAlignment::Center,
667 spacing: Computed::constant(10.0),
668 };
669
670 let mut child1 = MockSubView {
671 size: Size::new(50.0, 30.0),
672 stretch_axis: StretchAxis::None,
673 };
674 let mut child2 = MockSubView {
675 size: Size::new(60.0, 40.0),
676 stretch_axis: StretchAxis::None,
677 };
678
679 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
680
681 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
682
683 assert!((size.width - 120.0).abs() < f32::EPSILON); assert!((size.height - 40.0).abs() < f32::EPSILON); }
686
687 #[test]
688 fn test_hstack_with_spacer() {
689 let layout = HStackLayout {
690 alignment: VerticalAlignment::Center,
691 spacing: Computed::constant(0.0),
692 };
693
694 let mut child1 = MockSubView {
695 size: Size::new(30.0, 40.0),
696 stretch_axis: StretchAxis::None,
697 };
698 let mut spacer = MockSubView {
699 size: Size::zero(),
700 stretch_axis: StretchAxis::Both, };
702 let mut child2 = MockSubView {
703 size: Size::new(30.0, 40.0),
704 stretch_axis: StretchAxis::None,
705 };
706
707 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
708
709 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), None), &children);
711
712 assert!((size.width - 200.0).abs() < f32::EPSILON);
713
714 let bounds = Rect::new(Point::zero(), Size::new(200.0, 40.0));
716
717 let mut child1 = MockSubView {
718 size: Size::new(30.0, 40.0),
719 stretch_axis: StretchAxis::None,
720 };
721 let mut spacer = MockSubView {
722 size: Size::zero(),
723 stretch_axis: StretchAxis::Both,
724 };
725 let mut child2 = MockSubView {
726 size: Size::new(30.0, 40.0),
727 stretch_axis: StretchAxis::None,
728 };
729 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
730
731 let rects = layout.place(bounds, &children);
732
733 assert!((rects[0].width() - 30.0).abs() < f32::EPSILON);
734 assert!((rects[1].width() - 140.0).abs() < f32::EPSILON); assert!((rects[2].width() - 30.0).abs() < f32::EPSILON);
736 assert!((rects[2].x() - 170.0).abs() < f32::EPSILON); }
738
739 #[test]
744 fn a_cross_filling_child_still_sets_the_row_height() {
745 let layout = HStackLayout {
748 alignment: VerticalAlignment::Center,
749 spacing: Computed::constant(10.0),
750 };
751
752 let mut label = MockSubView {
753 size: Size::new(50.0, 20.0),
754 stretch_axis: StretchAxis::None,
755 };
756 let mut vertical_stretch = MockSubView {
758 size: Size::new(40.0, 100.0), stretch_axis: StretchAxis::Vertical, };
761 let mut button = MockSubView {
762 size: Size::new(80.0, 44.0),
763 stretch_axis: StretchAxis::None,
764 };
765
766 let children: Vec<&dyn SubView> = vec![&mut label, &mut vertical_stretch, &mut button];
767
768 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
769
770 assert!((size.width - 190.0).abs() < f32::EPSILON);
773 assert!(
775 (size.height - 100.0).abs() < f32::EPSILON,
776 "the stretching child's own height has to reach the row, got {}",
777 size.height
778 );
779 }
780
781 #[test]
782 fn test_hstack_compresses_equal_children_evenly() {
783 let layout = HStackLayout {
788 alignment: VerticalAlignment::Center,
789 spacing: Computed::constant(8.0),
790 };
791
792 let mut columns: Vec<CompressibleView> = (0..7)
793 .map(|_| CompressibleView {
794 ideal: Size::new(40.0, 40.0),
795 floor: 0.0,
796 })
797 .collect();
798 let children: Vec<&dyn SubView> = columns
799 .iter_mut()
800 .map(|child| child as &dyn SubView)
801 .collect();
802
803 let bounds = Rect::new(Point::zero(), Size::new(272.0, 40.0));
804 let rects = layout.place(bounds, &children);
805
806 for rect in &rects {
807 assert!(
808 (rect.width() - 32.0).abs() < 0.001,
809 "expected even 32pt columns, got {}",
810 rect.width()
811 );
812 }
813 for pair in rects.windows(2) {
814 assert!(
815 ((pair[1].x() - pair[0].x()) - 40.0).abs() < 0.001,
816 "expected uniform 40pt pitch, got {}",
817 pair[1].x() - pair[0].x()
818 );
819 }
820 }
821
822 #[test]
823 fn test_hstack_compression_keeps_small_children_intrinsic() {
824 let layout = HStackLayout {
828 alignment: VerticalAlignment::Center,
829 spacing: Computed::constant(10.0),
830 };
831
832 let mut label = MockSubView {
833 size: Size::new(50.0, 20.0),
834 stretch_axis: StretchAxis::None,
835 };
836 let mut long_text = ResponsiveSubView {
837 intrinsic: Size::new(200.0, 20.0),
838 wrapped_height: 40.0,
839 wrap_at_or_below: 60.0,
840 stretch_axis: StretchAxis::None,
841 };
842 let children: Vec<&dyn SubView> = vec![&mut label, &mut long_text];
843
844 let bounds = Rect::new(Point::zero(), Size::new(150.0, 40.0));
845 let rects = layout.place(bounds, &children);
846
847 assert!((rects[0].width() - 50.0).abs() < 0.001);
848 assert!((rects[1].width() - 90.0).abs() < 0.001);
849 }
850
851 #[test]
852 fn test_hstack_compression_never_goes_below_a_reported_minimum() {
853 let layout = HStackLayout {
857 alignment: VerticalAlignment::Center,
858 spacing: Computed::constant(0.0),
859 };
860
861 let mut first = CompressibleView {
862 ideal: Size::new(60.0, 20.0),
863 floor: 20.0,
864 };
865 let mut second = CompressibleView {
866 ideal: Size::new(60.0, 20.0),
867 floor: 20.0,
868 };
869 let children: Vec<&dyn SubView> = vec![&mut first, &mut second];
870
871 let bounds = Rect::new(Point::zero(), Size::new(10.0, 20.0));
872 let rects = layout.place(bounds, &children);
873
874 assert!((rects[0].width() - 20.0).abs() < 0.001);
875 assert!((rects[1].width() - 20.0).abs() < 0.001);
876 }
877
878 #[test]
879 fn test_hstack_measures_stretch_child_with_allocated_width_for_height() {
880 let layout = HStackLayout {
881 alignment: VerticalAlignment::Center,
882 spacing: Computed::constant(0.0),
883 };
884
885 let mut fixed = MockSubView {
886 size: Size::new(4.0, 10.0),
887 stretch_axis: StretchAxis::None,
888 };
889
890 let mut stretch = ResponsiveSubView {
892 intrinsic: Size::new(100.0, 20.0),
893 wrapped_height: 40.0,
894 wrap_at_or_below: 60.0,
895 stretch_axis: StretchAxis::Horizontal,
896 };
897
898 let children: Vec<&dyn SubView> = vec![&mut fixed, &mut stretch];
899
900 let size = layout.size_that_fits(ProposalSize::new(Some(40.0), None), &children);
901
902 assert!((size.width - 40.0).abs() < f32::EPSILON);
903 assert!((size.height - 40.0).abs() < f32::EPSILON);
904 }
905
906 #[test]
907 fn test_hstack_place_uses_stretch_child_wrapped_height() {
908 let layout = HStackLayout {
909 alignment: VerticalAlignment::Center,
910 spacing: Computed::constant(0.0),
911 };
912
913 let bounds = Rect::new(Point::zero(), Size::new(40.0, 40.0));
914
915 let mut fixed = MockSubView {
916 size: Size::new(4.0, 10.0),
917 stretch_axis: StretchAxis::None,
918 };
919
920 let mut stretch = ResponsiveSubView {
921 intrinsic: Size::new(100.0, 20.0),
922 wrapped_height: 40.0,
923 wrap_at_or_below: 60.0,
924 stretch_axis: StretchAxis::Horizontal,
925 };
926
927 let children: Vec<&dyn SubView> = vec![&mut fixed, &mut stretch];
928
929 let rects = layout.place(bounds, &children);
930
931 assert!((rects[0].width() - 4.0).abs() < f32::EPSILON);
932 assert!((rects[1].width() - 36.0).abs() < f32::EPSILON);
933 assert!((rects[1].height() - 40.0).abs() < f32::EPSILON);
934 }
935
936 #[test]
937 fn test_hstack_min_size_query_correctly_sums_children() {
938 let layout = HStackLayout {
942 alignment: VerticalAlignment::Center,
943 spacing: Computed::constant(10.0),
944 };
945
946 let mut label = MockSubView {
947 size: Size::new(50.0, 20.0),
948 stretch_axis: StretchAxis::None,
949 };
950 let mut spacer = MockSubView {
951 size: Size::zero(),
952 stretch_axis: StretchAxis::Both,
953 };
954 let mut button = MockSubView {
955 size: Size::new(80.0, 44.0),
956 stretch_axis: StretchAxis::None,
957 };
958
959 let children: Vec<&dyn SubView> = vec![&mut label, &mut spacer, &mut button];
960
961 let min_size = layout.size_that_fits(ProposalSize::ZERO, &children);
963
964 assert!(
966 (min_size.width - 150.0).abs() < f32::EPSILON,
967 "Min size query should return sum of child min widths (150.0), got {}",
968 min_size.width
969 );
970
971 let intrinsic_size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
973 assert!((intrinsic_size.width - 150.0).abs() < f32::EPSILON);
975 }
976}