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

commit844c45c1a36545db432d497ec56ceb90a18b9f5d
parent84dda66adc
authorLucas Galante <[email protected]>
date2026-06-03 10:27
Implement frosted glass blur effect for Dropdown popover and include recent UI enhancements

 Cargo.lock                |   26 +
 Cargo.toml                |    1 +
 src/color.rs              |   12 +-
 src/engine.rs             |  208 ++++-
 src/layout.rs             |   98 ++-
 src/main.rs               |   16 +-
 src/shader.wgsl           |   30 +-
 src/widget.rs             | 2116 +++++++++++++++++++++++++++++++++++++--------
 src/widget/json_layout.rs |  575 ++++++++++++
 9 files changed, 2695 insertions(+), 387 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4286121..8a0fc94 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -179,6 +179,7 @@ dependencies = [
  "raw-window-handle",
  "resvg",
  "serde",
+ "serde_json",
  "smithay-client-toolkit",
  "tokio",
  "wayland-backend",
@@ -649,6 +650,12 @@ dependencies = [
  "hashbrown 0.17.1",
 ]
 
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
 [[package]]
 name = "jni-sys"
 version = "0.3.1"
@@ -1262,6 +1269,19 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -2181,3 +2201,9 @@ name = "zeno"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
index 6607100..fc1e29b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,5 +23,6 @@ wayland-scanner = "0.31"
 xkeysym = "0.2"
 resvg = "0.41.0"
 serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
 libc = "0.2"
 
diff --git a/src/color.rs b/src/color.rs
index 80ebfe8..8507587 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -7,14 +7,14 @@ pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
 pub const NODE_IDLE: [f32; 4] = [0.10, 0.45, 0.70, 1.0];
 pub const NODE_SELECTED: [f32; 4] = [0.20, 0.65, 0.90, 1.0];
 pub const NODE_DRAG: [f32; 4] = [0.30, 0.80, 1.00, 1.0];
-pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
-pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 1.0];
-pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 1.0];
+pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 0.4];
+pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 0.6];
+pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 0.8];
 pub const STATUS_BG: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
 pub const STATUS_ACCENT: [f32; 4] = [0.20, 0.20, 0.25, 1.0];
-pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 1.0];
-pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 1.0];
-pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 1.0];
+pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 0.4];
+pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 0.6];
+pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 0.8];
 pub const CHECKBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
 pub const CHECKBOX_CHECKED: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
 pub const CHECKBOX_HOVER: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
diff --git a/src/engine.rs b/src/engine.rs
index 1547e01..a1e1787 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -38,12 +38,14 @@ use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
 pub struct Vertex {
     pub position: [f32; 2],
     pub color: [f32; 4],
+    pub clip_circle: [f32; 3], // [cx, cy, r]
 }
 
 impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+    const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
         0 => Float32x2,
         1 => Float32x4,
+        2 => Float32x3,
     ];
 
     pub fn desc() -> wgpu::VertexBufferLayout<'static> {
@@ -61,15 +63,170 @@ pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32;
     let x1 = ((x + w) / sw) * 2.0 - 1.0;
     let y1 = 1.0 - ((y + h) / sh) * 2.0;
     [
-        Vertex { position: [x0, y0], color: c },
-        Vertex { position: [x1, y0], color: c },
-        Vertex { position: [x0, y1], color: c },
-        Vertex { position: [x1, y0], color: c },
-        Vertex { position: [x1, y1], color: c },
-        Vertex { position: [x0, y1], color: c },
+        Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
     ]
 }
 
+pub fn quad_vertices_with_clip(
+    x: f32, y: f32, w: f32, h: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    clip_circle: [f32; 3],
+) -> [Vertex; 6] {
+    let x0 = (x / sw) * 2.0 - 1.0;
+    let y0 = 1.0 - (y / sh) * 2.0;
+    let x1 = ((x + w) / sw) * 2.0 - 1.0;
+    let y1 = 1.0 - ((y + h) / sh) * 2.0;
+    [
+        Vertex { position: [x0, y0], color, clip_circle },
+        Vertex { position: [x1, y0], color, clip_circle },
+        Vertex { position: [x0, y1], color, clip_circle },
+        Vertex { position: [x1, y0], color, clip_circle },
+        Vertex { position: [x1, y1], color, clip_circle },
+        Vertex { position: [x0, y1], color, clip_circle },
+    ]
+}
+
+pub fn quad_vertices_clipped(
+    x: f32, y: f32, w: f32, h: f32,
+    surface_w: f32, surface_h: f32,
+    color: [f32; 4],
+    clip: (f32, f32, f32, f32),
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let (cx0, cy0, cx1, cy1) = clip;
+    let ix0 = x.max(cx0);
+    let iy0 = y.max(cy0);
+    let ix1 = (x + w).min(cx1);
+    let iy1 = (y + h).min(cy1);
+    if ix1 <= ix0 || iy1 <= iy0 {
+        return Vec::new();
+    }
+    quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
+}
+
+pub fn widget_vertices(w: &dyn crate::widget::Widget, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
+    let (x, y, ww, h) = w.rect();
+    quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
+}
+
+pub fn circle_vertices(
+    cx: f32, cy: f32, r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let x0 = cx;
+        let y0 = cy;
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+        let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+        let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
+        let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+    }
+    verts
+}
+
+pub fn circle_border_vertices(
+    cx: f32, cy: f32, r: f32,
+    thickness: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        
+        let x0 = cx + (r - thickness) * theta1.cos();
+        let y0 = cy + (r - thickness) * theta1.sin();
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        let x3 = cx + (r - thickness) * theta2.cos();
+        let y3 = cy + (r - thickness) * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+    }
+    verts
+}
+
+pub fn arc_background_vertices(
+    cx: f32, cy: f32, r: f32,
+    thickness: f32,
+    start_angle: f32, end_angle: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
+        let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
+        
+        let x0 = cx + (r - thickness) * theta1.cos();
+        let y0 = cy + (r - thickness) * theta1.sin();
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        let x3 = cx + (r - thickness) * theta2.cos();
+        let y3 = cy + (r - thickness) * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+    }
+    verts
+}
+
+
 #[derive(Debug, Clone)]
 pub struct WindowSettings {
     pub title: String,
@@ -119,6 +276,18 @@ pub trait Application: Sized + 'static {
     fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
     fn text_items(&self) -> &[TextItem];
     
+    fn text_areas(&self, scale_f32: f32, bounds: TextBounds) -> Vec<TextArea<'_>> {
+        self.text_items().iter().map(|ti| TextArea {
+            buffer: &ti.buffer,
+            left: ti.x * scale_f32,
+            top: ti.y * scale_f32,
+            scale: scale_f32,
+            bounds,
+            default_color: ti.color,
+            custom_glyphs: &[],
+        }).collect()
+    }
+    
     fn clear_color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
@@ -393,16 +562,7 @@ impl<A: Application> EngineState<A> {
         text_viewport.update(queue, Resolution { width: pw, height: ph });
         
         let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
-        let text_items = self.inner.text_items();
-        let areas: Vec<TextArea> = text_items.iter().map(|ti| TextArea {
-            buffer: &ti.buffer,
-            left: ti.x * scale_f32,
-            top: ti.y * scale_f32,
-            scale: scale_f32,
-            bounds,
-            default_color: ti.color,
-            custom_glyphs: &[],
-        }).collect();
+        let areas = self.inner.text_areas(scale_f32, bounds);
         
         text_renderer.prepare(device, queue, &mut self.font_system, text_atlas, text_viewport, areas, &mut self.swash_cache).unwrap();
         
@@ -472,6 +632,20 @@ impl<A: Application> EngineState<A> {
     }
 }
 
+impl<A: Application> Drop for EngineState<A> {
+    fn drop(&mut self) {
+        self.wgpu_surface = None;
+        self.device = None;
+        self.queue = None;
+        self.render_pipeline = None;
+        self.vertex_buffer = None;
+        self.overlay_vertex_buffer = None;
+        self.text_atlas = None;
+        self.text_renderer = None;
+        self.text_viewport = None;
+    }
+}
+
 impl<A: Application> CompositorHandler for EngineState<A> {
     fn scale_factor_changed(
         &mut self,
diff --git a/src/layout.rs b/src/layout.rs
index 4a9dc49..494d697 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -139,11 +139,13 @@ impl Column {
     }
 
     pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+        let top_room = w.top_room();
+        self.y += top_room;
         let x = self.ax(x_off);
         let y = self.ay();
         w.set_row_rect(self.ox + self.cx + 8.0, self.cw - 16.0);
         render_widget(pc, w, x, y, ww, wh);
-        self.y += wh + w.top_room();
+        self.y += wh;
     }
 
     pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
@@ -319,6 +321,100 @@ impl<'a> VStack<'a> {
     }
 }
 
+pub struct SplitterLayout {
+    pub splitter1_x: f32,
+    pub splitter2_x: f32,
+    pub splitter_width: f32,
+    pub min_column_width: f32,
+}
+
+impl SplitterLayout {
+    pub fn new(width: f32, splitter_width: f32, min_column_width: f32) -> Self {
+        let s1 = (width - 2.0 * splitter_width) / 3.0;
+        let s2 = s1 + splitter_width + (width - 2.0 * splitter_width) / 3.0;
+        Self {
+            splitter1_x: s1,
+            splitter2_x: s2,
+            splitter_width,
+            min_column_width,
+        }
+    }
+
+    pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
+        if detached_circular_network {
+            let min_s2 = self.min_column_width;
+            let max_s2 = total_width - self.min_column_width;
+            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
+        } else {
+            let min_s1 = self.min_column_width;
+            let max_s1 = self.splitter2_x - self.splitter_width - self.min_column_width;
+            self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
+            let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
+            let max_s2 = total_width - self.min_column_width;
+            self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
+        }
+    }
+
+    pub fn scale(&mut self, factor: f32) {
+        self.splitter1_x *= factor;
+        self.splitter2_x *= factor;
+    }
+
+    pub fn left_col(&self) -> (f32, f32) { // (x, width)
+        (0.0, self.splitter1_x)
+    }
+
+    pub fn center_col(&self) -> (f32, f32) { // (x, width)
+        let x = self.splitter1_x + self.splitter_width;
+        (x, self.splitter2_x - x)
+    }
+
+    pub fn right_col(&self, total_width: f32) -> (f32, f32) { // (x, width)
+        let x = self.splitter2_x + self.splitter_width;
+        (x, (total_width - x).max(0.0))
+    }
+}
+
+pub struct CircularPaneLayout {
+    pub x: f32,
+    pub y: f32,
+    pub r: f32,
+}
+
+impl CircularPaneLayout {
+    pub fn new(x: f32, y: f32, r: f32) -> Self {
+        Self { x, y, r }
+    }
+
+    pub fn hit_test_content(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist_sq = dx * dx + dy * dy;
+        dist_sq <= self.r * self.r && cy >= self.y - self.r + 45.0 + menubar_h + breadcrumb_h
+    }
+
+    pub fn hit_test_menubar(&self, cx: f32, cy: f32, menubar_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist = (dx * dx + dy * dy).sqrt();
+        dist >= self.r - menubar_h && dist <= self.r && cy < self.y
+    }
+
+    pub fn hit_test_breadcrumb(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist_sq = dx * dx + dy * dy;
+        dist_sq <= self.r * self.r && cy >= self.y - self.r + menubar_h && cy < self.y - self.r + 45.0 + menubar_h + breadcrumb_h
+    }
+
+    pub fn hit_test_border(&self, cx: f32, cy: f32, border_thickness: f32) -> bool {
+        let dx = cx - self.x;
+        let dy = cy - self.y;
+        let dist = (dx * dx + dy * dy).sqrt();
+        dist >= self.r - border_thickness && dist <= self.r
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/main.rs b/src/main.rs
index 1a5de51..8f48d96 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -42,12 +42,14 @@ use calloop_wayland_source::WaylandSource;
 struct Vertex {
     position: [f32; 2],
     color: [f32; 4],
+    clip_circle: [f32; 3],
 }
 
 impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+    const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
         0 => Float32x2,
         1 => Float32x4,
+        2 => Float32x3,
     ];
 
     fn desc() -> wgpu::VertexBufferLayout<'static> {
@@ -70,12 +72,12 @@ fn quad_vertices(
     let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
 
     [
-        Vertex { position: [x0, y0], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x0, y1], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x1, y1], color },
-        Vertex { position: [x0, y1], color },
+        Vertex { position: [x0, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y1], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
     ]
 }
 
diff --git a/src/shader.wgsl b/src/shader.wgsl
index b2b2708..e9a6969 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -2,18 +2,21 @@ struct VertexOutput {
     @builtin(position) clip_position: vec4f,
     @location(0) color: vec4f,
     @location(1) ndc_position: vec2f,
-    @location(2) @interpolate(flat) is_background: f32,
+    @location(2) is_background: f32,
+    @location(3) clip_circle: vec3f,
 }
 
 @vertex
 fn vs_main(
     @location(0) position: vec2f,
     @location(1) color: vec4f,
+    @location(2) clip_circle: vec3f,
 ) -> VertexOutput {
     var out: VertexOutput;
     out.clip_position = vec4f(position, 0.0, 1.0);
     out.color = color;
     out.ndc_position = position;
+    out.clip_circle = clip_circle;
 
     // Check if the vertex is exactly at the NDC boundaries (meaning it is the background quad)
     if (abs(position.x) > 0.999 && abs(position.y) > 0.999) {
@@ -27,8 +30,15 @@ fn vs_main(
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+    if (in.clip_circle.z > 0.0) {
+        let dx = in.clip_position.x - in.clip_circle.x;
+        let dy = in.clip_position.y - in.clip_circle.y;
+        if (dx * dx + dy * dy > in.clip_circle.z * in.clip_circle.z) {
+            discard;
+        }
+    }
     var final_color = in.color;
-    if (in.is_background > 0.5) {
+    if (in.is_background > 0.999) {
         // Feather the background quad edges
         let dist_x = 1.0 - abs(in.ndc_position.x);
         let dist_y = 1.0 - abs(in.ndc_position.y);
@@ -38,5 +48,21 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
         let fade = smoothstep(0.0, 1.0, min_dist / feather);
         final_color.a = final_color.a * fade;
     }
+    if (abs(in.color.a - 0.699) < 0.001) {
+        // Frosted glass effect
+        // Generate pseudo-random high-frequency noise based on pixel position
+        let noise = fract(sin(dot(in.clip_position.xy, vec2f(12.9898, 78.233))) * 43758.5453);
+        let grain = (noise - 0.5) * 0.08;
+        
+        // Add a subtle diagonal light sheen
+        let sheen = sin((in.clip_position.x + in.clip_position.y) * 0.012) * 0.018;
+        
+        final_color = vec4f(
+            clamp(in.color.r + grain + sheen, 0.0, 1.0),
+            clamp(in.color.g + grain + sheen, 0.0, 1.0),
+            clamp(in.color.b + grain + sheen, 0.0, 1.0),
+            0.699
+        );
+    }
     return final_color;
 }
diff --git a/src/widget.rs b/src/widget.rs
index c108fff..bf716b9 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -60,6 +60,9 @@ pub struct KeyEvent {
     pub shift: bool,
 }
 
+pub mod json_layout;
+pub use json_layout::{JsonLayoutWidget, JsonLayoutConfig, JsonWidgetConfig, JsonPageConfig, JsonWidget};
+
 use crate::colors;
 
 pub mod focus {
@@ -828,6 +831,8 @@ pub trait Widget {
     fn label(&self) -> Option<String> { None }
     
     fn set_curved_circle(&mut self, _circle: Option<(f32, f32, f32)>) {}
+    fn set_uniform_background(&mut self, _uniform: bool) {}
+    fn set_network_opacity(&mut self, _opacity: f32) {}
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         if let Some(b) = self.base_mut() {
@@ -1002,10 +1007,21 @@ pub trait Widget {
             b.focused = false;
         }
     }
+    fn focused(&self) -> bool {
+        if let Some(b) = self.base() {
+            b.focused
+        } else {
+            false
+        }
+    }
+    fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> { Vec::new() }
     fn set_selected(&mut self, _selected: bool) {}
     fn keyboard_input(&mut self, _event: &KeyEvent) -> bool { false }
 
     fn menu_click(&mut self) -> Option<(usize, usize)> { None }
+    fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
+    fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
     fn set_item_checked(&mut self, _menu_idx: usize, _item_idx: usize, _checked: bool) {}
     fn set_menu_items(&mut self, _menu_idx: usize, _items: &[String]) {}
     fn is_menu_bar(&self) -> bool { false }
@@ -1369,6 +1385,11 @@ pub struct ParametersBg {
     dragging_param: Option<usize>,
     drag_offset: f32,
     pub focused_param: Option<usize>,
+    mouse_pos: Option<(f32, f32)>,
+    sliders: Vec<Option<Slider>>,
+    float3s: Vec<Option<Float3>>,
+    spinboxes: Vec<Option<Spinbox>>,
+    visible: bool,
 }
 
 impl ParametersBg {
@@ -1383,6 +1404,11 @@ impl ParametersBg {
             dragging_param: None,
             drag_offset: 0.0,
             focused_param: None,
+            mouse_pos: None,
+            sliders: Vec::new(),
+            float3s: Vec::new(),
+            spinboxes: Vec::new(),
+            visible: true,
         }
     }
 
@@ -1390,16 +1416,54 @@ impl ParametersBg {
         let mut rects = Vec::new();
         let mut cur_y = self.y + 30.0;
         for p in &self.display_params {
-            let h = if p.2 == "code" { 200.0 } else { 20.0 };
+            let h = if p.2 == "code" {
+                200.0
+            } else if p.2 == "section" {
+                24.0
+            } else if p.2.starts_with("float3") {
+                108.0
+            } else if p.2 == "text" {
+                24.0
+            } else {
+                20.0
+            };
             rects.push((self.x + 8.0, cur_y, self.w - 16.0, h));
             cur_y += h + 8.0;
         }
         rects
     }
+
+    fn update_slider_rects(&mut self) {
+        let rects = self.get_param_rects();
+        for (i, s_opt) in self.sliders.iter_mut().enumerate() {
+            if let Some(s) = s_opt {
+                let r = rects[i];
+                let track_x = self.x + 100.0;
+                let track_w = (self.w - 100.0 - 20.0).max(10.0);
+                let track_y = r.1 + 4.0;
+                let track_h = 12.0;
+                s.set_rect(track_x, track_y, track_w, track_h);
+            }
+        }
+        for (i, f_opt) in self.float3s.iter_mut().enumerate() {
+            if let Some(f) = f_opt {
+                let r = rects[i];
+                f.set_rect(r.0, r.1, r.2, r.3);
+            }
+        }
+        for (i, sb_opt) in self.spinboxes.iter_mut().enumerate() {
+            if let Some(sb) = sb_opt {
+                let r = rects[i];
+                let box_x = self.x + 100.0;
+                let box_w = (self.w - 100.0 - 16.0).max(10.0);
+                sb.set_rect(box_x, r.1, box_w, r.3);
+            }
+        }
+    }
 }
 
 fn parse_slider_range(ptype: &str) -> (f32, f32) {
-    if ptype.starts_with("slider:") {
+    if ptype.starts_with("slider:") || ptype.starts_with("float3:") {
         let parts: Vec<&str> = ptype.split(':').collect();
         if parts.len() >= 3 {
             if let (Ok(min), Ok(max)) = (parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
@@ -1410,13 +1474,67 @@ fn parse_slider_range(ptype: &str) -> (f32, f32) {
     (0.0, 2.0)
 }
 
+fn parse_spinbox_range(ptype: &str) -> (i32, i32, i32) {
+    if ptype.starts_with("spinbox:") {
+        let parts: Vec<&str> = ptype.split(':').collect();
+        if parts.len() >= 4 {
+            if let (Ok(min), Ok(max), Ok(step)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>(), parts[3].parse::<i32>()) {
+                return (min, max, step);
+            }
+        } else if parts.len() == 3 {
+            if let (Ok(min), Ok(max)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) {
+                return (min, max, 1);
+            }
+        }
+    }
+    (0, 10000, 1)
+}
+
+fn parse_float3_value(val_str: &str, min: f32, max: f32) -> [f32; 3] {
+    let mut out = [0.5, 0.5, 0.5];
+    let parts: Vec<&str> = val_str
+        .split(|c| c == ':' || c == ',' || c == ' ')
+        .filter(|s| !s.is_empty())
+        .collect();
+    for i in 0..3 {
+        if i < parts.len() {
+            if let Ok(v) = parts[i].parse::<f32>() {
+                let range = max - min;
+                if range != 0.0 {
+                    out[i] = ((v - min) / range).clamp(0.0, 1.0);
+                } else {
+                    out[i] = 0.0;
+                }
+            }
+        }
+    }
+    out
+}
+
 impl Widget for ParametersBg {
     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 color(&self) -> [f32; 4] { colors::PARAM_BG }
+    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.update_slider_rects();
+    }
+    fn color(&self) -> [f32; 4] {
+        if !self.visible {
+            return [0.0, 0.0, 0.0, 0.0];
+        }
+        colors::PARAM_BG
+    }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
+    fn set_visible(&mut self, visible: bool) {
+        self.visible = visible;
+    }
+    fn visible(&self) -> bool {
+        self.visible
+    }
     fn hit_test(&self, px: f32, py: f32) -> bool {
+        if !self.visible {
+            return false;
+        }
         if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
             return false;
         }
@@ -1424,11 +1542,109 @@ impl Widget for ParametersBg {
     }
 
     fn set_display_params(&mut self, params: &[(String, String, String)]) {
-        self.display_params = params.to_vec();
-        self.focused_param = None;
+        let mut layout_changed = self.display_params.len() != params.len();
+        if !layout_changed {
+            for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
+                if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
+                    layout_changed = true;
+                    break;
+                }
+            }
+        }
+
+        if layout_changed {
+            self.display_params = params.to_vec();
+            self.focused_param = None;
+            self.sliders = self.display_params.iter().map(|p| {
+                if p.2.starts_with("slider") {
+                    let val = p.1.parse::<f32>().unwrap_or(0.0);
+                    let (min, max) = parse_slider_range(&p.2);
+                    let t = if max - min != 0.0 {
+                        ((val - min) / (max - min)).clamp(0.0, 1.0)
+                    } else {
+                        0.0
+                    };
+                    Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
+                } else {
+                    None
+                }
+            }).collect();
+            self.float3s = self.display_params.iter().map(|p| {
+                if p.2.starts_with("float3") {
+                    let (min, max) = parse_slider_range(&p.2);
+                    let vals = parse_float3_value(&p.1, min, max);
+                    Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
+                } else {
+                    None
+                }
+            }).collect();
+            self.spinboxes = self.display_params.iter().map(|p| {
+                if p.2.starts_with("spinbox") {
+                    let (min, max, step) = parse_spinbox_range(&p.2);
+                    let val = p.1.parse::<i32>().unwrap_or(min);
+                    Some(Spinbox::new(val, min, max, step))
+                } else {
+                    None
+                }
+            }).collect();
+        } else {
+            for (i, p_new) in params.iter().enumerate() {
+                if Some(i) != self.focused_param && Some(i) != self.dragging_param {
+                    self.display_params[i].1 = p_new.1.clone();
+                    if let Some(ref mut s) = self.sliders[i] {
+                        let val = p_new.1.parse::<f32>().unwrap_or(0.0);
+                        let (min, max) = parse_slider_range(&p_new.2);
+                        let t = if max - min != 0.0 {
+                            ((val - min) / (max - min)).clamp(0.0, 1.0)
+                        } else {
+                            0.0
+                        };
+                        s.set_value(t);
+                    } else if let Some(ref mut f) = self.float3s[i] {
+                        let (min, max) = parse_slider_range(&p_new.2);
+                        let vals = parse_float3_value(&p_new.1, min, max);
+                        f.set_values(vals);
+                    } else if let Some(ref mut sb) = self.spinboxes[i] {
+                        if !sb.editing {
+                            let (min, max, _step) = parse_spinbox_range(&p_new.2);
+                            let val = p_new.1.parse::<i32>().unwrap_or(min);
+                            sb.value = val;
+                        }
+                    }
+                }
+            }
+        }
+        self.update_slider_rects();
     }
 
     fn unfocus(&mut self) {
+        if let Some(idx) = self.focused_param {
+            if idx < self.display_params.len() {
+                let p = &mut self.display_params[idx];
+                if p.2.starts_with("spinbox") {
+                    if let Some(sb) = &mut self.spinboxes[idx] {
+                        sb.unfocus();
+                        p.1 = sb.value.to_string();
+                    }
+                } else if p.2.starts_with("slider") {
+                    if let Some(s) = &mut self.sliders[idx] {
+                        s.unfocus();
+                        let (min, max) = parse_slider_range(&p.2);
+                        let new_val = min + s.value * (max - min);
+                        p.1 = format!("{:.2}", new_val);
+                    }
+                } else if p.2.starts_with("float3") {
+                    if let Some(f) = &mut self.float3s[idx] {
+                        f.unfocus();
+                        let (min, max) = parse_slider_range(&p.2);
+                        let val0 = min + f.values[0] * (max - min);
+                        let val1 = min + f.values[1] * (max - min);
+                        let val2 = min + f.values[2] * (max - min);
+                        p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+                    }
+                }
+            }
+        }
         self.focused_param = None;
     }
 
@@ -1437,7 +1653,8 @@ impl Widget for ParametersBg {
     }
 
     fn draggable(&self) -> bool {
-        self.display_params.iter().any(|p| p.2.starts_with("slider"))
+        self.dragging_param.is_some() 
+            || self.display_params.iter().any(|p| p.2.starts_with("slider") || p.2.starts_with("float3"))
     }
 
     fn is_dragging(&self) -> bool {
@@ -1451,48 +1668,51 @@ impl Widget for ParametersBg {
                 let r = rects[i];
                 let row_y = r.1;
                 if py >= row_y - 2.0 && py <= row_y + 18.0 {
-                    let val = p.1.parse::<f32>().unwrap_or(0.0);
-                    let (min, max) = parse_slider_range(&p.2);
-                    let denom = max - min;
-                    let t = if denom != 0.0 {
-                        ((val - min) / denom).clamp(0.0, 1.0)
-                    } else {
-                        0.0
-                    };
-                    let track_x = self.x + 100.0;
-                    let track_w = (self.w - 100.0 - 20.0).max(10.0);
-                    let thumb_size = 10.0;
-                    let thumb_x = track_x + t * (track_w - thumb_size);
-
-                    let click_offset = px - thumb_x;
-                    if click_offset >= 0.0 && click_offset <= thumb_size {
-                        self.drag_offset = click_offset;
-                    } else {
-                        self.drag_offset = thumb_size / 2.0;
+                    if let Some(s) = &mut self.sliders[i] {
+                        s.drag_begin(px, py);
+                        self.dragging_param = Some(i);
+                        break;
+                    }
+                }
+            } else if p.2.starts_with("float3") {
+                let r = rects[i];
+                if py >= r.1 && py <= r.1 + r.3 {
+                    if let Some(f) = &mut self.float3s[i] {
+                        if f.mouse_input(MouseButton::Left, ElementState::Pressed, px, py) {
+                            self.dragging_param = Some(i);
+                            break;
+                        }
                     }
-                    self.dragging_param = Some(i);
-                    break;
                 }
             }
         }
     }
 
-    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+    fn drag_update(&mut self, px: f32, py: f32) -> bool {
         if let Some(i) = self.dragging_param {
-            let track_x = self.x + 100.0;
-            let track_w = (self.w - 100.0 - 20.0).max(10.0);
-            let thumb_size = 10.0;
-            let range = track_w - thumb_size;
-            if range > 0.0 {
-                let raw = (px - self.drag_offset - track_x) / range;
-                let t = raw.clamp(0.0, 1.0);
-                let (min, max) = parse_slider_range(&self.display_params[i].2);
-                let new_val = min + t * (max - min);
-                let old_val = &self.display_params[i].1;
-                let new_val_str = format!("{:.2}", new_val);
-                if *old_val != new_val_str {
-                    self.display_params[i].1 = new_val_str;
-                    return true;
+            if let Some(s) = &mut self.sliders[i] {
+                if s.drag_update(px, py) {
+                    let (min, max) = parse_slider_range(&self.display_params[i].2);
+                    let new_val = min + s.value * (max - min);
+                    let old_val = &self.display_params[i].1;
+                    let new_val_str = format!("{:.2}", new_val);
+                    if *old_val != new_val_str {
+                        self.display_params[i].1 = new_val_str;
+                        return true;
+                    }
+                }
+            } else if let Some(f) = &mut self.float3s[i] {
+                if f.drag_update(px, py) {
+                    let (min, max) = parse_slider_range(&self.display_params[i].2);
+                    let val0 = min + f.values[0] * (max - min);
+                    let val1 = min + f.values[1] * (max - min);
+                    let val2 = min + f.values[2] * (max - min);
+                    let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+                    let old_val = &self.display_params[i].1;
+                    if *old_val != new_val_str {
+                        self.display_params[i].1 = new_val_str;
+                        return true;
+                    }
                 }
             }
         }
@@ -1500,25 +1720,84 @@ impl Widget for ParametersBg {
     }
 
     fn drag_end(&mut self) {
-        self.dragging_param = None;
+        if let Some(i) = self.dragging_param.take() {
+            if let Some(s) = &mut self.sliders[i] {
+                s.drag_end();
+            } else if let Some(f) = &mut self.float3s[i] {
+                f.drag_end();
+            }
+        }
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        self.mouse_pos = Some((px, py));
+        true
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button == MouseButton::Left && state == ElementState::Pressed {
             let rects = self.get_param_rects();
-            let mut clicked_any_code = false;
-            for (i, p) in self.display_params.iter().enumerate() {
+            let mut clicked_any_focusable = false;
+            for (i, p) in self.display_params.iter_mut().enumerate() {
                 if p.2 == "code" {
                     let r = rects[i];
                     if px >= r.0 && px <= r.0 + r.2 && py >= r.1 + 18.0 && py <= r.1 + r.3 {
                         self.focused_param = Some(i);
-                        clicked_any_code = true;
+                        clicked_any_focusable = true;
+                        break;
+                    }
+                } else if p.2 == "text" {
+                    let box_x = self.x + 100.0;
+                    let box_w = (self.w - 100.0 - 16.0).max(10.0);
+                    let r = rects[i];
+                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
+                        self.focused_param = Some(i);
+                        clicked_any_focusable = true;
                         break;
                     }
+                } else if p.2.starts_with("spinbox") {
+                    if let Some(sb) = &mut self.spinboxes[i] {
+                        if sb.mouse_input(button, state, px, py) {
+                            p.1 = sb.value.to_string();
+                            if sb.editing {
+                                self.focused_param = Some(i);
+                                clicked_any_focusable = true;
+                            } else {
+                                self.unfocus();
+                            }
+                            return true;
+                        }
+                    }
+                } else if p.2.starts_with("slider") {
+                    let r = rects[i];
+                    if py >= r.1 && py <= r.1 + r.3 {
+                        if let Some(s) = &mut self.sliders[i] {
+                            if s.mouse_input(button, state, px, py) {
+                                if s.editing {
+                                    self.focused_param = Some(i);
+                                    clicked_any_focusable = true;
+                                }
+                                break;
+                            }
+                        }
+                    }
+                } else if p.2.starts_with("float3") {
+                    let r = rects[i];
+                    if py >= r.1 && py <= r.1 + r.3 {
+                        if let Some(f) = &mut self.float3s[i] {
+                            if f.mouse_input(button, state, px, py) {
+                                if f.editing_idx.is_some() {
+                                    self.focused_param = Some(i);
+                                    clicked_any_focusable = true;
+                                }
+                                break;
+                            }
+                        }
+                    }
                 }
             }
-            if !clicked_any_code {
-                self.focused_param = None;
+            if !clicked_any_focusable {
+                self.unfocus();
             }
             return true;
         }
@@ -1529,26 +1808,84 @@ impl Widget for ParametersBg {
         if let Some(idx) = self.focused_param {
             if event.state == ElementState::Pressed {
                 let p = &mut self.display_params[idx];
-                match &event.logical_key {
-                    Key::Named(NamedKey::Backspace) => {
-                        if !p.1.is_empty() {
-                            p.1.pop();
+                if p.2 == "code" {
+                    match &event.logical_key {
+                        Key::Named(NamedKey::Backspace) => {
+                            if !p.1.is_empty() {
+                                p.1.pop();
+                                return true;
+                            }
+                        }
+                        Key::Named(NamedKey::Enter) => {
+                            p.1.push('\n');
+                            return true;
+                        }
+                        Key::Named(NamedKey::Escape) => {
+                            self.focused_param = None;
+                            return true;
+                        }
+                        Key::Character(s) => {
+                            p.1.push_str(s);
                             return true;
                         }
+                        _ => {}
                     }
-                    Key::Named(NamedKey::Enter) => {
-                        p.1.push('\n');
-                        return true;
+                } else if p.2 == "text" {
+                    match &event.logical_key {
+                        Key::Named(NamedKey::Backspace) => {
+                            if !p.1.is_empty() {
+                                p.1.pop();
+                                return true;
+                            }
+                        }
+                        Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => {
+                            self.focused_param = None;
+                            return true;
+                        }
+                        Key::Character(s) => {
+                            p.1.push_str(s);
+                            return true;
+                        }
+                        _ => {}
                     }
-                    Key::Named(NamedKey::Escape) => {
-                        self.focused_param = None;
-                        return true;
+                } else if p.2.starts_with("spinbox") {
+                    if let Some(sb) = &mut self.spinboxes[idx] {
+                        if sb.keyboard_input(event) {
+                            if !sb.editing {
+                                p.1 = sb.value.to_string();
+                                self.focused_param = None;
+                            } else {
+                                p.1 = sb.edit_buffer.clone();
+                            }
+                            return true;
+                        }
                     }
-                    Key::Character(s) => {
-                        p.1.push_str(s);
-                        return true;
+                } else if p.2.starts_with("slider") {
+                    if let Some(s) = &mut self.sliders[idx] {
+                        if s.keyboard_input(event) {
+                            let (min, max) = parse_slider_range(&p.2);
+                            let new_val = min + s.value * (max - min);
+                            p.1 = format!("{:.2}", new_val);
+                            if !s.editing {
+                                self.focused_param = None;
+                            }
+                            return true;
+                        }
+                    }
+                } else if p.2.starts_with("float3") {
+                    if let Some(f) = &mut self.float3s[idx] {
+                        if f.keyboard_input(event) {
+                            let (min, max) = parse_slider_range(&p.2);
+                            let val0 = min + f.values[0] * (max - min);
+                            let val1 = min + f.values[1] * (max - min);
+                            let val2 = min + f.values[2] * (max - min);
+                            p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+                            if f.editing_idx.is_none() {
+                                self.focused_param = None;
+                            }
+                            return true;
+                        }
                     }
-                    _ => {}
                 }
             }
         }
@@ -1563,19 +1900,75 @@ impl Widget for ParametersBg {
                 let r = rects[i];
                 let row_y = r.1;
                 if py >= row_y - 2.0 && py <= row_y + 18.0 && px >= self.x && px <= self.x + self.w {
-                    let val = p.1.parse::<f32>().unwrap_or(0.0);
-                    let (min, max) = parse_slider_range(&p.2);
-                    let scroll_amount = match delta {
-                        MouseScrollDelta::LineDelta(_x, y) => *y,
-                        MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
-                    };
-                    let step = (max - min) * 0.02;
-                    let new_val = (val + scroll_amount * step).clamp(min, max);
-                    let old_val = &p.1;
-                    let new_val_str = format!("{:.2}", new_val);
-                    if *old_val != new_val_str {
-                        p.1 = new_val_str;
-                        changed = true;
+                    if let Some(s) = &mut self.sliders[i] {
+                        let was_scroll = s.scroll_enabled;
+                        s.set_scroll(true);
+                        if s.mouse_wheel(delta, px, py) {
+                            let (min, max) = parse_slider_range(&p.2);
+                            let new_val = min + s.value * (max - min);
+                            let old_val = &p.1;
+                            let new_val_str = format!("{:.2}", new_val);
+                            if *old_val != new_val_str {
+                                p.1 = new_val_str;
+                                changed = true;
+                            }
+                        }
+                        s.set_scroll(was_scroll);
+                    }
+                }
+            } else if p.2.starts_with("float3") {
+                let r = rects[i];
+                let row_y = r.1;
+                if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
+                    if let Some(f) = &mut self.float3s[i] {
+                        let rects_inner = f.get_row_rects();
+                        for j in 0..3 {
+                            let r_inner = rects_inner[j];
+                            if py >= r_inner.1 && py <= r_inner.1 + r_inner.3 {
+                                let scroll_amount = match delta {
+                                    MouseScrollDelta::LineDelta(_x, y) => *y,
+                                    MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
+                                };
+                                let step = 0.02;
+                                let new_val = (f.values[j] - scroll_amount * step).clamp(0.0, 1.0);
+                                if (new_val - f.values[j]).abs() > 0.0001 {
+                                    f.values[j] = new_val;
+                                    if f.editing_idx == Some(j) {
+                                        let scaled_val = f.mins[j] + f.values[j] * (f.maxs[j] - f.mins[j]);
+                                        f.edit_buffer = format!("{:.2}", scaled_val);
+                                    }
+                                    let (min, max) = parse_slider_range(&p.2);
+                                    let val0 = min + f.values[0] * (max - min);
+                                    let val1 = min + f.values[1] * (max - min);
+                                    let val2 = min + f.values[2] * (max - min);
+                                    let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+                                    if p.1 != new_val_str {
+                                        p.1 = new_val_str;
+                                        changed = true;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if p.2.starts_with("spinbox") {
+                let r = rects[i];
+                let row_y = r.1;
+                if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
+                    if let Some(sb) = &mut self.spinboxes[i] {
+                        let scroll_amount = match delta {
+                            MouseScrollDelta::LineDelta(_x, y) => *y as i32,
+                            MouseScrollDelta::PixelDelta(pos) => {
+                                let dy = pos.y;
+                                if dy > 0.0 { 1 } else if dy < 0.0 { -1 } else { 0 }
+                            }
+                        };
+                        let new_val = (sb.value + scroll_amount * sb.step).clamp(sb.min, sb.max);
+                        if sb.value != new_val {
+                            sb.value = new_val;
+                            p.1 = new_val.to_string();
+                            changed = true;
+                        }
                     }
                 }
             }
@@ -1584,36 +1977,75 @@ impl Widget for ParametersBg {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
         let mut quads = Vec::new();
         let rects = self.get_param_rects();
-        for (i, p) in self.display_params.iter().enumerate() {
-            let r = rects[i];
-            if p.2.starts_with("slider") {
-                let val = p.1.parse::<f32>().unwrap_or(0.0);
-                let (min, max) = parse_slider_range(&p.2);
-                let denom = max - min;
-                let t = if denom != 0.0 {
-                    ((val - min) / denom).clamp(0.0, 1.0)
-                } else {
-                    0.0
-                };
-                let track_x = self.x + 100.0;
-                let track_w = (self.w - 100.0 - 20.0).max(10.0);
-                let track_y = r.1 + 6.0;
-                let track_h = 4.0;
 
-                let thumb_size = 10.0;
-                let thumb_x = track_x + t * (track_w - thumb_size);
-                let thumb_y = r.1 + 3.0;
+        // Find sections and their ranges
+        let mut sections = Vec::new();
+        let mut current_section: Option<(usize, usize)> = None;
+        let mut in_section = false;
+        for (i, p) in self.display_params.iter().enumerate() {
+            if p.2 == "section" {
+                if let Some((start, end)) = current_section {
+                    sections.push((start, end));
+                }
+                current_section = None;
+                in_section = true;
+            } else {
+                if in_section {
+                    if let Some((start, ref mut end)) = current_section {
+                        *end = i;
+                    } else {
+                        current_section = Some((i, i));
+                    }
+                }
+            }
+        }
+        if let Some((start, end)) = current_section {
+            sections.push((start, end));
+        }
 
-                quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
+        // Draw section border boxes
+        for (start, end) in sections {
+            if start <= end && start < rects.len() && end < rects.len() {
+                let r_start = rects[start];
+                let r_end = rects[end];
+                let bx = self.x + 4.0;
+                let bw = self.w - 8.0;
+                let by = r_start.1 - 4.0;
+                let bh = (r_end.1 + r_end.3 + 4.0) - by;
+                
+                let border_color = [0.18, 0.18, 0.27, 1.0];
+                let border_t = 1.0;
+                
+                // Top border
+                quads.push((bx, by, bw, border_t, border_color));
+                // Bottom border
+                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+                // Left border
+                quads.push((bx, by, border_t, bh, border_color));
+                // Right border
+                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+            }
+        }
 
-                let thumb_color = if self.dragging_param == Some(i) {
-                    colors::SLIDER_THUMB_DRAG
-                } else {
-                    colors::SLIDER_THUMB
-                };
-                quads.push((thumb_x, thumb_y, thumb_size, thumb_size, thumb_color));
+        for (i, p) in self.display_params.iter().enumerate() {
+            let r = rects[i];
+            if p.2.starts_with("slider") {
+                if let Some(s) = &self.sliders[i] {
+                    let (sx, sy, sw, sh) = s.rect();
+                    quads.push((sx, sy, sw, sh, s.color()));
+                    quads.extend(s.extra_quads());
+                }
+            } else if p.2 == "section" {
+                // Section header line is handled by the border box top border now
+            } else if p.2.starts_with("float3") {
+                if let Some(f) = &self.float3s[i] {
+                    quads.extend(f.extra_quads());
+                }
             } else if p.2 == "code" {
                 quads.push((r.0, r.1 + 18.0, r.2, r.3 - 18.0, [0.08, 0.08, 0.10, 1.0]));
                 let border_color = if self.focused_param == Some(i) {
@@ -1626,42 +2058,112 @@ impl Widget for ParametersBg {
                 quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
                 quads.push((bx, by, 1.0, bh, border_color));
                 quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
+            } else if p.2 == "text" {
+                let box_x = self.x + 100.0;
+                let box_w = (self.w - 100.0 - 16.0).max(10.0);
+                quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
+                let border_color = if self.focused_param == Some(i) {
+                    [0.25, 0.45, 0.85, 1.0]
+                } else {
+                    [0.20, 0.20, 0.25, 1.0]
+                };
+                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
+                let border_t = 1.0;
+                quads.push((bx, by, bw, border_t, border_color));
+                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+                quads.push((bx, by, border_t, bh, border_color));
+                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+            } else if p.2.starts_with("spinbox") {
+                if let Some(sb) = &self.spinboxes[i] {
+                    quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
+                    quads.extend(sb.extra_quads());
+                }
             }
         }
         quads
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible {
+            return Vec::new();
+        }
         let rects = self.get_param_rects();
-        self.display_params.iter().enumerate().map(|(i, (name, value, ptype))| {
+        let mut labels = Vec::new();
+        for (i, (name, value, ptype)) in self.display_params.iter().enumerate() {
             let r = rects[i];
             if ptype.starts_with("slider") {
-                let val = value.parse::<f32>().unwrap_or(0.0);
-                TextLabel {
-                    text: format!("{}: {:.2}", name, val),
+                labels.push(TextLabel {
+                    text: name.clone(),
                     x: self.x + 8.0,
                     y: r.1,
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
+                });
+                if let Some(s) = &self.sliders[i] {
+                    labels.extend(s.text_labels());
+                }
+            } else if ptype.starts_with("float3") {
+                if let Some(f) = &self.float3s[i] {
+                    labels.extend(f.text_labels());
                 }
+            } else if ptype == "section" {
+                labels.push(TextLabel {
+                    text: name.clone(),
+                    x: self.x + 12.0,
+                    y: r.1 + 2.0,
+                    font_size: 13.0,
+                    color: [0xee, 0xee, 0xf0],
+                });
             } else if ptype == "code" {
-                TextLabel {
+                labels.push(TextLabel {
                     text: format!("{}:\n{}", name, value),
                     x: self.x + 12.0,
                     y: r.1,
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
+                });
+            } else if ptype.starts_with("spinbox") {
+                labels.push(TextLabel {
+                    text: name.clone(),
+                    x: self.x + 8.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xaa, 0xaa, 0xbb],
+                });
+                if let Some(sb) = &self.spinboxes[i] {
+                    labels.extend(sb.text_labels());
                 }
+            } else if ptype == "text" {
+                labels.push(TextLabel {
+                    text: name.clone(),
+                    x: self.x + 8.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xaa, 0xaa, 0xbb],
+                });
+                let val_text = if self.focused_param == Some(i) {
+                    format!("{}|", value)
+                } else {
+                    value.clone()
+                };
+                labels.push(TextLabel {
+                    text: val_text,
+                    x: self.x + 106.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xee, 0xee, 0xf0],
+                });
             } else {
-                TextLabel {
+                labels.push(TextLabel {
                     text: format!("{}: {}", name, value),
                     x: self.x + 8.0,
                     y: r.1,
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
-                }
+                });
             }
-        }).collect()
+        }
+        labels
     }
 }
 
@@ -1706,6 +2208,9 @@ pub struct MenuBar {
     pub center_items: bool,
     pub curved_circle: Option<(f32, f32, f32)>,
     pub title_pos: Option<(f32, f32)>,
+    pub title_buf: Option<glyphon::Buffer>,
+    pub curved_title_char_bufs: Vec<glyphon::Buffer>,
+    pub network_opacity: f32,
 }
 
 impl MenuBar {
@@ -1730,6 +2235,9 @@ impl MenuBar {
             center_items: false,
             curved_circle: None,
             title_pos: None,
+            title_buf: None,
+            curved_title_char_bufs: Vec::new(),
+            network_opacity: 1.0,
         }
     }
 
@@ -1910,13 +2418,21 @@ impl Widget for MenuBar {
         }
     }
 
+    fn set_network_opacity(&mut self, opacity: f32) {
+        self.network_opacity = opacity;
+    }
+
     fn color(&self) -> [f32; 4] {
         if !self.visible {
             [0.0, 0.0, 0.0, 0.0]
         } else if self.focused {
-            colors::PANEL_MENU_FOCUSED
+            let mut c = colors::PANEL_MENU_FOCUSED;
+            c[3] *= self.network_opacity;
+            c
         } else {
-            colors::PANEL_MENU_BG
+            let mut c = colors::PANEL_MENU_BG;
+            c[3] *= self.network_opacity;
+            c
         }
     }
 
@@ -2024,17 +2540,25 @@ impl Widget for MenuBar {
                 changed = true;
             }
         }
+        if !self.is_menu_open() {
+            self.unfocus();
+        }
         changed
     }
 
     fn focus(&mut self) {
         if self.is_menu_open() {
+            self.focused = true;
             for menu in &mut self.menus {
                 if menu.is_menu_open() {
                     menu.focus();
                     return;
                 }
             }
+        } else {
+            self.focused = false;
+            focus::clear_if_matches(self);
+            return;
         }
         self.focused = true;
         focus::set_focused(self);
@@ -2048,6 +2572,10 @@ impl Widget for MenuBar {
         }
     }
 
+    fn focused(&self) -> bool {
+        self.focused || self.is_menu_open()
+    }
+
     fn set_selected(&mut self, selected: bool) {
         self.focused = selected;
         if !selected {
@@ -2085,6 +2613,7 @@ impl Widget for MenuBar {
         if let Some(menu) = self.menus.get_mut(menu_idx) {
             menu.items = items.to_vec();
             menu.item_checked = vec![Some(false); items.len()];
+            menu.item_bufs.clear();
         }
     }
 
@@ -2092,6 +2621,34 @@ impl Widget for MenuBar {
         self.visible
     }
 
+    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+        if !self.visible {
+            return None;
+        }
+        for (idx, menu) in self.menus.iter().enumerate() {
+            if menu.hit_test(px, py) {
+                let mut formatted_items = Vec::new();
+                for (i, item) in menu.items.iter().enumerate() {
+                    let checked = menu.item_checked.get(i).and_then(|&v| v);
+                    let prefix = match checked {
+                        Some(true) => "✓ ",
+                        Some(false) => "  ",
+                        None => "",
+                    };
+                    formatted_items.push(format!("{}{}", prefix, item));
+                }
+                return Some((idx, menu.active_title().to_string(), formatted_items, menu.base.x, menu.base.y, menu.base.w, menu.base.h));
+            }
+        }
+        None
+    }
+
+    fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+        if let Some(menu) = self.menus.get_mut(menu_idx) {
+            menu.clicked_item = Some(item_idx);
+        }
+    }
+
     fn is_menu_open(&self) -> bool {
         self.visible && self.menus.iter().any(|m| m.is_menu_open())
     }
@@ -2118,6 +2675,104 @@ impl Widget for MenuBar {
         arcs
     }
 
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        if !self.visible {
+            return;
+        }
+        if !self.title.is_empty() {
+            if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
+                if self.curved_title_char_bufs.len() != self.title.chars().count() {
+                    self.curved_title_char_bufs = self.title.chars()
+                        .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
+                        .collect();
+                }
+                self.title_buf = None;
+            } else {
+                if self.title_buf.is_none() {
+                    self.title_buf = Some(make_widget_text_buffer(fs, &self.title, 12.0, "Outfit"));
+                }
+                self.curved_title_char_bufs.clear();
+            }
+        } else {
+            self.title_buf = None;
+            self.curved_title_char_bufs.clear();
+        }
+        for menu in &mut self.menus {
+            menu.prepare_text(fs);
+        }
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut items = Vec::new();
+        let color = glyphon::Color::rgb(0xaa, 0xaa, 0xbb);
+
+        if let Some((ccx, ccy, ccr)) = self.curved_circle {
+            let r_mid = ccr - self.h / 2.0;
+            let mut total_width = 8.0;
+            if !self.title.is_empty() {
+                total_width += self.title.len() as f32 * 7.5 + 24.0;
+            }
+            for menu in &self.menus {
+                total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+            }
+            let total_angular_width = total_width / r_mid;
+            let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+            let mut current_angle = start_angle;
+
+            if !self.title.is_empty() {
+                let title_w = self.title.len() as f32 * 7.5 + 24.0;
+                let dtheta_title = title_w / r_mid;
+
+                let char_widths: Vec<f32> = self.title.chars().map(|c| {
+                    TextLabel::estimate_width(&c.to_string(), 12.0)
+                }).collect();
+                let total_chars_width: f32 = char_widths.iter().sum();
+                let mid_angle = (current_angle + current_angle + dtheta_title) / 2.0;
+                let angular_width = total_chars_width / r_mid;
+                let text_start_angle = mid_angle - angular_width / 2.0;
+                let mut cur_char_angle = text_start_angle;
+
+                for (char_idx, c_buf) in self.curved_title_char_bufs.iter().enumerate() {
+                    let cw = char_widths[char_idx];
+                    let dtheta = cw / r_mid;
+                    let char_center_angle = cur_char_angle + dtheta / 2.0;
+
+                    let tx = ccx + r_mid * char_center_angle.cos() - cw / 2.0;
+                    let ty = ccy + r_mid * char_center_angle.sin() - 12.0 / 2.0;
+
+                    items.push((c_buf, tx, ty, color));
+                    cur_char_angle += dtheta;
+                }
+                current_angle += dtheta_title;
+            }
+        } else {
+            let mut start_x = 8.0;
+            if self.center_items {
+                let mut total_width = 8.0;
+                if !self.title.is_empty() {
+                    total_width += self.title.len() as f32 * 7.5 + 24.0;
+                }
+                for menu in &self.menus {
+                    total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+                }
+                if self.w > total_width {
+                    start_x = (self.w - total_width) / 2.0;
+                }
+            }
+            if let Some(ref title_buf) = self.title_buf {
+                items.push((title_buf, self.x + start_x, self.y + 7.0, color));
+            }
+        }
+
+        for menu in &self.menus {
+            items.extend(menu.get_text_items());
+        }
+        items
+    }
+
     fn text_labels(&self) -> Vec<TextLabel> {
         if !self.visible {
             return Vec::new();
@@ -2213,41 +2868,45 @@ impl Drop for MenuBar {
 
 #[derive(Debug, Clone)]
 pub struct Menu {
-    x: f32, y: f32, w: f32, h: f32,
+    pub base: WidgetBase,
     pub title: String,
     pub vertical_title: String,
     pub items: Vec<String>,
     pub item_checked: Vec<Option<bool>>,
     pub open: bool,
     pub vertical: bool,
-    pub focused: bool,
-    hovering: bool,
     hovered_item: Option<usize>,
     clicked_item: Option<usize>,
     was_open: Option<usize>,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
     pub curved_arc: Option<(f32, f32, f32, f32, f32, f32)>,
+    pub title_buf: Option<glyphon::Buffer>,
+    pub item_bufs: Vec<glyphon::Buffer>,
+    pub check_buf: Option<glyphon::Buffer>,
+    pub curved_char_bufs: Vec<glyphon::Buffer>,
 }
 
 impl Menu {
     pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            base: WidgetBase::new(),
             title: title.to_string(),
             vertical_title: vertical_title.to_string(),
             items: items.to_vec(),
             item_checked: vec![None; items.len()],
             open: false,
             vertical: false,
-            focused: false,
-            hovering: false,
             hovered_item: None,
             clicked_item: None,
             was_open: None,
             parent: None,
             children: Vec::new(),
             curved_arc: None,
+            title_buf: None,
+            item_bufs: Vec::new(),
+            check_buf: None,
+            curved_char_bufs: Vec::new(),
         }
     }
 
@@ -2267,47 +2926,30 @@ impl Menu {
         }
         let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
         let dx = if self.vertical {
-            self.x + self.w
+            self.base.x + self.base.w
         } else {
-            self.x
+            self.base.x
         };
         let dy = if self.vertical {
-            self.y
+            self.base.y
         } else {
-            self.y + self.h
+            self.base.y + self.base.h
         };
         (dx, dy, dw, dh)
     }
 }
-
 impl Widget for Menu {
-    fn rect(&self) -> (f32, f32, f32, f32) {
-        (self.x, self.y, self.w, self.h)
-    }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
     fn label(&self) -> Option<String> {
         Some(self.active_title().to_string())
     }
 
-    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 color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
 
-    fn set_hovered(&mut self, v: bool) {
-        self.hovering = v;
-    }
-
-    fn hovered(&self) -> bool {
-        self.hovering
-    }
-
     fn hit_test(&self, px: f32, py: f32) -> bool {
         if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
             return false;
@@ -2349,8 +2991,8 @@ impl Widget for Menu {
 
     fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
         self.was_open = None;
-        let was_hovering = self.hovering;
-        self.hovering = self.hit_test(px, py);
+        let was_hovering = self.base.hovered;
+        self.base.hovered = self.hit_test(px, py);
         let old_item = self.hovered_item;
         self.hovered_item = None;
 
@@ -2364,7 +3006,7 @@ impl Widget for Menu {
             }
         }
 
-        was_hovering != self.hovering || old_item != self.hovered_item
+        was_hovering != self.base.hovered || old_item != self.hovered_item
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
@@ -2402,7 +3044,7 @@ impl Widget for Menu {
 
     fn focus(&mut self) {
         self.open = true;
-        self.focused = true;
+        self.base.focused = true;
         focus::set_focused(self);
     }
 
@@ -2411,13 +3053,13 @@ impl Widget for Menu {
             self.was_open = Some(0);
         }
         self.open = false;
-        self.focused = false;
+        self.base.focused = false;
         focus::clear_if_matches(self);
         self.hovered_item = None;
     }
 
     fn set_selected(&mut self, selected: bool) {
-        self.focused = selected;
+        self.base.focused = selected;
         if !selected {
             self.open = false;
             self.was_open = None;
@@ -2433,6 +3075,7 @@ impl Widget for Menu {
     fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
         if item_idx < self.item_checked.len() {
             self.item_checked[item_idx] = Some(checked);
+            self.item_bufs.clear();
         }
     }
 
@@ -2440,11 +3083,17 @@ impl Widget for Menu {
         self.open
     }
 
+    fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+        if self.curved_arc.is_some() {
+            None
+        } else {
+            let hc = self.highlight_color()?;
+            Some((self.base.x, self.base.y, self.base.w, self.base.h, hc))
+        }
+    }
+
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        if self.hovering && !self.open && self.curved_arc.is_none() {
-            quads.push((self.x, self.y, self.w, self.h, colors::PANEL_MENU_HOVER));
-        }
         if self.open {
             let (dx, dy, dw, dh) = self.dropdown_rect();
             if dh > 0.0 {
@@ -2460,7 +3109,7 @@ impl Widget for Menu {
     fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
         let mut arcs = Vec::new();
         if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
-            if self.hovering && !self.open {
+            if self.base.hovered && !self.open {
                 arcs.push((cx, cy, r, thickness, start_angle, end_angle, colors::PANEL_MENU_HOVER));
             }
         }
@@ -2481,8 +3130,8 @@ impl Widget for Menu {
         } else {
             labels.push(TextLabel {
                 text: self.active_title().to_string(),
-                x: self.x + 8.0,
-                y: self.y + 7.0,
+                x: self.base.x + 8.0,
+                y: self.base.y + 7.0,
                 font_size: 12.0,
                 color: [0xcc, 0xcc, 0xd4],
             });
@@ -2509,7 +3158,7 @@ impl Widget for Menu {
     }
 
     fn set_visible(&mut self, visible: bool) {
-        self.hovering = false;
+        self.base.hovered = false;
         if !visible {
             self.open = false;
             self.was_open = None;
@@ -2541,6 +3190,95 @@ impl Widget for Menu {
     fn is_vertical(&self) -> bool {
         self.vertical
     }
+
+    fn focused(&self) -> bool {
+        self.base.focused
+    }
+
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        let title_text = self.active_title();
+        if let Some((_cx, _cy, _r, _thickness, _start_angle, _end_angle)) = self.curved_arc {
+            if self.curved_char_bufs.len() != title_text.chars().count() {
+                self.curved_char_bufs = title_text.chars()
+                    .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
+                    .collect();
+            }
+            self.title_buf = None;
+        } else {
+            if self.title_buf.is_none() {
+                self.title_buf = Some(make_widget_text_buffer(fs, title_text, 12.0, "Outfit"));
+            }
+            self.curved_char_bufs.clear();
+        }
+
+        if self.open {
+            if self.item_bufs.len() != self.items.len() {
+                self.item_bufs = self.items.iter().enumerate().map(|(i, item)| {
+                    let checked = self.item_checked.get(i).and_then(|&v| v);
+                    let prefix = match checked {
+                        Some(true) => "\u{2713} ",
+                        Some(false) => "  ",
+                        None => "",
+                    };
+                    let text = format!("{}{}", prefix, item);
+                    make_widget_text_buffer(fs, &text, 12.0, "Outfit")
+                }).collect();
+            }
+        } else {
+            self.item_bufs.clear();
+        }
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        let mut items = Vec::new();
+        let color = glyphon::Color::rgb(0xcc, 0xcc, 0xd4);
+
+        if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
+            let r_mid = r - thickness / 2.0;
+            let active_title = self.active_title();
+            
+            let char_widths: Vec<f32> = active_title.chars().map(|c| {
+                TextLabel::estimate_width(&c.to_string(), 12.0)
+            }).collect();
+            let total_chars_width: f32 = char_widths.iter().sum();
+            
+            let angular_width = total_chars_width / r_mid;
+            let text_start_angle = (start_angle + end_angle) / 2.0 - angular_width / 2.0;
+            let mut cur_char_angle = text_start_angle;
+            
+            for (char_idx, c_buf) in self.curved_char_bufs.iter().enumerate() {
+                if char_idx < char_widths.len() {
+                    let cw = char_widths[char_idx];
+                    let dtheta = cw / r_mid;
+                    let char_center_angle = cur_char_angle + dtheta / 2.0;
+                    
+                    let tx = cx + r_mid * char_center_angle.cos() - cw / 2.0;
+                    let ty = cy + r_mid * char_center_angle.sin() - 12.0 / 2.0;
+                    
+                    items.push((c_buf, tx, ty, color));
+                    cur_char_angle += dtheta;
+                }
+            }
+        } else {
+            if let Some(ref title_buf) = self.title_buf {
+                items.push((title_buf, self.base.x + 8.0, self.base.y + 7.0, color));
+            }
+        }
+
+        if self.open {
+            let (dx, dy, _, _) = self.dropdown_rect();
+            for (i, item_buf) in self.item_bufs.iter().enumerate() {
+                items.push((
+                    item_buf,
+                    dx + 8.0,
+                    dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
+                    color,
+                ));
+            }
+        }
+
+        items
+    }
 }
 
 unsafe impl Send for Menu {}
@@ -3316,6 +4054,11 @@ pub struct Slider {
     value: f32,
     drag_offset: f32,
     scroll_enabled: bool,
+    show_readout: bool,
+    editing: bool,
+    edit_buffer: String,
+    min: f32,
+    max: f32,
 }
 
 impl Slider {
@@ -3326,9 +4069,20 @@ impl Slider {
             value: 0.5,
             drag_offset: 0.0,
             scroll_enabled: false,
+            show_readout: false,
+            editing: false,
+            edit_buffer: String::new(),
+            min: 0.0,
+            max: 1.0,
         }
     }
 
+    pub fn with_range(mut self, min: f32, max: f32) -> Self {
+        self.min = min;
+        self.max = max;
+        self
+    }
+
     pub fn with_label(mut self, label: &str) -> Self {
         self.base.label = Some(label.to_string());
         self
@@ -3351,34 +4105,78 @@ impl Slider {
     pub fn set_value(&mut self, val: f32) {
         self.value = val.clamp(0.0, 1.0);
     }
+
+    pub fn with_readout(mut self, enabled: bool) -> Self {
+        self.show_readout = enabled;
+        self
+    }
+
+    pub fn set_readout(&mut self, enabled: bool) {
+        self.show_readout = enabled;
+    }
+
+    pub fn value(&self) -> f32 {
+        self.value
+    }
+
+    pub fn get_scaled_value(&self) -> f32 {
+        self.min + self.value * (self.max - self.min)
+    }
 }
 
 impl Widget for Slider {
     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] { colors::slider_track() }
+    fn color(&self) -> [f32; 4] {
+        if self.show_readout {
+            [0.0, 0.0, 0.0, 0.0]
+        } else {
+            colors::slider_track()
+        }
+    }
 
     fn draggable(&self) -> bool { true }
     fn is_dragging(&self) -> bool { self.dragging }
 
     fn drag_update(&mut self, px: f32, _py: f32) -> bool {
-        let (sx, _, sw, _) = self.rect();
+        let (track_x, track_w) = if self.show_readout {
+            let readout_w = 60.0;
+            let gap = 8.0;
+            let tw = (self.base.w - readout_w - gap).max(10.0);
+            (self.base.x, tw)
+        } else {
+            (self.base.x, self.base.w)
+        };
         let thumb_size = self.base.h * 0.9;
-        let range = sw - thumb_size;
-        let raw = (px - self.drag_offset - sx) / range;
-        let new_val = raw.clamp(0.0, 1.0);
-        if (new_val - self.value).abs() > 0.001 {
-            self.value = new_val;
-            return true;
+        let range = track_w - thumb_size;
+        if range > 0.0 {
+            let raw = (px - self.drag_offset - track_x) / range;
+            let new_val = raw.clamp(0.0, 1.0);
+            if (new_val - self.value).abs() > 0.001 {
+                self.value = new_val;
+                if self.editing {
+                    let scaled_val = self.min + self.value * (self.max - self.min);
+                    self.edit_buffer = format!("{:.2}", scaled_val);
+                }
+                return true;
+            }
         }
         false
     }
 
     fn drag_begin(&mut self, px: f32, _py: f32) {
         self.dragging = true;
+        let (track_x, track_w) = if self.show_readout {
+            let readout_w = 60.0;
+            let gap = 8.0;
+            let tw = (self.base.w - readout_w - gap).max(10.0);
+            (self.base.x, tw)
+        } else {
+            (self.base.x, self.base.w)
+        };
         let thumb_size = self.base.h * 0.9;
-        let thumb_x = self.base.x + self.value * (self.base.w - thumb_size);
+        let thumb_x = track_x + self.value * (track_w - thumb_size);
         self.drag_offset = px - thumb_x;
     }
 
@@ -3389,7 +4187,15 @@ impl Widget for Slider {
             return false;
         }
         let (sx, sy, sw, sh) = self.rect();
-        if px >= sx && px <= sx + sw && py >= sy && py <= sy + sh {
+        let (track_x, track_w) = if self.show_readout {
+            let readout_w = 60.0;
+            let gap = 8.0;
+            let tw = (sw - readout_w - gap).max(10.0);
+            (sx, tw)
+        } else {
+            (sx, sw)
+        };
+        if px >= track_x && px <= track_x + track_w && py >= sy && py <= sy + sh {
             let scroll_amount = match delta {
                 MouseScrollDelta::LineDelta(_x, y) => *y,
                 MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
@@ -3398,23 +4204,194 @@ impl Widget for Slider {
             let new_val = (self.value - scroll_amount * step).clamp(0.0, 1.0);
             if (new_val - self.value).abs() > 0.0001 {
                 self.value = new_val;
+                if self.editing {
+                    let scaled_val = self.min + self.value * (self.max - self.min);
+                    self.edit_buffer = format!("{:.2}", scaled_val);
+                }
                 return true;
             }
         }
         false
     }
 
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left { return false; }
+        
+        if self.show_readout {
+            let readout_w = 60.0;
+            let rx = self.base.x + self.base.w - readout_w;
+            
+            if px >= rx && px <= rx + readout_w && py >= self.base.y && py <= self.base.y + self.base.h {
+                if state == ElementState::Pressed {
+                    if !self.editing {
+                        self.editing = true;
+                        let scaled_val = self.min + self.value * (self.max - self.min);
+                        self.edit_buffer = format!("{:.2}", scaled_val);
+                        focus::set_focused(self);
+                    }
+                }
+                return true;
+            }
+        }
+
+        match state {
+            ElementState::Pressed => {
+                let (track_x, track_w) = if self.show_readout {
+                    let readout_w = 60.0;
+                    let gap = 8.0;
+                    let tw = (self.base.w - readout_w - gap).max(10.0);
+                    (self.base.x, tw)
+                } else {
+                    (self.base.x, self.base.w)
+                };
+                let thumb_size = self.base.h * 0.9;
+                let thumb_x = track_x + self.value * (track_w - thumb_size);
+                
+                if px >= track_x && px <= track_x + track_w && py >= self.base.y && py <= self.base.y + self.base.h {
+                    self.dragging = true;
+                    self.drag_offset = px - thumb_x;
+                    return true;
+                }
+                false
+            }
+            ElementState::Released => {
+                if self.dragging {
+                    self.dragging = false;
+                    return true;
+                }
+                false
+            }
+        }
+    }
+
+    fn unfocus(&mut self) {
+        if self.editing {
+            self.editing = false;
+            if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
+                let range = self.max - self.min;
+                if range != 0.0 {
+                    self.value = ((new_val - self.min) / range).clamp(0.0, 1.0);
+                } else {
+                    self.value = 0.0;
+                }
+            }
+        }
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+        if !self.editing { return false; }
+        if event.state != ElementState::Pressed { return false; }
+        
+        match &event.logical_key {
+            Key::Named(NamedKey::Backspace) => {
+                if !self.edit_buffer.is_empty() {
+                    self.edit_buffer.pop();
+                    return true;
+                }
+            }
+            Key::Named(NamedKey::Enter) => {
+                self.unfocus();
+                return true;
+            }
+            Key::Named(NamedKey::Escape) => {
+                self.editing = false;
+                return true;
+            }
+            Key::Character(s) => {
+                for ch in s.chars() {
+                    if ch.is_ascii_digit() || ch == '.' || (ch == '-' && self.edit_buffer.is_empty()) {
+                        self.edit_buffer.push(ch);
+                    }
+                }
+                return true;
+            }
+            _ => {}
+        }
+        false
+    }
+
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        
+        let (track_x, track_w) = if self.show_readout {
+            let readout_w = 60.0;
+            let gap = 8.0;
+            let tw = (self.base.w - readout_w - gap).max(10.0);
+            
+            quads.push((self.base.x, self.base.y, tw, self.base.h, colors::slider_track()));
+            
+            let rx = self.base.x + self.base.w - readout_w;
+            let bg_color = if self.editing {
+                [0.06, 0.10, 0.18, 1.0]
+            } else {
+                [0.10, 0.10, 0.13, 1.0]
+            };
+            quads.push((rx, self.base.y, readout_w, self.base.h, bg_color));
+            
+            if self.base.focused || self.editing {
+                let border_color = [0.20, 0.50, 0.85, 1.0];
+                let border_t = 1.0;
+                quads.push((rx, self.base.y, readout_w, border_t, border_color));
+                quads.push((rx, self.base.y + self.base.h - border_t, readout_w, border_t, border_color));
+                quads.push((rx, self.base.y, border_t, self.base.h, border_color));
+                quads.push((rx + readout_w - border_t, self.base.y, border_t, self.base.h, border_color));
+            }
+
+            (self.base.x, tw)
+        } else {
+            (self.base.x, self.base.w)
+        };
+
         let thumb_size = self.base.h * 0.9;
-        let thumb_x = self.base.x + self.value * (self.base.w - thumb_size);
+        let thumb_x = track_x + self.value * (track_w - thumb_size);
         let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
         let thumb_color = if self.dragging {
             colors::SLIDER_THUMB_DRAG
         } else {
             colors::SLIDER_THUMB
         };
-        vec![(thumb_x, thumb_y, thumb_size, thumb_size, thumb_color)]
+        quads.push((thumb_x, thumb_y, thumb_size, thumb_size, thumb_color));
+        
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        
+        if let Some(ref label) = self.base.label {
+            labels.push(TextLabel {
+                text: label.clone(),
+                x: self.base.x,
+                y: self.base.y - 18.0,
+                font_size: 12.0,
+                color: [0x83, 0x83, 0x8a],
+            });
+        }
+        
+        if self.show_readout {
+            let readout_w = 60.0;
+            let rx = self.base.x + self.base.w - readout_w;
+            let ry = self.base.y + (self.base.h - 12.0) / 2.0;
+            
+            let text = if self.editing {
+                self.edit_buffer.clone()
+            } else {
+                let scaled_val = self.min + self.value * (self.max - self.min);
+                format!("{:.2}", scaled_val)
+            };
+            
+            labels.push(TextLabel {
+                text,
+                x: rx + 8.0,
+                y: ry,
+                font_size: 12.0,
+                color: [0xee, 0xee, 0xf0],
+            });
+        }
+        
+        labels
     }
+
     fn value(&self) -> i32 { (self.value * 100.0) as i32 }
 }
 
@@ -3473,106 +4450,405 @@ impl Widget for RangeSlider {
     fn draggable(&self) -> bool { true }
     fn is_dragging(&self) -> bool { self.active_thumb.is_some() }
 
-    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
-        let Some(active) = self.active_thumb else { return false; };
-        let thumb_size = self.base.h * 0.9;
-        let range = self.base.w - thumb_size;
-        if range <= 0.0 { return false; }
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        let Some(active) = self.active_thumb else { return false; };
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        if range <= 0.0 { return false; }
+        
+        let new_val = ((px - self.drag_offset - self.base.x) / range).clamp(0.0, 1.0);
+        match active {
+            ActiveThumb::Low => {
+                let constrained = new_val.min(self.value_high);
+                if (constrained - self.value_low).abs() > 0.001 {
+                    self.value_low = constrained;
+                    return true;
+                }
+            }
+            ActiveThumb::High => {
+                let constrained = new_val.max(self.value_low);
+                if (constrained - self.value_high).abs() > 0.001 {
+                    self.value_high = constrained;
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    fn drag_begin(&mut self, px: f32, _py: f32) {
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        let thumb_low_x = self.base.x + self.value_low * range;
+        let thumb_high_x = self.base.x + self.value_high * range;
+        let center_low = thumb_low_x + thumb_size / 2.0;
+        let center_high = thumb_high_x + thumb_size / 2.0;
+
+        let active = if (self.value_low - self.value_high).abs() < 0.001 {
+            if px < center_low {
+                ActiveThumb::Low
+            } else {
+                ActiveThumb::High
+            }
+        } else {
+            let dist_low = (px - center_low).abs();
+            let dist_high = (px - center_high).abs();
+            if dist_low < dist_high {
+                ActiveThumb::Low
+            } else {
+                ActiveThumb::High
+            }
+        };
+
+        self.active_thumb = Some(active);
+        let active_x = match active {
+            ActiveThumb::Low => thumb_low_x,
+            ActiveThumb::High => thumb_high_x,
+        };
+        self.drag_offset = px - active_x;
+    }
+
+    fn drag_end(&mut self) {
+        self.active_thumb = None;
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        let thumb_low_x = self.base.x + self.value_low * range;
+        let thumb_high_x = self.base.x + self.value_high * range;
+        
+        let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
+        
+        // Highlighted track segment
+        let highlight_x = thumb_low_x + thumb_size / 2.0;
+        let highlight_w = thumb_high_x - thumb_low_x;
+        let highlight_y = self.base.y + self.base.h * 0.35;
+        let highlight_h = self.base.h * 0.3;
+        
+        let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+
+        let high_color = if self.active_thumb == Some(ActiveThumb::High) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+
+        vec![
+            (highlight_x, highlight_y, highlight_w, highlight_h, colors::PROGRESS_FILL),
+            (thumb_low_x, thumb_y, thumb_size, thumb_size, low_color),
+            (thumb_high_x, thumb_y, thumb_size, thumb_size, high_color),
+        ]
+    }
+
+    fn value(&self) -> i32 {
+        ((self.value_low * 100.0) as i32) | (((self.value_high * 100.0) as i32) << 16)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Float3 {
+    base: WidgetBase,
+    pub values: [f32; 3],
+    mins: [f32; 3],
+    maxs: [f32; 3],
+    labels: [String; 3],
+    dragging_idx: Option<usize>,
+    drag_offset: f32,
+    pub editing_idx: Option<usize>,
+    pub edit_buffer: String,
+}
+
+impl Float3 {
+    pub fn new() -> Self {
+        Self {
+            base: WidgetBase::new(),
+            values: [0.5, 0.5, 0.5],
+            mins: [0.0, 0.0, 0.0],
+            maxs: [1.0, 1.0, 1.0],
+            labels: ["X".to_string(), "Y".to_string(), "Z".to_string()],
+            dragging_idx: None,
+            drag_offset: 0.0,
+            editing_idx: None,
+            edit_buffer: String::new(),
+        }
+    }
+
+    pub fn with_values(mut self, values: [f32; 3]) -> Self {
+        self.values = values;
+        self
+    }
+
+    pub fn with_range(mut self, min: f32, max: f32) -> Self {
+        self.mins = [min, min, min];
+        self.maxs = [max, max, max];
+        self
+    }
+
+    pub fn set_values(&mut self, values: [f32; 3]) {
+        self.values = values;
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
+    }
+
+    pub fn get_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
+        let mut rects = Vec::new();
+        let by = self.base.y + 20.0;
+        for i in 0..3 {
+            rects.push((self.base.x + 8.0, by + 6.0 + i as f32 * 26.0, self.base.w - 16.0, 20.0));
+        }
+        rects
+    }
+}
+
+impl Widget for Float3 {
+    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 rect(&self) -> (f32, f32, f32, f32) { (self.base.x, self.base.y, self.base.w, self.base.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.base.x = x; self.base.y = y; self.base.w = w; self.base.h = h;
+    }
+
+    fn draggable(&self) -> bool { self.dragging_idx.is_some() }
+    fn is_dragging(&self) -> bool { self.dragging_idx.is_some() }
+    
+    fn drag_begin(&mut self, _px: f32, _py: f32) {}
+    
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        if let Some(i) = self.dragging_idx {
+            let track_x = self.base.x + 100.0;
+            let track_w = self.base.w - 188.0;
+            let thumb_size = 12.0 * 0.9;
+            let range = track_w - thumb_size;
+            if range > 0.0 {
+                let raw = (px - self.drag_offset - track_x) / range;
+                let new_val = raw.clamp(0.0, 1.0);
+                if (new_val - self.values[i]).abs() > 0.001 {
+                    self.values[i] = new_val;
+                    if self.editing_idx == Some(i) {
+                        let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+                        self.edit_buffer = format!("{:.2}", scaled_val);
+                    }
+                    return true;
+                }
+            }
+        }
+        false
+    }
+    
+    fn drag_end(&mut self) {
+        self.dragging_idx = None;
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left { return false; }
+        let rects = self.get_row_rects();
         
-        let new_val = ((px - self.drag_offset - self.base.x) / range).clamp(0.0, 1.0);
-        match active {
-            ActiveThumb::Low => {
-                let constrained = new_val.min(self.value_high);
-                if (constrained - self.value_low).abs() > 0.001 {
-                    self.value_low = constrained;
-                    return true;
+        for i in 0..3 {
+            let r = rects[i];
+            let rx = self.base.x + self.base.w - 68.0;
+            let ry = r.1 + 4.0;
+            let rh = 12.0;
+            let readout_w = 60.0;
+            
+            if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
+                if state == ElementState::Pressed {
+                    if self.editing_idx != Some(i) {
+                        self.unfocus();
+                        self.editing_idx = Some(i);
+                        let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+                        self.edit_buffer = format!("{:.2}", scaled_val);
+                        focus::set_focused(self);
+                    }
                 }
+                return true;
             }
-            ActiveThumb::High => {
-                let constrained = new_val.max(self.value_low);
-                if (constrained - self.value_high).abs() > 0.001 {
-                    self.value_high = constrained;
+        }
+        
+        if state == ElementState::Pressed {
+            for i in 0..3 {
+                let r = rects[i];
+                let track_x = self.base.x + 100.0;
+                let track_w = self.base.w - 188.0;
+                let track_y = r.1 + 4.0;
+                let track_h = 12.0;
+                let thumb_size = track_h * 0.9;
+                let range = track_w - thumb_size;
+                let thumb_x = track_x + self.values[i] * range;
+                
+                if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
+                    self.dragging_idx = Some(i);
+                    self.drag_offset = px - thumb_x;
                     return true;
                 }
             }
+        } else if state == ElementState::Released {
+            if self.dragging_idx.is_some() {
+                self.dragging_idx = None;
+                return true;
+            }
         }
         false
     }
 
-    fn drag_begin(&mut self, px: f32, _py: f32) {
-        let thumb_size = self.base.h * 0.9;
-        let range = self.base.w - thumb_size;
-        let thumb_low_x = self.base.x + self.value_low * range;
-        let thumb_high_x = self.base.x + self.value_high * range;
-        let center_low = thumb_low_x + thumb_size / 2.0;
-        let center_high = thumb_high_x + thumb_size / 2.0;
-
-        let active = if (self.value_low - self.value_high).abs() < 0.001 {
-            if px < center_low {
-                ActiveThumb::Low
-            } else {
-                ActiveThumb::High
+    fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+        let _idx = match self.editing_idx {
+            Some(i) => i,
+            None => return false,
+        };
+        if event.state != ElementState::Pressed { return false; }
+        
+        match &event.logical_key {
+            Key::Named(NamedKey::Backspace) => {
+                if !self.edit_buffer.is_empty() {
+                    self.edit_buffer.pop();
+                    return true;
+                }
             }
-        } else {
-            let dist_low = (px - center_low).abs();
-            let dist_high = (px - center_high).abs();
-            if dist_low < dist_high {
-                ActiveThumb::Low
-            } else {
-                ActiveThumb::High
+            Key::Named(NamedKey::Enter) => {
+                self.unfocus();
+                return true;
             }
-        };
-
-        self.active_thumb = Some(active);
-        let active_x = match active {
-            ActiveThumb::Low => thumb_low_x,
-            ActiveThumb::High => thumb_high_x,
-        };
-        self.drag_offset = px - active_x;
+            Key::Named(NamedKey::Escape) => {
+                self.editing_idx = None;
+                return true;
+            }
+            Key::Character(s) => {
+                for ch in s.chars() {
+                    if ch.is_ascii_digit() || ch == '.' || (ch == '-' && self.edit_buffer.is_empty()) {
+                        self.edit_buffer.push(ch);
+                    }
+                }
+                return true;
+            }
+            _ => {}
+        }
+        false
     }
 
-    fn drag_end(&mut self) {
-        self.active_thumb = None;
+    fn unfocus(&mut self) {
+        if let Some(i) = self.editing_idx.take() {
+            if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
+                let range = self.maxs[i] - self.mins[i];
+                if range != 0.0 {
+                    self.values[i] = ((new_val - self.mins[i]) / range).clamp(0.0, 1.0);
+                } else {
+                    self.values[i] = 0.0;
+                }
+            }
+        }
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let thumb_size = self.base.h * 0.9;
-        let range = self.base.w - thumb_size;
-        let thumb_low_x = self.base.x + self.value_low * range;
-        let thumb_high_x = self.base.x + self.value_high * range;
+        let mut quads = Vec::new();
         
-        let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
+        // Outline border box
+        let bx = self.base.x + 4.0;
+        let bw = self.base.w - 8.0;
+        let by = self.base.y + 20.0;
+        let bh = 84.0;
+        let border_color = [0.18, 0.18, 0.27, 1.0];
+        let border_t = 1.0;
         
-        // Highlighted track segment
-        let highlight_x = thumb_low_x + thumb_size / 2.0;
-        let highlight_w = thumb_high_x - thumb_low_x;
-        let highlight_y = self.base.y + self.base.h * 0.35;
-        let highlight_h = self.base.h * 0.3;
+        quads.push((bx, by, bw, border_t, border_color));
+        quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+        quads.push((bx, by, border_t, bh, border_color));
+        quads.push((bx + bw - border_t, by, border_t, bh, border_color));
         
-        let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
-            colors::SLIDER_THUMB_DRAG
-        } else {
-            colors::SLIDER_THUMB
-        };
-
-        let high_color = if self.active_thumb == Some(ActiveThumb::High) {
-            colors::SLIDER_THUMB_DRAG
-        } else {
-            colors::SLIDER_THUMB
-        };
-
-        vec![
-            (highlight_x, highlight_y, highlight_w, highlight_h, colors::PROGRESS_FILL),
-            (thumb_low_x, thumb_y, thumb_size, thumb_size, low_color),
-            (thumb_high_x, thumb_y, thumb_size, thumb_size, high_color),
-        ]
+        let rects = self.get_row_rects();
+        for i in 0..3 {
+            let r = rects[i];
+            let track_x = self.base.x + 100.0;
+            let track_w = self.base.w - 188.0;
+            let track_y = r.1 + 4.0;
+            let track_h = 12.0;
+            
+            quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
+            
+            let thumb_size = track_h * 0.9;
+            let range = track_w - thumb_size;
+            let thumb_x = track_x + self.values[i] * range;
+            let thumb_color = if self.dragging_idx == Some(i) {
+                colors::SLIDER_THUMB_DRAG
+            } else {
+                colors::SLIDER_THUMB
+            };
+            quads.push((thumb_x, track_y + (track_h - thumb_size)/2.0, thumb_size, thumb_size, thumb_color));
+            
+            let rx = self.base.x + self.base.w - 68.0;
+            let bg_color = if self.editing_idx == Some(i) {
+                [0.06, 0.10, 0.18, 1.0]
+            } else {
+                [0.10, 0.10, 0.13, 1.0]
+            };
+            quads.push((rx, track_y, 60.0, track_h, bg_color));
+            
+            if self.editing_idx == Some(i) {
+                let border_color = [0.20, 0.50, 0.85, 1.0];
+                quads.push((rx, track_y, 60.0, border_t, border_color));
+                quads.push((rx, track_y + track_h - border_t, 60.0, border_t, border_color));
+                quads.push((rx, track_y, border_t, track_h, border_color));
+                quads.push((rx + 60.0 - border_t, track_y, border_t, track_h, border_color));
+            }
+        }
+        quads
     }
 
-    fn value(&self) -> i32 {
-        ((self.value_low * 100.0) as i32) | (((self.value_high * 100.0) as i32) << 16)
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        
+        if let Some(ref l) = self.base.label {
+            labels.push(TextLabel {
+                text: l.clone(),
+                x: self.base.x + 8.0,
+                y: self.base.y + 2.0,
+                font_size: 13.0,
+                color: [0xee, 0xee, 0xf0],
+            });
+        }
+        
+        let rects = self.get_row_rects();
+        for i in 0..3 {
+            let r = rects[i];
+            let track_y = r.1 + 4.0;
+            let ry = track_y;
+            
+            labels.push(TextLabel {
+                text: self.labels[i].clone(),
+                x: self.base.x + 16.0,
+                y: ry - 2.0,
+                font_size: 12.0,
+                color: [0xaa, 0xaa, 0xbb],
+            });
+            
+            let rx = self.base.x + self.base.w - 68.0;
+            let text = if self.editing_idx == Some(i) {
+                self.edit_buffer.clone()
+            } else {
+                let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+                format!("{:.2}", scaled_val)
+            };
+            labels.push(TextLabel {
+                text,
+                x: rx + 8.0,
+                y: ry - 2.0,
+                font_size: 12.0,
+                color: [0xee, 0xee, 0xf0],
+            });
+        }
+        labels
     }
 }
 
+
 pub struct ProgressBar {
     base: WidgetBase,
     value: f32,
@@ -3600,21 +4876,116 @@ impl Widget for ProgressBar {
     fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
 }
 
+fn make_widget_text_buffer(fs: &mut glyphon::FontSystem, text: &str, size: f32, font_family: &str) -> glyphon::Buffer {
+    let metrics = glyphon::Metrics::new(size, size * 1.4);
+    let mut buf = glyphon::Buffer::new(fs, metrics);
+    let attrs = glyphon::Attrs::new().family(glyphon::Family::Name(font_family));
+    buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+    buf.shape_until_scroll(fs, true);
+    buf
+}
+
 pub struct StatusBar {
     x: f32, y: f32, w: f32, h: f32,
     hovered: bool,
+    pub text: String,
+    pub text_buf: Option<glyphon::Buffer>,
+    pub text_offset_x: Option<f32>,
+    pub text_color: Option<[f32; 4]>,
+    pub bg_color: Option<[f32; 4]>,
 }
 
 impl StatusBar {
-    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+    pub fn new() -> Self {
+        Self {
+            x: 0.0,
+            y: 0.0,
+            w: 0.0,
+            h: 0.0,
+            hovered: false,
+            text: String::new(),
+            text_buf: None,
+            text_offset_x: None,
+            text_color: None,
+            bg_color: None,
+        }
+    }
+    pub fn with_text(mut self, text: &str) -> Self {
+        self.text = text.to_string();
+        self
+    }
+    pub fn with_text_offset_x(mut self, offset: f32) -> Self {
+        self.text_offset_x = Some(offset);
+        self
+    }
+    pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
+        self.text_color = Some(color);
+        self
+    }
+    pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
+        self.bg_color = Some(color);
+        self
+    }
+    pub fn set_text_offset_x(&mut self, offset: f32) {
+        self.text_offset_x = Some(offset);
+    }
+    pub fn set_text_color(&mut self, color: [f32; 4]) {
+        self.text_color = Some(color);
+    }
+    pub fn set_bg_color(&mut self, color: [f32; 4]) {
+        self.bg_color = Some(color);
+    }
 }
 
 impl Widget for StatusBar {
     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 color(&self) -> [f32; 4] { colors::STATUS_BG }
+    fn color(&self) -> [f32; 4] { self.bg_color.unwrap_or(colors::STATUS_BG) }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
+    fn set_text(&mut self, text: &str) {
+        if self.text != text {
+            self.text = text.to_string();
+            self.text_buf = None;
+        }
+    }
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        if !self.text.is_empty() && self.text_buf.is_none() {
+            self.text_buf = Some(make_widget_text_buffer(fs, &self.text, 12.0, "Outfit"));
+        }
+    }
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if let Some(ref text_buf) = self.text_buf {
+            let offset_x = self.text_offset_x.unwrap_or(12.0);
+            let color = self.text_color.map(|c| glyphon::Color::rgb(
+                (c[0] * 255.0) as u8,
+                (c[1] * 255.0) as u8,
+                (c[2] * 255.0) as u8,
+            )).unwrap_or_else(|| glyphon::Color::rgb(0xaa, 0xaa, 0xbb));
+            vec![(text_buf, self.x + offset_x, self.y + 4.0, color)]
+        } else {
+            Vec::new()
+        }
+    }
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.text.is_empty() {
+            let offset_x = self.text_offset_x.unwrap_or(12.0);
+            let color = self.text_color.map(|c| [
+                (c[0] * 255.0) as u8,
+                (c[1] * 255.0) as u8,
+                (c[2] * 255.0) as u8,
+            ]).unwrap_or([0xaa, 0xaa, 0xbb]);
+            vec![TextLabel {
+                text: self.text.clone(),
+                x: self.x + offset_x,
+                y: self.y + 4.0,
+                font_size: 12.0,
+                color,
+            }]
+        } else {
+            Vec::new()
+        }
+    }
 }
 
 pub struct Splitter {
@@ -4030,7 +5401,7 @@ impl Drop for Spinbox {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct ColorSelector {
     base: WidgetBase,
     pub color: [u8; 3],
@@ -4040,6 +5411,23 @@ pub struct ColorSelector {
     pub command: String,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
+    child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
+}
+
+impl Clone for ColorSelector {
+    fn clone(&self) -> Self {
+        Self {
+            base: self.base.clone(),
+            color: self.color,
+            just_clicked: self.just_clicked,
+            editing: self.editing,
+            edit_buffer: self.edit_buffer.clone(),
+            command: self.command.clone(),
+            parent: self.parent,
+            children: self.children.clone(),
+            child: std::sync::Arc::new(std::sync::Mutex::new(None)),
+        }
+    }
 }
 
 impl ColorSelector {
@@ -4053,6 +5441,7 @@ impl ColorSelector {
             command: "clear-color-interface".to_string(),
             parent: None,
             children: Vec::new(),
+            child: std::sync::Arc::new(std::sync::Mutex::new(None)),
         }
     }
 
@@ -4087,23 +5476,17 @@ impl Widget for ColorSelector {
         if state != ElementState::Pressed { return false; }
         if !self.hit_test(px, py) { return false; }
         if px >= self.base.x + self.base.w * 0.65 {
-            self.just_clicked = true;
             let hex = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
-            if let Ok(output) = std::process::Command::new(&self.command)
+            let mut child_guard = self.child.lock().unwrap();
+            if let Some(mut old_child) = child_guard.take() {
+                let _ = old_child.kill();
+            }
+            if let Ok(child) = std::process::Command::new(&self.command)
                 .arg(&hex)
-                .output()
+                .stdout(std::process::Stdio::piped())
+                .spawn()
             {
-                let stdout_str = String::from_utf8_lossy(&output.stdout);
-                let mut found_color = None;
-                for line in stdout_str.lines().rev() {
-                    if let Some(c) = parse_hex(line.trim()) {
-                        found_color = Some(c);
-                        break;
-                    }
-                }
-                if let Some(new_color) = found_color {
-                    self.color = new_color;
-                }
+                *child_guard = Some(child);
             }
             return true;
         }
@@ -4115,6 +5498,40 @@ impl Widget for ColorSelector {
         if self.just_clicked { self.just_clicked = false; true } else { false }
     }
 
+    fn tick(&mut self, _dt: f32) -> bool {
+        let mut child_opt = self.child.lock().unwrap();
+        if let Some(ref mut child) = *child_opt {
+            match child.try_wait() {
+                Ok(Some(status)) => {
+                    let child = child_opt.take().unwrap();
+                    if status.success() {
+                        if let Ok(output) = child.wait_with_output() {
+                            let stdout_str = String::from_utf8_lossy(&output.stdout);
+                            let mut found_color = None;
+                            for line in stdout_str.lines().rev() {
+                                if let Some(c) = parse_hex(line.trim()) {
+                                    found_color = Some(c);
+                                    break;
+                                }
+                            }
+                            if let Some(new_color) = found_color {
+                                self.color = new_color;
+                                self.just_clicked = true;
+                                return true;
+                            }
+                        }
+                    }
+                }
+                Ok(None) => {}
+                Err(e) => {
+                    eprintln!("Error checking color selector child process: {:?}", e);
+                    *child_opt = None;
+                }
+            }
+        }
+        false
+    }
+
     fn focus(&mut self) {
         self.editing = true;
         self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
@@ -4250,12 +5667,13 @@ pub struct Breadcrumb {
     path: Vec<String>,
     hovered_seg: Option<usize>,
     clicked_seg: Option<usize>,
+    pub network_opacity: f32,
 }
 
 impl Breadcrumb {
     pub fn new() -> Self {
         Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false,
-               path: Vec::new(), hovered_seg: None, clicked_seg: None }
+               path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
     }
 
     fn seg_at(&self, px: f32) -> Option<usize> {
@@ -4274,7 +5692,8 @@ impl Breadcrumb {
 impl Widget for Breadcrumb {
     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 color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, 1.0] }
+    fn set_network_opacity(&mut self, opacity: f32) { self.network_opacity = opacity; }
+    fn color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, self.network_opacity] }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
 
@@ -5752,12 +7171,21 @@ impl Dropdown {
         let dy = self.base.y + self.base.h;
         let dh = self.options.len() as f32 * 24.0;
         
-        pc.rect([0.22, 0.22, 0.28, 1.0], self.base.x, dy, self.base.w, dh); // border
-        pc.rect([0.06, 0.06, 0.09, 1.0], self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0); // bg
+        // 1. Soft layered drop shadows
+        pc.rect([0.02, 0.02, 0.05, 0.15], self.base.x + 1.0, dy + 1.0, self.base.w, dh);
+        pc.rect([0.02, 0.02, 0.05, 0.08], self.base.x + 3.0, dy + 3.0, self.base.w, dh);
+        pc.rect([0.02, 0.02, 0.05, 0.04], self.base.x + 5.0, dy + 5.0, self.base.w, dh);
+
+        // 2. High-contrast premium outer border
+        pc.rect([0.25, 0.35, 0.50, 0.40], self.base.x, dy, self.base.w, dh);
+        
+        // 3. Frosted glass background (matching magic alpha 0.699 in shader)
+        pc.rect([0.06, 0.06, 0.09, 0.699], self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0); // bg
         
         if let Some(h_idx) = self.hovered_item {
             let iy = dy + h_idx as f32 * 24.0;
-            pc.rect([0.20, 0.40, 0.65, 0.6], self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0);
+            // 4. Vibrantly colored translucent selection highlight
+            pc.rect([0.20, 0.45, 0.85, 0.50], self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0);
         }
         
         for (idx, opt) in self.options.iter().enumerate() {
@@ -7237,6 +8665,9 @@ pub struct Graph {
 
     // Hover tracking
     toggle_hovered_idx: Option<usize>,
+
+    uniform_background: bool,
+    network_opacity: f32,
 }
 
 impl Graph {
@@ -7262,6 +8693,8 @@ impl Graph {
             drag_oy: 0.0,
             drag_node_pos: None,
             toggle_hovered_idx: None,
+            uniform_background: false,
+            network_opacity: 0.95,
         }
     }
 
@@ -7310,7 +8743,15 @@ impl Graph {
 impl Widget for Graph {
     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 color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+    fn set_uniform_background(&mut self, uniform: bool) { self.uniform_background = uniform; }
+    fn set_network_opacity(&mut self, opacity: f32) { self.network_opacity = opacity; }
+    fn color(&self) -> [f32; 4] {
+        if self.uniform_background {
+            [0.10, 0.10, 0.13, self.network_opacity]
+        } else {
+            [0.0, 0.0, 0.0, 0.0]
+        }
+    }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
     fn hit_test(&self, px: f32, py: f32) -> bool {
@@ -7474,11 +8915,59 @@ impl Widget for Graph {
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
 
-        if self.show_network_grid && self.grid_size_x > 0.0 && self.grid_size_y > 0.0 {
-            let grid_color = [0.0, 0.0, 0.0, 0.0];
-            let max_alpha = colors::CONTENT_BG[3];
-            let steps = 20;
+        // Draw connection wires
+        let wire_color = [0.0, 0.75, 1.0, 0.7]; // Vibrant cyan glow
+        let wire_thickness = 3.0;
+        for i in 0..self.nodes.len() {
+            let node = &self.nodes[i];
+            if let Some((_, input_name, _)) = node.parameters.iter().find(|(name, _, _)| name.eq_ignore_ascii_case("input")) {
+                if let Some(src_idx) = self.nodes.iter().position(|n| n.name == *input_name) {
+                    if let (Some((sx, sy, sw, sh)), Some((ex, ey, ew, _eh))) = (self.node_rect(src_idx), self.node_rect(i)) {
+                        let start_x = sx + sw / 2.0;
+                        let start_y = sy + sh;
+                        let end_x = ex + ew / 2.0;
+                        let end_y = ey;
+
+                        let mid_y = start_y + (end_y - start_y) / 2.0;
+
+                        // Vertical segment 1
+                        let v1_min_y = start_y.min(mid_y);
+                        let v1_max_y = start_y.max(mid_y);
+                        quads.push((
+                            start_x - wire_thickness / 2.0,
+                            v1_min_y,
+                            wire_thickness,
+                            v1_max_y - v1_min_y,
+                            wire_color,
+                        ));
+
+                        // Horizontal segment
+                        let h_min_x = start_x.min(end_x);
+                        let h_max_x = start_x.max(end_x);
+                        quads.push((
+                            h_min_x,
+                            mid_y - wire_thickness / 2.0,
+                            h_max_x - h_min_x,
+                            wire_thickness,
+                            wire_color,
+                        ));
+
+                        // Vertical segment 2
+                        let v2_min_y = mid_y.min(end_y);
+                        let v2_max_y = mid_y.max(end_y);
+                        quads.push((
+                            end_x - wire_thickness / 2.0,
+                            v2_min_y,
+                            wire_thickness,
+                            v2_max_y - v2_min_y,
+                            wire_color,
+                        ));
+                    }
+                }
+            }
+        }
 
+        if self.show_network_grid && self.grid_size_x > 0.0 && self.grid_size_y > 0.0 && !self.uniform_background {
             let step_y = self.grid_size_y + self.skipped_row_h;
             let step_x = self.grid_size_x + self.skipped_col_w;
 
@@ -7493,101 +8982,20 @@ impl Widget for Graph {
                 let cx_start = cx_start.max(-100_000);
                 let cx_end = cx_end.min(100_000);
 
-                for ry in ry_start..=ry_end {
-                    let y1 = self.grid_origin_y + (ry as f32) * step_y;
-                    let draw_start_y = y1.max(self.y);
-                    let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
-                    if draw_start_y < draw_end_y {
-                        for cx in cx_start..=cx_end {
-                            let x1 = self.grid_origin_x + (cx as f32) * step_x;
-                            let draw_start_x = x1.max(self.x);
-                            let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
-                            if draw_start_x < draw_end_x {
-                                quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
-                            }
-                        }
-                    }
-                }
-
-                if self.skipped_row_h > 0.0 {
-                    for k in ry_start..=ry_end {
-                        let y1 = self.grid_origin_y + (k as f32) * step_y;
-                        let y2 = y1 + self.grid_size_y;
-                        if y1 < self.y + self.h {
-                            let draw_start_y = y2.max(self.y);
-                            let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
-                            if draw_start_y < draw_end_y {
-                                for cx in cx_start..=cx_end {
-                                    let x1 = self.grid_origin_x + (cx as f32) * step_x;
-                                    let x_mid = x1 + self.grid_size_x / 2.0;
-                                    let w_total = self.grid_size_x;
-                                    let sub_w = w_total / steps as f32;
-
-                                    for i in 0..steps {
-                                        let sx_start = x1 + i as f32 * sub_w;
-                                        let sx_end = sx_start + sub_w;
-                                        let draw_start_x = sx_start.max(self.x);
-                                        let draw_end_x = sx_end.min(self.x + self.w);
-                                        if draw_start_x < draw_end_x {
-                                            let sx_mid = (sx_start + sx_end) / 2.0;
-                                            let dist = (sx_mid - x_mid).abs();
-                                            let d = (dist / (w_total / 2.0)).min(1.0);
-                                            let alpha = max_alpha * (1.0 - d);
-                                            if alpha > 0.001 {
-                                                quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+                // Draw solid background color behind grid lines
+                quads.push((self.x, self.y, self.w, self.h, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], self.network_opacity]));
 
-                if self.skipped_col_w > 0.0 {
-                    for k in cx_start..=cx_end {
-                        let x1 = self.grid_origin_x + (k as f32) * step_x;
-                        let x2 = x1 + self.grid_size_x;
-                        if x1 < self.x + self.w {
-                            let draw_start_x = x2.max(self.x);
-                            let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
-                            if draw_start_x < draw_end_x {
-                                for ry in ry_start..=ry_end {
-                                    let y1 = self.grid_origin_y + (ry as f32) * step_y;
-                                    let y_mid = y1 + self.grid_size_y / 2.0;
-                                    let h_total = self.grid_size_y;
-                                    let sub_h = h_total / steps as f32;
-
-                                    for i in 0..steps {
-                                        let sy_start = y1 + i as f32 * sub_h;
-                                        let sy_end = sy_start + sub_h;
-                                        let draw_start_y = sy_start.max(self.y);
-                                        let draw_end_y = sy_end.min(self.y + self.h);
-                                        if draw_start_y < draw_end_y {
-                                            let sy_mid = (sy_start + sy_end) / 2.0;
-                                            let dist = (sy_mid - y_mid).abs();
-                                            let d = (dist / (h_total / 2.0)).min(1.0);
-                                            let alpha = max_alpha * (1.0 - d);
-                                            if alpha > 0.001 {
-                                                quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+                let grid_line_color = [0.18, 0.18, 0.22, 0.40];
 
                 for k in ry_start..=ry_end {
                     let y1 = self.grid_origin_y + (k as f32) * step_y;
                     let y2 = y1 + self.grid_size_y;
                     if y1 < self.y + self.h {
                         if y1 >= self.y {
-                            quads.push((self.x, y1, self.w, 1.0, grid_color));
+                            quads.push((self.x, y1, self.w, 1.0, grid_line_color));
                         }
                         if y2 >= self.y && y2 < self.y + self.h {
-                            quads.push((self.x, y2, self.w, 1.0, grid_color));
+                            quads.push((self.x, y2, self.w, 1.0, grid_line_color));
                         }
                     }
                 }
@@ -7597,10 +9005,10 @@ impl Widget for Graph {
                     let x2 = x1 + self.grid_size_x;
                     if x1 < self.x + self.w {
                         if x1 >= self.x {
-                            quads.push((x1, self.y, 1.0, self.h, grid_color));
+                            quads.push((x1, self.y, 1.0, self.h, grid_line_color));
                         }
                         if x2 >= self.x && x2 < self.x + self.w {
-                            quads.push((x2, self.y, 1.0, self.h, grid_color));
+                            quads.push((x2, self.y, 1.0, self.h, grid_line_color));
                         }
                     }
                 }
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
new file mode 100644
index 0000000..3bbd4a9
--- /dev/null
+++ b/src/widget/json_layout.rs
@@ -0,0 +1,575 @@
+use crate::widget::{
+    Widget, WidgetBase, Checkbox, Button, Label, Spinbox, ColorSelector, TextLabel, Paginator,
+    KeyEvent, MouseButton, ElementState, focus, Slider,
+};
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct JsonWidgetConfig {
+    #[serde(rename = "type")]
+    pub widget_type: String,
+    pub text: String,
+    pub id: Option<String>,
+    pub checked: Option<bool>,
+    pub value: Option<i32>,
+    pub min: Option<i32>,
+    pub max: Option<i32>,
+    pub step: Option<i32>,
+    pub decimals: Option<u32>,
+    pub color: Option<[u8; 3]>,
+    pub value_f32: Option<f32>,
+    pub min_f32: Option<f32>,
+    pub max_f32: Option<f32>,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct JsonPageConfig {
+    pub title: String,
+    pub widgets: Vec<JsonWidgetConfig>,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct JsonLayoutConfig {
+    pub width: Option<u32>,
+    pub height: Option<u32>,
+    pub widgets: Option<Vec<JsonWidgetConfig>>,
+    pub pages: Option<Vec<JsonPageConfig>>,
+}
+
+pub struct JsonWidget {
+    pub id: String,
+    pub widget_type: String,
+    pub text: String,
+    pub checkbox: Option<Checkbox>,
+    pub button: Option<Button>,
+    pub label: Option<Label>,
+    pub spinbox: Option<Spinbox>,
+    pub color_selector: Option<ColorSelector>,
+    pub slider: Option<Slider>,
+    pub x: f32,
+    pub y: f32,
+    pub w: f32,
+    pub h: f32,
+    pub label_text: Option<TextLabel>,
+    pub page_idx: usize,
+}
+
+pub struct JsonLayoutWidget {
+    base: WidgetBase,
+    pub widgets: Vec<JsonWidget>,
+    pub paginator: Option<Paginator>,
+    pub dragging_slider_idx: Option<usize>,
+}
+
+impl JsonLayoutWidget {
+    pub fn new(config: &JsonLayoutConfig) -> Self {
+        let mut widgets = Vec::new();
+        let mut page_titles = Vec::new();
+        let mut paginator = None;
+
+        if let Some(ref pages_conf) = config.pages {
+            for (page_idx, page) in pages_conf.iter().enumerate() {
+                page_titles.push(page.title.clone());
+                for (idx, w_conf) in page.widgets.iter().enumerate() {
+                    let id = w_conf.id.clone().unwrap_or_else(|| format!("widget_{}_{}", page_idx, 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;
+                    let mut spinbox = None;
+                    let mut color_selector = None;
+                    let mut slider = None;
+
+                    match widget_type.as_str() {
+                        "checkbox" => {
+                            let mut cb = Checkbox::new();
+                            if let Some(ch) = w_conf.checked {
+                                cb.set_checked(ch);
+                            }
+                            checkbox = Some(cb);
+                        }
+                        "button" => {
+                            button = Some(Button::new(0.0, 0.0, 0.0, 0.0).with_label(&text));
+                        }
+                        "label" => {
+                            label = Some(Label::new(&text).with_font_size(13.0).with_color([0xcc, 0xcc, 0xd4]));
+                        }
+                        "spinbox" => {
+                            let min_val = w_conf.min.unwrap_or(0);
+                            let max_val = w_conf.max.unwrap_or(100);
+                            let step_val = w_conf.step.unwrap_or(1);
+                            let mut sb = Spinbox::new(w_conf.value.unwrap_or(0), min_val, max_val, step_val)
+                                .with_label(&text);
+                            if let Some(dec) = w_conf.decimals {
+                                sb = sb.with_decimals(dec);
+                            }
+                            spinbox = Some(sb);
+                        }
+                        "color" => {
+                            let col = w_conf.color.unwrap_or([255, 255, 255]);
+                            let cs = ColorSelector::new(col).with_label(&text);
+                            color_selector = Some(cs);
+                        }
+                        "slider" => {
+                            let min_val = w_conf.min_f32.unwrap_or(0.0);
+                            let max_val = w_conf.max_f32.unwrap_or(1.0);
+                            let mut sl = Slider::new()
+                                .with_range(min_val, max_val)
+                                .with_label(&text)
+                                .with_readout(true);
+                            if let Some(val) = w_conf.value_f32 {
+                                let pct = if max_val > min_val { (val - min_val) / (max_val - min_val) } else { 0.0 };
+                                sl = sl.with_value(pct);
+                            }
+                            slider = Some(sl);
+                        }
+                        _ => {}
+                    }
+
+                    widgets.push(JsonWidget {
+                        id,
+                        widget_type,
+                        text,
+                        checkbox,
+                        button,
+                        label,
+                        spinbox,
+                        color_selector,
+                        slider,
+                        x: 0.0,
+                        y: 0.0,
+                        w: 0.0,
+                        h: 0.0,
+                        label_text: None,
+                        page_idx,
+                    });
+                }
+            }
+            paginator = Some(Paginator::new(150.0, page_titles));
+        } else if let Some(ref widgets_conf) = config.widgets {
+            for (idx, w_conf) in widgets_conf.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;
+                let mut spinbox = None;
+                let mut color_selector = None;
+                let mut slider = None;
+
+                match widget_type.as_str() {
+                    "checkbox" => {
+                        let mut cb = Checkbox::new();
+                        if let Some(ch) = w_conf.checked {
+                            cb.set_checked(ch);
+                        }
+                        checkbox = Some(cb);
+                    }
+                    "button" => {
+                        button = Some(Button::new(0.0, 0.0, 0.0, 0.0).with_label(&text));
+                    }
+                    "label" => {
+                        label = Some(Label::new(&text).with_font_size(13.0).with_color([0xcc, 0xcc, 0xd4]));
+                    }
+                    "spinbox" => {
+                        let min_val = w_conf.min.unwrap_or(0);
+                        let max_val = w_conf.max.unwrap_or(100);
+                        let step_val = w_conf.step.unwrap_or(1);
+                        let mut sb = Spinbox::new(w_conf.value.unwrap_or(0), min_val, max_val, step_val)
+                            .with_label(&text);
+                        if let Some(dec) = w_conf.decimals {
+                            sb = sb.with_decimals(dec);
+                        }
+                        spinbox = Some(sb);
+                    }
+                    "color" => {
+                        let col = w_conf.color.unwrap_or([255, 255, 255]);
+                        let cs = ColorSelector::new(col).with_label(&text);
+                        color_selector = Some(cs);
+                    }
+                    "slider" => {
+                        let min_val = w_conf.min_f32.unwrap_or(0.0);
+                        let max_val = w_conf.max_f32.unwrap_or(1.0);
+                        let mut sl = Slider::new()
+                            .with_range(min_val, max_val)
+                            .with_label(&text)
+                            .with_readout(true);
+                        if let Some(val) = w_conf.value_f32 {
+                            let pct = if max_val > min_val { (val - min_val) / (max_val - min_val) } else { 0.0 };
+                            sl = sl.with_value(pct);
+                        }
+                        slider = Some(sl);
+                    }
+                    _ => {}
+                }
+
+                widgets.push(JsonWidget {
+                    id,
+                    widget_type,
+                    text,
+                    checkbox,
+                    button,
+                    label,
+                    spinbox,
+                    color_selector,
+                    slider,
+                    x: 0.0,
+                    y: 0.0,
+                    w: 0.0,
+                    h: 0.0,
+                    label_text: None,
+                    page_idx: 0,
+                });
+            }
+        }
+
+        Self {
+            base: WidgetBase::new(),
+            widgets,
+            paginator,
+            dragging_slider_idx: None,
+        }
+    }
+
+    pub fn layout_children(&mut self) {
+        let (bx, by, bw, bh) = self.rect();
+        if let Some(paginator) = &mut self.paginator {
+            paginator.set_rect(bx, by, bw, bh);
+        }
+
+        let has_paginator = self.paginator.is_some();
+        let pad_x = if has_paginator { 170.0 } else { 16.0 };
+        let usable_w = if has_paginator { bw - pad_x - 16.0 } else { bw - 2.0 * 16.0 };
+        
+        let mut page_current_y = vec![16.0; 16]; // support up to 16 pages
+        let spacing = 12.0;
+
+        for w_state in &mut self.widgets {
+            let p_idx = w_state.page_idx;
+            if p_idx >= page_current_y.len() {
+                continue;
+            }
+            let current_y = &mut page_current_y[p_idx];
+            w_state.x = bx + pad_x;
+
+            let mut top_room = 0.0;
+            if let Some(sb) = &w_state.spinbox {
+                top_room = sb.top_room();
+            } else if let Some(cs) = &w_state.color_selector {
+                top_room = cs.top_room();
+            } else if let Some(cb) = &w_state.checkbox {
+                top_room = cb.top_room();
+            } else if let Some(btn) = &w_state.button {
+                top_room = btn.top_room();
+            } else if let Some(lbl) = &w_state.label {
+                top_room = lbl.top_room();
+            } else if let Some(sl) = &w_state.slider {
+                top_room = sl.top_room();
+            }
+
+            w_state.y = by + *current_y + top_room;
+            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(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, 24.0);
+                w_state.h = 24.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;
+            } else if let Some(sb) = &mut w_state.spinbox {
+                sb.set_rect(w_state.x, w_state.y, usable_w, 22.0);
+                w_state.h = 22.0;
+            } else if let Some(cs) = &mut w_state.color_selector {
+                cs.set_rect(w_state.x, w_state.y, usable_w, 24.0);
+                w_state.h = 24.0;
+            } else if let Some(sl) = &mut w_state.slider {
+                sl.set_rect(w_state.x, w_state.y, usable_w, 22.0);
+                w_state.h = 22.0;
+            }
+
+            *current_y += top_room + 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 tick(&mut self, dt: f32) -> bool {
+        let mut changed = false;
+        if let Some(paginator) = &mut self.paginator {
+            if paginator.tick(dt) {
+                changed = true;
+            }
+        }
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+        for w in &mut self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            if let Some(sb) = &mut w.spinbox {
+                if sb.tick(dt) {
+                    changed = true;
+                }
+            } else if let Some(cs) = &mut w.color_selector {
+                if cs.tick(dt) {
+                    changed = true;
+                }
+            }
+        }
+        changed
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if let Some(paginator) = &self.paginator {
+            quads.extend(paginator.extra_quads());
+            if let Some(hq) = paginator.highlight_quad() {
+                quads.push(hq);
+            }
+        }
+
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+
+        for w in &self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            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());
+            } else if let Some(sb) = &w.spinbox {
+                quads.push((sb.rect().0, sb.rect().1, sb.rect().2, sb.rect().3, sb.color()));
+                quads.extend(sb.extra_quads());
+            } else if let Some(cs) = &w.color_selector {
+                quads.push((cs.rect().0, cs.rect().1, cs.rect().2, cs.rect().3, cs.color()));
+                quads.extend(cs.extra_quads());
+            } else if let Some(sl) = &w.slider {
+                quads.push((sl.rect().0, sl.rect().1, sl.rect().2, sl.rect().3, sl.color()));
+                quads.extend(sl.extra_quads());
+            }
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        if let Some(paginator) = &self.paginator {
+            labels.extend(paginator.text_labels());
+        }
+
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+
+        for w in &self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            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());
+            } else if let Some(sb) = &w.spinbox {
+                labels.extend(sb.text_labels());
+            } else if let Some(cs) = &w.color_selector {
+                labels.extend(cs.text_labels());
+            } else if let Some(sl) = &w.slider {
+                labels.extend(sl.text_labels());
+            }
+        }
+        labels
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        let mut changed = false;
+        if let Some(paginator) = &mut self.paginator {
+            if paginator.on_cursor_moved(px, py) {
+                changed = true;
+            }
+        }
+
+        if let Some(idx) = self.dragging_slider_idx {
+            if let Some(w) = self.widgets.get_mut(idx) {
+                if let Some(ref mut sl) = w.slider {
+                    if sl.drag_update(px, py) {
+                        changed = true;
+                    }
+                }
+            }
+        }
+
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+
+        for (idx, w) in self.widgets.iter_mut().enumerate() {
+            if w.page_idx != active_page {
+                continue;
+            }
+            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;
+                }
+            } else if let Some(sb) = &mut w.spinbox {
+                if sb.cursor_moved(px, py) {
+                    changed = true;
+                }
+            } else if let Some(cs) = &mut w.color_selector {
+                if cs.cursor_moved(px, py) {
+                    changed = true;
+                }
+            } else if let Some(sl) = &mut w.slider {
+                if sl.on_cursor_moved(px, py) {
+                    changed = true;
+                }
+            }
+        }
+        changed
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left { return false; }
+        let mut changed = false;
+
+        if let Some(paginator) = &mut self.paginator {
+            if paginator.mouse_input(button, state, px, py) {
+                if paginator.take_click() {
+                    focus::clear_focus();
+                    self.layout_children();
+                }
+                changed = true;
+                return true;
+            }
+        }
+
+        if let Some(idx) = self.dragging_slider_idx {
+            if state == ElementState::Released {
+                if let Some(w) = self.widgets.get_mut(idx) {
+                    if let Some(ref mut sl) = w.slider {
+                        sl.mouse_input(button, state, px, py);
+                        changed = true;
+                    }
+                }
+                self.dragging_slider_idx = None;
+            }
+        }
+
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+
+        for (idx, w) in self.widgets.iter_mut().enumerate() {
+            if w.page_idx != active_page {
+                continue;
+            }
+            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 == ElementState::Pressed {
+                        changed = true;
+                    } else if state == 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;
+                }
+            } else if let Some(sb) = &mut w.spinbox {
+                if sb.mouse_input(button, state, px, py) {
+                    changed = true;
+                }
+            } else if let Some(cs) = &mut w.color_selector {
+                if cs.mouse_input(button, state, px, py) {
+                    changed = true;
+                }
+            } else if let Some(sl) = &mut w.slider {
+                let hit = px >= w.x && px <= w.x + w.w && py >= w.y && py <= w.y + w.h;
+                if hit {
+                    if state == ElementState::Pressed {
+                        sl.mouse_input(button, state, px, py);
+                        self.dragging_slider_idx = Some(idx);
+                        changed = true;
+                    }
+                }
+            }
+        }
+
+        if state == ElementState::Pressed && !changed {
+            focus::clear_focus();
+        }
+
+        changed
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+        for w in &mut self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            if let Some(sb) = &mut w.spinbox {
+                if sb.keyboard_input(event) {
+                    return true;
+                }
+            } else if let Some(cs) = &mut w.color_selector {
+                if cs.keyboard_input(event) {
+                    return true;
+                }
+            } else if let Some(sl) = &mut w.slider {
+                if sl.keyboard_input(event) {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+}