Skip to main content

cvkg_layout/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//   CVKG Extended: Section 2 of the CVKG Design Specification
24
25use cvkg_core::{Alignment, Distribution, LayoutCache, LayoutView, Rect, Size, SizeProposal};
26
27/// HStack - lays out children horizontally
28pub struct HStack {
29    spacing: f32,
30    alignment: Alignment,
31    distribution: Distribution,
32}
33
34impl HStack {
35    /// Create a new HStack with the given spacing, alignment, and distribution
36    pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
37        Self {
38            spacing,
39            alignment,
40            distribution,
41        }
42    }
43
44    /// Compute the layout rects for children without placing them.
45    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        // Pass 1: Categorize children and measure fixed ones
65        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); // Placeholder
71            } 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        // Pass 2: Measure and size flexible children
86        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            // Flexible children take the width assigned by flex, but height can still be intrinsic or frame-constrained
95            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), // Simplification for mixed flex/distribution
123        };
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
194/// VStack - lays out children vertically
195pub struct VStack {
196    spacing: f32,
197    alignment: Alignment,
198    distribution: Distribution,
199}
200
201impl VStack {
202    /// Create a new VStack with the given spacing, alignment, and distribution
203    pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
204        Self {
205            spacing,
206            alignment,
207            distribution,
208        }
209    }
210
211    /// Compute the layout rects for children without placing them.
212    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        // Pass 1: Categorize children and measure fixed ones
232        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); // Placeholder
238            } 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        // Pass 2: Measure and size flexible children
253        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
360/// ZStack - lays out children on top of each other
361pub struct ZStack {}
362
363impl Default for ZStack {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369impl ZStack {
370    /// Create a new ZStack
371    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        // For ZStack, we want the maximum width and height of all children
384        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        // In ZStack, all children get the same bounds (they stack on top of each other)
403        for child in subviews.iter_mut() {
404            child.place_subviews(bounds, &mut [], cache);
405        }
406    }
407}
408
409/// Spacer - a layout view that expands to fill available space
410pub 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
434/// Flex - a container that distributes space among its children flexibly
435pub 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
504/// Grid - lays out children in a 2D grid
505pub 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        }; // Flex
679        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        ); // 160 - 50 - 10 = 100
716    }
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}