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

commitd7743c41b2cc4492578c2d9f07e65826bb92a9e0
parentdba0332259
authorLucas Galante <[email protected]>
date2026-06-26 10:28
Fix grid layout spacing vertical alignment, scroll bar crash, and duplicate headers

 src/layout.rs                             | 15 ++++--
 src/widget/container/scroll_bar.rs        | 85 ++++++++-----------------------
 src/widget/container/scroll_box.rs        | 50 ++++++++----------
 src/widget/container/section_container.rs | 21 ++------
 4 files changed, 56 insertions(+), 115 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index 228d7d8..23d9779 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2945,9 +2945,16 @@ 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 += dy;
+        if self.grid.col_heights.len() >= 2 {
+            if self.last_col < self.grid.col_heights.len() {
+                self.grid.col_heights[self.last_col] += dy;
+            }
+            self.content_y = self.grid.max_height();
+        } else {
+            self.content_y += dy;
+            for h in &mut self.grid.col_heights {
+                *h += dy;
+            }
         }
     }
 
@@ -3493,7 +3500,7 @@ mod tests {
         let height_col_0_after = ctx.grid.col_heights[0];
         let height_col_1_after = ctx.grid.col_heights[1];
         assert_eq!(height_col_0_after, height_col_0_before + 12.0);
-        assert_eq!(height_col_1_after, height_col_1_before + 12.0);
+        assert_eq!(height_col_1_after, height_col_1_before);
         assert_ne!(height_col_0_after, height_col_1_after);
     }
 }
diff --git a/src/widget/container/scroll_bar.rs b/src/widget/container/scroll_bar.rs
index 8cf9262..2295a55 100644
--- a/src/widget/container/scroll_bar.rs
+++ b/src/widget/container/scroll_bar.rs
@@ -2,14 +2,10 @@ use crate::widget::*;
 use crate::context::UiContext;
 
 pub struct ScrollBar {
-    x: f32,
-    y: f32,
-    w: f32,
-    h: f32,
+    pub base: Widget,
     pub scroll_y: f32,
     pub content_h: f32,
     pub viewport_h: f32,
-    pub hovered: bool,
     pub dragging: bool,
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub children: Vec<*mut (dyn Element + 'static)>,
@@ -18,14 +14,10 @@ pub struct ScrollBar {
 impl std::fmt::Debug for ScrollBar {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("ScrollBar")
-            .field("x", &self.x)
-            .field("y", &self.y)
-            .field("w", &self.w)
-            .field("h", &self.h)
+            .field("base", &self.base)
             .field("scroll_y", &self.scroll_y)
             .field("content_h", &self.content_h)
             .field("viewport_h", &self.viewport_h)
-            .field("hovered", &self.hovered)
             .field("dragging", &self.dragging)
             .finish()
     }
@@ -34,14 +26,10 @@ impl std::fmt::Debug for ScrollBar {
 impl Clone for ScrollBar {
     fn clone(&self) -> Self {
         Self {
-            x: self.x,
-            y: self.y,
-            w: self.w,
-            h: self.h,
+            base: self.base.clone(),
             scroll_y: self.scroll_y,
             content_h: self.content_h,
             viewport_h: self.viewport_h,
-            hovered: self.hovered,
             dragging: self.dragging,
             parent: self.parent,
             children: self.children.clone(),
@@ -51,15 +39,13 @@ impl Clone for ScrollBar {
 
 impl ScrollBar {
     pub fn new() -> Self {
+        let mut base = Widget::new();
+        base.w = 6.0;
         Self {
-            x: 0.0,
-            y: 0.0,
-            w: 6.0,
-            h: 0.0,
+            base,
             scroll_y: 0.0,
             content_h: 0.0,
             viewport_h: 0.0,
-            hovered: false,
             dragging: false,
             parent: None,
             children: Vec::new(),
@@ -73,13 +59,13 @@ impl ScrollBar {
     }
 
     pub fn get_thumb_rect(&self) -> Option<(f32, f32, f32, f32)> {
-        if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || self.h <= 0.0 {
+        if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || self.base.h <= 0.0 {
             return None;
         }
-        let sb_x = self.x;
-        let sb_w = self.w;
-        let sb_track_h = self.h;
-        let sb_track_y = self.y;
+        let sb_x = self.base.x;
+        let sb_w = self.base.w;
+        let sb_track_h = self.base.h;
+        let sb_track_y = self.base.y;
 
         let visible_ratio = self.viewport_h / self.content_h;
         let thumb_h = if sb_track_h <= 20.0 {
@@ -96,63 +82,32 @@ impl ScrollBar {
 }
 
 impl Element for ScrollBar {
-    fn base(&self) -> Option<&Widget> { None }
-    fn base_mut(&mut self) -> Option<&mut Widget> { None }
-
-    fn rect(&self) -> (f32, f32, f32, f32) {
-        (self.x, self.y, self.w, self.h)
-    }
-
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        self.x = x;
-        self.y = y;
-        self.w = w;
-        self.h = h;
-    }
-
-    fn as_any(&self) -> &dyn std::any::Any { self }
-    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
-
-    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)
-    }
+    crate::impl_widget_base!(ScrollBar);
 
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
 
-    fn set_hovered(&mut self, hovered: bool) {
-        self.hovered = hovered;
-    }
-
-    fn hovered(&self) -> bool {
-        self.hovered
-    }
-
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
         if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
             return false;
         }
         let hit_margin = 6.0;
-        px >= self.x - hit_margin && px <= self.x + self.w + hit_margin && py >= self.y && py <= self.y + self.h
+        px >= self.base.x - hit_margin && px <= self.base.x + self.base.w + hit_margin && py >= self.base.y && py <= self.base.y + self.base.h
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         let mut changed = false;
         let is_hit = self.hit_test(px, py, ctx);
-        if self.hovered != is_hit {
-            self.hovered = is_hit;
+        if self.base.hovered != is_hit {
+            self.base.hovered = is_hit;
             changed = true;
         }
 
         if self.dragging {
             if let Some((_, _, _, thumb_h)) = self.get_thumb_rect() {
-                let sb_track_y = self.y;
-                let sb_track_h = self.h;
+                let sb_track_y = self.base.y;
+                let sb_track_h = self.base.h;
                 let track_scroll_range = sb_track_h - thumb_h;
                 if track_scroll_range > 0.0 {
                     let mouse_y_in_track = (py - sb_track_y).clamp(0.0, sb_track_h);
@@ -202,13 +157,13 @@ impl Element for ScrollBar {
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        if self.content_h > self.viewport_h && self.h > 0.0 {
-            quads.push((self.x, self.y, self.w, self.h, [0.15, 0.15, 0.20, 0.3]));
+        if self.content_h > self.viewport_h && self.base.h > 0.0 {
+            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, [0.15, 0.15, 0.20, 0.3]));
 
             if let Some((sb_x, thumb_y, sb_w, thumb_h)) = self.get_thumb_rect() {
                 let thumb_color = if self.dragging {
                     [0.70, 0.70, 0.75, 0.6]
-                } else if self.hovered {
+                } else if self.base.hovered {
                     [0.65, 0.65, 0.70, 0.5]
                 } else {
                     [0.60, 0.60, 0.65, 0.4]
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 6636325..a948fc1 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -2,14 +2,13 @@ use crate::widget::*;
 
 #[derive(Debug, Clone)]
 pub struct ScrollBox {
-    x: f32, y: f32, w: f32, h: f32,
+    pub base: Widget,
     pub scroll_y: f32,
     pub content_h: f32,
     pub viewport_y: f32,
     pub viewport_h: f32,
     viewport_offset_y: f32,
     viewport_offset_h: f32,
-    hovered: bool,
     pub show_border: bool,
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub children: Vec<*mut (dyn Element + 'static)>,
@@ -18,14 +17,13 @@ pub struct ScrollBox {
 impl ScrollBox {
     pub fn new() -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            base: Widget::new(),
             scroll_y: 0.0,
             content_h: 0.0,
             viewport_y: 0.0,
             viewport_h: 0.0,
             viewport_offset_y: 0.0,
             viewport_offset_h: 0.0,
-            hovered: false,
             show_border: true,
             parent: None,
             children: Vec::new(),
@@ -36,8 +34,8 @@ impl ScrollBox {
         self.content_h = content_h;
         self.viewport_y = viewport_y;
         self.viewport_h = viewport_h;
-        self.viewport_offset_y = viewport_y - self.y;
-        self.viewport_offset_h = viewport_h - self.h;
+        self.viewport_offset_y = viewport_y - self.base.y;
+        self.viewport_offset_h = viewport_h - self.base.h;
         let max_scroll = (content_h - viewport_h).max(0.0);
         self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
     }
@@ -53,25 +51,17 @@ impl ScrollBox {
 }
 
 impl Element for ScrollBox {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    crate::impl_widget_base!(ScrollBox);
+
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        self.x = x;
-        self.y = y;
-        self.w = w;
-        self.h = h;
+        self.base.x = x;
+        self.base.y = y;
+        self.base.w = w;
+        self.base.h = h;
         self.viewport_y = y + self.viewport_offset_y;
         self.viewport_h = h + self.viewport_offset_h;
     }
     fn color(&self) -> [f32; 4] { crate::color::scrollinglist_bg_color() }
-    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_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
-    fn highlight_color(&self, _ctx: &UiContext) -> Option<[f32; 4]> { None }
 
     fn focus(&mut self) {
         focus::set_focused(self);
@@ -89,9 +79,9 @@ impl Element for ScrollBox {
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        let was = self.hovered;
-        self.hovered = self.hit_test(px, py, ctx);
-        was != self.hovered
+        let was = self.base.hovered;
+        self.base.hovered = self.hit_test(px, py, ctx);
+        was != self.base.hovered
     }
 
     fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
@@ -114,24 +104,24 @@ impl Element for ScrollBox {
         let mut quads = Vec::new();
         
         // Background
-        quads.push((self.x, self.y, self.w, self.h, crate::color::scrollinglist_bg_color()));
+        quads.push((self.base.x, self.base.y, self.base.w, self.base.h, crate::color::scrollinglist_bg_color()));
 
         // Border lines
         let box_border_color = if focus::is_focused(self) {
             [0.30, 0.50, 0.32, 1.0] // Focused green
-        } else if self.hovered {
+        } else if self.base.hovered {
             [0.25, 0.25, 0.35, 1.0] // Hovered
         } else {
             [0.18, 0.18, 0.24, 1.0] // Default
         };
-        quads.push((self.x, self.y, self.w, 1.0, box_border_color)); // Top
-        quads.push((self.x, self.y + self.h - 1.0, self.w, 1.0, box_border_color)); // Bottom
-        quads.push((self.x, self.y, 1.0, self.h, box_border_color)); // Left
-        quads.push((self.x + self.w - 1.0, self.y, 1.0, self.h, box_border_color)); // Right
+        quads.push((self.base.x, self.base.y, self.base.w, 1.0, box_border_color)); // Top
+        quads.push((self.base.x, self.base.y + self.base.h - 1.0, self.base.w, 1.0, box_border_color)); // Bottom
+        quads.push((self.base.x, self.base.y, 1.0, self.base.h, box_border_color)); // Left
+        quads.push((self.base.x + self.base.w - 1.0, self.base.y, 1.0, self.base.h, box_border_color)); // Right
 
         // Scrollbar
         if self.content_h > self.viewport_h {
-            let sb_x = self.x + self.w - 8.0;
+            let sb_x = self.base.x + self.base.w - 8.0;
             let sb_w = 4.0;
             let sb_track_h = self.viewport_h - 8.0;
             let sb_track_y = self.viewport_y + 4.0;
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
index 7f2df81..3b451de 100644
--- a/src/widget/container/section_container.rs
+++ b/src/widget/container/section_container.rs
@@ -114,34 +114,23 @@ impl Element for SectionContainer {
     }
 
     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
+        self.container.all_quads(ctx)
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
-        let mut labels = self.header.text_labels();
-        labels.extend(self.container.text_labels());
-        labels
+        self.container.text_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
+        self.container.text_labels_with_bounds(ctx)
     }
 
     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
+        self.container.text_labels_with_font_and_bounds(ctx)
     }
 
     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
+        self.container.get_text_items()
     }
 
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {