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

commitf201dd625ac3913a10f9c0f2ec74e002cf1abebd
parente1a346cb0b
authorLucas Galante <[email protected]>
date2026-07-04 21:10
Implement ScrollBox delegation and spacing updates for ControlPanel

 src/widget/container/control_panel.rs | 265 +++++++++++++++++++++++++++-------
 1 file changed, 212 insertions(+), 53 deletions(-)

diff --git a/src/widget/container/control_panel.rs b/src/widget/container/control_panel.rs
index 257b8b0..3e21822 100644
--- a/src/widget/container/control_panel.rs
+++ b/src/widget/container/control_panel.rs
@@ -1,23 +1,30 @@
 use crate::colors;
 use crate::widget::*;
+use crate::widget::container::scroll_box::ScrollBox;
 
 pub struct ControlPanel {
     pub base: Widget,
     pub children: Vec<*mut (dyn Element + 'static)>,
     pub parent: Option<*mut (dyn Element + 'static)>,
+    pub scroll_box: ScrollBox,
 }
 
 impl ControlPanel {
     pub fn new() -> Self {
+        let mut sb = ScrollBox::new();
+        sb.show_border = false;
+        sb.show_background = false;
         Self {
             base: Widget::new(),
             children: Vec::new(),
             parent: None,
+            scroll_box: sb,
         }
     }
 
     pub fn add_child(&mut self, child: *mut (dyn Element + 'static)) {
         self.children.push(child);
+        self.scroll_box.children.push(child);
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
@@ -34,7 +41,7 @@ impl Element for ControlPanel {
             return false;
         }
 
-        // Check if cursor is inside child popovers first
+        // Check if cursor is inside child popovers first (scrolled coordinates)
         if let Some(pop_rect) = self.popover_rect() {
             if px >= pop_rect.0 && px <= pop_rect.0 + pop_rect.2 && py >= pop_rect.1 && py <= pop_rect.1 + pop_rect.3 {
                 return true;
@@ -50,7 +57,7 @@ impl Element for ControlPanel {
     }
 
     fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
-        let mut changed = false;
+        let mut changed = self.scroll_box.tick(dt, ctx);
         unsafe {
             for child_ptr in &self.children {
                 if (**child_ptr).tick(dt, ctx) {
@@ -80,13 +87,15 @@ impl Element for ControlPanel {
         self.base.w = w;
         self.base.h = h;
 
+        self.scroll_box.set_rect(x, y, w, h);
+
         let self_ptr = self.as_ptr_mut();
         let mut dummy = crate::context::UiContext::new();
         let self_ptr_option = Some(self_ptr);
 
-        let padding = 12.0;
-        let mut curr_y = y + padding;
-        let content_w = w - 2.0 * padding;
+        let padding = 16.0;
+        let mut curr_y = padding;
+        let content_w = w - 2.0 * padding - 12.0; // 12px reserved for scrollbar track
 
         unsafe {
             let mut create_btn: Option<*mut dyn Element> = None;
@@ -131,7 +140,6 @@ impl Element for ControlPanel {
                     "Border Width" => border_width_spin = Some(child_ptr),
                     "Bevel Shape..." => bevel_shape_btn = Some(child_ptr),
                     _ => {
-                        // Slider has no label string in widget base, let's identify it by type
                         if child.base().is_some() && child.base().unwrap().label.is_none() {
                             slider = Some(child_ptr);
                         }
@@ -141,102 +149,194 @@ impl Element for ControlPanel {
 
             // --- SECTION 1: Window Actions ---
             if let (Some(c), Some(t)) = (create_btn, tile_btn) {
-                let btn_w = (content_w - 10.0) / 2.0;
-                (*c).set_rect(x + padding, curr_y, btn_w, 28.0);
-                (*t).set_rect(x + padding + btn_w + 10.0, curr_y, btn_w, 28.0);
-                curr_y += 28.0 + 10.0;
+                let btn_w = (content_w - 12.0) / 2.0;
+                (*c).set_rect(x + padding, y + curr_y, btn_w, 28.0);
+                (*t).set_rect(x + padding + btn_w + 12.0, y + curr_y, btn_w, 28.0);
+                curr_y += 28.0 + 24.0;
             }
 
             // --- SECTION 2: Dimensions ---
             if let (Some(w_sp), Some(h_sp)) = (width_spin, height_spin) {
-                let spin_w = (content_w - 10.0) / 2.0;
-                (*w_sp).set_rect(x + padding, curr_y, spin_w, 20.0);
-                (*h_sp).set_rect(x + padding + spin_w + 10.0, curr_y, spin_w, 20.0);
-                curr_y += 20.0 + 12.0;
+                let spin_w = (content_w - 12.0) / 2.0;
+                (*w_sp).set_rect(x + padding, y + curr_y, spin_w, 20.0);
+                (*h_sp).set_rect(x + padding + spin_w + 12.0, y + curr_y, spin_w, 20.0);
+                curr_y += 20.0 + 24.0;
             }
 
-            // --- SECTION 3: Dropdowns ---
+            // --- SECTION 3: Window Type & Shape ---
+            let mut has_dd = false;
             if let Some(t_dd) = type_dd {
-                (*t_dd).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 12.0;
+                (*t_dd).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 16.0;
+                has_dd = true;
             }
             if let Some(s_dd) = shape_dd {
-                (*s_dd).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 12.0;
+                (*s_dd).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 24.0;
+                has_dd = true;
+            }
+            if !has_dd {
+                // Keep spacing consistent
             }
 
             // --- SECTION 4: Opacity & Transparency ---
             if let Some(op_t) = opacity_toggle {
-                (*op_t).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 10.0;
+                (*op_t).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 16.0;
             }
             if let Some(sl_lbl) = slider_label {
-                (*sl_lbl).set_rect(x + padding, curr_y, content_w, 12.0);
-                curr_y += 12.0 + 2.0;
+                (*sl_lbl).set_rect(x + padding, y + curr_y, content_w, 12.0);
+                curr_y += 12.0 + 4.0;
             }
             if let Some(sl) = slider {
-                (*sl).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 12.0;
+                (*sl).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 24.0;
             }
 
             // --- SECTION 5: Window Elements ---
             if let Some(w_s) = win_sec {
-                (*w_s).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 10.0;
+                (*w_s).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 16.0;
             }
             let toggles = [backplate_toggle, menubar_toggle, statusbar_toggle];
             let active_toggles: Vec<*mut dyn Element> = toggles.iter().filter_map(|&t| t).collect();
             if !active_toggles.is_empty() {
-                let t_w = (content_w - (active_toggles.len() as f32 - 1.0) * 8.0) / active_toggles.len() as f32;
+                let t_w = (content_w - (active_toggles.len() as f32 - 1.0) * 10.0) / active_toggles.len() as f32;
                 for (idx, &t) in active_toggles.iter().enumerate() {
-                    (*t).set_rect(x + padding + idx as f32 * (t_w + 8.0), curr_y, t_w, 20.0);
+                    (*t).set_rect(x + padding + idx as f32 * (t_w + 10.0), y + curr_y, t_w, 20.0);
                 }
-                curr_y += 20.0 + 12.0;
+                curr_y += 20.0 + 24.0;
             }
 
             // --- SECTION 6: Border & Bevel ---
             if let Some(b_s) = border_sec {
-                (*b_s).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 10.0;
+                (*b_s).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 16.0;
             }
             if let (Some(b_tg), Some(bev_tg)) = (border_toggle, bevel_toggle) {
-                let tg_w = (content_w - 10.0) / 2.0;
-                (*b_tg).set_rect(x + padding, curr_y, tg_w, 20.0);
-                (*bev_tg).set_rect(x + padding + tg_w + 10.0, curr_y, tg_w, 20.0);
-                curr_y += 20.0 + 10.0;
+                let tg_w = (content_w - 12.0) / 2.0;
+                (*b_tg).set_rect(x + padding, y + curr_y, tg_w, 20.0);
+                (*bev_tg).set_rect(x + padding + tg_w + 12.0, y + curr_y, tg_w, 20.0);
+                curr_y += 20.0 + 16.0;
             }
             if let Some(bw_sp) = border_width_spin {
-                (*bw_sp).set_rect(x + padding, curr_y, content_w, 20.0);
-                curr_y += 20.0 + 10.0;
+                (*bw_sp).set_rect(x + padding, y + curr_y, content_w, 20.0);
+                curr_y += 20.0 + 16.0;
             }
             if let Some(bs_btn) = bevel_shape_btn {
-                (*bs_btn).set_rect(x + padding, curr_y, content_w, 28.0);
+                (*bs_btn).set_rect(x + padding, y + curr_y, content_w, 28.0);
+                curr_y += 28.0;
+            }
+
+            curr_y += padding;
+            self.scroll_box.update_bounds(curr_y, y, h);
+        }
+    }
+
+    fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+        let mut quads = Vec::new();
+        let (x, y, w, h) = self.rect();
+
+        // 1. Background
+        quads.push((x, y, w, h, 0.0, self.color(), (false, false, false, false)));
+
+        // 2. Borders
+        let border_color = colors::ramp_border_color();
+        quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
+        quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
+        quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+        quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+
+        // 3. Child elements clipped to viewport bounds
+        let scroll_y = self.scroll_box.scroll_y;
+        let y_start = y;
+        let y_end = y + h;
+
+        unsafe {
+            for child_ptr in &self.children {
+                for (qx, qy, qw, qh, qr, qc, qcorners) in (**child_ptr).all_rounded_quads(ctx) {
+                    let qy_shifted = qy - scroll_y;
+                    let qy_top = qy_shifted;
+                    let qy_bottom = qy_shifted + qh;
+                    if qy_bottom > y_start && qy_top < y_end {
+                        let visible_top = qy_top.max(y_start);
+                        let visible_bottom = qy_bottom.min(y_end);
+                        let visible_h = visible_bottom - visible_top;
+                        if visible_h > 0.0 {
+                            let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
+                                0.0
+                            } else {
+                                qr
+                            };
+                            quads.push((qx, visible_top, qw, visible_h, radii_adjusted, qc, qcorners));
+                        }
+                    }
+                }
             }
         }
+
+        // 4. Scrollbar
+        for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
+            quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
+        }
+
+        quads
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
         let (x, y, w, h) = self.rect();
 
-        // 1. Draw main container background
         quads.push((x, y, w, h, self.color()));
 
-        // 2. Draw border
         let border_color = colors::ramp_border_color();
-        quads.push((x, y, w, 1.0, border_color));             // Top
-        quads.push((x, y + h - 1.0, w, 1.0, border_color));     // Bottom
-        quads.push((x, y, 1.0, h, border_color));             // Left
-        quads.push((x + w - 1.0, y, 1.0, h, border_color));     // Right
+        quads.push((x, y, w, 1.0, border_color));
+        quads.push((x, y + h - 1.0, w, 1.0, border_color));
+        quads.push((x, y, 1.0, h, border_color));
+        quads.push((x + w - 1.0, y, 1.0, h, border_color));
+
+        let scroll_y = self.scroll_box.scroll_y;
+        let y_start = y;
+        let y_end = y + h;
+        let ctx_dummy = crate::context::UiContext::new();
+
+        unsafe {
+            for child_ptr in &self.children {
+                for (qx, qy, qw, qh, qc) in (**child_ptr).all_quads(&ctx_dummy) {
+                    let qy_shifted = qy - scroll_y;
+                    let qy_top = qy_shifted;
+                    let qy_bottom = qy_shifted + qh;
+                    if qy_bottom > y_start && qy_top < y_end {
+                        let visible_top = qy_top.max(y_start);
+                        let visible_bottom = qy_bottom.min(y_end);
+                        let visible_h = visible_bottom - visible_top;
+                        if visible_h > 0.0 {
+                            quads.push((qx, visible_top, qw, visible_h, qc));
+                        }
+                    }
+                }
+            }
+        }
 
+        quads.extend(self.scroll_box.extra_quads());
         quads
     }
 
     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        let scroll_y = self.scroll_box.scroll_y;
         unsafe {
             for child_ptr in &self.children {
-                if let Some(r) = (**child_ptr).popover_rect() {
-                    return Some(r);
+                let child = &mut **child_ptr;
+                let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+                if let Some(b) = child.base_mut() {
+                    b.y = old_y - scroll_y;
+                }
+                let res = child.popover_rect();
+                if let Some(b) = child.base_mut() {
+                    b.y = old_y;
+                }
+                if res.is_some() {
+                    return res;
                 }
             }
         }
@@ -244,18 +344,40 @@ impl Element for ControlPanel {
     }
 
     fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+        let scroll_y = self.scroll_box.scroll_y;
         unsafe {
             for child_ptr in &self.children {
-                (**child_ptr).render_popover(pc);
+                if (**child_ptr).popover_rect().is_some() {
+                    let child = &mut **child_ptr;
+                    let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+                    if let Some(b) = child.base_mut() {
+                        b.y = old_y - scroll_y;
+                    }
+                    child.render_popover(pc);
+                    if let Some(b) = child.base_mut() {
+                        b.y = old_y;
+                    }
+                }
             }
         }
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if self.scroll_box.mouse_input(button, state, px, py, ctx) {
+            return true;
+        }
+
+        let scroll_y = self.scroll_box.scroll_y;
+        let py_translated = py + scroll_y;
+        let (_x, y, _w, h) = self.rect();
+
         unsafe {
             for child_ptr in &self.children {
-                if (**child_ptr).mouse_input(button, state, px, py, ctx) {
-                    return true;
+                // Only dispatch if the click is physically inside the ControlPanel viewport
+                if py >= y && py <= y + h {
+                    if (**child_ptr).mouse_input(button, state, px, py_translated, ctx) {
+                        return true;
+                    }
                 }
             }
         }
@@ -263,10 +385,13 @@ impl Element for ControlPanel {
     }
 
     fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        let mut changed = false;
+        let mut changed = self.scroll_box.cursor_moved(px, py, ctx);
+
+        let scroll_y = self.scroll_box.scroll_y;
+        let py_translated = py + scroll_y;
         unsafe {
             for child_ptr in &self.children {
-                if (**child_ptr).cursor_moved(px, py, ctx) {
+                if (**child_ptr).cursor_moved(px, py_translated, ctx) {
                     changed = true;
                 }
             }
@@ -274,11 +399,45 @@ impl Element for ControlPanel {
         changed
     }
 
+    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if self.scroll_box.mouse_wheel(delta, px, py, ctx) {
+            return true;
+        }
+
+        let scroll_y = self.scroll_box.scroll_y;
+        let py_translated = py + scroll_y;
+        unsafe {
+            for child_ptr in &self.children {
+                if (**child_ptr).mouse_wheel(delta, px, py_translated, ctx) {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
     fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
         let mut labels = Vec::new();
+        let scroll_y = self.scroll_box.scroll_y;
+        let (x, y, w, h) = self.rect();
         unsafe {
             for child_ptr in &self.children {
-                labels.extend((**child_ptr).text_labels_with_font_and_bounds(ctx));
+                let mut child_labels = (**child_ptr).text_labels_with_font_and_bounds(ctx);
+                for (label, _font, bounds) in &mut child_labels {
+                    label.y -= scroll_y;
+                    
+                    let new_bounds = if let Some([l, t, r, b]) = *bounds {
+                        let nl = l.max(x);
+                        let nt = (t - scroll_y).max(y);
+                        let nr = r.min(x + w);
+                        let nb = (b - scroll_y).min(y + h);
+                        Some([nl, nt, nr, nb])
+                    } else {
+                        Some([x, y, x + w, y + h])
+                    };
+                    *bounds = new_bounds;
+                }
+                labels.extend(child_labels);
             }
         }
         labels