git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit0ecf417a362b218f19d3ef36b50edfb47d60bd2f
parent632e7b9332
authorLucas Galante <[email protected]>
date2026-06-02 13:24
Update system configuration and interface modules

 Cargo.toml  |   4 +
 src/main.rs | 519 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 518 insertions(+), 5 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 256f31c..87aacb7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,4 +15,8 @@ wgpu = "24"
 bytemuck = { version = "1", features = ["derive"] }
 pollster = "0.4"
 glyphon = "0.8"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
 tokio = { version = "1", features = ["full"] }
+
+
diff --git a/src/main.rs b/src/main.rs
index 26d545a..923938f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1156,12 +1156,235 @@ impl Widget for FuzzelWidget {
     }
 }
 
+#[derive(serde::Deserialize, Debug, Clone)]
+struct JsonWidgetConfig {
+    #[serde(rename = "type")]
+    widget_type: String,
+    text: String,
+    id: Option<String>,
+    checked: Option<bool>,
+}
+
+#[derive(serde::Deserialize, Debug, Clone)]
+struct JsonLayoutConfig {
+    width: Option<u32>,
+    height: Option<u32>,
+    widgets: Vec<JsonWidgetConfig>,
+}
+
+use clear_ui::widget::WidgetBase;
+
+struct JsonWidget {
+    id: String,
+    _widget_type: String,
+    text: String,
+    checkbox: Option<clear_ui::widget::Checkbox>,
+    button: Option<clear_ui::widget::Button>,
+    label: Option<clear_ui::widget::Label>,
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
+    label_text: Option<clear_ui::widget::TextLabel>,
+}
+
+struct JsonLayoutWidget {
+    base: WidgetBase,
+    widgets: Vec<JsonWidget>,
+}
+
+impl JsonLayoutWidget {
+    fn new(configs: &[JsonWidgetConfig]) -> Self {
+        let mut widgets = Vec::new();
+        for (idx, w_conf) in configs.iter().enumerate() {
+            let id = w_conf.id.clone().unwrap_or_else(|| format!("widget_{}", idx));
+            let widget_type = w_conf.widget_type.clone();
+            let text = w_conf.text.clone();
+            let mut checkbox = None;
+            let mut button = None;
+            let mut label = None;
+
+            match widget_type.as_str() {
+                "checkbox" => {
+                    let mut cb = clear_ui::widget::Checkbox::new();
+                    if let Some(ch) = w_conf.checked {
+                        cb.set_checked(ch);
+                    }
+                    checkbox = Some(cb);
+                }
+                "button" => {
+                    button = Some(clear_ui::widget::Button::new(0.0, 0.0, 0.0, 0.0).with_label(&text));
+                }
+                "label" => {
+                    label = Some(clear_ui::widget::Label::new(&text).with_font_size(13.0).with_color([0xcc, 0xcc, 0xd4]));
+                }
+                _ => {}
+            }
+
+            widgets.push(JsonWidget {
+                id,
+                _widget_type: widget_type,
+                text,
+                checkbox,
+                button,
+                label,
+                x: 0.0,
+                y: 0.0,
+                w: 0.0,
+                h: 0.0,
+                label_text: None,
+            });
+        }
+
+        Self {
+            base: WidgetBase::new(),
+            widgets,
+        }
+    }
+
+    fn layout_children(&mut self) {
+        let (bx, by, bw, _bh) = self.rect();
+        let pad_x = 16.0;
+        let spacing = 12.0;
+        let usable_w = bw - 2.0 * pad_x;
+        
+        let mut current_y = 16.0;
+        for w_state in &mut self.widgets {
+            w_state.x = bx + pad_x;
+            w_state.y = by + current_y;
+            w_state.w = usable_w;
+            
+            if let Some(cb) = &mut w_state.checkbox {
+                cb.set_rect(w_state.x, w_state.y + 2.0, 18.0, 18.0);
+                w_state.h = 22.0;
+                w_state.label_text = Some(clear_ui::widget::TextLabel {
+                    text: w_state.text.clone(),
+                    x: w_state.x + 28.0,
+                    y: w_state.y + 2.0,
+                    font_size: 13.0,
+                    color: [0xcc, 0xcc, 0xd4],
+                });
+            } else if let Some(btn) = &mut w_state.button {
+                btn.set_rect(w_state.x, w_state.y, usable_w, 32.0);
+                w_state.h = 32.0;
+            } else if let Some(lbl) = &mut w_state.label {
+                lbl.set_rect(w_state.x, w_state.y, usable_w, 18.0);
+                w_state.h = 18.0;
+            }
+            current_y += w_state.h + spacing;
+        }
+    }
+}
+
+impl Widget for JsonLayoutWidget {
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        if let Some(b) = self.base_mut() {
+            b.x = x;
+            b.y = y;
+            b.w = w;
+            b.h = h;
+        }
+        self.layout_children();
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        for w in &self.widgets {
+            if let Some(cb) = &w.checkbox {
+                quads.push((cb.rect().0, cb.rect().1, cb.rect().2, cb.rect().3, cb.color()));
+                quads.extend(cb.extra_quads());
+                if let Some(hq) = cb.highlight_quad() {
+                    quads.push(hq);
+                }
+            } else if let Some(btn) = &w.button {
+                quads.push((btn.rect().0, btn.rect().1, btn.rect().2, btn.rect().3, btn.color()));
+                quads.extend(btn.extra_quads());
+                if let Some(hq) = btn.highlight_quad() {
+                    quads.push(hq);
+                }
+            } else if let Some(lbl) = &w.label {
+                quads.push((lbl.rect().0, lbl.rect().1, lbl.rect().2, lbl.rect().3, lbl.color()));
+                quads.extend(lbl.extra_quads());
+            }
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<clear_ui::widget::TextLabel> {
+        let mut labels = Vec::new();
+        for w in &self.widgets {
+            if let Some(_cb) = &w.checkbox {
+                if let Some(tl) = &w.label_text {
+                    labels.push(tl.clone());
+                }
+            } else if let Some(btn) = &w.button {
+                labels.extend(btn.text_labels());
+            } else if let Some(lbl) = &w.label {
+                labels.extend(lbl.text_labels());
+            }
+        }
+        labels
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        let mut changed = false;
+        for w in &mut self.widgets {
+            if let Some(cb) = &mut w.checkbox {
+                let was = cb.hovered();
+                let hit = px >= w.x && px <= w.x + w.w && py >= w.y && py <= w.y + w.h;
+                cb.set_hovered(hit);
+                if was != hit {
+                    changed = true;
+                }
+            } else if let Some(btn) = &mut w.button {
+                if btn.cursor_moved(px, py) {
+                    changed = true;
+                }
+            } else if let Some(lbl) = &mut w.label {
+                if lbl.cursor_moved(px, py) {
+                    changed = true;
+                }
+            }
+        }
+        changed
+    }
+
+    fn mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState, px: f32, py: f32) -> bool {
+        if button != clear_ui::widget::MouseButton::Left { return false; }
+        let mut changed = false;
+        for w in &mut self.widgets {
+            if let Some(cb) = &mut w.checkbox {
+                let hit = px >= w.x && px <= w.x + w.w && py >= w.y && py <= w.y + w.h;
+                if hit {
+                    if state == clear_ui::widget::ElementState::Pressed {
+                        changed = true;
+                    } else if state == clear_ui::widget::ElementState::Released {
+                        let new_checked = !cb.checked();
+                        cb.set_checked(new_checked);
+                        changed = true;
+                    }
+                }
+            } else if let Some(btn) = &mut w.button {
+                if btn.mouse_input(button, state, px, py) {
+                    changed = true;
+                }
+            }
+        }
+        changed
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 enum LauncherMode {
     Dmenu,
     Path,
     Apps,
     Color,
+    Json,
 }
 
 #[derive(Debug, Clone)]
@@ -1213,6 +1436,7 @@ struct State {
 
     fuzzel: FuzzelWidget,
     color_picker: Option<ColorPickerWidget>,
+    json_layout: Option<JsonLayoutWidget>,
     font_system: FontSystem,
     swash_cache: SwashCache,
     text_atlas: TextAtlas,
@@ -1251,9 +1475,31 @@ impl State {
         y_pos: Option<i32>,
         scale: f64,
         select_item: Option<String>,
+        json_layout_config: Option<JsonLayoutConfig>,
     ) -> Self {
         let (width, height) = if mode == LauncherMode::Color {
             (WIN_W as u32, WIN_H as u32)
+        } else if mode == LauncherMode::Json {
+            if let Some(ref config) = json_layout_config {
+                let w = config.width.unwrap_or(300);
+                let h = config.height.unwrap_or_else(|| {
+                    let mut current_y = 16.0f32;
+                    for w_conf in &config.widgets {
+                        let h = match w_conf.widget_type.as_str() {
+                            "label" => 18.0,
+                            "checkbox" => 22.0,
+                            "button" => 32.0,
+                            _ => 20.0,
+                        };
+                        current_y += h + 12.0;
+                    }
+                    current_y += 4.0;
+                    current_y.round() as u32
+                });
+                (w, h)
+            } else {
+                (300, 400)
+            }
         } else {
             (600, 800)
         };
@@ -1435,7 +1681,7 @@ impl State {
                 lock_state.items = app_names;
                 lock_state.new_data = true;
             }
-        } else {
+        } else if mode == LauncherMode::Path {
             let path_items = scan_path();
             if let Ok(mut lock_state) = stdin_state.lock() {
                 lock_state.items = path_items;
@@ -1455,6 +1701,18 @@ impl State {
             None
         };
 
+        let json_layout = if mode == LauncherMode::Json {
+            if let Some(ref config) = json_layout_config {
+                let mut jl = JsonLayoutWidget::new(&config.widgets);
+                jl.set_rect(0.0, 0.0, lw, lh);
+                Some(jl)
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+
         let opacity = read_opacity_if_configured();
 
         let mut state = Self {
@@ -1469,6 +1727,7 @@ impl State {
             vertex_count: 0,
             fuzzel,
             color_picker,
+            json_layout,
             font_system,
             swash_cache,
             text_atlas,
@@ -1525,7 +1784,7 @@ impl State {
     }
 
     fn update_desired_size(&mut self) {
-        if self.mode == LauncherMode::Color {
+        if self.mode == LauncherMode::Color || self.mode == LauncherMode::Json {
             return;
         }
         let num_items = self.fuzzel.filtered_items.len();
@@ -1586,6 +1845,10 @@ impl State {
             if let Some(cp) = &mut self.color_picker {
                 cp.rebuild_layout(self.scale as f32);
             }
+        } else if self.mode == LauncherMode::Json {
+            if let Some(jl) = &mut self.json_layout {
+                jl.set_rect(0.0, 0.0, w, h);
+            }
         } else {
             self.fuzzel.set_rect(0.0, 0.0, w, h);
         }
@@ -1596,12 +1859,26 @@ impl State {
         let sh = self.height;
         if self.mode == LauncherMode::Color {
             if let Some(cp) = &self.color_picker {
-                cp.collect_vertices(self.physical_width as f32, self.physical_height as f32)
+                let pw = self.physical_width as f32;
+                let ph = self.physical_height as f32;
+                let mut verts = quad_vertices(0.0, 0.0, pw, ph, pw, ph, [0.05, 0.05, 0.08, self.opacity]).to_vec();
+                verts.extend(cp.collect_vertices(pw, ph));
+                verts
+            } else {
+                Vec::new()
+            }
+        } else if self.mode == LauncherMode::Json {
+            if let Some(jl) = &self.json_layout {
+                let mut verts = quad_vertices(0.0, 0.0, sw, sh, sw, sh, [0.05, 0.05, 0.08, self.opacity]).to_vec();
+                verts.extend(widget_vertices(jl, sw, sh));
+                verts
             } else {
                 Vec::new()
             }
         } else {
-            widget_vertices(&self.fuzzel, sw, sh)
+            let mut verts = quad_vertices(0.0, 0.0, sw, sh, sw, sh, [0.05, 0.05, 0.08, self.opacity]).to_vec();
+            verts.extend(widget_vertices(&self.fuzzel, sw, sh));
+            verts
         }
     }
 
@@ -1643,6 +1920,7 @@ impl State {
             scale,
             ref fuzzel,
             ref color_picker,
+            ref json_layout,
             ref mode,
             ..
         } = self;
@@ -1657,12 +1935,19 @@ impl State {
         let mut widget_labels: Vec<TextLabel> = Vec::new();
 
         let is_color_mode = *mode == LauncherMode::Color;
+        let is_json_mode = *mode == LauncherMode::Json;
         if is_color_mode {
             if let Some(cp) = color_picker {
                 for label in &cp.labels {
                     widget_labels.push(label.clone());
                 }
             }
+        } else if is_json_mode {
+            if let Some(jl) = json_layout {
+                for label in jl.text_labels() {
+                    widget_labels.push(label);
+                }
+            }
         } else {
             for label in fuzzel.text_labels() {
                 widget_labels.push(label);
@@ -1756,7 +2041,7 @@ impl State {
                     resolve_target: None,
                     ops: wgpu::Operations {
                         load: wgpu::LoadOp::Clear(wgpu::Color {
-                            r: 0.05, g: 0.05, b: 0.08, a: (self.opacity * fade_factor) as f64,
+                            r: 0.0, g: 0.0, b: 0.0, a: 0.0,
                         }),
                         store: wgpu::StoreOp::Store,
                     },
@@ -1986,6 +2271,17 @@ impl PointerHandler for AppState {
                                 st.upload_vertices();
                                 self.redraw = true;
                             }
+                        } else if st.mode == LauncherMode::Json {
+                            let mut changed = false;
+                            if let Some(jl) = &mut st.json_layout {
+                                if jl.on_cursor_moved(event.position.0 as f32, event.position.1 as f32) {
+                                    changed = true;
+                                }
+                            }
+                            if changed {
+                                st.upload_vertices();
+                                self.redraw = true;
+                            }
                         }
                     }
                     PointerEventKind::Press { button, .. } => {
@@ -2022,6 +2318,22 @@ impl PointerHandler for AppState {
                                         }
                                     }
                                 }
+                            } else if st.mode == LauncherMode::Json {
+                                let mut changed = false;
+                                if let Some(jl) = &mut st.json_layout {
+                                    if jl.mouse_input(
+                                        clear_ui::widget::MouseButton::Left,
+                                        clear_ui::widget::ElementState::Pressed,
+                                        event.position.0 as f32,
+                                        event.position.1 as f32,
+                                    ) {
+                                        changed = true;
+                                    }
+                                }
+                                if changed {
+                                    st.upload_vertices();
+                                    self.redraw = true;
+                                }
                             } else {
                                 let prev_selected = st.fuzzel.selected;
                                 let changed = st.fuzzel.mouse_input(
@@ -2045,6 +2357,7 @@ impl PointerHandler for AppState {
                                                 }
                                                 LauncherMode::Dmenu => {}
                                                 LauncherMode::Color => {}
+                                                LauncherMode::Json => {}
                                             }
                                             should_close = true;
                                         }
@@ -2070,6 +2383,47 @@ impl PointerHandler for AppState {
                                     st.upload_vertices();
                                     self.redraw = true;
                                 }
+                            } else if st.mode == LauncherMode::Json {
+                                let mut changed = false;
+                                let mut clicked_btn_id = None;
+                                if let Some(jl) = &mut st.json_layout {
+                                    if jl.mouse_input(
+                                        clear_ui::widget::MouseButton::Left,
+                                        clear_ui::widget::ElementState::Released,
+                                        event.position.0 as f32,
+                                        event.position.1 as f32,
+                                    ) {
+                                        changed = true;
+                                    }
+                                    for w in &mut jl.widgets {
+                                        if let Some(btn) = &mut w.button {
+                                            if btn.take_click() {
+                                                clicked_btn_id = Some(w.id.clone());
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                                if changed {
+                                    st.upload_vertices();
+                                    self.redraw = true;
+                                }
+                                if let Some(btn_id) = clicked_btn_id {
+                                    let mut checkboxes = std::collections::HashMap::new();
+                                    if let Some(jl) = &st.json_layout {
+                                        for w in &jl.widgets {
+                                            if let Some(cb) = &w.checkbox {
+                                                checkboxes.insert(w.id.clone(), cb.checked());
+                                            }
+                                        }
+                                    }
+                                    let out_val = serde_json::json!({
+                                        "button": btn_id,
+                                        "checkboxes": checkboxes
+                                    });
+                                    println!("{}", out_val.to_string());
+                                    should_close = true;
+                                }
                             }
                         }
                     }
@@ -2269,6 +2623,15 @@ impl AppState {
                         handled = false;
                     }
                 }
+            } else if st.mode == LauncherMode::Json {
+                match &logical_key {
+                    Key::Named(NamedKey::Escape) => {
+                        should_close = true;
+                    }
+                    _ => {
+                        handled = false;
+                    }
+                }
             } else {
                 match &logical_key {
                     Key::Named(NamedKey::Escape) => {
@@ -2288,6 +2651,7 @@ impl AppState {
                                 }
                                 LauncherMode::Dmenu => {}
                                 LauncherMode::Color => {}
+                                LauncherMode::Json => {}
                             }
                             should_close = true;
                         }
@@ -2433,11 +2797,38 @@ fn main() {
             } else {
                 i += 1;
             }
+        } else if arg == "--json" || arg == "--layout" {
+            mode = LauncherMode::Json;
+            i += 1;
         } else {
             i += 1;
         }
     }
 
+    let mut json_layout_config: Option<JsonLayoutConfig> = None;
+    if mode == LauncherMode::Json {
+        use std::io::Read;
+        let mut json_str = String::new();
+        let mut stdin = std::io::stdin();
+        match stdin.read_to_string(&mut json_str) {
+            Ok(_) => {
+                match serde_json::from_str::<JsonLayoutConfig>(&json_str) {
+                    Ok(cfg) => {
+                        json_layout_config = Some(cfg);
+                    }
+                    Err(e) => {
+                        eprintln!("Failed to parse JSON layout: {}", e);
+                        std::process::exit(1);
+                    }
+                }
+            }
+            Err(e) => {
+                eprintln!("Failed to read JSON layout from stdin: {}", e);
+                std::process::exit(1);
+            }
+        }
+    }
+
     let conn = Connection::connect_to_env().unwrap();
     let conn_clone = conn.clone();
     let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
@@ -2490,6 +2881,7 @@ fn main() {
         y_pos,
         scale,
         select_item,
+        json_layout_config,
     ));
 
     app.window = Some(state.window.clone());
@@ -2555,3 +2947,120 @@ fn main() {
     drop(app);
     let _ = conn_clone.roundtrip();
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_json_layout_parsing() {
+        let json_str = r#"{
+            "width": 320,
+            "height": 240,
+            "widgets": [
+                { "type": "label", "text": "Select Option:" },
+                { "id": "feat_a", "type": "checkbox", "text": "Enable Feature A", "checked": true },
+                { "id": "btn_ok", "type": "button", "text": "OK" }
+            ]
+        }"#;
+
+        let config: JsonLayoutConfig = serde_json::from_str(json_str).expect("Failed to parse JSON");
+        assert_eq!(config.width, Some(320));
+        assert_eq!(config.height, Some(240));
+        assert_eq!(config.widgets.len(), 3);
+
+        assert_eq!(config.widgets[0].widget_type, "label");
+        assert_eq!(config.widgets[0].text, "Select Option:");
+
+        assert_eq!(config.widgets[1].widget_type, "checkbox");
+        assert_eq!(config.widgets[1].id.as_deref(), Some("feat_a"));
+        assert_eq!(config.widgets[1].checked, Some(true));
+    }
+
+    #[test]
+    fn test_json_layout_widget_flow() {
+        use clear_ui::widget::Widget;
+
+        let widgets_conf = vec![
+            JsonWidgetConfig {
+                widget_type: "label".to_string(),
+                text: "Label 1".to_string(),
+                id: None,
+                checked: None,
+            },
+            JsonWidgetConfig {
+                widget_type: "checkbox".to_string(),
+                text: "Check 1".to_string(),
+                id: Some("chk".to_string()),
+                checked: Some(false),
+            },
+            JsonWidgetConfig {
+                widget_type: "button".to_string(),
+                text: "Click 1".to_string(),
+                id: Some("btn".to_string()),
+                checked: None,
+            },
+        ];
+
+        let mut layout = JsonLayoutWidget::new(&widgets_conf);
+        layout.set_rect(0.0, 0.0, 300.0, 400.0);
+
+        // Verify sub-widgets are populated and positioned correctly
+        assert_eq!(layout.widgets.len(), 3);
+        
+        let w_label_y = layout.widgets[0].y;
+        let w_label_h = layout.widgets[0].h;
+        let w_label_x = layout.widgets[0].x;
+        let w_label_w = layout.widgets[0].w;
+
+        let w_check_y = layout.widgets[1].y;
+        let w_check_h = layout.widgets[1].h;
+        let w_check_x = layout.widgets[1].x;
+        let w_check_w = layout.widgets[1].w;
+
+        let w_btn_y = layout.widgets[2].y;
+        let w_btn_h = layout.widgets[2].h;
+        let w_btn_x = layout.widgets[2].x;
+        let w_btn_w = layout.widgets[2].w;
+
+        assert!(layout.widgets[0].label.is_some());
+        assert!(layout.widgets[1].checkbox.is_some());
+        assert!(layout.widgets[2].button.is_some());
+
+        // Check vertical sequence positions
+        assert_eq!(w_label_y, 16.0);
+        assert_eq!(w_label_h, 18.0);
+
+        assert_eq!(w_check_y, 16.0 + 18.0 + 12.0); // y_prev + h_prev + spacing
+        assert_eq!(w_check_h, 22.0);
+
+        assert_eq!(w_btn_y, w_check_y + 22.0 + 12.0);
+        assert_eq!(w_btn_h, 32.0);
+
+        // Check horizontal positioning (should match usable width: 300 - 2 * 16 = 268)
+        assert_eq!(w_label_x, 16.0);
+        assert_eq!(w_label_w, 268.0);
+        assert_eq!(w_check_x, 16.0);
+        assert_eq!(w_check_w, 268.0);
+        assert_eq!(w_btn_x, 16.0);
+        assert_eq!(w_btn_w, 268.0);
+
+        // Verify Checkbox initial state
+        assert_eq!(layout.widgets[1].checkbox.as_ref().unwrap().checked(), false);
+
+        // Simulate click on Checkbox row
+        let changed = layout.mouse_input(
+            clear_ui::widget::MouseButton::Left,
+            clear_ui::widget::ElementState::Released,
+            w_check_x + 5.0,
+            w_check_y + 5.0,
+        );
+        assert!(changed);
+        assert_eq!(layout.widgets[1].checkbox.as_ref().unwrap().checked(), true);
+
+        // Simulate hover on button
+        let changed_hover = layout.on_cursor_moved(w_btn_x + 10.0, w_btn_y + 10.0);
+        assert!(changed_hover);
+        assert!(layout.widgets[2].button.as_ref().unwrap().base().unwrap().hovered);
+    }
+}