1use cvkg_core::{Alignment, Distribution, LayoutCache, LayoutView, Rect, Size, SizeProposal};
26
27pub struct HStack {
29 spacing: f32,
30 alignment: Alignment,
31 distribution: Distribution,
32}
33
34impl HStack {
35 pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
37 Self {
38 spacing,
39 alignment,
40 distribution,
41 }
42 }
43
44 pub fn compute_layout(
46 spacing: f32,
47 alignment: Alignment,
48 distribution: Distribution,
49 bounds: Rect,
50 subviews: &[&dyn LayoutView],
51 cache: &mut LayoutCache,
52 ) -> Vec<Rect> {
53 let n = subviews.len();
54 if n == 0 {
55 return Vec::new();
56 }
57
58 let mut rects = vec![Rect::zero(); n];
59 let mut child_sizes = Vec::with_capacity(n);
60 let mut total_fixed_width = 0.0;
61 let mut total_flex_weight = 0.0;
62 let mut flex_indices = Vec::new();
63
64 for (i, child) in subviews.iter().enumerate() {
66 let weight = child.flex_weight();
67 if weight > 0.0 {
68 total_flex_weight += weight;
69 flex_indices.push(i);
70 child_sizes.push(Size::ZERO); } else {
72 let desired = child.size_that_fits(
73 SizeProposal::new(Some(bounds.width), Some(bounds.height)),
74 &[],
75 cache,
76 );
77 child_sizes.push(desired);
78 total_fixed_width += desired.width;
79 }
80 }
81
82 let total_spacing = spacing * (n - 1) as f32;
83 let available_for_flex = (bounds.width - total_fixed_width - total_spacing).max(0.0);
84
85 for &idx in &flex_indices {
87 let weight = subviews[idx].flex_weight();
88 let flex_width = (weight / total_flex_weight) * available_for_flex;
89 let desired = subviews[idx].size_that_fits(
90 SizeProposal::new(Some(flex_width), Some(bounds.height)),
91 &[],
92 cache,
93 );
94 child_sizes[idx] = Size {
96 width: flex_width,
97 height: desired.height,
98 };
99 }
100
101 let content_width = if total_flex_weight > 0.0 {
102 bounds.width - total_spacing
103 } else {
104 total_fixed_width
105 } + total_spacing;
106
107 let (mut x, actual_spacing) = match distribution {
108 Distribution::Leading | Distribution::Fill if total_flex_weight > 0.0 => {
109 (bounds.x, spacing)
110 }
111 Distribution::Leading | Distribution::Fill => (bounds.x, spacing),
112 Distribution::Trailing => (bounds.x + bounds.width - content_width, spacing),
113 Distribution::Center => (bounds.x + (bounds.width - content_width) / 2.0, spacing),
114 Distribution::SpaceBetween => {
115 let s = if n > 1 {
116 (bounds.width - (total_fixed_width + available_for_flex)) / (n - 1) as f32
117 } else {
118 0.0
119 };
120 (bounds.x, s)
121 }
122 _ => (bounds.x, spacing), };
124
125 for i in 0..n {
126 let size = child_sizes[i];
127 let y = match alignment {
128 Alignment::Top => bounds.y,
129 Alignment::Bottom => bounds.y + bounds.height - size.height,
130 _ => bounds.y + (bounds.height - size.height) / 2.0,
131 };
132
133 rects[i] = Rect {
134 x,
135 y,
136 width: size.width,
137 height: size.height,
138 };
139 x += size.width + actual_spacing;
140 }
141 rects
142 }
143}
144
145impl LayoutView for HStack {
146 fn size_that_fits(
147 &self,
148 proposal: SizeProposal,
149 subviews: &[&dyn LayoutView],
150 cache: &mut LayoutCache,
151 ) -> Size {
152 let mut width = 0.0f32;
153 let mut height = 0.0f32;
154
155 for (i, child) in subviews.iter().enumerate() {
156 let child_size = child.size_that_fits(proposal, &[], cache);
157 width += child_size.width;
158 height = height.max(child_size.height);
159
160 if i < subviews.len() - 1 {
161 width += self.spacing;
162 }
163 }
164
165 Size {
166 width: proposal.width.unwrap_or(width),
167 height: proposal.height.unwrap_or(height),
168 }
169 }
170
171 fn place_subviews(
172 &self,
173 bounds: Rect,
174 subviews: &mut [&mut dyn LayoutView],
175 cache: &mut LayoutCache,
176 ) {
177 let views: Vec<&dyn LayoutView> =
178 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
179 let rects = Self::compute_layout(
180 self.spacing,
181 self.alignment,
182 self.distribution,
183 bounds,
184 &views,
185 cache,
186 );
187
188 for (child, rect) in subviews.iter_mut().zip(rects) {
189 child.place_subviews(rect, &mut [], cache);
190 }
191 }
192}
193
194pub struct VStack {
196 spacing: f32,
197 alignment: Alignment,
198 distribution: Distribution,
199}
200
201impl VStack {
202 pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
204 Self {
205 spacing,
206 alignment,
207 distribution,
208 }
209 }
210
211 pub fn compute_layout(
213 spacing: f32,
214 alignment: Alignment,
215 distribution: Distribution,
216 bounds: Rect,
217 subviews: &[&dyn LayoutView],
218 cache: &mut LayoutCache,
219 ) -> Vec<Rect> {
220 let n = subviews.len();
221 if n == 0 {
222 return Vec::new();
223 }
224
225 let mut rects = vec![Rect::zero(); n];
226 let mut child_sizes = Vec::with_capacity(n);
227 let mut total_fixed_height = 0.0;
228 let mut total_flex_weight = 0.0;
229 let mut flex_indices = Vec::new();
230
231 for (i, child) in subviews.iter().enumerate() {
233 let weight = child.flex_weight();
234 if weight > 0.0 {
235 total_flex_weight += weight;
236 flex_indices.push(i);
237 child_sizes.push(Size::ZERO); } else {
239 let desired = child.size_that_fits(
240 SizeProposal::new(Some(bounds.width), Some(bounds.height)),
241 &[],
242 cache,
243 );
244 child_sizes.push(desired);
245 total_fixed_height += desired.height;
246 }
247 }
248
249 let total_spacing = spacing * (n - 1) as f32;
250 let available_for_flex = (bounds.height - total_fixed_height - total_spacing).max(0.0);
251
252 for &idx in &flex_indices {
254 let weight = subviews[idx].flex_weight();
255 let flex_height = (weight / total_flex_weight) * available_for_flex;
256 let desired = subviews[idx].size_that_fits(
257 SizeProposal::new(Some(bounds.width), Some(flex_height)),
258 &[],
259 cache,
260 );
261 child_sizes[idx] = Size {
262 width: desired.width,
263 height: flex_height,
264 };
265 }
266
267 let content_height = if total_flex_weight > 0.0 {
268 bounds.height - total_spacing
269 } else {
270 total_fixed_height
271 } + total_spacing;
272
273 let (mut y, actual_spacing) = match distribution {
274 Distribution::Leading | Distribution::Fill if total_flex_weight > 0.0 => {
275 (bounds.y, spacing)
276 }
277 Distribution::Leading | Distribution::Fill => (bounds.y, spacing),
278 Distribution::Trailing => (bounds.y + bounds.height - content_height, spacing),
279 Distribution::Center => (bounds.y + (bounds.height - content_height) / 2.0, spacing),
280 Distribution::SpaceBetween => {
281 let s = if n > 1 {
282 (bounds.height - (total_fixed_height + available_for_flex)) / (n - 1) as f32
283 } else {
284 0.0
285 };
286 (bounds.y, s)
287 }
288 _ => (bounds.y, spacing),
289 };
290
291 for i in 0..n {
292 let size = child_sizes[i];
293 let x = match alignment {
294 Alignment::Leading => bounds.x,
295 Alignment::Trailing => bounds.x + bounds.width - size.width,
296 _ => bounds.x + (bounds.width - size.width) / 2.0,
297 };
298
299 rects[i] = Rect {
300 x,
301 y,
302 width: size.width,
303 height: size.height,
304 };
305 y += size.height + actual_spacing;
306 }
307 rects
308 }
309}
310
311impl LayoutView for VStack {
312 fn size_that_fits(
313 &self,
314 proposal: SizeProposal,
315 subviews: &[&dyn LayoutView],
316 cache: &mut LayoutCache,
317 ) -> Size {
318 let mut width = 0.0f32;
319 let mut height = 0.0f32;
320
321 for (i, child) in subviews.iter().enumerate() {
322 let child_size = child.size_that_fits(proposal, &[], cache);
323 width = width.max(child_size.width);
324 height += child_size.height;
325
326 if i < subviews.len() - 1 {
327 height += self.spacing;
328 }
329 }
330
331 Size {
332 width: proposal.width.unwrap_or(width),
333 height: proposal.height.unwrap_or(height),
334 }
335 }
336
337 fn place_subviews(
338 &self,
339 bounds: Rect,
340 subviews: &mut [&mut dyn LayoutView],
341 cache: &mut LayoutCache,
342 ) {
343 let views: Vec<&dyn LayoutView> =
344 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
345 let rects = Self::compute_layout(
346 self.spacing,
347 self.alignment,
348 self.distribution,
349 bounds,
350 &views,
351 cache,
352 );
353
354 for (child, rect) in subviews.iter_mut().zip(rects) {
355 child.place_subviews(rect, &mut [], cache);
356 }
357 }
358}
359
360pub struct ZStack {}
362
363impl Default for ZStack {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl ZStack {
370 pub fn new() -> Self {
372 Self {}
373 }
374}
375
376impl LayoutView for ZStack {
377 fn size_that_fits(
378 &self,
379 proposal: SizeProposal,
380 subviews: &[&dyn LayoutView],
381 cache: &mut LayoutCache,
382 ) -> Size {
383 let mut width = 0.0f32;
385 let mut height = 0.0f32;
386
387 for child in subviews.iter() {
388 let child_size = child.size_that_fits(proposal, &[], cache);
389 width = width.max(child_size.width);
390 height = height.max(child_size.height);
391 }
392
393 Size { width, height }
394 }
395
396 fn place_subviews(
397 &self,
398 bounds: Rect,
399 subviews: &mut [&mut dyn LayoutView],
400 cache: &mut LayoutCache,
401 ) {
402 for child in subviews.iter_mut() {
404 child.place_subviews(bounds, &mut [], cache);
405 }
406 }
407}
408
409pub struct Spacer;
411
412impl LayoutView for Spacer {
413 fn size_that_fits(
414 &self,
415 proposal: SizeProposal,
416 _subviews: &[&dyn LayoutView],
417 _cache: &mut LayoutCache,
418 ) -> Size {
419 Size {
420 width: proposal.width.unwrap_or(0.0),
421 height: proposal.height.unwrap_or(0.0),
422 }
423 }
424
425 fn place_subviews(
426 &self,
427 _bounds: Rect,
428 _subviews: &mut [&mut dyn LayoutView],
429 _cache: &mut LayoutCache,
430 ) {
431 }
432}
433
434pub struct Flex {
436 pub orientation: cvkg_core::Orientation,
437 pub spacing: f32,
438}
439
440impl Flex {
441 pub fn new(orientation: cvkg_core::Orientation, spacing: f32) -> Self {
442 Self {
443 orientation,
444 spacing,
445 }
446 }
447}
448
449impl LayoutView for Flex {
450 fn size_that_fits(
451 &self,
452 proposal: SizeProposal,
453 _subviews: &[&dyn LayoutView],
454 _cache: &mut LayoutCache,
455 ) -> Size {
456 Size {
457 width: proposal.width.unwrap_or(100.0),
458 height: proposal.height.unwrap_or(100.0),
459 }
460 }
461
462 fn place_subviews(
463 &self,
464 bounds: Rect,
465 subviews: &mut [&mut dyn LayoutView],
466 cache: &mut LayoutCache,
467 ) {
468 if subviews.is_empty() {
469 return;
470 }
471
472 let n = subviews.len() as f32;
473 match self.orientation {
474 cvkg_core::Orientation::Horizontal => {
475 let total_spacing = self.spacing * (n - 1.0);
476 let item_width = (bounds.width - total_spacing) / n;
477 for (i, child) in subviews.iter_mut().enumerate() {
478 let child_rect = Rect {
479 x: bounds.x + i as f32 * (item_width + self.spacing),
480 y: bounds.y,
481 width: item_width,
482 height: bounds.height,
483 };
484 child.place_subviews(child_rect, &mut [], cache);
485 }
486 }
487 cvkg_core::Orientation::Vertical => {
488 let total_spacing = self.spacing * (n - 1.0);
489 let item_height = (bounds.height - total_spacing) / n;
490 for (i, child) in subviews.iter_mut().enumerate() {
491 let child_rect = Rect {
492 x: bounds.x,
493 y: bounds.y + i as f32 * (item_height + self.spacing),
494 width: bounds.width,
495 height: item_height,
496 };
497 child.place_subviews(child_rect, &mut [], cache);
498 }
499 }
500 }
501 }
502}
503
504pub struct Grid {
506 pub rows: usize,
507 pub cols: usize,
508 pub spacing: f32,
509}
510
511impl Grid {
512 pub fn new(rows: usize, cols: usize, spacing: f32) -> Self {
513 Self {
514 rows,
515 cols,
516 spacing,
517 }
518 }
519
520 pub fn compute_layout(
521 rows: usize,
522 cols: usize,
523 spacing: f32,
524 bounds: Rect,
525 subviews: &[&dyn LayoutView],
526 _cache: &mut LayoutCache,
527 ) -> Vec<Rect> {
528 if subviews.is_empty() || rows == 0 || cols == 0 {
529 return Vec::new();
530 }
531
532 let mut rects = Vec::with_capacity(subviews.len());
533 let item_width = (bounds.width - (cols - 1) as f32 * spacing) / cols as f32;
534 let item_height = (bounds.height - (rows - 1) as f32 * spacing) / rows as f32;
535
536 for (i, _) in subviews.iter().enumerate() {
537 let r = i / cols;
538 let c = i % cols;
539
540 if r >= rows {
541 break;
542 }
543
544 rects.push(Rect {
545 x: bounds.x + c as f32 * (item_width + spacing),
546 y: bounds.y + r as f32 * (item_height + spacing),
547 width: item_width,
548 height: item_height,
549 });
550 }
551 rects
552 }
553}
554
555impl LayoutView for Grid {
556 fn size_that_fits(
557 &self,
558 proposal: SizeProposal,
559 _subviews: &[&dyn LayoutView],
560 _cache: &mut LayoutCache,
561 ) -> Size {
562 Size {
563 width: proposal.width.unwrap_or(200.0),
564 height: proposal.height.unwrap_or(200.0),
565 }
566 }
567
568 fn place_subviews(
569 &self,
570 bounds: Rect,
571 subviews: &mut [&mut dyn LayoutView],
572 cache: &mut LayoutCache,
573 ) {
574 let views: Vec<&dyn LayoutView> =
575 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
576 let rects = Self::compute_layout(self.rows, self.cols, self.spacing, bounds, &views, cache);
577
578 for (child, rect) in subviews.iter_mut().zip(rects) {
579 child.place_subviews(rect, &mut [], cache);
580 }
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 struct MockView {
589 size: Size,
590 flex: f32,
591 }
592
593 impl LayoutView for MockView {
594 fn size_that_fits(
595 &self,
596 _p: SizeProposal,
597 _s: &[&dyn LayoutView],
598 _c: &mut LayoutCache,
599 ) -> Size {
600 self.size
601 }
602 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
603 fn flex_weight(&self) -> f32 {
604 self.flex
605 }
606 }
607
608 #[test]
609 fn test_hstack_basic() {
610 let v1 = MockView {
611 size: Size {
612 width: 50.0,
613 height: 50.0,
614 },
615 flex: 0.0,
616 };
617 let v2 = MockView {
618 size: Size {
619 width: 100.0,
620 height: 100.0,
621 },
622 flex: 0.0,
623 };
624 let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
625 let mut cache = LayoutCache::new();
626 let bounds = Rect {
627 x: 0.0,
628 y: 0.0,
629 width: 300.0,
630 height: 200.0,
631 };
632
633 let rects = HStack::compute_layout(
634 10.0,
635 Alignment::Center,
636 Distribution::Leading,
637 bounds,
638 &views,
639 &mut cache,
640 );
641
642 assert_eq!(rects.len(), 2);
643 assert_eq!(
644 rects[0],
645 Rect {
646 x: 0.0,
647 y: 75.0,
648 width: 50.0,
649 height: 50.0
650 }
651 );
652 assert_eq!(
653 rects[1],
654 Rect {
655 x: 60.0,
656 y: 50.0,
657 width: 100.0,
658 height: 100.0
659 }
660 );
661 }
662
663 #[test]
664 fn test_vstack_flex() {
665 let v1 = MockView {
666 size: Size {
667 width: 100.0,
668 height: 50.0,
669 },
670 flex: 0.0,
671 };
672 let v2 = MockView {
673 size: Size {
674 width: 100.0,
675 height: 0.0,
676 },
677 flex: 1.0,
678 }; let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
680 let mut cache = LayoutCache::new();
681 let bounds = Rect {
682 x: 0.0,
683 y: 0.0,
684 width: 200.0,
685 height: 160.0,
686 };
687
688 let rects = VStack::compute_layout(
689 10.0,
690 Alignment::Leading,
691 Distribution::Fill,
692 bounds,
693 &views,
694 &mut cache,
695 );
696
697 assert_eq!(rects.len(), 2);
698 assert_eq!(
699 rects[0],
700 Rect {
701 x: 0.0,
702 y: 0.0,
703 width: 100.0,
704 height: 50.0
705 }
706 );
707 assert_eq!(
708 rects[1],
709 Rect {
710 x: 0.0,
711 y: 60.0,
712 width: 100.0,
713 height: 100.0
714 }
715 ); }
717
718 #[test]
719 fn test_grid_layout() {
720 let v1 = MockView {
721 size: Size::ZERO,
722 flex: 0.0,
723 };
724 let v2 = MockView {
725 size: Size::ZERO,
726 flex: 0.0,
727 };
728 let v3 = MockView {
729 size: Size::ZERO,
730 flex: 0.0,
731 };
732 let views: Vec<&dyn LayoutView> = vec![&v1, &v2, &v3];
733 let mut cache = LayoutCache::new();
734 let bounds = Rect {
735 x: 0.0,
736 y: 0.0,
737 width: 210.0,
738 height: 210.0,
739 };
740
741 let rects = Grid::compute_layout(2, 2, 10.0, bounds, &views, &mut cache);
742
743 assert_eq!(rects.len(), 3);
744 assert_eq!(
745 rects[0],
746 Rect {
747 x: 0.0,
748 y: 0.0,
749 width: 100.0,
750 height: 100.0
751 }
752 );
753 assert_eq!(
754 rects[1],
755 Rect {
756 x: 110.0,
757 y: 0.0,
758 width: 100.0,
759 height: 100.0
760 }
761 );
762 assert_eq!(
763 rects[2],
764 Rect {
765 x: 0.0,
766 y: 110.0,
767 width: 100.0,
768 height: 100.0
769 }
770 );
771 }
772}