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

commit0af492fe2e8c6c0359e308cd67c072ee8f08bb7c
parent34960a1b28
authorLucas Galante <[email protected]>
date2026-06-26 08:11
widget: introduce ContainerLayout strategies and SectionContainer

 src/widget/container/container.rs         |  34 ++---
 src/widget/container/container_layout.rs  | 231 ++++++++++++++++++++++++++++++
 src/widget/container/mod.rs               |   4 +
 src/widget/container/page.rs              |  56 +++++---
 src/widget/container/section_container.rs | 156 ++++++++++++++++++++
 src/widget/mod.rs                         |   3 +-
 6 files changed, 442 insertions(+), 42 deletions(-)

diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 7e56ecd..0d6b07e 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -1,15 +1,27 @@
 use crate::widget::*;
+use super::container_layout::{ContainerLayout, OverlayLayout};
 
 #[derive(Clone)]
 pub struct Container {
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub children: Vec<*mut (dyn Element + 'static)>,
     pub base: Widget,
+    pub layout: Box<dyn ContainerLayout>,
 }
 
 impl Container {
     pub fn new() -> Self {
-        Self { parent: None, children: Vec::new(), base: Widget::new() }
+        Self {
+            parent: None,
+            children: Vec::new(),
+            base: Widget::new(),
+            layout: Box::new(OverlayLayout),
+        }
+    }
+
+    pub fn with_layout<L: ContainerLayout + 'static>(mut self, layout: L) -> Self {
+        self.layout = Box::new(layout);
+        self
     }
 }
 
@@ -45,29 +57,13 @@ impl Element for Container {
     }
 
     fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size {
-        let mut max_w = 0.0f32;
-        let mut max_h = 0.0f32;
-        for &child in &self.children {
-            unsafe {
-                let size = (*child).measure(constraints, ctx);
-                max_w = max_w.max(size.width);
-                max_h = max_h.max(size.height);
-            }
-        }
-        Size {
-            width: max_w.clamp(constraints.min_width, constraints.max_width),
-            height: max_h.clamp(constraints.min_height, constraints.max_height),
-        }
+        self.layout.measure(constraints, &self.children, ctx)
     }
 
     fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
         let size = self.measure(constraints, ctx);
         self.set_rect(origin.x, origin.y, size.width, size.height);
-        for &child in &self.children {
-            unsafe {
-                (*child).layout(origin, constraints, ctx);
-            }
-        }
+        self.layout.layout(origin.x, origin.y, size.width, size.height, &self.children, ctx);
     }
 
     fn focus(&mut self) {
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
new file mode 100644
index 0000000..d74b7e6
--- /dev/null
+++ b/src/widget/container/container_layout.rs
@@ -0,0 +1,231 @@
+use crate::widget::*;
+use crate::context::UiContext;
+
+pub trait ContainerLayout: std::fmt::Debug {
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32;
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size;
+    fn box_clone(&self) -> Box<dyn ContainerLayout>;
+}
+
+impl Clone for Box<dyn ContainerLayout> {
+    fn clone(&self) -> Self {
+        self.box_clone()
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct OverlayLayout;
+
+impl ContainerLayout for OverlayLayout {
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+        for &child_ptr in children {
+            unsafe {
+                (*child_ptr).set_rect(x, y, w, h);
+            }
+        }
+        h
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+        let mut max_w = 0.0f32;
+        let mut max_h = 0.0f32;
+        for &child_ptr in children {
+            unsafe {
+                let size = (*child_ptr).measure(constraints, ctx);
+                max_w = max_w.max(size.width);
+                max_h = max_h.max(size.height);
+            }
+        }
+        Size {
+            width: max_w.clamp(constraints.min_width, constraints.max_width),
+            height: max_h.clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+        Box::new(*self)
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct VerticalLayout {
+    pub padding_x: f32,
+    pub padding_y: f32,
+    pub spacing: f32,
+}
+
+impl Default for VerticalLayout {
+    fn default() -> Self {
+        Self {
+            padding_x: 0.0,
+            padding_y: 0.0,
+            spacing: 8.0,
+        }
+    }
+}
+
+impl ContainerLayout for VerticalLayout {
+    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+        let left_x = x + self.padding_x;
+        let available_w = (w - 2.0 * self.padding_x).max(1.0);
+        let mut current_y = y + self.padding_y;
+
+        for &child_ptr in children {
+            unsafe {
+                let child = &mut *child_ptr;
+                let ch = child.preferred_height().unwrap_or(child.rect().3);
+                let use_h = if ch > 0.0 { ch } else { 44.0 };
+                child.set_rect(left_x, current_y, available_w, use_h);
+                current_y += use_h + self.spacing;
+            }
+        }
+        (current_y - y).max(0.0)
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+        let mut total_h = self.padding_y * 2.0;
+        let mut max_w = 0.0f32;
+        let spacing = self.spacing;
+
+        for (i, &child_ptr) in children.iter().enumerate() {
+            unsafe {
+                let size = (*child_ptr).measure(constraints, ctx);
+                max_w = max_w.max(size.width);
+                total_h += size.height;
+                if i > 0 {
+                    total_h += spacing;
+                }
+            }
+        }
+
+        Size {
+            width: (max_w + self.padding_x * 2.0).clamp(constraints.min_width, constraints.max_width),
+            height: total_h.clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+        Box::new(*self)
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct GridLayout {
+    pub columns: usize,
+    pub gap: f32,
+    pub padding_x: f32,
+    pub padding_y: f32,
+}
+
+impl ContainerLayout for GridLayout {
+    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+        let count = children.len();
+        if count == 0 {
+            return 0.0;
+        }
+        let cols = self.columns.max(1);
+        let total_gap = self.gap * (cols - 1) as f32;
+        let available_w = (w - 2.0 * self.padding_x - total_gap).max(1.0);
+        let col_w = available_w / cols as f32;
+        
+        let mut col_heights = vec![y + self.padding_y; cols];
+
+        for &child_ptr in children {
+            unsafe {
+                let child = &mut *child_ptr;
+                let ch = child.preferred_height().unwrap_or(child.rect().3);
+                let use_h = if ch > 0.0 { ch } else { 44.0 };
+                
+                let mut min_col = 0;
+                let mut min_h = col_heights[0];
+                for i in 1..cols {
+                    if col_heights[i] < min_h {
+                        min_h = col_heights[i];
+                        min_col = i;
+                    }
+                }
+                
+                let cx = x + self.padding_x + min_col as f32 * (col_w + self.gap);
+                let cy = col_heights[min_col];
+                child.set_rect(cx, cy, col_w, use_h);
+                col_heights[min_col] += use_h + self.gap;
+            }
+        }
+        
+        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+        (max_h - y).max(0.0)
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+        let cols = self.columns.max(1);
+        let mut col_heights = vec![self.padding_y; cols];
+        let total_gap = self.gap * (cols - 1) as f32;
+        let available_w = (constraints.max_width - 2.0 * self.padding_x - total_gap).max(1.0);
+        let col_w = available_w / cols as f32;
+        
+        let child_constraints = LayoutConstraints::new(col_w, col_w, constraints.min_height, constraints.max_height);
+
+        for &child_ptr in children {
+            unsafe {
+                let size = (*child_ptr).measure(child_constraints, ctx);
+                let mut min_col = 0;
+                let mut min_h = col_heights[0];
+                for i in 1..cols {
+                    if col_heights[i] < min_h {
+                        min_h = col_heights[i];
+                        min_col = i;
+                    }
+                }
+                col_heights[min_col] += size.height + self.gap;
+            }
+        }
+        
+        let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+        Size {
+            width: constraints.max_width,
+            height: (max_h + self.padding_y).clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+        Box::new(*self)
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct AdaptiveGridLayout {
+    pub min_col_width: f32,
+    pub gap: f32,
+    pub padding_x: f32,
+    pub padding_y: f32,
+}
+
+impl ContainerLayout for AdaptiveGridLayout {
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
+        let usable_w = (w - 2.0 * self.padding_x).max(1.0);
+        let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
+        let grid = GridLayout {
+            columns: cols,
+            gap: self.gap,
+            padding_x: self.padding_x,
+            padding_y: self.padding_y,
+        };
+        grid.layout(x, y, w, h, children, ctx)
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+        let usable_w = (constraints.max_width - 2.0 * self.padding_x).max(1.0);
+        let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
+        let grid = GridLayout {
+            columns: cols,
+            gap: self.gap,
+            padding_x: self.padding_x,
+            padding_y: self.padding_y,
+        };
+        grid.measure(constraints, children, ctx)
+    }
+
+    fn box_clone(&self) -> Box<dyn ContainerLayout> {
+        Box::new(*self)
+    }
+}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 53fbb89..c6a7449 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -1,4 +1,6 @@
 pub mod container;
+pub mod container_layout;
+pub mod section_container;
 pub mod header;
 pub mod content_bg;
 pub mod viewport_bg;
@@ -17,6 +19,8 @@ pub mod paginator;
 pub mod scroll_bar;
 
 pub use container::Container;
+pub use container_layout::{ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout};
+pub use section_container::SectionContainer;
 pub use header::Header;
 pub use content_bg::ContentBg;
 pub use viewport_bg::ViewportBg;
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index d57da3e..d8c0ca0 100644
--- a/src/widget/container/page.rs
+++ b/src/widget/container/page.rs
@@ -4,7 +4,7 @@ use crate::widget::display::TextLabel;
 use super::layer::Layer;
 
 pub trait PageLayout {
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) -> f32;
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32;
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -25,7 +25,7 @@ impl Default for VerticalLayout {
 }
 
 impl PageLayout for VerticalLayout {
-    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)]) -> f32 {
+    fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
         let padding_x = self.padding_x;
         let padding_y = self.padding_y;
         let left_x = x + padding_x;
@@ -37,7 +37,11 @@ impl PageLayout for VerticalLayout {
             let child = unsafe { &mut *child_ptr };
             let (_, _, _, ch) = child.rect();
             let use_h = if ch > 0.0 { ch } else { 42.0 };
-            child.set_rect(left_x, current_y, available_w, use_h);
+            child.layout(
+                Point { x: left_x, y: current_y },
+                LayoutConstraints::new(available_w, available_w, use_h, use_h),
+                ctx,
+            );
             current_y += use_h + spacing;
         }
         current_y - y
@@ -62,7 +66,7 @@ impl Default for ColumnsLayout {
 }
 
 impl PageLayout for ColumnsLayout {
-    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) -> f32 {
+    fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
         let count = children.len();
         if count == 0 {
             return 0.0;
@@ -77,7 +81,11 @@ impl PageLayout for ColumnsLayout {
         let mut current_x = x + self.padding_x;
         for &child_ptr in children {
             let child = unsafe { &mut *child_ptr };
-            child.set_rect(current_x, start_y, col_w, use_h);
+            child.layout(
+                Point { x: current_x, y: start_y },
+                LayoutConstraints::new(col_w, col_w, use_h, use_h),
+                ctx,
+            );
             current_x += col_w + self.spacing;
         }
         use_h + 2.0 * self.padding_y
@@ -178,24 +186,28 @@ impl Element for Page {
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         self.base.set_rect(x, y, w, h);
-        if !self.visible {
-            return;
-        }
-        let layout_content_h = self.layout.layout(x, y, w, h, &self.base.children);
-        self.content_h = self.content_h.max(layout_content_h);
-
-        let max_scroll = (self.content_h - h).max(0.0);
-        self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
-
-        // Position the scroll bar
-        let sb_w = 6.0;
-        let sb_padding = 2.0;
-        let sb_x = x + w - sb_w - sb_padding;
-        self.scroll_bar.set_rect(sb_x, y + 4.0, sb_w, h - 8.0);
-        self.scroll_bar.update(self.scroll_y, self.content_h, h);
+    }
+
+    fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
+        let size = self.measure(constraints, ctx);
+        self.set_rect(origin.x, origin.y, size.width, size.height);
         
-        let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
-        self.scroll_bar.parent = Some(self_ptr);
+        if self.visible {
+            let layout_content_h = self.layout.layout(origin.x, origin.y, size.width, size.height, &self.base.children, ctx);
+            self.content_h = self.content_h.max(layout_content_h);
+
+            let max_scroll = (self.content_h - size.height).max(0.0);
+            self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
+
+            let sb_w = 6.0;
+            let sb_padding = 2.0;
+            let sb_x = origin.x + size.width - sb_w - sb_padding;
+            self.scroll_bar.set_rect(sb_x, origin.y + 4.0, sb_w, size.height - 8.0);
+            self.scroll_bar.update(self.scroll_y, self.content_h, size.height);
+            
+            let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
+            self.scroll_bar.parent = Some(self_ptr);
+        }
     }
 
     fn color(&self) -> [f32; 4] {
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
new file mode 100644
index 0000000..7f2df81
--- /dev/null
+++ b/src/widget/container/section_container.rs
@@ -0,0 +1,156 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+use super::container::Container;
+use super::container_layout::ContainerLayout;
+
+#[derive(Clone)]
+pub struct SectionContainer {
+    pub header: SectionHeader,
+    pub container: Container,
+    pub base: Widget,
+    pub parent: Option<*mut (dyn Element + 'static)>,
+}
+
+impl SectionContainer {
+    pub fn new(title: &str) -> Self {
+        Self {
+            header: SectionHeader::new(title),
+            container: Container::new(),
+            base: Widget::new(),
+            parent: None,
+        }
+    }
+
+    pub fn with_layout<L: ContainerLayout + 'static>(mut self, layout: L) -> Self {
+        let container = std::mem::replace(&mut self.container, Container::new());
+        self.container = container.with_layout(layout);
+        self
+    }
+}
+
+impl Default for SectionContainer {
+    fn default() -> Self {
+        Self::new("")
+    }
+}
+
+impl Element for SectionContainer {
+    fn base(&self) -> Option<&Widget> { Some(&self.base) }
+    fn blocks_backplate_drag(&self) -> bool { false }
+    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    }
+    fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
+        self as *mut Self as *mut (dyn Element + 'static)
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.base.x = x;
+        self.base.y = y;
+        self.base.w = w;
+        self.base.h = h;
+
+        self.header.set_rect(x, y, w, 24.0);
+        self.container.set_rect(x, y + 28.0, w, (h - 28.0).max(0.0));
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size {
+        let remaining_constraints = LayoutConstraints::new(
+            constraints.min_width,
+            constraints.max_width,
+            (constraints.min_height - 28.0).max(0.0),
+            (constraints.max_height - 28.0).max(0.0),
+        );
+        let container_size = self.container.measure(remaining_constraints, ctx);
+        Size {
+            width: container_size.width,
+            height: container_size.height + 28.0,
+        }
+    }
+
+    fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
+        let size = self.measure(constraints, ctx);
+        self.set_rect(origin.x, origin.y, size.width, size.height);
+        
+        let self_ptr = self.as_ptr_mut();
+        self.header.set_parent(Some(self_ptr), ctx);
+        self.container.set_parent(Some(self_ptr), ctx);
+
+        self.header.layout(origin, LayoutConstraints::new(size.width, size.width, 24.0, 24.0), ctx);
+        self.container.layout(
+            Point { x: origin.x, y: origin.y + 28.0 },
+            LayoutConstraints::new(size.width, size.width, (size.height - 28.0).max(0.0), (size.height - 28.0).max(0.0)),
+            ctx,
+        );
+    }
+
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.parent }
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) { self.parent = parent; }
+    
+    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        vec![
+            &self.header as *const SectionHeader as *mut SectionHeader as *mut (dyn Element + 'static),
+            &self.container as *const Container as *mut Container as *mut (dyn Element + 'static),
+        ]
+    }
+    
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+        self.container.add_child(child, ctx);
+        let id = self.base.id();
+        unsafe {
+            let c_id = (*child).base().map(|b| b.id()).unwrap();
+            ctx.register_widget(c_id, child);
+            ctx.link_ids(id, c_id);
+            (*child).set_parent(Some(self.container.as_ptr_mut()), ctx);
+        }
+    }
+    
+    fn clear_children(&mut self, ctx: &mut UiContext) {
+        self.container.clear_children(ctx);
+        ctx.clear_children_ids(self.base.id());
+    }
+
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = self.header.all_quads(ctx);
+        quads.extend(self.header.extra_quads());
+        quads.extend(self.container.all_quads(ctx));
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = self.header.text_labels();
+        labels.extend(self.container.text_labels());
+        labels
+    }
+
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        let mut result = self.header.text_labels_with_bounds(ctx);
+        result.extend(self.container.text_labels_with_bounds(ctx));
+        result
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let mut result = self.header.text_labels_with_font_and_bounds(ctx);
+        result.extend(self.container.text_labels_with_font_and_bounds(ctx));
+        result
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        let mut result = self.header.get_text_items();
+        result.extend(self.container.get_text_items());
+        result
+    }
+
+    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+        self.header.hit_test(px, py, ctx) || self.container.hit_test(px, py, ctx)
+    }
+}
+
+impl Drop for SectionContainer {
+    fn drop(&mut self) {
+        clear_widget_references(self);
+    }
+}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index cda76b9..46a768e 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -572,7 +572,8 @@ pub use self::input::{
     KeybindsControl, KeybindRow
 };
 pub use self::container::{
-    Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
+    Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
+    SectionContainer, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
     ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
     Switcher, Layer, Page, Backplate, Paginator, ScrollBar
 };