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

commit6e5a845950136553cdb4fab4894ce28735f810eb
parentf9f463b1b3
authorLucas Galante <[email protected]>
date2026-07-05 02:11
Implement MosaicLayout using dynamic 2D Guillotine Bin Packing

 src/layout.rs                            |  32 +++++++
 src/widget/container/container_layout.rs | 152 +++++++++++++++++++++++++++++++
 src/widget/container/mod.rs              |   2 +-
 src/widget/mod.rs                        |   1 +
 4 files changed, 186 insertions(+), 1 deletion(-)

diff --git a/src/layout.rs b/src/layout.rs
index f59f3ba..a40260b 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -5684,5 +5684,37 @@ mod tests {
         assert_eq!(crate::color::graph_connector_color(), [0.3, 0.3, 0.3, 1.0]);
         assert_eq!(crate::color::graph_connector_highlight_color(), [0.4, 0.4, 0.4, 1.0]);
     }
+
+    #[test]
+    fn test_mosaic_layout() {
+        use crate::widget::MosaicLayout;
+        use crate::layout::LayoutStrategy;
+        
+        let layout = MosaicLayout {
+            gap: 10.0,
+            padding_x: 5.0,
+            padding_y: 5.0,
+        };
+
+        let mut dummy = crate::context::UiContext::new();
+        let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
+        let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
+        let mut w3 = MockWidget { x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
+        
+        let children = vec![
+            &mut w1 as *mut MockWidget as *mut (dyn Element + 'static),
+            &mut w2 as *mut MockWidget as *mut (dyn Element + 'static),
+            &mut w3 as *mut MockWidget as *mut (dyn Element + 'static),
+        ];
+
+        let _ = layout.layout(10.0, 20.0, 250.0, 500.0, &children, &mut dummy);
+
+        assert_eq!(w1.x, 15.0);
+        assert_eq!(w1.y, 25.0);
+        assert_eq!(w2.x, 15.0);
+        assert_eq!(w2.y, 85.0);
+        assert_eq!(w3.x, 125.0);
+        assert_eq!(w3.y, 25.0);
+    }
 }
 
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
index 393606b..d8c7750 100644
--- a/src/widget/container/container_layout.rs
+++ b/src/widget/container/container_layout.rs
@@ -445,3 +445,155 @@ impl ContainerLayout for ColumnsLayout {
         Box::new(*self)
     }
 }
+
+#[derive(Debug, Clone, Copy)]
+pub struct MosaicLayout {
+    pub gap: f32,
+    pub padding_x: f32,
+    pub padding_y: f32,
+}
+
+impl Default for MosaicLayout {
+    fn default() -> Self {
+        Self {
+            gap: 8.0,
+            padding_x: 8.0,
+            padding_y: 10.0,
+        }
+    }
+}
+
+struct Packer {
+    free_rects: Vec<(f32, f32, f32, f32)>, // (x, y, w, h)
+    max_w: f32,
+    max_h: f32,
+    gap: f32,
+}
+
+impl Packer {
+    fn new(start_x: f32, start_y: f32, max_width: f32, gap: f32) -> Self {
+        Self {
+            free_rects: vec![(start_x, start_y, max_width, 100000.0)],
+            max_w: max_width,
+            max_h: 0.0,
+            gap,
+        }
+    }
+
+    fn pack(&mut self, cw: f32, ch: f32) -> (f32, f32) {
+        let cw_clamped = cw.min(self.max_w);
+        
+        let mut best_idx = None;
+        let mut best_y = f32::MAX;
+        let mut best_x = f32::MAX;
+
+        for (idx, &(rx, ry, rw, rh)) in self.free_rects.iter().enumerate() {
+            if rw >= cw_clamped && rh >= ch {
+                if ry < best_y || (ry == best_y && rx < best_x) {
+                    best_y = ry;
+                    best_x = rx;
+                    best_idx = Some(idx);
+                }
+            }
+        }
+
+        let chosen_idx = match best_idx {
+            Some(idx) => idx,
+            None => {
+                let new_y = self.max_h + self.gap;
+                let new_rect = (self.free_rects[0].0, new_y, self.max_w, 100000.0);
+                self.free_rects.push(new_rect);
+                self.free_rects.len() - 1
+            }
+        };
+
+        let (fx, fy, fw, fh) = self.free_rects.remove(chosen_idx);
+        let px = fx;
+        let py = fy;
+
+        let rx = fx + cw_clamped + self.gap;
+        let rw = fw - cw_clamped - self.gap;
+        if rw > 0.0 && ch > 0.0 {
+            self.free_rects.push((rx, py, rw, ch));
+        }
+
+        let by = py + ch + self.gap;
+        let bh = fh - ch - self.gap;
+        if bh > 0.0 && fw > 0.0 {
+            self.free_rects.push((fx, by, fw, bh));
+        }
+
+        self.max_h = self.max_h.max(py + ch);
+
+        (px, py)
+    }
+}
+
+impl crate::layout::LayoutStrategy for MosaicLayout {
+    fn init(&mut self, _left: f32, _top: f32, _width: f32, _height: f32) {}
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) { (0.0, 0.0, ww, wh) }
+    fn get_gap(&self) -> f32 { self.gap }
+
+    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 total_padding_x = self.padding_x * 2.0;
+        let available_w = (w - total_padding_x).max(1.0);
+
+        let mut packer = Packer::new(x + self.padding_x, y + self.padding_y, available_w, self.gap);
+
+        for &child_ptr in children {
+            unsafe {
+                let child = &mut *child_ptr;
+                let child_rect = child.rect();
+                let child_w = child_rect.2;
+                let child_h = child.preferred_height().unwrap_or(child_rect.3);
+                let use_h = if child_h > 0.0 { child_h } else { 44.0 };
+                
+                let (px, py) = packer.pack(child_w, use_h);
+                child.layout(
+                    Point { x: px, y: py },
+                    LayoutConstraints::new(child_w.min(available_w), child_w.min(available_w), use_h, use_h),
+                    ctx,
+                );
+            }
+        }
+
+        (packer.max_h - y).max(0.0)
+    }
+
+    fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+        let count = children.len();
+        if count == 0 {
+            return Size { width: constraints.min_width, height: constraints.min_height };
+        }
+        let total_padding_x = self.padding_x * 2.0;
+        let available_w = (constraints.max_width - total_padding_x).max(1.0);
+
+        let mut packer = Packer::new(self.padding_x, self.padding_y, available_w, self.gap);
+
+        for &child_ptr in children {
+            unsafe {
+                let size = (*child_ptr).measure(constraints, ctx);
+                packer.pack(size.width, size.height);
+            }
+        }
+
+        Size {
+            width: constraints.max_width,
+            height: (packer.max_h + self.padding_y).clamp(constraints.min_height, constraints.max_height),
+        }
+    }
+
+    fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+        Box::new(*self)
+    }
+}
+
+impl ContainerLayout for MosaicLayout {
+    fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
+        Box::new(*self)
+    }
+}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index a2b6ffd..a4ffb73 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -24,7 +24,7 @@ pub mod hbox;
 
 pub use container::Container;
 pub use control_panel::ControlPanel;
-pub use container_layout::{ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout, ColumnsLayout};
+pub use container_layout::{ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout, ColumnsLayout, MosaicLayout};
 pub use section_container::SectionContainer;
 pub use header::Header;
 pub use content_bg::ContentBg;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index f871d40..abf5f76 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -685,6 +685,7 @@ pub use self::input::{
 };
 pub use self::container::{
     Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
+    ColumnsLayout, MosaicLayout,
     SectionContainer, Header, ContentBg, ViewportBg, ParametersBg, List,
     ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
     Switcher, Layer, Page, Backplate, Paginator, ScrollBar, TreeList, TreeElement, ControlPanel