1use alloc::{vec, vec::Vec};
4use nami::collection::Collection;
5use waterui_core::{AnyView, View, env::with, id::Identifiable, view::TupleViews, views::ForEach};
6
7use crate::{
8 Layout, LazyContainer, Point, ProposalSize, Rect, Size, StretchAxis, SubView,
9 container::FixedContainer,
10 stack::{Axis, VerticalAlignment},
11};
12
13#[derive(Debug, Clone)]
44pub struct HStack<C> {
45 layout: HStackLayout,
46 contents: C,
47}
48
49#[derive(Debug, Clone)]
51pub struct HStackLayout {
52 pub alignment: VerticalAlignment,
54 pub spacing: f32,
56}
57
58impl Default for HStackLayout {
59 fn default() -> Self {
60 Self {
61 alignment: VerticalAlignment::Center,
62 spacing: 10.0,
63 }
64 }
65}
66
67struct ChildMeasurement {
69 size: Size,
70 stretch_axis: StretchAxis,
71}
72
73impl ChildMeasurement {
74 const fn stretches_main_axis(&self) -> bool {
79 matches!(
80 self.stretch_axis,
81 StretchAxis::Horizontal | StretchAxis::Both | StretchAxis::MainAxis
82 )
83 }
84
85 const fn stretches_cross_axis(&self) -> bool {
89 matches!(
90 self.stretch_axis,
91 StretchAxis::Vertical | StretchAxis::Both | StretchAxis::CrossAxis
92 )
93 }
94}
95
96#[allow(clippy::cast_precision_loss)]
97#[allow(clippy::too_many_lines)]
98impl Layout for HStackLayout {
99 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
100 if children.is_empty() {
101 return Size::zero();
102 }
103
104 let total_spacing = if children.len() > 1 {
105 (children.len() - 1) as f32 * self.spacing
106 } else {
107 0.0
108 };
109
110 let intrinsic_proposal = ProposalSize::new(None, proposal.height);
112 let mut measurements: Vec<ChildMeasurement> = children
113 .iter()
114 .map(|child| ChildMeasurement {
115 size: child.size_that_fits(intrinsic_proposal),
116 stretch_axis: child.stretch_axis(),
117 })
118 .collect();
119
120 let has_main_axis_stretch = measurements
122 .iter()
123 .any(ChildMeasurement::stretches_main_axis);
124 let main_axis_stretch_indices: Vec<usize> = measurements
125 .iter()
126 .enumerate()
127 .filter(|(_, m)| m.stretches_main_axis())
128 .map(|(idx, _)| idx)
129 .collect();
130 let main_axis_stretch_count = main_axis_stretch_indices.len();
131
132 let intrinsic_width_all: f32 =
133 measurements.iter().map(|m| m.size.width).sum::<f32>() + total_spacing;
134
135 let intrinsic_width = intrinsic_width_all;
139
140 let final_width = proposal.width.map_or(intrinsic_width, |proposed| {
142 if has_main_axis_stretch {
143 proposed
144 } else {
145 intrinsic_width.min(proposed)
146 }
147 });
148
149 let available_for_children = (final_width - total_spacing).max(0.0);
152
153 let fixed_indices: Vec<usize> = if main_axis_stretch_count > 0 && proposal.width.is_some() {
154 measurements
155 .iter()
156 .enumerate()
157 .filter(|(_, m)| !m.stretches_main_axis())
158 .map(|(idx, _)| idx)
159 .collect()
160 } else {
161 (0..measurements.len()).collect()
162 };
163
164 let fixed_width: f32 = fixed_indices
165 .iter()
166 .map(|&idx| measurements[idx].size.width)
167 .sum();
168
169 if proposal.width.is_some()
170 && !fixed_indices.is_empty()
171 && fixed_width > available_for_children
172 {
173 let overflow = fixed_width - available_for_children;
176
177 let mut compress_indices = fixed_indices;
179 compress_indices.sort_by(|&a, &b| {
180 measurements[b]
181 .size
182 .width
183 .partial_cmp(&measurements[a].size.width)
184 .unwrap()
185 });
186
187 let mut remaining_overflow = overflow;
189 for &idx in &compress_indices {
190 if remaining_overflow <= 0.0 {
191 break;
192 }
193
194 let current_width = measurements[idx].size.width;
195 let min_width = 20.0_f32.min(current_width);
197 let max_reduction = current_width - min_width;
198 let reduction = remaining_overflow.min(max_reduction);
199
200 if reduction > 0.0 {
201 let new_width = current_width - reduction;
202 let constrained_proposal = ProposalSize::new(Some(new_width), proposal.height);
203 measurements[idx].size = children[idx].size_that_fits(constrained_proposal);
204 remaining_overflow -= reduction;
205 }
206 }
207 }
208
209 if proposal.width.is_some() && main_axis_stretch_count > 0 {
212 let fixed_width: f32 = measurements
213 .iter()
214 .enumerate()
215 .filter(|(_, m)| !m.stretches_main_axis())
216 .map(|(_, m)| m.size.width)
217 .sum();
218
219 let remaining_width = (available_for_children - fixed_width).max(0.0);
220 let stretch_width = remaining_width / main_axis_stretch_count as f32;
221
222 for idx in main_axis_stretch_indices {
223 let constrained_proposal = ProposalSize::new(Some(stretch_width), proposal.height);
224 measurements[idx].size = children[idx].size_that_fits(constrained_proposal);
225 measurements[idx].size.width = measurements[idx].size.width.min(stretch_width);
226 }
227 }
228
229 let max_height = measurements
233 .iter()
234 .filter(|m| !m.stretches_cross_axis())
235 .map(|m| m.size.height)
236 .max_by(f32::total_cmp)
237 .unwrap_or(0.0);
238
239 Size::new(final_width, max_height)
240 }
241
242 #[allow(clippy::too_many_lines)]
243 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
244 if children.is_empty() {
245 return vec![];
246 }
247
248 let total_spacing = if children.len() > 1 {
249 (children.len() - 1) as f32 * self.spacing
250 } else {
251 0.0
252 };
253
254 let available_width = bounds.width() - total_spacing;
255
256 let intrinsic_proposal = ProposalSize::new(None, Some(bounds.height()));
258 let mut measurements: Vec<ChildMeasurement> = children
259 .iter()
260 .map(|child| ChildMeasurement {
261 size: child.size_that_fits(intrinsic_proposal),
262 stretch_axis: child.stretch_axis(),
263 })
264 .collect();
265
266 let main_axis_stretch_indices: Vec<usize> = measurements
268 .iter()
269 .enumerate()
270 .filter(|(_, m)| m.stretches_main_axis())
271 .map(|(idx, _)| idx)
272 .collect();
273 let main_axis_stretch_count = main_axis_stretch_indices.len();
274
275 let fixed_indices: Vec<usize> = if main_axis_stretch_count > 0 {
276 measurements
277 .iter()
278 .enumerate()
279 .filter(|(_, m)| !m.stretches_main_axis())
280 .map(|(idx, _)| idx)
281 .collect()
282 } else {
283 (0..measurements.len()).collect()
284 };
285
286 let fixed_width: f32 = fixed_indices
287 .iter()
288 .map(|&idx| measurements[idx].size.width)
289 .sum();
290
291 let needs_compression = !fixed_indices.is_empty() && fixed_width > available_width;
293
294 if needs_compression {
295 let overflow = fixed_width - available_width;
297
298 let mut compress_indices = fixed_indices;
300 compress_indices.sort_by(|&a, &b| {
301 measurements[b]
302 .size
303 .width
304 .partial_cmp(&measurements[a].size.width)
305 .unwrap()
306 });
307
308 let mut remaining_overflow = overflow;
310 for &idx in &compress_indices {
311 if remaining_overflow <= 0.0 {
312 break;
313 }
314
315 let current_width = measurements[idx].size.width;
316 let min_width = 20.0_f32.min(current_width);
318 let max_reduction = current_width - min_width;
319 let reduction = remaining_overflow.min(max_reduction);
320
321 if reduction > 0.0 {
322 let new_width = current_width - reduction;
323 let constrained_proposal =
324 ProposalSize::new(Some(new_width), Some(bounds.height()));
325 measurements[idx].size = children[idx].size_that_fits(constrained_proposal);
326 measurements[idx].size.width = measurements[idx].size.width.min(new_width);
327 remaining_overflow -= reduction;
328 }
329 }
330 }
331
332 let actual_fixed_width: f32 = measurements
334 .iter()
335 .enumerate()
336 .filter(|(_, m)| !m.stretches_main_axis())
337 .map(|(_, m)| m.size.width)
338 .sum();
339
340 let remaining_width = (available_width - actual_fixed_width).max(0.0);
341 let stretch_width = if main_axis_stretch_count > 0 {
342 remaining_width / main_axis_stretch_count as f32
343 } else {
344 0.0
345 };
346
347 if main_axis_stretch_count > 0 {
349 for idx in &main_axis_stretch_indices {
350 let constrained_proposal =
351 ProposalSize::new(Some(stretch_width), Some(bounds.height()));
352 measurements[*idx].size = children[*idx].size_that_fits(constrained_proposal);
353 measurements[*idx].size.width = measurements[*idx].size.width.min(stretch_width);
354 }
355 }
356
357 let mut rects = Vec::with_capacity(children.len());
359 let mut current_x = bounds.x();
360
361 for (i, measurement) in measurements.iter().enumerate() {
362 if i > 0 {
363 current_x += self.spacing;
364 }
365
366 let child_height = if measurement.stretches_cross_axis() {
368 bounds.height()
370 } else if measurement.size.height.is_infinite() {
371 bounds.height()
372 } else {
373 measurement.size.height.min(bounds.height())
374 };
375
376 let child_width = if measurement.stretches_main_axis() {
377 stretch_width
378 } else {
379 measurement.size.width
380 };
381
382 let y = match self.alignment {
383 VerticalAlignment::Top => bounds.y(),
384 VerticalAlignment::Center => bounds.y() + (bounds.height() - child_height) / 2.0,
385 VerticalAlignment::Bottom => bounds.y() + bounds.height() - child_height,
386 };
387
388 let rect = Rect::new(
389 Point::new(current_x, y),
390 Size::new(child_width, child_height),
391 );
392 rects.push(rect);
393
394 current_x += child_width;
395 }
396
397 rects
398 }
399}
400
401impl<C> HStack<(C,)> {
402 pub const fn new(alignment: VerticalAlignment, spacing: f32, contents: C) -> Self {
405 Self {
406 layout: HStackLayout { alignment, spacing },
407 contents: (contents,),
408 }
409 }
410}
411
412impl<C> HStack<C> {
413 #[must_use]
415 pub const fn alignment(mut self, alignment: VerticalAlignment) -> Self {
416 self.layout.alignment = alignment;
417 self
418 }
419
420 #[must_use]
422 pub const fn spacing(mut self, spacing: f32) -> Self {
423 self.layout.spacing = spacing;
424 self
425 }
426}
427
428impl<V> FromIterator<V> for HStack<(Vec<AnyView>,)>
429where
430 V: View,
431{
432 fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
433 let contents = iter.into_iter().map(AnyView::new).collect();
434 Self::new(VerticalAlignment::default(), 10.0, contents)
435 }
436}
437
438pub const fn hstack<C>(contents: C) -> HStack<(C,)> {
440 HStack::new(VerticalAlignment::Center, 10.0, contents)
441}
442
443impl<C, F, V> View for HStack<ForEach<C, F, V>>
444where
445 C: Collection,
446 C::Item: Identifiable,
447 F: 'static + Fn(C::Item) -> V,
448 V: View,
449{
450 fn body(self, _env: &waterui_core::Environment) -> impl View {
451 with(
453 LazyContainer::new(self.layout, self.contents),
454 Axis::Horizontal,
455 )
456 }
457}
458
459impl<C: TupleViews + 'static> View for HStack<(C,)> {
460 fn body(self, _env: &waterui_core::Environment) -> impl View {
461 with(
463 FixedContainer::new(self.layout, self.contents.0),
464 Axis::Horizontal,
465 )
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 struct MockSubView {
474 size: Size,
475 stretch_axis: StretchAxis,
476 }
477
478 impl SubView for MockSubView {
479 fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
480 self.size
481 }
482 fn stretch_axis(&self) -> StretchAxis {
483 self.stretch_axis
484 }
485 fn priority(&self) -> i32 {
486 0
487 }
488 }
489
490 struct ResponsiveSubView {
491 intrinsic: Size,
492 wrapped_height: f32,
493 wrap_at_or_below: f32,
494 stretch_axis: StretchAxis,
495 }
496
497 impl SubView for ResponsiveSubView {
498 fn size_that_fits(&self, proposal: ProposalSize) -> Size {
499 match proposal.width {
500 Some(width) if width <= self.wrap_at_or_below => {
501 Size::new(width, self.wrapped_height)
502 }
503 Some(width) => Size::new(width, self.intrinsic.height),
504 None => self.intrinsic,
505 }
506 }
507
508 fn stretch_axis(&self) -> StretchAxis {
509 self.stretch_axis
510 }
511
512 fn priority(&self) -> i32 {
513 0
514 }
515 }
516
517 #[test]
518 fn test_hstack_size_two_children() {
519 let layout = HStackLayout {
520 alignment: VerticalAlignment::Center,
521 spacing: 10.0,
522 };
523
524 let mut child1 = MockSubView {
525 size: Size::new(50.0, 30.0),
526 stretch_axis: StretchAxis::None,
527 };
528 let mut child2 = MockSubView {
529 size: Size::new(60.0, 40.0),
530 stretch_axis: StretchAxis::None,
531 };
532
533 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
534
535 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
536
537 assert!((size.width - 120.0).abs() < f32::EPSILON); assert!((size.height - 40.0).abs() < f32::EPSILON); }
540
541 #[test]
542 fn test_hstack_with_spacer() {
543 let layout = HStackLayout {
544 alignment: VerticalAlignment::Center,
545 spacing: 0.0,
546 };
547
548 let mut child1 = MockSubView {
549 size: Size::new(30.0, 40.0),
550 stretch_axis: StretchAxis::None,
551 };
552 let mut spacer = MockSubView {
553 size: Size::zero(),
554 stretch_axis: StretchAxis::Both, };
556 let mut child2 = MockSubView {
557 size: Size::new(30.0, 40.0),
558 stretch_axis: StretchAxis::None,
559 };
560
561 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
562
563 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), None), &children);
565
566 assert!((size.width - 200.0).abs() < f32::EPSILON);
567
568 let bounds = Rect::new(Point::zero(), Size::new(200.0, 40.0));
570
571 let mut child1 = MockSubView {
572 size: Size::new(30.0, 40.0),
573 stretch_axis: StretchAxis::None,
574 };
575 let mut spacer = MockSubView {
576 size: Size::zero(),
577 stretch_axis: StretchAxis::Both,
578 };
579 let mut child2 = MockSubView {
580 size: Size::new(30.0, 40.0),
581 stretch_axis: StretchAxis::None,
582 };
583 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
584
585 let rects = layout.place(bounds, &children);
586
587 assert!((rects[0].width() - 30.0).abs() < f32::EPSILON);
588 assert!((rects[1].width() - 140.0).abs() < f32::EPSILON); assert!((rects[2].width() - 30.0).abs() < f32::EPSILON);
590 assert!((rects[2].x() - 170.0).abs() < f32::EPSILON); }
592
593 #[test]
594 fn test_hstack_with_vertical_stretch() {
595 let layout = HStackLayout {
598 alignment: VerticalAlignment::Center,
599 spacing: 10.0,
600 };
601
602 let mut label = MockSubView {
603 size: Size::new(50.0, 20.0),
604 stretch_axis: StretchAxis::None,
605 };
606 let mut vertical_stretch = MockSubView {
608 size: Size::new(40.0, 100.0), stretch_axis: StretchAxis::Vertical, };
611 let mut button = MockSubView {
612 size: Size::new(80.0, 44.0),
613 stretch_axis: StretchAxis::None,
614 };
615
616 let children: Vec<&dyn SubView> = vec![&mut label, &mut vertical_stretch, &mut button];
617
618 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
619
620 assert!((size.width - 190.0).abs() < f32::EPSILON);
623 assert!((size.height - 44.0).abs() < f32::EPSILON);
626 }
627
628 #[test]
629 fn test_hstack_measures_stretch_child_with_allocated_width_for_height() {
630 let layout = HStackLayout {
631 alignment: VerticalAlignment::Center,
632 spacing: 0.0,
633 };
634
635 let mut fixed = MockSubView {
636 size: Size::new(4.0, 10.0),
637 stretch_axis: StretchAxis::None,
638 };
639
640 let mut stretch = ResponsiveSubView {
642 intrinsic: Size::new(100.0, 20.0),
643 wrapped_height: 40.0,
644 wrap_at_or_below: 60.0,
645 stretch_axis: StretchAxis::Horizontal,
646 };
647
648 let children: Vec<&dyn SubView> = vec![&mut fixed, &mut stretch];
649
650 let size = layout.size_that_fits(ProposalSize::new(Some(40.0), None), &children);
651
652 assert!((size.width - 40.0).abs() < f32::EPSILON);
653 assert!((size.height - 40.0).abs() < f32::EPSILON);
654 }
655
656 #[test]
657 fn test_hstack_place_uses_stretch_child_wrapped_height() {
658 let layout = HStackLayout {
659 alignment: VerticalAlignment::Center,
660 spacing: 0.0,
661 };
662
663 let bounds = Rect::new(Point::zero(), Size::new(40.0, 40.0));
664
665 let mut fixed = MockSubView {
666 size: Size::new(4.0, 10.0),
667 stretch_axis: StretchAxis::None,
668 };
669
670 let mut stretch = ResponsiveSubView {
671 intrinsic: Size::new(100.0, 20.0),
672 wrapped_height: 40.0,
673 wrap_at_or_below: 60.0,
674 stretch_axis: StretchAxis::Horizontal,
675 };
676
677 let children: Vec<&dyn SubView> = vec![&mut fixed, &mut stretch];
678
679 let rects = layout.place(bounds, &children);
680
681 assert!((rects[0].width() - 4.0).abs() < f32::EPSILON);
682 assert!((rects[1].width() - 36.0).abs() < f32::EPSILON);
683 assert!((rects[1].height() - 40.0).abs() < f32::EPSILON);
684 }
685}