git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitf93fcbec764582fff53d585deb0bd1cd0c473b99
parent25bcf61734
authorLucas Galante <[email protected]>
date2026-06-19 10:57
Implement adaptive grid layout for SectionContext

 src/layout.rs | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 212 insertions(+), 28 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index 6b97d93..ddd758c 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1971,6 +1971,8 @@ pub struct Section {
     pub cw: f32,
     pub label_width: f32,
     pub is_child: bool,
+    pub grid: Grid,
+    pub last_col: usize,
 }
 
 impl Section {
@@ -2015,7 +2017,21 @@ impl Section {
             left + (cw - label_width) / 2.0
         };
         pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
-        Self { left, top, content_y: top + font_size + 5.0, cw, label_width, is_child }
+
+        let pad = if is_child {
+            section_padding().max(8.0)
+        } else {
+            section_padding()
+        };
+        let margin_x = pad + 12.0;
+        let usable_w = (cw - 2.0 * margin_x).max(1.0);
+        let min_col_width = 130.0;
+        let gap = 8.0;
+        let max_cols = ((usable_w + gap) / (min_col_width + gap)).floor().max(1.0) as usize;
+        let content_start_y = top + font_size + 5.0;
+        let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
+
+        Self { left, top, content_y: content_start_y, cw, label_width, is_child, grid, last_col: 0 }
     }
 
     pub fn ax(&self, x_off: f32) -> f32 {
@@ -2025,25 +2041,69 @@ impl Section {
 
     pub fn ay(&self) -> f32 { self.content_y }
 
-    pub fn spacing(&mut self, dy: f32) { self.content_y += dy; }
+    pub fn spacing(&mut self, dy: f32) {
+        self.content_y += dy;
+        for h in &mut self.grid.col_heights {
+            *h = self.content_y;
+        }
+    }
 
     pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
         pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
     }
 
-    pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+    pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
         if let Some(pref) = w.preferred_height() {
             wh = pref;
         }
         let pad = self.padding();
-        w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
-        let x = self.ax(x_off);
-        let right_edge = self.left + self.cw - pad;
-        let clamped_w = ww.min((right_edge - x).max(0.0));
         let top_room = crate::widget::label_offset(w);
         let total_h = wh + top_room;
-        render_widget(pc, w, x, self.ay(), clamped_w, total_h, ctx);
-        self.content_y += total_h;
+
+        let name = w.type_name();
+        let span_full = name == "KeybindsControl"
+            || name == "MultiControl"
+            || name == "Trackpad"
+            || name == "Canvas"
+            || name == "UsageBar"
+            || name == "ProgressBar"
+            || name == "ButtonStrip"
+            || name == "Spreadsheet"
+            || name == "Graph";
+
+        if span_full {
+            let margin_x = pad + 12.0;
+            let x = self.left + margin_x;
+            let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
+            let max_h = self.grid.max_height().max(self.content_y);
+            let y = max_h;
+
+            w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
+            render_widget(pc, w, x, y, clamped_w, total_h, ctx);
+
+            let new_bottom = y + total_h;
+            self.content_y = new_bottom;
+            for h in &mut self.grid.col_heights {
+                *h = new_bottom;
+            }
+        } else {
+            let max_h = self.grid.max_height();
+            if self.content_y > max_h {
+                for h in &mut self.grid.col_heights {
+                    *h = self.content_y;
+                }
+            }
+
+            let col = self.grid.next_column();
+            self.last_col = col;
+            let x = self.grid.col_lefts[col];
+            let y = self.grid.col_heights[col];
+
+            w.set_row_rect(x, self.grid.col_width);
+            render_widget(pc, w, x, y, self.grid.col_width, total_h, ctx);
+            self.grid.col_heights[col] += total_h;
+            self.content_y = self.grid.max_height();
+        }
     }
 
     pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32, ctx: &mut UiContext) {
@@ -2055,14 +2115,22 @@ impl Section {
     pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
         let pad = self.padding();
         let x = self.ax(pad);
-        let y = self.ay();
+        let max_h = self.grid.max_height().max(self.content_y);
+        let y = max_h;
         pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
-        self.content_y += 8.0;
+        self.content_y = max_h + 8.0;
+        for h in &mut self.grid.col_heights {
+            *h = self.content_y;
+        }
     }
 
     pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
-        pc.rect(color, self.ax(x_off), self.ay(), w, h);
-        self.content_y += h;
+        let max_h = self.grid.max_height().max(self.content_y);
+        pc.rect(color, self.ax(x_off), max_h, w, h);
+        self.content_y = max_h + h;
+        for col_h in &mut self.grid.col_heights {
+            *col_h = self.content_y;
+        }
     }
 
     pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
@@ -2086,11 +2154,21 @@ impl Section {
     where
         F: FnMut(usize, f32, f32),
     {
+        let max_h = self.grid.max_height().max(self.content_y);
+        for col_h in &mut self.grid.col_heights {
+            *col_h = max_h;
+        }
+        self.content_y = max_h;
+
         let cols = self.row_layout(count, gap);
         for (i, &(x, w)) in cols.iter().enumerate() {
             f(i, x, w);
         }
         self.content_y += h;
+
+        for col_h in &mut self.grid.col_heights {
+            *col_h = self.content_y;
+        }
     }
 
     pub fn finish(&mut self, pc: &mut dyn RenderTarget) -> f32 {
@@ -2627,6 +2705,8 @@ pub struct SectionContext<'a, P> {
     pub label_width: f32,
     pub focused: bool,
     pub is_child: bool,
+    pub grid: Grid,
+    pub last_col: usize,
 }
 
 impl<'a, P: RenderTarget> SectionContext<'a, P> {
@@ -2667,15 +2747,31 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
             left + (cw - label_width) / 2.0
         };
         pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
+
+        let pad = if is_child {
+            section_padding().max(8.0)
+        } else {
+            section_padding()
+        };
+        let margin_x = pad + 12.0;
+        let usable_w = (cw - 2.0 * margin_x).max(1.0);
+        let min_col_width = 130.0;
+        let gap = 8.0;
+        let max_cols = ((usable_w + gap) / (min_col_width + gap)).floor().max(1.0) as usize;
+        let content_start_y = top + font_size + 5.0;
+        let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
+
         Self {
             pc,
             left,
             top,
-            content_y: top + font_size + 5.0,
+            content_y: content_start_y,
             cw,
             label_width,
             focused,
             is_child,
+            grid,
+            last_col: 0,
         }
     }
 
@@ -2690,26 +2786,67 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
 
     pub fn spacing(&mut self, dy: f32) {
         self.content_y += dy;
+        for h in &mut self.grid.col_heights {
+            *h = self.content_y;
+        }
     }
 
     pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
         self.pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
     }
 
-    pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+    pub fn widget<T: Element + 'static>(&mut self, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
         if let Some(pref) = w.preferred_height() {
             wh = pref;
         }
         let pad = self.padding();
-        w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
-        let x = self.ax(x_off);
-        let y = self.ay();
-        let right_edge = self.left + self.cw - pad;
-        let clamped_w = ww.min((right_edge - x).max(0.0));
         let top_room = crate::widget::label_offset(w);
         let total_h = wh + top_room;
-        render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
-        self.content_y += total_h;
+
+        let name = w.type_name();
+        let span_full = name == "KeybindsControl"
+            || name == "MultiControl"
+            || name == "Trackpad"
+            || name == "Canvas"
+            || name == "UsageBar"
+            || name == "ProgressBar"
+            || name == "ButtonStrip"
+            || name == "Spreadsheet"
+            || name == "Graph";
+
+        if span_full {
+            let margin_x = pad + 12.0;
+            let x = self.left + margin_x;
+            let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
+            let max_h = self.grid.max_height().max(self.content_y);
+            let y = max_h;
+
+            w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
+            render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
+
+            let new_bottom = y + total_h;
+            self.content_y = new_bottom;
+            for h in &mut self.grid.col_heights {
+                *h = new_bottom;
+            }
+        } else {
+            let max_h = self.grid.max_height();
+            if self.content_y > max_h {
+                for h in &mut self.grid.col_heights {
+                    *h = self.content_y;
+                }
+            }
+
+            let col = self.grid.next_column();
+            self.last_col = col;
+            let x = self.grid.col_lefts[col];
+            let y = self.grid.col_heights[col];
+
+            w.set_row_rect(x, self.grid.col_width);
+            render_widget(self.pc, w, x, y, self.grid.col_width, total_h, ctx);
+            self.grid.col_heights[col] += total_h;
+            self.content_y = self.grid.max_height();
+        }
     }
 
     pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32, ctx: &mut UiContext) {
@@ -2721,14 +2858,22 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
     pub fn separator(&mut self) {
         let pad = self.padding();
         let x = self.ax(pad);
-        let y = self.ay();
+        let max_h = self.grid.max_height().max(self.content_y);
+        let y = max_h;
         self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
-        self.content_y += 8.0;
+        self.content_y = max_h + 8.0;
+        for h in &mut self.grid.col_heights {
+            *h = self.content_y;
+        }
     }
 
     pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
-        self.pc.rect(color, self.ax(x_off), self.ay(), w, h);
-        self.content_y += h;
+        let max_h = self.grid.max_height().max(self.content_y);
+        self.pc.rect(color, self.ax(x_off), max_h, w, h);
+        self.content_y = max_h + h;
+        for col_h in &mut self.grid.col_heights {
+            *col_h = self.content_y;
+        }
     }
 
     pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
@@ -2752,11 +2897,21 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
     where
         F: FnMut(usize, f32, f32),
     {
+        let max_h = self.grid.max_height().max(self.content_y);
+        for col_h in &mut self.grid.col_heights {
+            *col_h = max_h;
+        }
+        self.content_y = max_h;
+
         let cols = self.row_layout(count, gap);
         for (i, &(x, w)) in cols.iter().enumerate() {
             f(i, x, w);
         }
         self.content_y += h;
+
+        for col_h in &mut self.grid.col_heights {
+            *col_h = self.content_y;
+        }
     }
 
     pub fn vstack(&mut self, spacing: f32) -> VStack<'_, 'a, P> {
@@ -2772,12 +2927,18 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
     {
         let pad = self.padding();
         let left = self.ax(0.0) + pad;
-        let top = self.content_y;
+        let max_h = self.grid.max_height().max(self.content_y);
+        let top = max_h;
         let cw = self.cw - 2.0 * pad;
 
         let mut sub_ctx = SectionContext::new(self.pc, left, top, cw, label, focused, true);
         render_fn(&mut sub_ctx);
-        self.content_y = sub_ctx.finish();
+        let new_bottom = sub_ctx.finish();
+
+        self.content_y = new_bottom;
+        for h in &mut self.grid.col_heights {
+            *h = new_bottom;
+        }
     }
 
     pub fn finish(self) -> f32 {
@@ -3076,5 +3237,28 @@ mod tests {
             println!("FAMILY: {:?}", face.families);
         }
     }
+
+    #[test]
+    fn test_section_context_grid() {
+        let mut mock_pc = MockRenderTarget { rects: Vec::new() };
+        let mut ctx = SectionContext::new(&mut mock_pc, 10.0, 20.0, 500.0, "Test Section", false, false);
+        assert_eq!(ctx.left, 10.0);
+        assert_eq!(ctx.top, 20.0);
+        assert_eq!(ctx.cw, 500.0);
+
+        let mut ui_ctx = crate::context::UiContext::new();
+        let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+        ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx);
+
+        let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+        ctx.widget(&mut w2, 12.0, 100.0, 30.0, &mut ui_ctx);
+
+        // Since cw=500, we should have multiple columns!
+        // The first widget goes into column 0, second into column 1.
+        assert_ne!(w1.x, w2.x);
+        
+        let bottom = ctx.finish();
+        assert!(bottom > 20.0);
+    }
 }