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

commit1cd93946aa556d010cd1d807dbaf83208b76ccf1
parent12696b9c2b
authorLucas Galante <[email protected]>
date2026-06-16 22:27
Implement standalone widget styling support for MultiControl and add Dropdown corner radius config and spinbox control

 src/backend/window_runner.rs      |  29 ++++++++++
 src/layout.rs                     | 117 ++++++++++++++++++++++++++++++++++++++
 src/main.rs                       |  76 +++++++++++++++++++++++--
 src/widget/input/checkbox.rs      |   4 ++
 src/widget/input/dropdown.rs      |  13 +++++
 src/widget/input/multi_control.rs |  47 ++++++++++-----
 6 files changed, 269 insertions(+), 17 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b8b0472..c185a09 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -592,6 +592,35 @@ fn get_child_widget_for_quad<'a>(
             }
         }
     }
+    if let Some(mc) = w.as_any().downcast_ref::<crate::widget::input::MultiControl>() {
+        let (bx, by, bw, bh) = mc.add_button.rect();
+        if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+            return &mc.add_button;
+        }
+        for row in &mc.rows {
+            let (kx, ky, kw, kh) = row.key_input.rect();
+            if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+                return &row.key_input;
+            }
+            let (tx, ty, tw, th) = row.type_dropdown.rect();
+            if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+                return &row.type_dropdown;
+            }
+            let (rx, ry, rw, rh) = row.remove_button.rect();
+            if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+                return &row.remove_button;
+            }
+            let (vx, vy, vw, vh) = row.value_widget.rect();
+            if qx >= vx - 0.1 && qx + qw <= vx + vw + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + vh + 0.1 {
+                return match &row.value_widget {
+                    crate::widget::input::InstancedWidget::TextBox(w) => w,
+                    crate::widget::input::InstancedWidget::Spinbox(w) => w,
+                    crate::widget::input::InstancedWidget::Toggle(w) => w,
+                    crate::widget::input::InstancedWidget::Slider(w) => w,
+                };
+            }
+        }
+    }
     w
 }
 
diff --git a/src/layout.rs b/src/layout.rs
index d03e17b..a8eb1f1 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -57,6 +57,7 @@ static BUTTON_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 static SPINBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 static TEXTBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 static FONT_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
+static DROPDOWN_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 
 /// Standard line height multiplier for text layout in cce-ui.
 pub const TEXT_LINE_HEIGHT_MULTIPLIER: f32 = 1.4;
@@ -181,6 +182,15 @@ pub fn reload_config() {
                     }
                 }
             }
+            if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                let val_str = rest.trim_end_matches('"').trim();
+                if let Ok(val) = val_str.parse::<f32>() {
+                    if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+                        *lock = val;
+                    }
+                }
+            }
             if let Some(rest) = trimmed.strip_prefix("toggle_height") {
                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
                 let val_str = rest.trim_end_matches('"').trim();
@@ -1048,6 +1058,34 @@ pub fn set_font_selector_corner_radius(radius: f32) {
     }
 }
 
+pub fn dropdown_corner_radius() -> f32 {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let val_str = rest.trim_end_matches('"').trim();
+                    if let Ok(val) = val_str.parse::<f32>() {
+                        if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+                            *lock = val;
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *DROPDOWN_CORNER_RADIUS.read().unwrap()
+}
+
+pub fn set_dropdown_corner_radius(radius: f32) {
+    if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+        *lock = radius;
+    }
+}
+
 pub fn color_selector_preview_margin() -> f32 {
     use std::sync::Once;
     static INIT: Once = Once::new();
@@ -1370,6 +1408,67 @@ impl RenderTarget for PopoverCollector {
     }
 }
 
+fn get_multicontrol_sub_widget_info(
+    mc: &crate::widget::input::MultiControl,
+    qx: f32, qy: f32, qw: f32, qh: f32,
+) -> Option<((bool, bool, bool, bool), f32, (f32, f32, f32, f32), f32)> {
+    // Check add_button
+    let (bx, by, bw, bh) = mc.add_button.rect();
+    if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+        return Some((
+            mc.add_button.rounded_corners(),
+            mc.add_button.corner_radius(),
+            (bx, by, bw, bh),
+            crate::widget::label_offset(&mc.add_button),
+        ));
+    }
+    // Check rows
+    for row in &mc.rows {
+        // key_input
+        let (kx, ky, kw, kh) = row.key_input.rect();
+        if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+            return Some((
+                row.key_input.rounded_corners(),
+                row.key_input.corner_radius(),
+                (kx, ky, kw, kh),
+                crate::widget::label_offset(&row.key_input),
+            ));
+        }
+        // type_dropdown
+        let (tx, ty, tw, th) = row.type_dropdown.rect();
+        if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+            return Some((
+                row.type_dropdown.rounded_corners(),
+                row.type_dropdown.corner_radius(),
+                (tx, ty, tw, th),
+                crate::widget::label_offset(&row.type_dropdown),
+            ));
+        }
+        // remove_button
+        let (rx, ry, rw, rh) = row.remove_button.rect();
+        if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+            return Some((
+                row.remove_button.rounded_corners(),
+                row.remove_button.corner_radius(),
+                (rx, ry, rw, rh),
+                crate::widget::label_offset(&row.remove_button),
+            ));
+        }
+        // value_widget
+        let (vx, vy, vw, vh) = row.value_widget.rect();
+        if qx >= vx - 0.1 && qx + qw <= vx + vw + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + vh + 0.1 {
+            let (corners, radius, label_offset) = match &row.value_widget {
+                crate::widget::input::InstancedWidget::TextBox(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+                crate::widget::input::InstancedWidget::Spinbox(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+                crate::widget::input::InstancedWidget::Toggle(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+                crate::widget::input::InstancedWidget::Slider(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+            };
+            return Some((corners, radius, (vx, vy, vw, vh), label_offset));
+        }
+    }
+    None
+}
+
 pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
     w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
     let corners = w.rounded_corners();
@@ -1384,6 +1483,24 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
     whh -= top_room;
 
     for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
+        let mut corners = corners;
+        let mut r = r;
+        let mut wx = wx;
+        let mut wy = wy;
+        let mut www = www;
+        let mut whh = whh;
+
+        if let Some(mc) = w.as_any().downcast_ref::<crate::widget::input::MultiControl>() {
+            if let Some((sub_corners, sub_radius, (sub_x, sub_y, sub_w, sub_h), sub_label_offset)) = get_multicontrol_sub_widget_info(mc, qx, qy, qw, qh) {
+                corners = sub_corners;
+                r = sub_radius;
+                wx = sub_x;
+                wy = sub_y + sub_label_offset;
+                www = sub_w;
+                whh = sub_h - sub_label_offset;
+            }
+        }
+
         if r <= 0.1 || corners == (false, false, false, false) {
             pc.rect_with_radius_corners(qc, qx, qy, qw, qh, 0.0, (false, false, false, false));
             continue;
diff --git a/src/main.rs b/src/main.rs
index 9fa802a..c592949 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -186,13 +186,81 @@ fn extra_quad_vertices(
     sw: f32, sh: f32,
     qc: [f32; 4],
 ) -> Vec<Vertex> {
-    let corners = w.rounded_corners();
+    let mut corners = w.rounded_corners();
+    let mut r = w.corner_radius();
+    let (mut wx, mut wy, mut ww, mut wh) = w.rect();
+    let mut top_room = cce_ui::widget::label_offset(w);
+
+    if let Some(mc) = w.as_any().downcast_ref::<cce_ui::widget::input::MultiControl>() {
+        // check add_button
+        let (bx, by, b_w, b_h) = mc.add_button.rect();
+        if qx >= bx - 0.1 && qx + qw <= bx + b_w + 0.1 && qy >= by - 0.1 && qy + qh <= by + b_h + 0.1 {
+            corners = mc.add_button.rounded_corners();
+            r = mc.add_button.corner_radius();
+            wx = bx;
+            wy = by;
+            ww = b_w;
+            wh = b_h;
+            top_room = cce_ui::widget::label_offset(&mc.add_button);
+        }
+        for row in &mc.rows {
+            // key_input
+            let (kx, ky, kw, kh) = row.key_input.rect();
+            if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+                corners = row.key_input.rounded_corners();
+                r = row.key_input.corner_radius();
+                wx = kx;
+                wy = ky;
+                ww = kw;
+                wh = kh;
+                top_room = cce_ui::widget::label_offset(&row.key_input);
+            }
+            // type_dropdown
+            let (tx, ty, tw, th) = row.type_dropdown.rect();
+            if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+                corners = row.type_dropdown.rounded_corners();
+                r = row.type_dropdown.corner_radius();
+                wx = tx;
+                wy = ty;
+                ww = tw;
+                wh = th;
+                top_room = cce_ui::widget::label_offset(&row.type_dropdown);
+            }
+            // remove_button
+            let (rx, ry, rw, rh) = row.remove_button.rect();
+            if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+                corners = row.remove_button.rounded_corners();
+                r = row.remove_button.corner_radius();
+                wx = rx;
+                wy = ry;
+                ww = rw;
+                wh = rh;
+                top_room = cce_ui::widget::label_offset(&row.remove_button);
+            }
+            // value_widget
+            let (vx, vy, v_w, v_h) = row.value_widget.rect();
+            if qx >= vx - 0.1 && qx + qw <= vx + v_w + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + v_h + 0.1 {
+                let (sub_corners, sub_radius, sub_top_room) = match &row.value_widget {
+                    cce_ui::widget::input::InstancedWidget::TextBox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+                    cce_ui::widget::input::InstancedWidget::Spinbox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+                    cce_ui::widget::input::InstancedWidget::Toggle(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+                    cce_ui::widget::input::InstancedWidget::Slider(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+                };
+                corners = sub_corners;
+                r = sub_radius;
+                wx = vx;
+                wy = vy;
+                ww = v_w;
+                wh = v_h;
+                top_room = sub_top_room;
+            }
+        }
+    }
+
     if corners == (false, false, false, false) {
         return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
     }
 
-    let (wx, mut wy, ww, mut wh) = w.rect();
-    let top_room = cce_ui::widget::label_offset(w);
     wy += top_room;
     wh -= top_room;
     let extra_corners = (
@@ -202,7 +270,7 @@ fn extra_quad_vertices(
         corners.3 && qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5,
     );
 
-    rounded_rect_vertices_corners(qx, qy, qw, qh, w.corner_radius(), sw, sh, qc, extra_corners)
+    rounded_rect_vertices_corners(qx, qy, qw, qh, r, sw, sh, qc, extra_corners)
 }
 
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 179738c..a0aba44 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -206,6 +206,10 @@ impl Toggle {
 impl Element for Toggle {
     crate::impl_widget_base!(Toggle);
 
+    fn preferred_height(&self) -> Option<f32> {
+        Some(crate::layout::toggle_height())
+    }
+
     fn get_value_string(&self) -> Option<String> {
         Some(self.toggled.to_string())
     }
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 153b4cb..789aa3c 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -166,6 +166,19 @@ impl Element for Dropdown {
         Some(crate::layout::dropdown_height())
     }
 
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+        let r = crate::layout::dropdown_corner_radius();
+        if r > 0.0 {
+            (true, true, true, true)
+        } else {
+            (false, false, false, false)
+        }
+    }
+
+    fn corner_radius(&self) -> f32 {
+        crate::layout::dropdown_corner_radius()
+    }
+
     fn widget_font(&self) -> Option<String> {
         Some(self.font_family.clone())
     }
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index f47c600..bb2978a 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -47,6 +47,15 @@ impl InstancedWidget {
         }
     }
 
+    pub fn preferred_height(&self) -> Option<f32> {
+        match self {
+            InstancedWidget::TextBox(w) => w.preferred_height(),
+            InstancedWidget::Spinbox(w) => w.preferred_height(),
+            InstancedWidget::Toggle(w) => w.preferred_height(),
+            InstancedWidget::Slider(w) => w.preferred_height(),
+        }
+    }
+
     pub fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
         match self {
             InstancedWidget::TextBox(w) => w.layout(origin, constraints, ctx),
@@ -262,16 +271,24 @@ impl Element for MultiControl {
     crate::impl_widget_base!(MultiControl);
 
     fn preferred_height(&self) -> Option<f32> {
-        let line_h = 44.0;
         let gap_between_lines = 4.0;
         let gap_between_rows = 12.0;
-        let row_h = 2.0 * line_h + gap_between_lines;
         let add_btn_h = 36.0;
-        let total_h = if self.rows.is_empty() {
-            add_btn_h + 16.0
+
+        let key_h = crate::layout::textbox_height();
+        let type_h = crate::layout::dropdown_height();
+        let line1_h = key_h.max(type_h);
+
+        let mut total_h = 0.0;
+        if self.rows.is_empty() {
+            total_h = add_btn_h + 16.0;
         } else {
-            self.rows.len() as f32 * row_h + (self.rows.len() - 1) as f32 * gap_between_rows + add_btn_h + 24.0
-        };
+            for row in &self.rows {
+                let line2_h = row.value_widget.preferred_height().unwrap_or(44.0);
+                total_h += line1_h + gap_between_lines + line2_h;
+            }
+            total_h += (self.rows.len() - 1) as f32 * gap_between_rows + add_btn_h + 28.0;
+        }
         Some(total_h)
     }
 
@@ -285,10 +302,13 @@ impl Element for MultiControl {
 
         let pad_x = 8.0;
         let pad_y = 8.0;
-        let line_h = 44.0;
         let gap_between_lines = 4.0;
         let gap_between_rows = 12.0;
 
+        let key_h = crate::layout::textbox_height();
+        let type_h = crate::layout::dropdown_height();
+        let line1_h = key_h.max(type_h);
+
         let usable_w = (size.width - 2.0 * pad_x).max(1.0);
         let gap_x = 6.0;
         let top_usable_w = usable_w - 2.0 * gap_x;
@@ -304,18 +324,19 @@ impl Element for MultiControl {
         for row in &mut self.rows {
             // Line 1: Label, Type, Remove
             let key_x = origin.x + pad_x;
-            row.key_input.layout(Point { x: key_x, y: curr_y }, LayoutConstraints::new(key_w, key_w, line_h, line_h), ctx);
+            row.key_input.layout(Point { x: key_x, y: curr_y }, LayoutConstraints::new(key_w, key_w, line1_h, line1_h), ctx);
 
             let type_x = key_x + key_w + gap_x;
-            row.type_dropdown.layout(Point { x: type_x, y: curr_y }, LayoutConstraints::new(type_w, type_w, line_h, line_h), ctx);
+            row.type_dropdown.layout(Point { x: type_x, y: curr_y }, LayoutConstraints::new(type_w, type_w, line1_h, line1_h), ctx);
 
             let remove_x = type_x + type_w + gap_x;
-            row.remove_button.layout(Point { x: remove_x, y: curr_y }, LayoutConstraints::new(remove_w, remove_w, line_h, line_h), ctx);
+            row.remove_button.layout(Point { x: remove_x, y: curr_y }, LayoutConstraints::new(remove_w, remove_w, line1_h, line1_h), ctx);
 
             // Line 2: Value Control Widget
-            let value_y = curr_y + line_h + gap_between_lines;
+            let line2_h = row.value_widget.preferred_height().unwrap_or(44.0);
+            let value_y = curr_y + line1_h + gap_between_lines;
             let value_x = origin.x + pad_x;
-            row.value_widget.layout(Point { x: value_x, y: value_y }, LayoutConstraints::new(usable_w, usable_w, line_h, line_h), ctx);
+            row.value_widget.layout(Point { x: value_x, y: value_y }, LayoutConstraints::new(usable_w, usable_w, line2_h, line2_h), ctx);
 
             link_child(self_ptr, self_id, &mut row.key_input, ctx);
             link_child(self_ptr, self_id, &mut row.type_dropdown, ctx);
@@ -327,7 +348,7 @@ impl Element for MultiControl {
             }
             link_child(self_ptr, self_id, &mut row.remove_button, ctx);
 
-            curr_y += 2.0 * line_h + gap_between_lines + gap_between_rows;
+            curr_y += line1_h + gap_between_lines + line2_h + gap_between_rows;
         }
 
         // Lay out add button