git.lucas.co / cce-gallery
widget gallery and compositor test bench

commitf25e2c05135c7a25238f1986e758dd17a9d3117d
parent4042daa944
authorLucas Galante <[email protected]>
date2026-07-04 20:09
Support Bevel Shape ColorRamp editor and add float Ramp widgets to Controls page

 src/main.rs | 278 +++++++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 221 insertions(+), 57 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c2d9108..f53a140 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use cce_ui::widget::{
     Button, Checkbox, ContentBg, Dropdown, Label, Paginator, Panel, ProgressBar, RangeSlider, Slider, Spinbox, StatusBar,
     TextLabel, Toggle, Element, Trackpad, hover_animation, TextBox, Plate, CornerRadii, Backplate, MenuBar, SectionContainer,
-    Ramp,
+    Ramp, ColorRamp, ColorRampKey,
 };
 use cce_ui::engine::{Vertex, quad_vertices, push_rounded_rect_vertices_corners};
 
@@ -97,6 +97,85 @@ struct State {
     border_bevel: bool,
     child_type: Option<String>,
     ui_context: cce_ui::context::UiContext,
+    bevel_ramp: Vec<ColorRampKey>,
+    last_ramp_mod: Option<std::time::SystemTime>,
+}
+
+fn save_bevel_ramp(keys: &[ColorRampKey]) {
+    let mut s = String::new();
+    s.push_str("keys {\n");
+    for k in keys {
+        s.push_str(&format!("    key pos={} r={} g={} b={}\n", k.pos, k.color[0], k.color[1], k.color[2]));
+    }
+    s.push_str("}\n");
+    let path = cce_ui::config::get_config_path().parent().unwrap().join("bevel_ramp.kdl");
+    let _ = std::fs::write(path, s);
+}
+
+fn load_bevel_ramp() -> Vec<ColorRampKey> {
+    let path = cce_ui::config::get_config_path().parent().unwrap().join("bevel_ramp.kdl");
+    if let Ok(content) = std::fs::read_to_string(path) {
+        let mut keys = Vec::new();
+        for line in content.lines() {
+            let line = line.trim();
+            if line.starts_with("key ") {
+                let mut pos = 0.0;
+                let mut r = 0.0;
+                let mut g = 0.0;
+                let mut b = 0.0;
+                for part in line.split_whitespace() {
+                    if let Some(rest) = part.strip_prefix("pos=") {
+                        pos = rest.parse().unwrap_or(0.0);
+                    } else if let Some(rest) = part.strip_prefix("r=") {
+                        r = rest.parse().unwrap_or(0.0);
+                    } else if let Some(rest) = part.strip_prefix("g=") {
+                        g = rest.parse().unwrap_or(0.0);
+                    } else if let Some(rest) = part.strip_prefix("b=") {
+                        b = rest.parse().unwrap_or(0.0);
+                    }
+                }
+                keys.push(ColorRampKey { pos, color: [r, g, b] });
+            }
+        }
+        if !keys.is_empty() {
+            keys.sort_by(|a, b| a.pos.partial_cmp(&b.pos).unwrap());
+            return keys;
+        }
+    }
+    // Default keys
+    vec![
+        ColorRampKey { pos: 0.0, color: [0.0, 0.0, 0.0] },
+        ColorRampKey { pos: 1.0, color: [1.0, 1.0, 1.0] },
+    ]
+}
+
+fn interpolate_ramp_color(keys: &[ColorRampKey], u: f32) -> [f32; 3] {
+    if keys.is_empty() {
+        return [0.0, 0.0, 0.0];
+    }
+    if u <= keys[0].pos {
+        return keys[0].color;
+    }
+    if u >= keys[keys.len() - 1].pos {
+        return keys[keys.len() - 1].color;
+    }
+    for i in 0..keys.len() - 1 {
+        let k1 = &keys[i];
+        let k2 = &keys[i+1];
+        if u >= k1.pos && u <= k2.pos {
+            let range = k2.pos - k1.pos;
+            if range.abs() < 0.0001 {
+                return k1.color;
+            }
+            let w = (u - k1.pos) / range;
+            return [
+                k1.color[0] * (1.0 - w) + k2.color[0] * w,
+                k1.color[1] * (1.0 - w) + k2.color[1] * w,
+                k1.color[2] * (1.0 - w) + k2.color[2] * w,
+            ];
+        }
+    }
+    keys[0].color
 }
 
 impl State {
@@ -262,7 +341,19 @@ impl State {
             } else {
                 Box::new(ContentBg::new())
             };
-            if child_type.as_deref() == Some("Ramp") {
+            if child_type.as_deref() == Some("ColorRamp") {
+                vec![
+                    bg,                      // 0
+                    Box::new({
+                        let mut ramp = ColorRamp::new();
+                        ramp.keys = load_bevel_ramp();
+                        ramp
+                    }), // 1
+                    Box::new(Button::new(0.0, 0.0, 100.0, 35.0).with_label("Close")), // 2
+                    Box::new(Label::new("").with_font_size(12.0)), // 3
+                    Box::new(Label::new("").with_font_size(12.0)), // 4
+                ]
+            } else if child_type.as_deref() == Some("Ramp") {
                 vec![
                     bg,                      // 0
                     Box::new(Ramp::new()), // 1
@@ -381,7 +472,10 @@ cascades in cce."
                 Box::new(Spinbox::new(1, 1, 10, 1).with_label("Border Width")), // 43 Spinbox: Border Width
                 Box::new(SectionContainer::new("Window Elements")), // 44 Section: Window Elements
                 Box::new(Dropdown::new(vec!["Controls".to_string(), "Windows".to_string(), "XDG".to_string()], 0).with_open_upward(true)), // 45 Dropdown: Page selector
-                Box::new(Button::new(0.0, 0.0, 120.0, 28.0).with_label("Ramp Control...")), // 46 Button: Ramp Control
+                Box::new(Button::new(0.0, 0.0, 120.0, 28.0).with_label("Color Ramp...")), // 46 Button: Color Ramp
+                Box::new(Button::new(0.0, 0.0, 120.0, 28.0).with_label("Bevel Shape...")), // 47 Button: Bevel Shape
+                Box::new(Ramp::new()), // 48 Ramp: Controls page ramp
+                Box::new(Button::new(0.0, 0.0, 120.0, 28.0).with_label("Ramp...")), // 49 Button: Ramp
             ]
         };
 
@@ -438,6 +532,8 @@ cascades in cce."
             border_bevel,
             child_type: child_type.clone(),
             ui_context: cce_ui::context::UiContext::new(),
+            bevel_ramp: load_bevel_ramp(),
+            last_ramp_mod: None,
         };
 
         cce_ui::scale::set_scale_factor(scale as f32);
@@ -457,8 +553,8 @@ cascades in cce."
         }
         match index {
             0..=2 | 45 => true,
-            3..=13 | 30 | 31 | 36 | 37 | 46 => self.current_page == Page::Controls,
-            14..=29 | 38..=44 => self.current_page == Page::Windows,
+            3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => self.current_page == Page::Controls,
+            14..=29 | 38..=44 | 47 => self.current_page == Page::Windows,
             32..=35 => self.current_page == Page::Xdg,
             _ => false,
         }
@@ -713,41 +809,56 @@ cascades in cce."
                         wx, wy, ww, wh, radii, t, sw, sh, border_color, [0.0, 0.0, -1.0], &mut verts
                     );
                     if self.border_bevel {
-                        let light_color = [
-                            (border_color[0] + 0.2).min(1.0),
-                            (border_color[1] + 0.2).min(1.0),
-                            (border_color[2] + 0.2).min(1.0),
-                            border_color[3]
-                        ];
-                        let dark_color = [
-                            (border_color[0] - 0.2).max(0.0),
-                            (border_color[1] - 0.2).max(0.0),
-                            (border_color[2] - 0.2).max(0.0),
-                            border_color[3]
-                        ];
-                        verts.extend(quad_vertices(wx + r, wy, ww - 2.0 * r, t, sw, sh, light_color));
-                        verts.extend(quad_vertices(wx, wy + r, t, wh - 2.0 * r, sw, sh, light_color));
-                        verts.extend(quad_vertices(wx + r, wy + wh - t, ww - 2.0 * r, t, sw, sh, dark_color));
-                        verts.extend(quad_vertices(wx + ww - t, wy + r, t, wh - 2.0 * r, sw, sh, dark_color));
+                        let slices = 5;
+                        let slice_w = t / slices as f32;
+                        for i in 0..slices {
+                            let u = i as f32 / (slices - 1).max(1) as f32;
+                            let col = interpolate_ramp_color(&self.bevel_ramp, u);
+                            let light_color = [
+                                (border_color[0] + 0.35 * col[0]).min(1.0),
+                                (border_color[1] + 0.35 * col[1]).min(1.0),
+                                (border_color[2] + 0.35 * col[2]).min(1.0),
+                                border_color[3]
+                            ];
+                            let dark_color = [
+                                (border_color[0] - 0.35 * col[0]).max(0.0),
+                                (border_color[1] - 0.35 * col[1]).max(0.0),
+                                (border_color[2] - 0.35 * col[2]).max(0.0),
+                                border_color[3]
+                            ];
+                            let offset = i as f32 * slice_w;
+                            let r_offset = (r - offset).max(0.0);
+                            verts.extend(quad_vertices(wx + r_offset, wy + offset, ww - 2.0 * r_offset, slice_w, sw, sh, light_color));
+                            verts.extend(quad_vertices(wx + offset, wy + r_offset, slice_w, wh - 2.0 * r_offset, sw, sh, light_color));
+                            verts.extend(quad_vertices(wx + r_offset, wy + wh - offset - slice_w, ww - 2.0 * r_offset, slice_w, sw, sh, dark_color));
+                            verts.extend(quad_vertices(wx + ww - offset - slice_w, wy + r_offset, slice_w, wh - 2.0 * r_offset, sw, sh, dark_color));
+                        }
                     }
                 } else {
                     if self.border_bevel {
-                        let light_color = [
-                            (border_color[0] + 0.2).min(1.0),
-                            (border_color[1] + 0.2).min(1.0),
-                            (border_color[2] + 0.2).min(1.0),
-                            border_color[3]
-                        ];
-                        let dark_color = [
-                            (border_color[0] - 0.2).max(0.0),
-                            (border_color[1] - 0.2).max(0.0),
-                            (border_color[2] - 0.2).max(0.0),
-                            border_color[3]
-                        ];
-                        verts.extend(quad_vertices(wx, wy, ww, t, sw, sh, light_color));
-                        verts.extend(quad_vertices(wx, wy, t, wh, sw, sh, light_color));
-                        verts.extend(quad_vertices(wx, wy + wh - t, ww, t, sw, sh, dark_color));
-                        verts.extend(quad_vertices(wx + ww - t, wy, t, wh, sw, sh, dark_color));
+                        let slices = 5;
+                        let slice_w = t / slices as f32;
+                        for i in 0..slices {
+                            let u = i as f32 / (slices - 1).max(1) as f32;
+                            let col = interpolate_ramp_color(&self.bevel_ramp, u);
+                            let light_color = [
+                                (border_color[0] + 0.35 * col[0]).min(1.0),
+                                (border_color[1] + 0.35 * col[1]).min(1.0),
+                                (border_color[2] + 0.35 * col[2]).min(1.0),
+                                border_color[3]
+                            ];
+                            let dark_color = [
+                                (border_color[0] - 0.35 * col[0]).max(0.0),
+                                (border_color[1] - 0.35 * col[1]).max(0.0),
+                                (border_color[2] - 0.35 * col[2]).max(0.0),
+                                border_color[3]
+                            ];
+                            let offset = i as f32 * slice_w;
+                            verts.extend(quad_vertices(wx + offset, wy + offset, ww - 2.0 * offset, slice_w, sw, sh, light_color));
+                            verts.extend(quad_vertices(wx + offset, wy + offset, slice_w, wh - 2.0 * offset, sw, sh, light_color));
+                            verts.extend(quad_vertices(wx + offset, wy + wh - offset - slice_w, ww - 2.0 * offset, slice_w, sw, sh, dark_color));
+                            verts.extend(quad_vertices(wx + ww - offset - slice_w, wy + offset, slice_w, wh - 2.0 * offset, sw, sh, dark_color));
+                        }
                     } else {
                         verts.extend(quad_vertices(wx, wy, ww, t, sw, sh, border_color));
                         verts.extend(quad_vertices(wx, wy + wh - t, ww, t, sw, sh, border_color));
@@ -834,8 +945,8 @@ cascades in cce."
             } else {
                 match index {
                     0..=2 | 45 => true,
-                    3..=13 | 30 | 31 | 36 | 37 | 46 => current_page == Page::Controls,
-                    14..=29 | 38..=44 => current_page == Page::Windows,
+                    3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => current_page == Page::Controls,
+                    14..=29 | 38..=44 | 47 => current_page == Page::Windows,
                     32..=35 => current_page == Page::Xdg,
                     _ => false,
                 }
@@ -1079,6 +1190,18 @@ cascades in cce."
 
     fn tick(&mut self, dt: f32) -> bool {
         let mut changed = false;
+        if !self.is_child {
+            let path = cce_ui::config::get_config_path().parent().unwrap().join("bevel_ramp.kdl");
+            if let Ok(metadata) = std::fs::metadata(&path) {
+                if let Ok(mod_time) = metadata.modified() {
+                    if Some(mod_time) != self.last_ramp_mod {
+                        self.last_ramp_mod = Some(mod_time);
+                        self.bevel_ramp = load_bevel_ramp();
+                        changed = true;
+                    }
+                }
+            }
+        }
         if hover_animation::tick(dt) {
             changed = true;
         }
@@ -1097,8 +1220,8 @@ cascades in cce."
             } else {
                 match index {
                     0..=2 | 45 => true,
-                    3..=13 | 30 | 31 | 36 | 37 | 46 => current_page == Page::Controls,
-                    14..=29 | 38..=44 => current_page == Page::Windows,
+                    3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => current_page == Page::Controls,
+                    14..=29 | 38..=44 | 47 => current_page == Page::Windows,
                     32..=35 => current_page == Page::Xdg,
                     _ => false,
                 }
@@ -1108,6 +1231,11 @@ cascades in cce."
             if is_visible(i) {
                 if w.tick(dt, &mut self.ui_context) {
                     changed = true;
+                    if self.is_child && self.child_type.as_deref() == Some("ColorRamp") && i == 1 {
+                        if let Some(ramp) = w.as_any().downcast_ref::<ColorRamp>() {
+                            save_bevel_ramp(&ramp.keys);
+                        }
+                    }
                 }
             }
         }
@@ -1124,6 +1252,7 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
     let tgh = cce_ui::layout::toggle_height();
     let slh = cce_ui::layout::slider_height();
     let bh = cce_ui::layout::button_height();
+    let ddh = cce_ui::layout::dropdown_height();
 
     // Dynamic calculations for Controls page layout
     let mut ctrl_y = 60.0;
@@ -1134,10 +1263,11 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
     let ctrl_verify_layout_pos = (base_x + 300.0, ctrl_y, 140.0, bh);
     ctrl_y += bh + 20.0;
     
-    // Row 2: diagnostics & reset & ramp
+    // Row 2: diagnostics & reset & color ramp & ramp button
     let ctrl_diagnostics_pos = (base_x, ctrl_y, 140.0, bh);
     let ctrl_reset_pos = (base_x + 150.0, ctrl_y, 140.0, bh);
-    let ctrl_ramp_pos = (base_x + 300.0, ctrl_y, 140.0, bh);
+    let ctrl_color_ramp_btn_pos = (base_x + 300.0, ctrl_y, 140.0, bh);
+    let ctrl_ramp_btn_pos = (base_x + 450.0, ctrl_y, 140.0, bh);
     ctrl_y += bh + 20.0;
     
     // Row 3: checkbox, toggle, progress_bar
@@ -1151,6 +1281,10 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
     let row4_h = sph.max(slh);
     let ctrl_slider_pos = (base_x, ctrl_y + (row4_h - slh)/2.0, 300.0, slh);
     let ctrl_spinbox_pos = (base_x + 330.0, ctrl_y + (row4_h - sph)/2.0, 120.0, sph);
+    ctrl_y += row4_h + 20.0;
+
+    // Row 5: Ramp widget
+    let ctrl_ramp_widget_pos = (base_x, ctrl_y, 400.0, 120.0);
 
     // Dynamic calculations for Windows page layout
     let mut left_y = 80.0;
@@ -1165,12 +1299,12 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
     left_y += bh + 15.0; // 385.0
     
     // Window Type Dropdown
-    let type_dd_pos = (base_x, left_y, 360.0, 35.0);
-    left_y += 35.0 + 15.0; // 435.0
+    let type_dd_pos = (base_x, left_y, 240.0, ddh);
+    left_y += ddh + 15.0;
     
     // Window Shape Dropdown
-    let shape_dd_pos = (base_x, left_y, 360.0, 35.0);
-    left_y += 35.0 + 15.0; // 485.0
+    let shape_dd_pos = (base_x, left_y, 240.0, ddh);
+    left_y += ddh + 15.0;
     
     // Opacity Toggle & Slider
     let opacity_toggle_pos = (base_x, left_y, 110.0, 32.0);
@@ -1194,11 +1328,12 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
     right_y += sph + 15.0;
     
     // Border Section Container
-    let border_sec_h = 28.0 + 32.0 + 10.0 + sph + 10.0;
+    let border_sec_h = 28.0 + 32.0 + 10.0 + sph + 10.0 + bh + 10.0;
     let border_sec_pos = (rx, right_y, rw, border_sec_h);
     let border_enable_pos = (rx + 10.0, right_y + 28.0 + 5.0, 110.0, 32.0);
     let bevel_toggle_pos = (rx + 130.0, right_y + 28.0 + 5.0, 110.0, 32.0);
     let border_width_pos = (rx + 10.0, right_y + 28.0 + 5.0 + 32.0 + 10.0, rw - 20.0, sph);
+    let bevel_shape_pos = (rx + 10.0, right_y + 28.0 + 5.0 + 32.0 + 10.0 + sph + 10.0, rw - 20.0, bh);
     right_y += border_sec_h + 15.0;
     
     // Window Elements Section Container
@@ -1262,8 +1397,11 @@ fn demo_positions(sw: f32, sh: f32, sidebar_w: f32) -> Vec<(f32, f32, f32, f32)>
         bevel_toggle_pos,                     // 42 Toggle: Bevel
         border_width_pos,                     // 43 Spinbox: Border Width
         win_sec_pos,                          // 44 SectionContainer: Window Elements
-        (sw - 140.0, sh - 25.0, 120.0, 22.0),         // 45 Dropdown: Page selector
-        ctrl_ramp_pos,                        // 46 Button: Ramp Control
+        (sw - 140.0, sh - 14.0 - ddh / 2.0, 120.0, ddh), // 45 Dropdown: Page selector
+        ctrl_color_ramp_btn_pos,              // 46 Button: Color Ramp
+        bevel_shape_pos,                      // 47 Button: Bevel Shape
+        ctrl_ramp_widget_pos,                 // 48 Ramp: Controls page ramp
+        ctrl_ramp_btn_pos,                    // 49 Button: Ramp
     ]
 }
 
@@ -1275,7 +1413,7 @@ fn child_positions(
     use_statusbar: bool,
     child_type: Option<&str>,
 ) -> Vec<(f32, f32, f32, f32)> {
-    if child_type == Some("Ramp") {
+    if child_type == Some("Ramp") || child_type == Some("ColorRamp") {
         let bg_y = 0.0;
         let bg_h = sh;
         let ramp_pos = (10.0, 10.0, sw - 20.0, sh - 60.0);
@@ -1523,8 +1661,8 @@ impl PointerHandler for AppState {
                                 } else {
                                     match index {
                                         0..=2 | 45 => true,
-                                        3..=13 | 30 | 31 | 36 | 37 | 46 => current_page == Page::Controls,
-                                        14..=29 | 38..=44 => current_page == Page::Windows,
+                                        3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => current_page == Page::Controls,
+                                        14..=29 | 38..=44 | 47 => current_page == Page::Windows,
                                         32..=35 => current_page == Page::Xdg,
                                         _ => false,
                                     }
@@ -1569,8 +1707,8 @@ impl PointerHandler for AppState {
                             } else {
                                 match index {
                                     0..=2 | 45 => true,
-                                    3..=13 | 30 | 31 | 36 | 37 | 46 => current_page == Page::Controls,
-                                    14..=29 | 38..=44 => current_page == Page::Windows,
+                                    3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => current_page == Page::Controls,
+                                    14..=29 | 38..=44 | 47 => current_page == Page::Windows,
                                     32..=35 => current_page == Page::Xdg,
                                     _ => false,
                                 }
@@ -1651,8 +1789,8 @@ impl PointerHandler for AppState {
                             } else {
                                 match index {
                                     0..=2 | 45 => true,
-                                    3..=13 | 30 | 31 | 36 | 37 | 46 => current_page == Page::Controls,
-                                    14..=29 | 38..=44 => current_page == Page::Windows,
+                                    3..=13 | 30 | 31 | 36 | 37 | 46 | 48 | 49 => current_page == Page::Controls,
+                                    14..=29 | 38..=44 | 47 => current_page == Page::Windows,
                                     32..=35 => current_page == Page::Xdg,
                                     _ => false,
                                 }
@@ -1716,6 +1854,19 @@ impl PointerHandler for AppState {
                                     } else if st.widgets[8].take_click() {
                                         do_reset = true;
                                     } else if st.widgets[46].take_click() {
+                                        if let Ok(exe) = std::env::current_exe() {
+                                            let mut cmd = std::process::Command::new(exe);
+                                            cmd.arg("--child")
+                                               .arg("--type")
+                                               .arg("ColorRamp")
+                                               .arg("--width")
+                                               .arg("450")
+                                               .arg("--height")
+                                               .arg("200")
+                                               .arg("--backplate");
+                                            let _ = cmd.spawn();
+                                        }
+                                    } else if st.widgets[49].take_click() {
                                         if let Ok(exe) = std::env::current_exe() {
                                             let mut cmd = std::process::Command::new(exe);
                                             cmd.arg("--child")
@@ -1775,6 +1926,19 @@ impl PointerHandler for AppState {
                                         create_window = true;
                                     } else if st.widgets[16].take_click() {
                                         tile_windows = true;
+                                    } else if st.widgets[47].take_click() {
+                                        if let Ok(exe) = std::env::current_exe() {
+                                            let mut cmd = std::process::Command::new(exe);
+                                            cmd.arg("--child")
+                                               .arg("--type")
+                                               .arg("ColorRamp")
+                                               .arg("--width")
+                                               .arg("450")
+                                               .arg("--height")
+                                               .arg("200")
+                                               .arg("--backplate");
+                                            let _ = cmd.spawn();
+                                        }
                                     } else {
                                         let mut dropdown_clicked = false;
                                         for i in [17, 19, 21, 22, 23, 25, 26, 38, 39, 40, 42, 43] {