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

commit908b3e0e8d5ba0a64bba8d5b38639fb3f97ac0e5
parent3579cdbadc
authorLucas Galante <[email protected]>
date2026-06-05 20:13
feat: add scale module, custom vertex rendering, Column/Grid/Radial layout strategies, and Plate widget

 Makefile                  |   14 +
 src/color.rs              |  120 +++-
 src/engine.rs             |  714 ++++++++++++++++++++-
 src/layout.rs             |  665 ++++++++++++++++++-
 src/lib.rs                |    1 +
 src/main.rs               |  631 ++++++++++++++----
 src/scale.rs              |   13 +
 src/shader.wgsl           |   15 -
 src/widget.rs             | 1569 ++++++++++++++++++++++++++++++---------------
 src/widget/json_layout.rs |  142 +++-
 10 files changed, 3178 insertions(+), 706 deletions(-)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..72a0653
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+	cargo build --release
+
+install: build
+	mkdir -p ~/.local/bin
+	install -m 755 target/release/clear-ui ~/.local/bin/clear-ui
+
+run:
+	cargo run
+
+clean:
+	cargo clean
diff --git a/src/color.rs b/src/color.rs
index d47aef6..ed61384 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -32,6 +32,9 @@ static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
 static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
 static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
 static PAGINATOR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
+static OPACITY: RwLock<Option<f32>> = RwLock::new(None);
+static TOGGLE_ON_COLOR: RwLock<[f32; 4]> = RwLock::new(TOGGLE_ON);
+static TOGGLE_OFF_COLOR: RwLock<[f32; 4]> = RwLock::new(TOGGLE_OFF);
 
 
 pub fn node_color() -> [f32; 4] {
@@ -92,7 +95,11 @@ pub fn page_low_color() -> [f32; 4] {
             }
         }
     });
-    *PAGE_LOW_COLOR.read().unwrap()
+    let mut color = *PAGE_LOW_COLOR.read().unwrap();
+    if let Some(opacity) = read_opacity_if_configured() {
+        color[3] = opacity;
+    }
+    color
 }
 
 pub fn set_page_low_color(color: [f32; 4]) {
@@ -259,7 +266,11 @@ pub fn sidebar_bg_color() -> [f32; 4] {
             }
         }
     });
-    *SIDEBAR_BG_COLOR.read().unwrap()
+    let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
+    if let Some(opacity) = read_opacity_if_configured() {
+        color[3] = opacity;
+    }
+    color
 }
 
 pub fn set_sidebar_bg_color(color: [f32; 4]) {
@@ -342,3 +353,108 @@ pub fn set_paginator_tab_label_color(color: [f32; 4]) {
     }
 }
 
+pub fn read_opacity_if_configured() -> Option<f32> {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+            let mut in_section = false;
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if trimmed == "[transparency]" {
+                    in_section = true;
+                    continue;
+                }
+                if trimmed.starts_with('[') && in_section {
+                    break;
+                }
+                if in_section && trimmed.starts_with("opacity") {
+                    if let Some(val) = trimmed.split('=').nth(1) {
+                        if let Ok(o) = val.trim().parse::<f32>() {
+                            if let Ok(mut lock) = OPACITY.write() {
+                                *lock = Some(o.clamp(0.0, 1.0));
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *OPACITY.read().unwrap()
+}
+
+pub fn toggle_on_color() -> [f32; 4] {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("toggle_enabled_color") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
+                    if hex.len() >= 6 {
+                        if let (Ok(r), Ok(g), Ok(b)) = (
+                            u8::from_str_radix(&hex[0..2], 16),
+                            u8::from_str_radix(&hex[2..4], 16),
+                            u8::from_str_radix(&hex[4..6], 16),
+                        ) {
+                            let r_f = srgb_to_linear(r as f32 / 255.0);
+                            let g_f = srgb_to_linear(g as f32 / 255.0);
+                            let b_f = srgb_to_linear(b as f32 / 255.0);
+                            if let Ok(mut lock) = TOGGLE_ON_COLOR.write() {
+                                *lock = [r_f, g_f, b_f, 1.0];
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *TOGGLE_ON_COLOR.read().unwrap()
+}
+
+pub fn set_toggle_on_color(color: [f32; 4]) {
+    if let Ok(mut lock) = TOGGLE_ON_COLOR.write() {
+        *lock = color;
+    }
+}
+
+pub fn toggle_off_color() -> [f32; 4] {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("toggle_disabled_color") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
+                    if hex.len() >= 6 {
+                        if let (Ok(r), Ok(g), Ok(b)) = (
+                            u8::from_str_radix(&hex[0..2], 16),
+                            u8::from_str_radix(&hex[2..4], 16),
+                            u8::from_str_radix(&hex[4..6], 16),
+                        ) {
+                            let r_f = srgb_to_linear(r as f32 / 255.0);
+                            let g_f = srgb_to_linear(g as f32 / 255.0);
+                            let b_f = srgb_to_linear(b as f32 / 255.0);
+                            if let Ok(mut lock) = TOGGLE_OFF_COLOR.write() {
+                                *lock = [r_f, g_f, b_f, 1.0];
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *TOGGLE_OFF_COLOR.read().unwrap()
+}
+
+pub fn set_toggle_off_color(color: [f32; 4]) {
+    if let Ok(mut lock) = TOGGLE_OFF_COLOR.write() {
+        *lock = color;
+    }
+}
+
diff --git a/src/engine.rs b/src/engine.rs
index d7904fd..f487f75 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -2,7 +2,7 @@ use std::time::Instant;
 use smithay_client_toolkit::{
     compositor::{CompositorHandler, CompositorState},
     delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
-    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
+    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output, delegate_xdg_popup,
     registry::{ProvidesRegistryState, RegistryState},
     output::{OutputHandler, OutputState},
     seat::{
@@ -24,13 +24,48 @@ use wayland_client::{
     protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_registry},
     Connection, QueueHandle, Proxy,
 };
+use smithay_client_toolkit::shell::xdg::popup::{Popup, PopupHandler, PopupConfigure};
+use smithay_client_toolkit::shell::xdg::{XdgPositioner, XdgSurface};
+use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity, ConstraintAdjustment};
+
 use calloop::EventLoop;
 use calloop_wayland_source::WaylandSource;
 use glyphon::{
     Cache, FontSystem, Resolution, SwashCache, TextArea, TextAtlas,
-    TextBounds, TextRenderer, Viewport,
+    TextBounds, TextRenderer, Viewport, Buffer, Attrs, Metrics,
 };
 use crate::widget::{TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey};
+
+pub struct ActivePopup {
+    pub sctk_popup: Popup,
+    pub wgpu_surface: wgpu::Surface<'static>,
+    pub config: wgpu::SurfaceConfiguration,
+    pub logical_width: f32,
+    pub logical_height: f32,
+    pub configured: bool,
+    pub viewport: Viewport,
+    pub x: f32,
+    pub y: f32,
+    pub vertex_buffer: Option<wgpu::Buffer>,
+}
+
+fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+    let metrics = Metrics::new(size, size * 1.4);
+    let mut buf = Buffer::new(fs, metrics);
+    let mut attrs = Attrs::new();
+    if let Some(font_name) = font {
+        let family = match font_name {
+            "monospace" => glyphon::Family::Monospace,
+            "sans-serif" => glyphon::Family::SansSerif,
+            "serif" => glyphon::Family::Serif,
+            _ => glyphon::Family::Name(font_name),
+        };
+        attrs = attrs.family(family);
+    }
+    buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+    buf.shape_until_scroll(fs, true);
+    buf
+}
 use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
 
 #[repr(C)]
@@ -110,9 +145,382 @@ pub fn quad_vertices_clipped(
     quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
 }
 
+pub fn line_vertices(
+    x1: f32, y1: f32, x2: f32, y2: f32,
+    thickness: f32,
+    sw: f32, sh: f32,
+    c: [f32; 4]
+) -> [Vertex; 6] {
+    let dx = x2 - x1;
+    let dy = y2 - y1;
+    let len = (dx * dx + dy * dy).sqrt();
+    if len < 0.001 {
+        return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c);
+    }
+    let ux = dx / len;
+    let uy = dy / len;
+    let nx = -uy;
+    let ny = ux;
+    
+    let half_t = thickness * 0.5;
+    let p0x = x1 + nx * half_t;
+    let p0y = y1 + ny * half_t;
+    let p1x = x1 - nx * half_t;
+    let p1y = y1 - ny * half_t;
+    let p2x = x2 - nx * half_t;
+    let p2y = y2 - ny * half_t;
+    let p3x = x2 + nx * half_t;
+    let p3y = y2 + ny * half_t;
+
+    let ndc_p0x = (p0x / sw) * 2.0 - 1.0;
+    let ndc_p0y = 1.0 - (p0y / sh) * 2.0;
+    let ndc_p1x = (p1x / sw) * 2.0 - 1.0;
+    let ndc_p1y = 1.0 - (p1y / sh) * 2.0;
+    let ndc_p2x = (p2x / sw) * 2.0 - 1.0;
+    let ndc_p2y = 1.0 - (p2y / sh) * 2.0;
+    let ndc_p3x = (p3x / sw) * 2.0 - 1.0;
+    let ndc_p3y = 1.0 - (p3y / sh) * 2.0;
+
+    let clip_circle = [0.0, 0.0, 0.0];
+    [
+        Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
+        Vertex { position: [ndc_p1x, ndc_p1y], color: c, clip_circle },
+        Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
+        Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
+        Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
+        Vertex { position: [ndc_p3x, ndc_p3y], color: c, clip_circle },
+    ]
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
+pub enum LineCap {
+    Arrow,
+    Round,
+    Flat,
+}
+
+pub fn vector_vertices(
+    x1: f32, y1: f32, x2: f32, y2: f32,
+    thickness: f32,
+    sw: f32, sh: f32,
+    c: [f32; 4],
+    line_cap: LineCap,
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    let dx = x2 - x1;
+    let dy = y2 - y1;
+    let len = (dx * dx + dy * dy).sqrt();
+    if len < 0.001 {
+        return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c).to_vec();
+    }
+    
+    match line_cap {
+        LineCap::Arrow => {
+            let ux = dx / len;
+            let uy = dy / len;
+            let nx = -uy;
+            let ny = ux;
+            
+            let arrow_len = (thickness * 3.0).max(10.0).min(len);
+            let arrow_width = (thickness * 2.5).max(8.0);
+            
+            let line_x2 = x2 - ux * arrow_len;
+            let line_y2 = y2 - uy * arrow_len;
+            
+            if len > arrow_len {
+                verts.extend_from_slice(&line_vertices(x1, y1, line_x2, line_y2, thickness, sw, sh, c));
+            }
+            
+            let bx = line_x2;
+            let by = line_y2;
+            
+            let w1x = bx + nx * (arrow_width * 0.5);
+            let w1y = by + ny * (arrow_width * 0.5);
+            let w2x = bx - nx * (arrow_width * 0.5);
+            let w2y = by - ny * (arrow_width * 0.5);
+            
+            let ndc_tip_x = (x2 / sw) * 2.0 - 1.0;
+            let ndc_tip_y = 1.0 - (y2 / sh) * 2.0;
+            let ndc_w1x = (w1x / sw) * 2.0 - 1.0;
+            let ndc_w1y = 1.0 - (w1y / sh) * 2.0;
+            let ndc_w2x = (w2x / sw) * 2.0 - 1.0;
+            let ndc_w2y = 1.0 - (w2y / sh) * 2.0;
+            
+            let clip_circle = [0.0, 0.0, 0.0];
+            verts.push(Vertex { position: [ndc_tip_x, ndc_tip_y], color: c, clip_circle });
+            verts.push(Vertex { position: [ndc_w1x, ndc_w1y], color: c, clip_circle });
+            verts.push(Vertex { position: [ndc_w2x, ndc_w2y], color: c, clip_circle });
+        }
+        LineCap::Round => {
+            verts.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
+            let clip_circle = [0.0, 0.0, 0.0];
+            verts.extend(circle_vertices(x2, y2, thickness / 2.0, sw, sh, c, 16, clip_circle));
+        }
+        LineCap::Flat => {
+            verts.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
+        }
+    }
+    
+    verts
+}
+
+pub fn rounded_rect_vertices_corners(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    clip_circle: [f32; 3],
+    corners: (bool, bool, bool, bool),
+    clip_rect: Option<(f32, f32, f32, f32)>,
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    let r = r.min(ww * 0.5).min(h * 0.5);
+
+    let clamp_x = |val: f32| -> f32 {
+        if let Some((cx0, _, cx1, _)) = clip_rect {
+            val.max(cx0).min(cx1)
+        } else {
+            val
+        }
+    };
+    let clamp_y = |val: f32| -> f32 {
+        if let Some((_, cy0, _, cy1)) = clip_rect {
+            val.max(cy0).min(cy1)
+        } else {
+            val
+        }
+    };
+
+    let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
+        let x0 = clamp_x(qx);
+        let y0 = clamp_y(qy);
+        let x1 = clamp_x(qx + qw);
+        let y1 = clamp_y(qy + qh);
+        
+        if x1 <= x0 || y1 <= y0 {
+            return;
+        }
+
+        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;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+    };
+
+    if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
+        push_quad(&mut verts, x, y, ww, h);
+        return verts;
+    }
+
+    // 1. Center rectangle
+    push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
+    
+    // 2. Left rectangle
+    push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
+    
+    // 3. Right rectangle
+    push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
+
+    // 4. Four corners
+    let corner_configs = [
+        // Top-left
+        (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
+        // Top-right
+        (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
+        // Bottom-right
+        (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
+        // Bottom-left
+        (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
+    ];
+
+    let segments = 16;
+    for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
+        if is_rounded {
+            for i in 0..segments {
+                let theta1 = start + (i as f32) * (end - start) / (segments as f32);
+                let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
+                
+                let x0 = clamp_x(cx);
+                let y0 = clamp_y(cy);
+                let x1 = clamp_x(cx + r * theta1.cos());
+                let y1 = clamp_y(cy + r * theta1.sin());
+                let x2 = clamp_x(cx + r * theta2.cos());
+                let y2 = clamp_y(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 });
+            }
+        } else {
+            push_quad(&mut verts, sqx, sqy, r, r);
+        }
+    }
+
+    verts
+}
+
+pub fn rounded_rect_vertices(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, clip_circle, (true, true, true, true), None)
+}
+
+pub fn plate_bevel_vertices(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    t: f32,
+    sw: f32, sh: f32,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+
+    // 3D Bevel Colors
+    // Highlight (light) for top and left edges
+    let highlight_color = [1.0, 1.0, 1.0, 0.15];
+    // Shadow (dark) for bottom and right edges
+    let shadow_color = [0.0, 0.0, 0.0, 0.25];
+
+    // Straight borders on the inside edge
+    // Top border (light)
+    verts.extend(quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, highlight_color, clip_circle));
+    // Left border (light)
+    verts.extend(quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, highlight_color, clip_circle));
+    // Bottom border (dark)
+    verts.extend(quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, shadow_color, clip_circle));
+    // Right border (dark)
+    verts.extend(quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, shadow_color, clip_circle));
+
+    // Corner arcs on the inside edge
+    let segments = 16;
+
+    // Top-left corner (fully light)
+    verts.extend(arc_background_vertices(
+        x + r, y + r, r, t,
+        std::f32::consts::PI, 1.5 * std::f32::consts::PI,
+        sw, sh, highlight_color, segments, clip_circle,
+    ));
+
+    // Top-right corner (split at 45 degrees)
+    verts.extend(arc_background_vertices(
+        x + ww - r, y + r, r, t,
+        1.5 * std::f32::consts::PI, 1.75 * std::f32::consts::PI,
+        sw, sh, highlight_color, segments / 2, clip_circle,
+    ));
+    verts.extend(arc_background_vertices(
+        x + ww - r, y + r, r, t,
+        1.75 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
+        sw, sh, shadow_color, segments / 2, clip_circle,
+    ));
+
+    // Bottom-right corner (fully dark)
+    verts.extend(arc_background_vertices(
+        x + ww - r, y + h - r, r, t,
+        0.0, 0.5 * std::f32::consts::PI,
+        sw, sh, shadow_color, segments, clip_circle,
+    ));
+
+    // Bottom-left corner (split at 45 degrees)
+    verts.extend(arc_background_vertices(
+        x + r, y + h - r, r, t,
+        0.5 * std::f32::consts::PI, 0.75 * std::f32::consts::PI,
+        sw, sh, shadow_color, segments / 2, clip_circle,
+    ));
+    verts.extend(arc_background_vertices(
+        x + r, y + h - r, r, t,
+        0.75 * std::f32::consts::PI, std::f32::consts::PI,
+        sw, sh, highlight_color, segments / 2, clip_circle,
+    ));
+
+    verts
+}
+
 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()
+    let corners = w.rounded_corners();
+    let mut verts = if corners != (false, false, false, false) {
+        rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), clip_circle, corners, None)
+    } else {
+        quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
+    };
+
+    if w.is_plate() {
+        verts.extend(plate_bevel_vertices(x, y, ww, h, 12.0, 1.5, sw, sh, clip_circle));
+    }
+
+    verts
+}
+
+pub fn extra_quad_vertices(
+    w: &dyn crate::widget::Widget,
+    qx: f32, qy: f32, qw: f32, qh: f32,
+    sw: f32, sh: f32,
+    qc: [f32; 4],
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let corners = w.rounded_corners();
+    if corners == (false, false, false, false) {
+        return quad_vertices_with_clip(qx, qy, qw, qh, sw, sh, qc, clip_circle).to_vec();
+    }
+
+    let (wx, wy, ww, wh) = w.rect();
+    let extra_corners = (
+        corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+        corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+        corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+        corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+    );
+
+    rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, None)
+}
+
+pub fn extra_quad_vertices_clipped(
+    w: &dyn crate::widget::Widget,
+    qx: f32, qy: f32, qw: f32, qh: f32,
+    sw: f32, sh: f32,
+    qc: [f32; 4],
+    clip: (f32, f32, f32, f32),
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let corners = w.rounded_corners();
+    if corners == (false, false, false, false) {
+        let (cx0, cy0, cx1, cy1) = clip;
+        let ix0 = qx.max(cx0);
+        let iy0 = qy.max(cy0);
+        let ix1 = (qx + qw).min(cx1);
+        let iy1 = (qy + qh).min(cy1);
+        if ix1 <= ix0 || iy1 <= iy0 {
+            return Vec::new();
+        }
+        return quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, sw, sh, qc, clip_circle).to_vec();
+    }
+
+    let (wx, wy, ww, wh) = w.rect();
+    let extra_corners = (
+        corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+        corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+        corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+        corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+    );
+
+    rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, Some(clip))
 }
 
 pub fn circle_vertices(
@@ -273,18 +681,31 @@ pub trait Application: Sized + 'static {
     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
     fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64);
+    fn view_vectors(&mut self, _vectors: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], LineCap)>, _size: LogicalSize, _scale: f64) {}
     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: &[],
+        self.text_items().iter().map(|ti| {
+            let item_bounds = if let Some([l, t, r, b]) = ti.bounds {
+                TextBounds {
+                    left: (l * scale_f32).round() as i32,
+                    top: (t * scale_f32).round() as i32,
+                    right: (r * scale_f32).round() as i32,
+                    bottom: (b * scale_f32).round() as i32,
+                }
+            } else {
+                bounds
+            };
+            TextArea {
+                buffer: &ti.buffer,
+                left: ti.x * scale_f32,
+                top: ti.y * scale_f32,
+                scale: scale_f32,
+                bounds: item_bounds,
+                default_color: ti.color,
+                custom_glyphs: &[],
+            }
         }).collect()
     }
     
@@ -336,6 +757,7 @@ pub struct EngineState<A: Application> {
     
     pub inner: A,
     
+    pub instance: Option<wgpu::Instance>,
     pub wgpu_surface: Option<wgpu::Surface<'static>>,
     pub device: Option<wgpu::Device>,
     pub queue: Option<wgpu::Queue>,
@@ -363,6 +785,8 @@ pub struct EngineState<A: Application> {
     pub shift_pressed: bool,
     pub pressed_key: Option<PressedKey>,
     pub sender: calloop::channel::Sender<A::Message>,
+    pub active_popup: Option<ActivePopup>,
+    pub qh: QueueHandle<EngineState<A>>,
 }
 
 impl<A: Application> EngineState<A> {
@@ -382,6 +806,7 @@ impl<A: Application> EngineState<A> {
             backends: wgpu::Backends::VULKAN,
             ..Default::default()
         });
+        self.instance = Some(instance.clone());
         
         let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create wgpu surface");
         let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
@@ -397,7 +822,16 @@ impl<A: Application> EngineState<A> {
             memory_hints: wgpu::MemoryHints::MemoryUsage,
         }, None).await.expect("failed to request device");
         
-        let config = wgpu_surface.get_default_config(&adapter, pw.max(1), ph.max(1)).expect("failed to get surface configuration");
+        let mut config = wgpu_surface.get_default_config(&adapter, pw.max(1), ph.max(1)).expect("failed to get surface configuration");
+        let capabilities = wgpu_surface.get_capabilities(&adapter);
+        let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
+            wgpu::CompositeAlphaMode::PreMultiplied
+        } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
+            wgpu::CompositeAlphaMode::PostMultiplied
+        } else {
+            capabilities.alpha_modes[0]
+        };
+        config.alpha_mode = alpha_mode;
         
         let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
             label: Some("Shader"),
@@ -498,6 +932,9 @@ impl<A: Application> EngineState<A> {
         let mut quads = Vec::new();
         self.inner.view(&mut quads, LogicalSize::new(logical_w, logical_h), scale_factor);
         
+        let mut vectors = Vec::new();
+        self.inner.view_vectors(&mut vectors, LogicalSize::new(logical_w, logical_h), scale_factor);
+        
         let device = self.device.as_ref().unwrap();
         let queue = self.queue.as_ref().unwrap();
         let wgpu_surface = self.wgpu_surface.as_ref().unwrap();
@@ -512,6 +949,9 @@ impl<A: Application> EngineState<A> {
         for &(qx, qy, qw, qh, qc) in &quads {
             verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
         }
+        for &(vx1, vy1, vx2, vy2, vthickness, vcolor, vcap) in &vectors {
+            verts.extend(vector_vertices(vx1, vy1, vx2, vy2, vthickness, logical_w, logical_h, vcolor, vcap));
+        }
         self.vertex_count = verts.len() as u32;
         if self.vertex_count > 0 {
             let data = bytemuck::cast_slice(&verts);
@@ -628,12 +1068,140 @@ impl<A: Application> EngineState<A> {
         
         queue.submit(std::iter::once(encoder.finish()));
         output.present();
+        
+        if let Some(ref mut popup) = self.active_popup {
+            if popup.configured {
+                let mut collector = crate::layout::PopoverCollector::new();
+                crate::layout::render_popovers(&mut collector);
+
+                let mut verts = Vec::new();
+                for &(color, qx, qy, qw, qh) in &collector.rects {
+                    let qx_local = qx - popup.x;
+                    let qy_local = qy - popup.y;
+                    verts.extend(quad_vertices(qx_local, qy_local, qw, qh, popup.logical_width, popup.logical_height, color));
+                }
+
+                let vertex_count = verts.len() as u32;
+                if vertex_count > 0 {
+                    let data = bytemuck::cast_slice(&verts);
+                    let needed = data.len() as wgpu::BufferAddress;
+                    let mut vbuf = popup.vertex_buffer.as_ref();
+                    if vbuf.map_or(true, |v| needed > v.size()) {
+                        let new_vbuf = device.create_buffer(&wgpu::BufferDescriptor {
+                            label: Some("Popup Vertex Buffer"),
+                            size: needed,
+                            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                            mapped_at_creation: false,
+                        });
+                        popup.vertex_buffer = Some(new_vbuf);
+                        vbuf = popup.vertex_buffer.as_ref();
+                    }
+                    queue.write_buffer(vbuf.unwrap(), 0, data);
+                }
+
+                let mut text_items = Vec::new();
+                for (content, size, tx, ty, color, font, bounds) in collector.texts {
+                    let tx_local = tx - popup.x;
+                    let ty_local = ty - popup.y;
+                    text_items.push(TextItem {
+                        buffer: make_text_buffer_with_font(&mut self.font_system, &content, size, font.as_deref()),
+                        x: tx_local,
+                        y: ty_local,
+                        color: glyphon::Color::rgb(
+                            (color[0] * 255.0).clamp(0.0, 255.0) as u8,
+                            (color[1] * 255.0).clamp(0.0, 255.0) as u8,
+                            (color[2] * 255.0).clamp(0.0, 255.0) as u8,
+                        ),
+                        bounds,
+                    });
+                }
+
+                let scale_f32 = scale_factor as f32;
+                let bounds = TextBounds {
+                    left: 0,
+                    top: 0,
+                    right: (popup.logical_width * scale_f32) as i32,
+                    bottom: (popup.logical_height * scale_f32) as i32,
+                };
+                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();
+
+                popup.viewport.update(queue, Resolution {
+                    width: (popup.logical_width * scale_f32) as u32,
+                    height: (popup.logical_height * scale_f32) as u32,
+                });
+
+                text_renderer.prepare(
+                    device,
+                    queue,
+                    &mut self.font_system,
+                    text_atlas,
+                    &popup.viewport,
+                    areas,
+                    &mut self.swash_cache,
+                ).unwrap();
+
+                let popup_output = match popup.wgpu_surface.get_current_texture() {
+                    Ok(t) => t,
+                    Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+                        popup.wgpu_surface.configure(device, &popup.config);
+                        return;
+                    }
+                    Err(wgpu::SurfaceError::Timeout) => return,
+                    Err(e) => {
+                        eprintln!("Popup surface error: {e:?}");
+                        return;
+                    }
+                };
+                let popup_view = popup_output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+                let mut popup_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                    label: Some("Popup Encoder"),
+                });
+
+                {
+                    let mut pass = popup_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("Popup Render Pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: &popup_view,
+                            resolve_target: None,
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: None,
+                        timestamp_writes: None,
+                        occlusion_query_set: None,
+                    });
+
+                    if vertex_count > 0 {
+                        pass.set_pipeline(render_pipeline);
+                        pass.set_vertex_buffer(0, popup.vertex_buffer.as_ref().unwrap().slice(..));
+                        pass.draw(0..vertex_count, 0..1);
+                    }
+
+                    text_renderer.render(text_atlas, &popup.viewport, &mut pass).unwrap();
+                }
+
+                queue.submit(std::iter::once(popup_encoder.finish()));
+                popup_output.present();
+            }
+        }
+
         text_atlas.trim();
     }
 }
 
 impl<A: Application> Drop for EngineState<A> {
     fn drop(&mut self) {
+        self.instance = None;
         self.wgpu_surface = None;
         self.device = None;
         self.queue = None;
@@ -820,8 +1388,15 @@ impl<A: Application> PointerHandler for EngineState<A> {
         use smithay_client_toolkit::seat::pointer::PointerEventKind;
         for event in events {
             let (x, y) = event.position;
-            let lx = x as f32;
-            let ly = y as f32;
+            let mut lx = x as f32;
+            let mut ly = y as f32;
+            
+            if let Some(ref popup) = self.active_popup {
+                if event.surface == *popup.sctk_popup.wl_surface() {
+                    lx += popup.x;
+                    ly += popup.y;
+                }
+            }
             
             match &event.kind {
                 PointerEventKind::Enter { .. } => {
@@ -1039,10 +1614,37 @@ impl<A: Application> wayland_client::Dispatch<crate::protocol::zclear_inspector_
     ) {}
 }
 
+impl<A: Application> PopupHandler for EngineState<A> {
+    fn configure(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _popup: &Popup,
+        _config: PopupConfigure,
+    ) {
+        if let Some(ref mut p) = self.active_popup {
+            p.configured = true;
+        }
+        self.redraw = true;
+    }
+
+    fn done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _popup: &Popup) {
+        for popover_ptr in crate::widget::popovers::get_active() {
+            unsafe {
+                let popover = &mut *(popover_ptr as *mut dyn crate::widget::Widget);
+                popover.unfocus();
+            }
+        }
+        self.active_popup = None;
+        self.redraw = true;
+    }
+}
+
 // Delegate macros for generic EngineState
 delegate_compositor!(@<A: Application> EngineState<A>);
 delegate_xdg_shell!(@<A: Application> EngineState<A>);
 delegate_xdg_window!(@<A: Application> EngineState<A>);
+delegate_xdg_popup!(@<A: Application> EngineState<A>);
 delegate_shm!(@<A: Application> EngineState<A>);
 delegate_seat!(@<A: Application> EngineState<A>);
 delegate_pointer!(@<A: Application> EngineState<A>);
@@ -1082,6 +1684,7 @@ pub fn run<A: Application>() {
         window: None,
         surface: None,
         inner,
+        instance: None,
         wgpu_surface: None,
         device: None,
         queue: None,
@@ -1106,6 +1709,8 @@ pub fn run<A: Application>() {
         shift_pressed: false,
         pressed_key: None,
         sender,
+        active_popup: None,
+        qh: qh.clone(),
     };
 
     event_queue.roundtrip(&mut engine_state).unwrap();
@@ -1133,7 +1738,7 @@ pub fn run<A: Application>() {
 
     let mut event_loop = EventLoop::try_new().unwrap();
     let loop_handle = event_loop.handle();
-    WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
+    WaylandSource::new(conn.clone(), event_queue).insert(loop_handle.clone()).unwrap();
 
     loop_handle.insert_source(channel, |event, _metadata, app_state: &mut EngineState<A>| {
         if let calloop::channel::Event::Msg(msg) = event {
@@ -1148,6 +1753,7 @@ pub fn run<A: Application>() {
     const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
     const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
 
+    let mut last_title = settings.title.clone();
     let mut last_tick = std::time::Instant::now();
     loop {
         event_loop.dispatch(std::time::Duration::from_millis(16), &mut engine_state).unwrap();
@@ -1195,6 +1801,84 @@ pub fn run<A: Application>() {
                 }
             }
         }
+        let current_title = engine_state.inner.settings().title;
+        if current_title != last_title {
+            if let Some(ref window) = engine_state.window {
+                window.set_title(&current_title);
+                window.commit();
+            }
+            last_title = current_title;
+        }
+
+        let active_popovers = crate::widget::popovers::get_active();
+        if !active_popovers.is_empty() {
+            let popover_widget = unsafe { &*active_popovers[0] };
+            if let Some((px, py, pw, ph)) = popover_widget.popover_rect() {
+                if engine_state.active_popup.is_none() {
+                    let wl_surface = engine_state.compositor_state.create_surface(&engine_state.qh);
+                    wl_surface.set_buffer_scale(engine_state.scale_factor as i32);
+                    
+                    let positioner = XdgPositioner::new(&engine_state.xdg_shell_state).unwrap();
+                    positioner.set_size(pw as i32, ph as i32);
+                    
+                    let (rx, ry, rw, rh) = popover_widget.rect();
+                    positioner.set_anchor_rect(rx as i32, ry as i32, rw as i32, rh as i32);
+                    positioner.set_anchor(Anchor::BottomLeft);
+                    positioner.set_gravity(Gravity::BottomRight);
+                    positioner.set_constraint_adjustment(
+                        ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY
+                    );
+                    
+                    let parent_xdg_surface = XdgSurface::xdg_surface(engine_state.window.as_ref().unwrap());
+                    let sctk_popup = Popup::new(
+                        parent_xdg_surface,
+                        &positioner,
+                        &engine_state.qh,
+                        &engine_state.compositor_state,
+                        &engine_state.xdg_shell_state,
+                    ).unwrap();
+                    
+                    let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
+                    let surface_ptr = sctk_popup.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
+                    let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
+                        display_ptr,
+                        surface_ptr,
+                    }));
+                    let instance = engine_state.instance.as_ref().unwrap();
+                    let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create popup wgpu surface");
+                    
+                    let device = engine_state.device.as_ref().unwrap();
+                    let main_config = engine_state.config.as_ref().unwrap();
+                    
+                    let scale_f32 = engine_state.scale_factor as f32;
+                    let mut popup_config = main_config.clone();
+                    popup_config.width = (pw * scale_f32) as u32;
+                    popup_config.height = (ph * scale_f32) as u32;
+                    wgpu_surface.configure(device, &popup_config);
+                    
+                    let cache = Cache::new(device);
+                    let viewport = Viewport::new(device, &cache);
+                    
+                    engine_state.active_popup = Some(ActivePopup {
+                        sctk_popup,
+                        wgpu_surface,
+                        config: popup_config,
+                        logical_width: pw,
+                        logical_height: ph,
+                        configured: false,
+                        viewport,
+                        x: px,
+                        y: py,
+                        vertex_buffer: None,
+                    });
+                }
+            }
+        } else {
+            if engine_state.active_popup.is_some() {
+                engine_state.active_popup = None;
+                engine_state.redraw = true;
+            }
+        }
 
         if engine_state.redraw {
             engine_state.redraw = false;
diff --git a/src/layout.rs b/src/layout.rs
index 494d697..c513710 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -6,11 +6,17 @@ pub trait RenderTarget {
     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
         self.text(content, x, y, size, color);
     }
+    fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _bounds: Option<[f32; 4]>) {
+        self.text(content, x, y, size, color);
+    }
+    fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, _bounds: Option<[f32; 4]>) {
+        self.text_with_font(content, x, y, size, color, font);
+    }
 }
 
 pub struct PopoverCollector {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
-    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>)>,
+    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
 }
 
 impl PopoverCollector {
@@ -25,11 +31,19 @@ impl RenderTarget for PopoverCollector {
     }
 
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
-        self.texts.push((content.to_string(), size, x, y, color, None));
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
     }
 
     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
-        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+    }
+
+    fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+    }
+
+    fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
     }
 }
 
@@ -39,7 +53,7 @@ pub fn render_widget<T: Widget + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
         pc.rect(qc, qx, qy, qw, qh);
     }
     let font_opt = w.widget_font();
-    for label in w.text_labels() {
+    for (label, bounds) in w.text_labels_with_bounds() {
         let color_f32 = [
             label.color[0] as f32 / 255.0,
             label.color[1] as f32 / 255.0,
@@ -47,9 +61,9 @@ pub fn render_widget<T: Widget + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
             1.0,
         ];
         if let Some(ref font) = font_opt {
-            pc.text_with_font(&label.text, label.x, label.y, label.font_size, color_f32, font);
+            pc.text_with_font_and_bounds(&label.text, label.x, label.y, label.font_size, color_f32, font, bounds);
         } else {
-            pc.text(&label.text, label.x, label.y, label.font_size, color_f32);
+            pc.text_with_bounds(&label.text, label.x, label.y, label.font_size, color_f32, bounds);
         }
     }
     if w.popover_rect().is_some() {
@@ -80,7 +94,7 @@ impl UiFrame {
         if let Some((qx, qy, qw, qh, qc)) = crate::widget::hover_animation::get_quad() {
             pc.rect(qc, qx, qy, qw, qh);
         }
-        render_popovers(pc);
+        // render_popovers(pc);
     }
 }
 
@@ -140,12 +154,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 total_h = wh + 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;
+        let clamped_w = ww.min((self.cw - x_off).max(0.0));
+        render_widget(pc, w, x, y, clamped_w, total_h);
+        self.y += total_h;
     }
 
     pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
@@ -196,6 +211,7 @@ pub struct Section {
     top: f32,
     pub content_y: f32,
     pub cw: f32,
+    label_width: f32,
 }
 
 impl Section {
@@ -203,10 +219,26 @@ impl Section {
     pub const DEFAULT_MARGIN_X: f32 = 12.0;
     pub const DEFAULT_ROW_GAP: f32 = 8.0;
 
+    fn estimate_label_width(label: &str) -> f32 {
+        let mut width = 0.0;
+        for c in label.chars() {
+            let factor = match c {
+                'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
+                'r' | 's' | 'J' | 'c' | 'z' => 0.42,
+                'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
+                'A'..='Z' => 0.68,
+                _ => 0.55,
+            };
+            width += factor * 14.0;
+        }
+        width
+    }
+
     pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
-        pc.text(label, left + Self::DEFAULT_MARGIN_X, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
-        pc.rect([0.18, 0.18, 0.27, 1.0], left + Self::ROW_PADDING_X, top + 22.0, cw - 2.0 * Self::ROW_PADDING_X, 1.0);
-        Self { left, top, content_y: top + 34.0, cw }
+        let label_width = Self::estimate_label_width(label);
+        let label_x = left + (cw - label_width) / 2.0;
+        pc.text(label, label_x, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
+        Self { left, top, content_y: top + 19.0, cw, label_width }
     }
 
     pub fn ax(&self, x_off: f32) -> f32 {
@@ -224,10 +256,19 @@ impl Section {
 
     pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
         w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+        let x = self.ax(x_off);
+        let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+        let clamped_w = ww.min((right_edge - x).max(0.0));
         let top_room = w.top_room();
-        self.content_y += top_room;
-        render_widget(pc, w, self.ax(x_off), self.ay(), ww, wh);
-        self.content_y += wh;
+        let total_h = wh + top_room;
+        render_widget(pc, w, x, self.ay(), clamped_w, total_h);
+        self.content_y += total_h;
+    }
+
+    pub fn widget_full<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
+        let x_off = 12.0;
+        let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off); // cw - 40.0
+        self.widget(pc, w, x_off, ww, wh);
     }
 
     pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
@@ -281,10 +322,27 @@ impl Section {
             [0.25, 0.25, 0.35, 1.0] // Default gray
         };
         let x = self.left + Self::ROW_PADDING_X;
-        let y = self.top + 22.0;
+        let y = self.top + 7.0;
         let w = self.cw - 2.0 * Self::ROW_PADDING_X;
         let h = self.content_y - y;
-        pc.rect(border, x, y, w, 1.0);
+        
+        let left_edge = x;
+        let right_edge = x + w;
+        if self.label_width > 0.0 {
+            let label_x = self.left + (self.cw - self.label_width) / 2.0;
+            let gap_margin = 6.0;
+            let gap_start = label_x - gap_margin;
+            let gap_end = label_x + self.label_width + gap_margin;
+            if gap_start > left_edge {
+                pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+            }
+            if right_edge > gap_end {
+                pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+            }
+        } else {
+            pc.rect(border, left_edge, y, w, 1.0);
+        }
+
         pc.rect(border, x, y + h + 12.0, w, 1.0);
         pc.rect(border, x, y, 1.0, h + 12.0);
         pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
@@ -321,6 +379,180 @@ impl<'a> VStack<'a> {
     }
 }
 
+pub struct Subsection {
+    left: f32,
+    top: f32,
+    pub content_y: f32,
+    pub cw: f32,
+    label_width: f32,
+}
+
+impl Subsection {
+    pub const ROW_PADDING_X: f32 = 8.0;
+    pub const DEFAULT_MARGIN_X: f32 = 12.0;
+    pub const DEFAULT_ROW_GAP: f32 = 8.0;
+
+    fn estimate_label_width(label: &str) -> f32 {
+        let mut width = 0.0;
+        for c in label.chars() {
+            let factor = match c {
+                'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
+                'r' | 's' | 'J' | 'c' | 'z' => 0.42,
+                'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
+                'A'..='Z' => 0.68,
+                _ => 0.55,
+            };
+            width += factor * 12.0;
+        }
+        width
+    }
+
+    pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
+        let label_width = Self::estimate_label_width(label);
+        let label_x = left + (cw - label_width) / 2.0;
+        pc.text(label, label_x, top, 12.0, [0.53, 0.53, 0.60, 1.0]);
+        Self { left, top, content_y: top + 17.0, cw, label_width }
+    }
+
+    pub fn ax(&self, x_off: f32) -> f32 {
+        let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+        self.left + x_off + shift
+    }
+
+    pub fn ay(&self) -> f32 { self.content_y }
+
+    pub fn spacing(&mut self, dy: f32) { self.content_y += dy; }
+
+    pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+        pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
+    }
+
+    pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+        w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+        let x = self.ax(x_off);
+        let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+        let clamped_w = ww.min((right_edge - x).max(0.0));
+        let top_room = w.top_room();
+        let total_h = wh + top_room;
+        render_widget(pc, w, x, self.ay(), clamped_w, total_h);
+        self.content_y += total_h;
+    }
+
+    pub fn widget_full<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
+        let x_off = 12.0;
+        let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off);
+        self.widget(pc, w, x_off, ww, wh);
+    }
+
+    pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
+        let x = self.ax(Self::ROW_PADDING_X);
+        let y = self.ay();
+        pc.rect([0.15, 0.15, 0.22, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
+        self.content_y += 8.0;
+    }
+
+    pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+        pc.rect(color, self.ax(x_off), self.ay(), w, h);
+        self.content_y += h;
+    }
+
+    pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
+        let margin_x = Self::ROW_PADDING_X + 12.0;
+        let usable_w = self.cw - 2.0 * margin_x;
+        if count == 0 {
+            return Vec::new();
+        }
+        let total_gap = gap * (count - 1) as f32;
+        let col_w = (usable_w - total_gap).max(0.0) / count as f32;
+
+        let mut cols = Vec::with_capacity(count);
+        for i in 0..count {
+            let x = self.left + margin_x + i as f32 * (col_w + gap);
+            cols.push((x, col_w));
+        }
+        cols
+    }
+
+    pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
+    where
+        F: FnMut(usize, f32, f32),
+    {
+        let cols = self.row_layout(count, gap);
+        for (i, &(x, w)) in cols.iter().enumerate() {
+            f(i, x, w);
+        }
+        self.content_y += h;
+    }
+
+    pub fn finish(&mut self, pc: &mut dyn RenderTarget) -> f32 {
+        self.finish_focused(pc, false)
+    }
+
+    pub fn finish_focused(&mut self, pc: &mut dyn RenderTarget, focused: bool) -> f32 {
+        let border: [f32; 4] = if focused {
+            [0.22, 0.38, 0.24, 1.0]
+        } else {
+            [0.18, 0.18, 0.25, 1.0]
+        };
+        let x = self.left + Self::ROW_PADDING_X;
+        let y = self.top + 7.0;
+        let w = self.cw - 2.0 * Self::ROW_PADDING_X;
+        let h = self.content_y - y;
+        
+        let left_edge = x;
+        let right_edge = x + w;
+        if self.label_width > 0.0 {
+            let label_x = self.left + (self.cw - self.label_width) / 2.0;
+            let gap_margin = 6.0;
+            let gap_start = label_x - gap_margin;
+            let gap_end = label_x + self.label_width + gap_margin;
+            if gap_start > left_edge {
+                pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+            }
+            if right_edge > gap_end {
+                pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+            }
+        } else {
+            pc.rect(border, left_edge, y, w, 1.0);
+        }
+
+        pc.rect(border, x, y + h + 12.0, w, 1.0);
+        pc.rect(border, x, y, 1.0, h + 12.0);
+        pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
+        self.content_y + 20.0
+    }
+
+    pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SubVStack<'a> {
+        SubVStack {
+            subsection: self,
+            pc,
+            spacing,
+        }
+    }
+}
+
+pub struct SubVStack<'a> {
+    subsection: &'a mut Subsection,
+    pc: &'a mut dyn RenderTarget,
+    spacing: f32,
+}
+
+impl<'a> SubVStack<'a> {
+    pub fn add_widget<T: Widget + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
+        self.subsection.widget(self.pc, w, Subsection::DEFAULT_MARGIN_X, ww, wh);
+        self.subsection.spacing(self.spacing);
+    }
+
+    pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
+    where
+        F: FnMut(usize, f32, f32),
+    {
+        self.subsection.row(count, gap, h, f);
+        self.subsection.spacing(self.spacing);
+    }
+}
+
+
 pub struct SplitterLayout {
     pub splitter1_x: f32,
     pub splitter2_x: f32,
@@ -343,14 +575,14 @@ impl SplitterLayout {
     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;
+            let max_s2 = (total_width - self.min_column_width).max(min_s2);
             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;
+            let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
             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;
+            let max_s2 = (total_width - self.min_column_width).max(min_s2);
             self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
         }
     }
@@ -375,6 +607,68 @@ impl SplitterLayout {
     }
 }
 
+pub struct Grid {
+    pub left: f32,
+    pub top: f32,
+    pub width: f32,
+    pub col_width: f32,
+    pub gap: f32,
+    pub col_heights: Vec<f32>,
+    pub col_lefts: Vec<f32>,
+}
+
+impl Grid {
+    pub fn new(left: f32, top: f32, width: f32, min_col_width: f32, gap: f32, count: usize) -> Self {
+        let total_gap = gap * (count - 1) as f32;
+        let total_grid_width = count as f32 * min_col_width + total_gap;
+        let left_offset = if total_grid_width < width {
+            (width - total_grid_width) / 2.0
+        } else {
+            0.0
+        };
+
+        let col_width = min_col_width;
+
+        let mut col_lefts = Vec::with_capacity(count);
+        let col_heights = vec![top; count];
+        for i in 0..count {
+            col_lefts.push(left + left_offset + i as f32 * (col_width + gap));
+        }
+
+        Self {
+            left,
+            top,
+            width,
+            col_width,
+            gap,
+            col_heights,
+            col_lefts,
+        }
+    }
+
+    pub fn next_column(&self) -> usize {
+        let mut min_idx = 0;
+        let mut min_h = self.col_heights[0];
+        for i in 1..self.col_heights.len() {
+            if self.col_heights[i] < min_h {
+                min_h = self.col_heights[i];
+                min_idx = i;
+            }
+        }
+        min_idx
+    }
+
+    pub fn max_height(&self) -> f32 {
+        let mut max_h = self.col_heights[0];
+        for i in 1..self.col_heights.len() {
+            if self.col_heights[i] > max_h {
+                max_h = self.col_heights[i];
+            }
+        }
+        max_h
+    }
+}
+
 pub struct CircularPaneLayout {
     pub x: f32,
     pub y: f32,
@@ -415,6 +709,259 @@ impl CircularPaneLayout {
     }
 }
 
+pub struct Radial {
+    pub center_x: f32,
+    pub center_y: f32,
+    pub aspect_ratio: f32,
+    pub base_spacing: f32,
+}
+
+impl Radial {
+    pub fn new(center_x: f32, center_y: f32, aspect_ratio: f32, base_spacing: f32) -> Self {
+        Self { center_x, center_y, aspect_ratio, base_spacing }
+    }
+
+    pub fn widget_rect(&self, idx: usize, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        if idx == 0 {
+            (self.center_x - ww / 2.0, self.center_y - wh / 2.0, ww, wh)
+        } else {
+            let mut ring = 1;
+            let mut ring_start = 1;
+            loop {
+                let ring_capacity = ring * 6;
+                if idx < ring_start + ring_capacity {
+                    let pos_in_ring = idx - ring_start;
+                    let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
+                    let radius = (ring as f32) * self.base_spacing;
+
+                    let x_offset = radius * angle.cos() * self.aspect_ratio;
+                    let y_offset = radius * angle.sin();
+
+                    return (
+                        self.center_x + x_offset - ww / 2.0,
+                        self.center_y + y_offset - wh / 2.0,
+                        ww,
+                        wh,
+                    );
+                }
+                ring_start += ring_capacity;
+                ring += 1;
+            }
+        }
+    }
+
+    pub fn layout_widgets<T: Widget + 'static>(&self, widgets: &mut [&mut T]) {
+        let mut active_idx = 0;
+        for w in widgets.iter_mut() {
+            if !w.layout_ignore() {
+                let (_, _, ww, wh) = w.rect();
+                let use_w = if ww > 0.0 { ww } else { 100.0 };
+                let use_h = if wh > 0.0 { wh } else { 50.0 };
+                let (x, y, rw, rh) = self.widget_rect(active_idx, use_w, use_h);
+                w.set_rect(x, y, rw, rh);
+                active_idx += 1;
+            }
+        }
+    }
+
+    pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Widget + 'static)]) {
+        let mut active_idx = 0;
+        for &w_ptr in widgets {
+            let w = unsafe { &mut *w_ptr };
+            if !w.layout_ignore() {
+                let (_, _, ww, wh) = w.rect();
+                let use_w = if ww > 0.0 { ww } else { 100.0 };
+                let use_h = if wh > 0.0 { wh } else { 50.0 };
+                let (x, y, rw, rh) = self.widget_rect(active_idx, use_w, use_h);
+                w.set_rect(x, y, rw, rh);
+                active_idx += 1;
+            }
+        }
+    }
+}
+
+pub trait LayoutStrategy {
+    fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
+    fn set_section_count(&mut self, _count: usize) {}
+}
+
+pub struct ColumnLayout {
+    left: f32,
+    top: f32,
+    width: f32,
+    current_y: f32,
+    gap: f32,
+}
+
+impl ColumnLayout {
+    pub fn new(gap: f32) -> Self {
+        Self {
+            left: 0.0,
+            top: 0.0,
+            width: 0.0,
+            current_y: 0.0,
+            gap,
+        }
+    }
+}
+
+impl LayoutStrategy for ColumnLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+        self.left = left;
+        self.top = top;
+        self.width = width;
+        self.current_y = top;
+    }
+
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        let x = self.left;
+        let y = self.current_y;
+        self.current_y += wh + self.gap;
+        (x, y, ww, wh)
+    }
+}
+
+pub struct GridLayout {
+    grid: Option<Grid>,
+    min_col_width: f32,
+    gap: f32,
+    num_sections: Option<usize>,
+}
+
+impl GridLayout {
+    pub fn new(min_col_width: f32, gap: f32) -> Self {
+        Self {
+            grid: None,
+            min_col_width,
+            gap,
+            num_sections: None,
+        }
+    }
+}
+
+impl LayoutStrategy for GridLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+        let max_cols = ((width + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0) as usize;
+        let count = if let Some(n) = self.num_sections {
+            n.min(max_cols).max(1)
+        } else {
+            max_cols
+        };
+        self.grid = Some(Grid::new(left, top, width, self.min_col_width, self.gap, count));
+    }
+
+    fn allocate(&mut self, _ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        if let Some(ref mut grid) = self.grid {
+            let col = grid.next_column();
+            let x = grid.col_lefts[col];
+            let y = grid.col_heights[col];
+            grid.col_heights[col] += wh + grid.gap;
+            (x, y, grid.col_width, wh)
+        } else {
+            (0.0, 0.0, 0.0, wh)
+        }
+    }
+
+    fn set_section_count(&mut self, count: usize) {
+        self.num_sections = Some(count);
+    }
+}
+
+pub struct RadialLayout {
+    radial: Option<Radial>,
+    aspect_ratio: f32,
+    base_spacing: f32,
+    idx: usize,
+}
+
+impl RadialLayout {
+    pub fn new(aspect_ratio: f32, base_spacing: f32) -> Self {
+        Self {
+            radial: None,
+            aspect_ratio,
+            base_spacing,
+            idx: 0,
+        }
+    }
+}
+
+impl LayoutStrategy for RadialLayout {
+    fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
+        let cx = left + width / 2.0;
+        let cy = top + height / 2.0;
+        let aspect = if self.aspect_ratio > 0.0 {
+            self.aspect_ratio
+        } else {
+            let screen_aspect = (width / height.max(1.0)).max(0.1);
+            1.0 + (screen_aspect - 1.0) * 0.4
+        };
+        self.radial = Some(Radial::new(cx, cy, aspect, self.base_spacing));
+        self.idx = 0;
+    }
+
+    fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+        if let Some(ref radial) = self.radial {
+            let rect = radial.widget_rect(self.idx, ww, wh);
+            self.idx += 1;
+            rect
+        } else {
+            (0.0, 0.0, ww, wh)
+        }
+    }
+}
+
+pub struct PageLayoutBuilder<'a, P> {
+    pub strategy: &'a mut dyn LayoutStrategy,
+    pub cx: f32,
+    pub cy: f32,
+    pub cw: f32,
+    pub ch: f32,
+    pub section_width: f32,
+    pub idx: usize,
+    _phantom: std::marker::PhantomData<P>,
+}
+
+impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
+    pub fn new(
+        strategy: &'a mut dyn LayoutStrategy,
+        cx: f32,
+        cy: f32,
+        cw: f32,
+        ch: f32,
+        section_width: f32,
+    ) -> Self {
+        strategy.init(cx, cy, cw, ch);
+        Self {
+            strategy,
+            cx,
+            cy,
+            cw,
+            ch,
+            section_width,
+            idx: 0,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+
+    pub fn with_section_count(mut self, count: usize) -> Self {
+        self.strategy.set_section_count(count);
+        self.strategy.init(self.cx, self.cy, self.cw, self.ch);
+        self
+    }
+
+    pub fn add_section<F>(&mut self, final_pc: &mut P, mut render_fn: F)
+    where
+        F: FnMut(&mut P, f32, f32) -> f32,
+    {
+        let mut dummy = P::default();
+        let wh = render_fn(&mut dummy, 0.0, 0.0);
+        let (rx, ry, _, _) = self.strategy.allocate(self.section_width, wh);
+        render_fn(final_pc, rx, ry);
+        self.idx += 1;
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -462,13 +1009,13 @@ mod tests {
 
     impl Widget for MockWidgetWithLabel {
         fn rect(&self) -> (f32, f32, f32, f32) {
-            (self.x, self.y, self.w, self.h)
+            (self.x, self.y - self.top_room, self.w, self.h + self.top_room)
         }
         fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
             self.x = x;
-            self.y = y;
+            self.y = y + self.top_room;
             self.w = w;
-            self.h = h;
+            self.h = (h - self.top_room).max(0.0);
         }
         fn color(&self) -> [f32; 4] {
             [0.0, 0.0, 0.0, 0.0]
@@ -505,4 +1052,74 @@ mod tests {
         // Third widget has top_room = 15.0, so its y should be shifted by 15.0
         assert_eq!(w3.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + 15.0);
     }
+
+    #[test]
+    fn test_grid_layout() {
+        // Test single column layout (width = 200, min_col_width = 300)
+        let grid1 = Grid::new(10.0, 20.0, 200.0, 300.0, 10.0, 1);
+        assert_eq!(grid1.col_heights.len(), 1);
+        assert_eq!(grid1.col_lefts[0], 10.0);
+        assert_eq!(grid1.col_width, 300.0);
+
+        // Test multi column layout (width = 700, min_col_width = 300, gap = 20)
+        // count = floor((700 + 20) / (300 + 20)) = floor(720 / 320) = 2.
+        // total_gap = 20 * 1 = 20.
+        // col_width = (700 - 20) / 2 = 340.
+        let mut grid2 = Grid::new(5.0, 15.0, 700.0, 300.0, 20.0, 2);
+        assert_eq!(grid2.col_heights.len(), 2);
+        assert_eq!(grid2.col_lefts[0], 45.0);
+        assert_eq!(grid2.col_lefts[1], 365.0);
+        assert_eq!(grid2.col_width, 300.0);
+
+        assert_eq!(grid2.next_column(), 0);
+        grid2.col_heights[0] += 50.0; // Column 0 height becomes 65.0
+        assert_eq!(grid2.next_column(), 1);
+        grid2.col_heights[1] += 30.0; // Column 1 height becomes 45.0
+        assert_eq!(grid2.next_column(), 1);
+        grid2.col_heights[1] += 30.0; // Column 1 height becomes 75.0
+        assert_eq!(grid2.next_column(), 0);
+        
+        assert_eq!(grid2.max_height(), 75.0);
+    }
+
+    #[test]
+    fn test_subsection() {
+        let mut pc = PopoverCollector::new();
+        let mut subsec = Subsection::new(&mut pc, 10.0, 20.0, 300.0, "Test Subsec");
+        assert_eq!(subsec.left, 10.0);
+        assert_eq!(subsec.top, 20.0);
+        assert_eq!(subsec.cw, 300.0);
+        
+        let mut w = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+        subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0);
+        
+        let bottom = subsec.finish(&mut pc);
+        assert!(bottom > 20.0);
+    }
+
+    #[test]
+    fn test_radial_layout() {
+        let radial = Radial::new(100.0, 100.0, 1.5, 50.0);
+        
+        // Check first widget is centered at (100.0, 100.0)
+        let rect0 = radial.widget_rect(0, 40.0, 30.0);
+        assert_eq!(rect0, (100.0 - 20.0, 100.0 - 15.0, 40.0, 30.0));
+        
+        // Check Ring 1 (idx = 1) vs Ring 2 (idx = 7)
+        let rect1 = radial.widget_rect(1, 40.0, 30.0);
+        let rect7 = radial.widget_rect(7, 40.0, 30.0);
+        
+        let c1_x = rect1.0 + rect1.2 / 2.0;
+        let c1_y = rect1.1 + rect1.3 / 2.0;
+        let c7_x = rect7.0 + rect7.2 / 2.0;
+        let c7_y = rect7.1 + rect7.3 / 2.0;
+        
+        let d1 = ((c1_x - 100.0).powi(2) + (c1_y - 100.0).powi(2)).sqrt();
+        let d7 = ((c7_x - 100.0).powi(2) + (c7_y - 100.0).powi(2)).sqrt();
+        
+        // Ring 2 should be further out than Ring 1
+        assert!(d7 > d1);
+        assert!(d1 > 0.0);
+    }
 }
+
diff --git a/src/lib.rs b/src/lib.rs
index 8b2c731..b5fec9f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,6 +4,7 @@ pub mod layout;
 pub mod wayland;
 pub mod protocol;
 pub mod engine;
+pub mod scale;
 
 pub mod colors {
     pub use crate::color::*;
diff --git a/src/main.rs b/src/main.rs
index 8f48d96..303724a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use clear_ui::widget::{
     Button, Checkbox, ContentBg, Header, Panel, ProgressBar, RangeSlider, Sidebar, Slider, Spinbox,
-    StatusBar, TextLabel, Toggle, Widget,
+    StatusBar, TextLabel, Toggle, Widget, JsonLayoutWidget, JsonLayoutConfig,
 };
 
 use glyphon::{
@@ -81,9 +81,125 @@ fn quad_vertices(
     ]
 }
 
+fn rounded_rect_vertices_corners(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    corners: (bool, bool, bool, bool),
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    let r = r.min(ww * 0.5).min(h * 0.5);
+
+    let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
+        let x0 = qx;
+        let y0 = qy;
+        let x1 = qx + qw;
+        let y1 = qy + qh;
+        
+        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 clip_circle = [0.0, 0.0, 0.0];
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+    };
+
+    if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
+        push_quad(&mut verts, x, y, ww, h);
+        return verts;
+    }
+
+    push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
+    push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
+    push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
+
+    let corner_configs = [
+        (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
+        (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
+        (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
+        (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
+    ];
+
+    let segments = 16;
+    for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
+        if is_rounded {
+            for i in 0..segments {
+                let theta1 = start + (i as f32) * (end - start) / (segments as f32);
+                let theta2 = start + ((i + 1) as f32) * (end - start) / (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;
+                
+                let clip_circle = [0.0, 0.0, 0.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 });
+            }
+        } else {
+            push_quad(&mut verts, sqx, sqy, r, r);
+        }
+    }
+
+    verts
+}
+
+fn rounded_rect_vertices(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+) -> Vec<Vertex> {
+    rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, (true, true, true, true))
+}
+
 fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
     let (x, y, ww, h) = w.rect();
-    quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+    let corners = w.rounded_corners();
+    if corners != (false, false, false, false) {
+        rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), corners)
+    } else {
+        quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+    }
+}
+
+fn extra_quad_vertices(
+    w: &dyn Widget,
+    qx: f32, qy: f32, qw: f32, qh: f32,
+    sw: f32, sh: f32,
+    qc: [f32; 4],
+) -> Vec<Vertex> {
+    let corners = w.rounded_corners();
+    if corners == (false, false, false, false) {
+        return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
+    }
+
+    let (wx, wy, ww, wh) = w.rect();
+    let extra_corners = (
+        corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+        corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+        corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+        corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+    );
+
+    rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, extra_corners)
 }
 
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -94,6 +210,24 @@ fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buff
     buffer
 }
 
+fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+    let metrics = Metrics::new(size, size * 1.4);
+    let mut buffer = Buffer::new(font_system, metrics);
+    let mut attrs = Attrs::new();
+    if let Some(font_name) = font {
+        let family = match font_name {
+            "monospace" => glyphon::Family::Monospace,
+            "sans-serif" => glyphon::Family::SansSerif,
+            "serif" => glyphon::Family::Serif,
+            _ => glyphon::Family::Name(font_name),
+        };
+        attrs = attrs.family(family);
+    }
+    buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
+    buffer.shape_until_scroll(font_system, true);
+    buffer
+}
+
 struct State {
     surface: wgpu::Surface<'static>,
     device: wgpu::Device,
@@ -127,10 +261,19 @@ struct State {
     physical_width: u32,
     physical_height: u32,
     scale: f64,
+    json_layout: Option<JsonLayoutWidget>,
+    layout_mode: bool,
 }
 
 impl State {
-    async fn new(wayland_handle: &'static clear_ui::wayland::WaylandSurfaceHandle, pw: u32, ph: u32, scale: f64) -> Self {
+    async fn new(
+        wayland_handle: &'static clear_ui::wayland::WaylandSurfaceHandle,
+        pw: u32,
+        ph: u32,
+        scale: f64,
+        json_layout_config: Option<JsonLayoutConfig>,
+    ) -> Self {
+        clear_ui::scale::set_scale_factor(scale as f32);
         let lw = pw as f32 / scale as f32;
         let lh = ph as f32 / scale as f32;
         let sw = lw;
@@ -236,26 +379,36 @@ impl State {
         let label_buffer = make_text_buffer(&mut font_system, "Hello, Clear UI!", 16.0);
         let status_buffer = make_text_buffer(&mut font_system, "Click a button to interact", 12.0);
 
-        let widgets: Vec<Box<dyn Widget>> = vec![
-            Box::new(Header::new()),
-            Box::new(Sidebar::new(60.0)),
-            Box::new(ContentBg::new()),
-            Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
-            Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
-            Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
-            Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
-            Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
-            Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
-            Box::new(Checkbox::new()),
-            Box::new(Toggle::new()),
-            Box::new(ProgressBar::new(0.65)),
-            Box::new(Slider::new()),
-            Box::new(Spinbox::new(0, -10, 10, 1)),
-            Box::new(RangeSlider::new()),
-            Box::new(StatusBar::new()),
-        ];
-
-        let positions = demo_positions(sw, sh);
+        let layout_mode = json_layout_config.is_some();
+        let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
+        let mut positions = Vec::new();
+        let json_layout = if let Some(ref config) = json_layout_config {
+            let mut jl = JsonLayoutWidget::new(config);
+            jl.set_rect(0.0, 0.0, sw, sh);
+            positions.push((0.0, 0.0, sw, sh));
+            Some(jl)
+        } else {
+            widgets = vec![
+                Box::new(Header::new()),
+                Box::new(Sidebar::new(60.0)),
+                Box::new(ContentBg::new()),
+                Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
+                Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
+                Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
+                Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
+                Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
+                Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
+                Box::new(Checkbox::new()),
+                Box::new(Toggle::new()),
+                Box::new(ProgressBar::new(0.65)),
+                Box::new(Slider::new()),
+                Box::new(Spinbox::new(0, -10, 10, 1)),
+                Box::new(RangeSlider::new()),
+                Box::new(StatusBar::new()),
+            ];
+            positions = demo_positions(sw, sh);
+            None
+        };
 
         let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some("Vertex Buffer"),
@@ -291,6 +444,8 @@ impl State {
             physical_width: pw,
             physical_height: ph,
             scale,
+            json_layout,
+            layout_mode,
         };
 
         state.apply_layout();
@@ -299,13 +454,20 @@ impl State {
     }
 
     fn apply_layout(&mut self) {
-        for (i, pos) in self.positions.iter().enumerate() {
-            if let Some(widget) = self.widgets.get_mut(i) {
-                if widget.is_dragging() {
-                    continue;
+        if self.layout_mode {
+            if let Some(jl) = &mut self.json_layout {
+                clear_ui::scale::set_scale_factor(self.scale as f32);
+                jl.set_rect(0.0, 0.0, self.width, self.height);
+            }
+        } else {
+            for (i, pos) in self.positions.iter().enumerate() {
+                if let Some(widget) = self.widgets.get_mut(i) {
+                    if widget.is_dragging() {
+                        continue;
+                    }
+                    let (x, y, w, h) = *pos;
+                    widget.set_rect(x, y, w, h);
                 }
-                let (x, y, w, h) = *pos;
-                widget.set_rect(x, y, w, h);
             }
         }
     }
@@ -314,32 +476,42 @@ impl State {
         let sw = self.width;
         let sh = self.height;
         let mut verts = Vec::new();
-        let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
-        draw_order.sort_by_key(|&i| self.widgets[i].z_index());
-        for &i in &draw_order {
-            let w = &self.widgets[i];
-            verts.extend(widget_vertices(w.as_ref(), sw, sh));
-            for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+        if self.layout_mode {
+            verts.extend(quad_vertices(0.0, 0.0, sw, sh, sw, sh, [0.05, 0.05, 0.08, 1.0]));
+            if let Some(jl) = &self.json_layout {
+                verts.extend(widget_vertices(jl, sw, sh));
+                for (qx, qy, qw, qh, qc) in jl.extra_quads() {
+                    verts.extend(extra_quad_vertices(jl, qx, qy, qw, qh, sw, sh, qc));
+                }
             }
-        }
-
-        // Draw popover quads on top
-        let mut popover_pc = clear_ui::layout::PopoverCollector::new();
-        for &i in &draw_order {
-            let w = &self.widgets[i];
-            if w.popover_rect().is_some() {
-                w.render_popover(&mut popover_pc);
+        } else {
+            let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
+            draw_order.sort_by_key(|&i| self.widgets[i].z_index());
+            for &i in &draw_order {
+                let w = &self.widgets[i];
+                verts.extend(widget_vertices(w.as_ref(), sw, sh));
+                for (qx, qy, qw, qh, qc) in w.extra_quads() {
+                    verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc));
+                }
             }
-        }
-        for (qc, qx, qy, qw, qh) in popover_pc.rects {
-            verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
-        }
 
-        if clear_ui::widget::context_menu::is_visible() {
-            for (qx, qy, qw, qh, qc) in clear_ui::widget::context_menu::extra_quads() {
+            // Draw popover quads on top
+            let mut popover_pc = clear_ui::layout::PopoverCollector::new();
+            for &i in &draw_order {
+                let w = &self.widgets[i];
+                if w.popover_rect().is_some() {
+                    w.render_popover(&mut popover_pc);
+                }
+            }
+            for (qc, qx, qy, qw, qh) in popover_pc.rects {
                 verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
             }
+
+            if clear_ui::widget::context_menu::is_visible() {
+                for (qx, qy, qw, qh, qc) in clear_ui::widget::context_menu::extra_quads() {
+                    verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+                }
+            }
         }
         verts
     }
@@ -381,6 +553,8 @@ impl State {
             physical_width,
             physical_height,
             scale,
+            ref json_layout,
+            layout_mode,
             ..
         } = self;
 
@@ -389,8 +563,9 @@ impl State {
 
         let scale_f32 = *scale as f32;
 
-        let mut areas: Vec<TextArea> = vec![
-            TextArea {
+        let mut areas: Vec<TextArea> = Vec::new();
+        if !*layout_mode {
+            areas.push(TextArea {
                 buffer: label_buffer,
                 left: 80.0 * scale_f32,
                 top: 12.0 * scale_f32,
@@ -403,8 +578,8 @@ impl State {
                 },
                 default_color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
                 custom_glyphs: &[],
-            },
-            TextArea {
+            });
+            areas.push(TextArea {
                 buffer: status_buffer,
                 left: 12.0 * scale_f32,
                 top: *physical_height as f32 - 24.0 * scale_f32,
@@ -417,78 +592,109 @@ impl State {
                 },
                 default_color: glyphon::Color::rgb(0x55, 0x55, 0x66),
                 custom_glyphs: &[],
-            },
-        ];
+            });
+        }
 
         let mut widget_buffers: Vec<Buffer> = Vec::new();
-        let mut widget_labels: Vec<TextLabel> = Vec::new();
-        for (i, w) in self.widgets.iter().enumerate() {
-            for label in w.text_labels() {
-                let mut covered = false;
-                for (pi, pw) in self.widgets.iter().enumerate() {
-                    if pi != i {
-                        if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
-                            if label.is_covered_by(px, py, pw_val, ph) {
-                                covered = true;
-                                break;
+        let mut widget_labels: Vec<(TextLabel, Option<[f32; 4]>)> = Vec::new();
+        if *layout_mode {
+            if let Some(jl) = json_layout {
+                for (label, font, bounds) in jl.text_labels_with_font_and_bounds() {
+                    widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
+                    widget_labels.push((label, bounds));
+                }
+            }
+        } else {
+            for (i, w) in self.widgets.iter().enumerate() {
+                for (label, font, bounds) in w.text_labels_with_font_and_bounds() {
+                    let mut covered = false;
+                    for (pi, pw) in self.widgets.iter().enumerate() {
+                        if pi != i {
+                            if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
+                                if label.is_covered_by(px, py, pw_val, ph) {
+                                    covered = true;
+                                    break;
+                                }
                             }
                         }
                     }
-                }
-                if !covered {
-                    widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
-                    widget_labels.push(label);
+                    if !covered {
+                        widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
+                        widget_labels.push((label, bounds));
+                    }
                 }
             }
-        }
 
-        if clear_ui::widget::context_menu::is_visible() {
-            for label in clear_ui::widget::context_menu::text_labels() {
-                widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
-                widget_labels.push(label);
+            if clear_ui::widget::context_menu::is_visible() {
+                for label in clear_ui::widget::context_menu::text_labels() {
+                    widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+                    widget_labels.push((label, None));
+                }
             }
         }
 
-        for (buf, label) in widget_buffers.iter().zip(widget_labels.iter()) {
+        for (buf, (label, bounds)) in widget_buffers.iter().zip(widget_labels.iter()) {
+            let item_bounds = if let Some([l, t, r, b]) = bounds {
+                TextBounds {
+                    left: (l * scale_f32).round() as i32,
+                    top: (t * scale_f32).round() as i32,
+                    right: (r * scale_f32).round() as i32,
+                    bottom: (b * scale_f32).round() as i32,
+                }
+            } else {
+                TextBounds {
+                    left: 0,
+                    top: 0,
+                    right: *physical_width as i32,
+                    bottom: *physical_height as i32,
+                }
+            };
             areas.push(TextArea {
                 buffer: buf,
                 left: label.x * scale_f32,
                 top: label.y * scale_f32,
                 scale: scale_f32,
-                bounds: TextBounds {
-                    left: 0,
-                    top: 0,
-                    right: *physical_width as i32,
-                    bottom: *physical_height as i32,
-                },
+                bounds: item_bounds,
                 default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
                 custom_glyphs: &[],
             });
         }
 
-        // Draw popover texts on top
         let mut popover_pc = clear_ui::layout::PopoverCollector::new();
-        for w in &self.widgets {
-            if w.popover_rect().is_some() {
-                w.render_popover(&mut popover_pc);
-            }
-        }
         let mut popover_buffers = Vec::new();
-        for (t, size, _x, _y, _tc, _font_opt) in &popover_pc.texts {
-            popover_buffers.push(make_text_buffer(font_system, t, *size));
-        }
-        for (buf, (_, size, x, y, tc, _font_opt)) in popover_buffers.iter().zip(popover_pc.texts.iter()) {
-            areas.push(TextArea {
-                buffer: buf,
-                left: *x * scale_f32,
-                top: *y * scale_f32,
-                scale: scale_f32,
-                bounds: TextBounds {
-                    left: 0,
-                    top: 0,
-                    right: *physical_width as i32,
-                    bottom: *physical_height as i32,
-                },
+
+        if !*layout_mode {
+            // Draw popover texts on top
+            for w in &self.widgets {
+                if w.popover_rect().is_some() {
+                    w.render_popover(&mut popover_pc);
+                }
+            }
+            for (t, size, _x, _y, _tc, _font_opt, _bounds) in &popover_pc.texts {
+                popover_buffers.push(make_text_buffer(font_system, t, *size));
+            }
+            for (buf, (_, size, x, y, tc, _font_opt, bounds)) in popover_buffers.iter().zip(popover_pc.texts.iter()) {
+                let item_bounds = if let Some([l, t, r, b]) = bounds {
+                    TextBounds {
+                        left: (l * scale_f32).round() as i32,
+                        top: (t * scale_f32).round() as i32,
+                        right: (r * scale_f32).round() as i32,
+                        bottom: (b * scale_f32).round() as i32,
+                    }
+                } else {
+                    TextBounds {
+                        left: 0,
+                        top: 0,
+                        right: *physical_width as i32,
+                        bottom: *physical_height as i32,
+                    }
+                };
+                areas.push(TextArea {
+                    buffer: buf,
+                    left: *x * scale_f32,
+                    top: *y * scale_f32,
+                    scale: scale_f32,
+                    bounds: item_bounds,
                 default_color: glyphon::Color::rgb(
                     (tc[0] * 255.0) as u8,
                     (tc[1] * 255.0) as u8,
@@ -497,6 +703,7 @@ impl State {
                 custom_glyphs: &[],
             });
         }
+        }
 
         text_renderer
             .prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache)
@@ -512,7 +719,11 @@ impl State {
             self.config.width = width;
             self.config.height = height;
             self.surface.configure(&self.device, &self.config);
-            self.positions = demo_positions(self.width, self.height);
+            if self.layout_mode {
+                self.positions = vec![(0.0, 0.0, self.width, self.height)];
+            } else {
+                self.positions = demo_positions(self.width, self.height);
+            }
             self.apply_layout();
             self.upload_vertices();
         }
@@ -642,6 +853,8 @@ struct AppState {
     ctrl_pressed: bool,
     shift_pressed: bool,
     pressed_key: Option<PressedKey>,
+    key_repeat_delay: std::time::Duration,
+    key_repeat_interval: std::time::Duration,
     inspector: Option<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1>,
 }
 
@@ -801,6 +1014,12 @@ impl PointerHandler for AppState {
                             if clear_ui::widget::context_menu::cursor_moved(state.cursor_x, state.cursor_y) {
                                 changed = true;
                             }
+                        } else if state.layout_mode {
+                            if let Some(jl) = &mut state.json_layout {
+                                if jl.cursor_moved(state.cursor_x, state.cursor_y) {
+                                    changed = true;
+                                }
+                            }
                         } else {
                             if let Some(idx) = state.drag_widget {
                                 if state.widgets[idx].drag_update(state.cursor_x, state.cursor_y) {
@@ -834,6 +1053,12 @@ impl PointerHandler for AppState {
                             if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
                                 changed = true;
                             }
+                        } else if st.layout_mode {
+                            if let Some(jl) = &mut st.json_layout {
+                                if jl.mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
+                                    changed = true;
+                                }
+                            }
                         } else {
                             let mut clicked_idx = None;
                             for i in (0..st.widgets.len()).rev() {
@@ -884,10 +1109,60 @@ impl PointerHandler for AppState {
                     };
                     if let Some(st) = &mut self.state {
                         let mut changed = false;
+                        let mut should_close = false;
                         if clear_ui::widget::context_menu::is_visible() {
                             if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
                                 changed = true;
                             }
+                        } else if st.layout_mode {
+                            let mut clicked_btn_id = None;
+                            if let Some(jl) = &mut st.json_layout {
+                                if jl.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
+                                    changed = true;
+                                }
+                                if btn == clear_ui::widget::MouseButton::Left {
+                                    for w in &mut jl.widgets {
+                                        let active_page = jl.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+                                        if w.page_idx != active_page {
+                                            continue;
+                                        }
+                                        if let Some(btn_w) = &mut w.button {
+                                            if btn_w.take_click() {
+                                                clicked_btn_id = Some(w.id.clone());
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            if let Some(btn_id) = clicked_btn_id {
+                                let mut checkboxes = std::collections::HashMap::new();
+                                let mut spinboxes = std::collections::HashMap::new();
+                                let mut colors = std::collections::HashMap::new();
+                                let mut sliders = std::collections::HashMap::new();
+                                if let Some(jl) = &st.json_layout {
+                                    for w in &jl.widgets {
+                                        if let Some(cb) = &w.checkbox {
+                                            checkboxes.insert(w.id.clone(), cb.checked());
+                                        } else if let Some(sb) = &w.spinbox {
+                                            spinboxes.insert(w.id.clone(), sb.value);
+                                        } else if let Some(cs) = &w.color_selector {
+                                            colors.insert(w.id.clone(), cs.color);
+                                        } else if let Some(sl) = &w.slider {
+                                            sliders.insert(w.id.clone(), sl.get_scaled_value());
+                                        }
+                                    }
+                                }
+                                let out_val = serde_json::json!({
+                                    "button": btn_id,
+                                    "checkboxes": checkboxes,
+                                    "spinboxes": spinboxes,
+                                    "colors": colors,
+                                    "sliders": sliders
+                                });
+                                println!("{}", out_val.to_string());
+                                should_close = true;
+                            }
                         } else {
                             if btn == clear_ui::widget::MouseButton::Left {
                                 if let Some(idx) = st.drag_widget {
@@ -918,6 +1193,9 @@ impl PointerHandler for AppState {
                             st.upload_vertices();
                             self.redraw = true;
                         }
+                        if should_close {
+                            self.exit = true;
+                        }
                     }
                 }
                 PointerEventKind::Axis { horizontal, vertical, .. } => {
@@ -927,9 +1205,17 @@ impl PointerHandler for AppState {
                         
                         let delta = clear_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
                         let mut changed = false;
-                        for w in &mut state.widgets {
-                            if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
-                                changed = true;
+                        if !state.layout_mode {
+                            for w in &mut state.widgets {
+                                if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+                                    changed = true;
+                                }
+                            }
+                        } else {
+                            if let Some(jl) = &mut state.json_layout {
+                                if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+                                    changed = true;
+                                }
                             }
                         }
                         if changed {
@@ -1003,6 +1289,25 @@ impl KeyboardHandler for AppState {
         self.ctrl_pressed = modifiers.ctrl;
         self.shift_pressed = modifiers.shift;
     }
+
+    fn update_repeat_info(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _keyboard: &wl_keyboard::WlKeyboard,
+        info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
+    ) {
+        match info {
+            smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
+                self.key_repeat_delay = std::time::Duration::from_millis(delay as u64);
+                let interval_ms = 1000 / rate.get() as u64;
+                self.key_repeat_interval = std::time::Duration::from_millis(interval_ms);
+            }
+            smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {
+                self.key_repeat_delay = std::time::Duration::from_secs(999999);
+            }
+        }
+    }
 }
 
 impl AppState {
@@ -1059,7 +1364,15 @@ impl AppState {
         }
 
         if let Some(st) = &mut self.state {
-            if let Some(idx) = st.focused_widget {
+            if st.layout_mode {
+                if let Some(jl) = &mut st.json_layout {
+                    let mut changed = jl.keyboard_input(&custom_event);
+                    if changed {
+                        st.upload_vertices();
+                        self.redraw = true;
+                    }
+                }
+            } else if let Some(idx) = st.focused_widget {
                 let val = st.widgets[idx].value();
                 let mut changed = st.widgets[idx].keyboard_input(&custom_event);
                 if st.widgets[idx].value() != val {
@@ -1146,6 +1459,44 @@ delegate_registry!(AppState);
 delegate_output!(AppState);
 
 fn main() {
+    let mut layout_mode = false;
+    let mut json_layout_config: Option<JsonLayoutConfig> = None;
+
+    let args = std::env::args().skip(1).collect::<Vec<String>>();
+    let mut idx = 0;
+    while idx < args.len() {
+        let arg = &args[idx];
+        if arg == "--layout" || arg == "--json" {
+            layout_mode = true;
+            idx += 1;
+        } else {
+            idx += 1;
+        }
+    }
+
+    if layout_mode {
+        use std::io::Read;
+        let mut json_str = String::new();
+        let mut stdin = std::io::stdin();
+        match stdin.read_to_string(&mut json_str) {
+            Ok(_) => {
+                match serde_json::from_str::<JsonLayoutConfig>(&json_str) {
+                    Ok(cfg) => {
+                        json_layout_config = Some(cfg);
+                    }
+                    Err(e) => {
+                        eprintln!("Failed to parse JSON layout: {}", e);
+                        std::process::exit(1);
+                    }
+                }
+            }
+            Err(e) => {
+                eprintln!("Failed to read JSON layout from stdin: {}", e);
+                std::process::exit(1);
+            }
+        }
+    }
+
     let conn = Connection::connect_to_env().unwrap();
     let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
     let qh = event_queue.handle();
@@ -1175,6 +1526,8 @@ fn main() {
         ctrl_pressed: false,
         shift_pressed: false,
         pressed_key: None,
+        key_repeat_delay: std::time::Duration::from_millis(500),
+        key_repeat_interval: std::time::Duration::from_millis(50),
         inspector,
     };
 
@@ -1186,13 +1539,23 @@ fn main() {
     let surface = app.compositor_state.create_surface(&qh);
     surface.set_buffer_scale(scale as i32);
 
-    let pw = (1024.0 * scale) as u32;
-    let ph = (768.0 * scale) as u32;
+    let mut win_w = 1024.0;
+    let mut win_h = 768.0;
+    if let Some(ref cfg) = json_layout_config {
+        if let Some(w) = cfg.width {
+            win_w = w as f64;
+        }
+        if let Some(h) = cfg.height {
+            win_h = h as f64;
+        }
+    }
+    let pw = (win_w * scale) as u32;
+    let ph = (win_h * scale) as u32;
 
     let window = app.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
     window.set_title("Clear UI - Test Window");
     window.set_app_id("clear-ui");
-    window.set_min_size(Some((pw, ph)));
+    window.set_min_size(Some((win_w as u32, win_h as u32)));
     window.commit();
 
     if let Some(ref inspector) = app.inspector {
@@ -1204,7 +1567,7 @@ fn main() {
         surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
     }));
 
-    let state = pollster::block_on(State::new(wayland_handle, pw, ph, scale));
+    let state = pollster::block_on(State::new(wayland_handle, pw, ph, scale, json_layout_config));
 
     app.window = Some(window);
     app.surface = Some(surface);
@@ -1214,8 +1577,9 @@ fn main() {
     let loop_handle = event_loop.handle();
     WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
 
-    const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
-    const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
+
+
+    let mut last_tick = std::time::Instant::now();
 
     loop {
         event_loop
@@ -1225,10 +1589,35 @@ fn main() {
             break;
         }
 
+        let now = std::time::Instant::now();
+        let dt = now.duration_since(last_tick).as_secs_f32();
+        last_tick = now;
+
+        if let Some(ref mut st) = app.state {
+            let mut tick_changed = false;
+            if st.layout_mode {
+                if let Some(ref mut jl) = &mut st.json_layout {
+                    if jl.tick(dt) {
+                        tick_changed = true;
+                    }
+                }
+            } else {
+                for w in &mut st.widgets {
+                    if w.tick(dt) {
+                        tick_changed = true;
+                    }
+                }
+            }
+            if tick_changed {
+                st.upload_vertices();
+                app.redraw = true;
+            }
+        }
+
         if let Some(ref mut pk) = app.pressed_key {
             let now = std::time::Instant::now();
-            if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
-                if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
+            if now.duration_since(pk.first_pressed) >= app.key_repeat_delay {
+                if now.duration_since(pk.last_repeated) >= app.key_repeat_interval {
                     pk.last_repeated = now;
                     let custom_event = clear_ui::widget::KeyEvent {
                         state: clear_ui::widget::ElementState::Pressed,
@@ -1239,7 +1628,15 @@ fn main() {
                         shift: app.shift_pressed,
                     };
                     if let Some(st) = &mut app.state {
-                        if let Some(idx) = st.focused_widget {
+                        if st.layout_mode {
+                            if let Some(jl) = &mut st.json_layout {
+                                let mut changed = jl.keyboard_input(&custom_event);
+                                if changed {
+                                    st.upload_vertices();
+                                    app.redraw = true;
+                                }
+                            }
+                        } else if let Some(idx) = st.focused_widget {
                             let val = st.widgets[idx].value();
                             let mut changed = st.widgets[idx].keyboard_input(&custom_event);
                             if st.widgets[idx].value() != val {
diff --git a/src/scale.rs b/src/scale.rs
new file mode 100644
index 0000000..5c4b475
--- /dev/null
+++ b/src/scale.rs
@@ -0,0 +1,13 @@
+use std::sync::RwLock;
+
+static SCALE_FACTOR: RwLock<f32> = RwLock::new(1.0);
+
+pub fn scale_factor() -> f32 {
+    *SCALE_FACTOR.read().unwrap()
+}
+
+pub fn set_scale_factor(scale: f32) {
+    if let Ok(mut lock) = SCALE_FACTOR.write() {
+        *lock = scale;
+    }
+}
diff --git a/src/shader.wgsl b/src/shader.wgsl
index d9d4dd0..bc75fcc 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -48,20 +48,5 @@ 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 with a smooth blur simulation (no high-frequency noise/grain)
-        // We use low-frequency wave combinations to create a soft, smooth organic glow/sheen
-        let wave1 = sin(in.clip_position.x * 0.02) * 0.02;
-        let wave2 = cos(in.clip_position.y * 0.02) * 0.02;
-        let sheen = sin((in.clip_position.x + in.clip_position.y) * 0.008) * 0.03;
-        let glow = wave1 + wave2 + sheen;
-        
-        final_color = vec4f(
-            clamp(in.color.r + glow, 0.0, 1.0),
-            clamp(in.color.g + glow, 0.0, 1.0),
-            clamp(in.color.b + glow, 0.0, 1.0),
-            0.699
-        );
-    }
     return final_color;
 }
diff --git a/src/widget.rs b/src/widget.rs
index b1eaaf1..a99d97f 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -1074,6 +1074,34 @@ pub trait Widget {
     fn menu_items(&self) -> Vec<String> { vec![] }
     fn menu_item_checked(&self) -> Vec<Option<bool>> { vec![] }
     fn is_vertical(&self) -> bool { false }
+
+    fn is_plate(&self) -> bool { false }
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }
+    fn layout_ignore(&self) -> bool { false }
+    fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        self.text_labels().into_iter().map(|l| (l, None)).collect()
+    }
+    fn text_labels_with_font_and_bounds(&self) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        self.text_labels().into_iter().map(|l| (l, None, None)).collect()
+    }
+
+    fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
+    fn menu_names(&self) -> Vec<String> { vec![] }
+    fn selected_page(&self) -> usize { 0 }
+    fn set_selected_page(&mut self, _page: usize) {}
+    fn is_page_hidden(&self) -> bool { false }
+    fn set_page_hidden(&mut self, _hidden: bool) {}
+    fn set_pages(&mut self, _pages: Vec<String>) {}
+    fn sidebar_w(&self) -> f32 { 0.0 }
+    fn set_sidebar_mode(&mut self, _enabled: bool) {}
+    fn set_sidebar_label(&mut self, _label: Option<String>) {}
+    fn add_widget_to_page(&mut self, _page_idx: usize, _widget: *mut (dyn Widget + 'static)) {}
+    fn clear_page_widgets(&mut self, _page_idx: usize) {}
+    fn menu_items_list(&self) -> Vec<Vec<String>> { vec![] }
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> { vec![] }
+    fn color_u8(&self) -> Option<[u8; 4]> { None }
+    fn update_bounds(&mut self, _count: usize, _viewport_y: f32, _viewport_h: f32) {}
+    fn get_item_draw_y(&self, _idx: usize, _offset: f32) -> Option<f32> { None }
 }
 
 #[derive(Clone)]
@@ -2588,6 +2616,16 @@ impl Widget for MenuBar {
         }
     }
 
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        for menu in &mut self.menus {
+            menu.set_modifiers(ctrl, shift, alt);
+        }
+    }
+
+    fn menu_names(&self) -> Vec<String> {
+        self.menu_items.clone()
+    }
+
     fn menu_click(&mut self) -> Option<(usize, usize)> {
         for (idx, menu) in self.menus.iter_mut().enumerate() {
             if let Some((_, item_idx)) = menu.menu_click() {
@@ -2861,6 +2899,14 @@ impl Widget for MenuBar {
     fn set_center_items(&mut self, center: bool) {
         self.center_items = center;
     }
+
+    fn menu_items_list(&self) -> Vec<Vec<String>> {
+        self.menu_dropdowns.clone()
+    }
+
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+        self.menu_dropdown_checked.clone()
+    }
 }
 
 impl Drop for MenuBar {
@@ -5509,6 +5555,10 @@ impl Widget for ColorSelector {
     fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
     fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
+    fn color_u8(&self) -> Option<[u8; 4]> {
+        Some([self.color[0], self.color[1], self.color[2], 255])
+    }
+
 
 
     fn color(&self) -> [f32; 4] {
@@ -5722,6 +5772,7 @@ impl Drop for ColorSelector {
 const BREADCRUMB_PADDING: f32 = 8.0;
 const SEGMENT_GAP: f32 = 4.0;
 
+#[derive(Debug, Clone)]
 pub struct Breadcrumb {
     x: f32, y: f32, w: f32, h: f32,
     hovered: bool,
@@ -6234,6 +6285,7 @@ pub struct ScrollBox {
     pub viewport_y: f32,
     pub viewport_h: f32,
     hovered: bool,
+    pub show_border: bool,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
 }
@@ -6247,6 +6299,7 @@ impl ScrollBox {
             viewport_y: 0.0,
             viewport_h: 0.0,
             hovered: false,
+            show_border: true,
             parent: None,
             children: Vec::new(),
         }
@@ -7070,6 +7123,7 @@ pub struct TextItem {
     pub x: f32,
     pub y: f32,
     pub color: glyphon::Color,
+    pub bounds: Option<[f32; 4]>,
 }
 
 // Styled label builder with optional strikethrough
@@ -7127,6 +7181,7 @@ impl StyledLabel {
             x,
             y,
             color: self.g_color,
+            bounds: None,
         });
         w
     }
@@ -7251,6 +7306,14 @@ impl Widget for ScrollingList {
     fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.scroll_box.children() }
     fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.scroll_box.add_child(child); }
     fn clear_children(&mut self) { self.scroll_box.clear_children(); }
+
+    fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
+        self.update_bounds(count, viewport_y, viewport_h);
+    }
+
+    fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
+        self.get_item_draw_y(idx, offset)
+    }
 }
 
 unsafe impl Send for Container {}
@@ -8546,239 +8609,863 @@ pub fn get_font_db() -> &'static resvg::usvg::fontdb::Database {
     })
 }
 
-pub struct Paginator {
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
-    sidebar_w: f32,
-    pages: Vec<String>,
-    selected_page: usize,
-    hovered_tab: Option<usize>,
-    pressed_tab: Option<usize>,
-    page_changed: bool,
-    current_y: Option<f32>,
-    target_y: f32,
-    pub tabs_at_top: bool,
-    pub tab_y_offset: f32,
-    pub tabs_rotated: bool,
-    current_x: Option<f32>,
-    target_x: f32,
-    parent: Option<*mut (dyn Widget + 'static)>,
-    children: Vec<*mut (dyn Widget + 'static)>,
-    pub page_widgets: Vec<Vec<*mut (dyn Widget + 'static)>>,
-    pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
-    pub sidebar_scroll_y: f32,
+pub struct Plate {
+    pub base: WidgetBase,
+    pub dragging: bool,
+    pub drag_ox: f32,
+    pub drag_oy: f32,
+    pub drag_start_x: f32,
+    pub drag_start_y: f32,
+    pub bounds: Option<(f32, f32, f32, f32)>,
+    pub color: Option<[f32; 4]>,
+    pub curved_circle: Option<(f32, f32, f32)>,
+    pub network_opacity: f32,
+    pub blur: bool,
+    pub children: Vec<*mut (dyn Widget + 'static)>,
+    pub parent: Option<*mut (dyn Widget + 'static)>,
+    pub visible: bool,
+    pub column_layout: bool,
 }
 
-impl Paginator {
-    pub fn new(sidebar_w: f32, pages: Vec<String>) -> Self {
-        let num_pages = pages.len();
+impl Plate {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
-            hovered: false,
-            sidebar_w,
-            pages,
-            selected_page: 0,
-            hovered_tab: None,
-            pressed_tab: None,
-            page_changed: false,
-            current_y: Some(10.0),
-            target_y: 10.0,
-            tabs_at_top: false,
-            tab_y_offset: 10.0,
-            tabs_rotated: true,
-            current_x: Some(0.0),
-            target_x: 0.0,
-            parent: None,
+            base: WidgetBase::new_rect(x, y, w, h),
+            dragging: false,
+            drag_ox: 0.0,
+            drag_oy: 0.0,
+            drag_start_x: 0.0,
+            drag_start_y: 0.0,
+            bounds: None,
+            color: None,
+            curved_circle: None,
+            network_opacity: 1.0,
+            blur: true,
             children: Vec::new(),
-            page_widgets: vec![Vec::new(); num_pages],
-            tab_text_quads: Vec::new(),
-            sidebar_scroll_y: 0.0,
+            parent: None,
+            visible: true,
+            column_layout: false,
         }
     }
 
-    pub fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
-        if page_idx < self.page_widgets.len() {
-            self.page_widgets[page_idx].push(widget);
-            self.add_child(widget);
-        }
+    pub fn with_color(mut self, color: [f32; 4]) -> Self {
+        self.color = Some(color);
+        self
     }
 
-    pub fn clear_page_widgets(&mut self, page_idx: usize) {
-        if page_idx < self.page_widgets.len() {
-            for widget_ptr in &self.page_widgets[page_idx] {
-                self.children.retain(|&c| !std::ptr::addr_eq(c, *widget_ptr));
-                unsafe {
-                    (&mut **widget_ptr).set_parent(None);
-                }
-            }
-            self.page_widgets[page_idx].clear();
-        }
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
     }
 
-    pub fn with_tabs_at_top(mut self, _top: bool) -> Self {
-        self.tabs_at_top = false;
-        self.update_target_pos();
+    pub fn with_blur(mut self, blur: bool) -> Self {
+        self.blur = blur;
         self
     }
 
-    pub fn with_tab_y_offset(mut self, offset: f32) -> Self {
-        self.tab_y_offset = offset;
-        if self.current_y == Some(10.0) {
-            self.current_y = Some(offset);
+    pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
+        self.bounds = Some((bx, by, bw, bh));
+    }
+}
+
+impl Widget for Plate {
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn is_plate(&self) -> bool { true }
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
+    fn top_room(&self) -> f32 { 0.0 }
+    fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        for &child_ptr in &self.children {
+            unsafe {
+                (*child_ptr).set_modifiers(ctrl, shift, alt);
+            }
         }
-        self.update_target_pos();
-        self
     }
 
-    pub fn with_tabs_rotated(mut self, _rotated: bool) -> Self {
-        self.tabs_rotated = true;
-        self.update_target_pos();
-        self
+    fn visible(&self) -> bool {
+        self.visible
     }
 
-    pub fn vertical_tab_size(&self) -> (f32, f32) {
-        if self.tabs_rotated {
-            ((self.sidebar_w - 16.0).clamp(24.0, 120.0), 120.0)
+    fn color(&self) -> [f32; 4] {
+        let mut c = if let Some(c) = self.color {
+            c
+        } else if self.dragging {
+            colors::PANEL_DRAG
         } else {
-            (self.sidebar_w - 10.0, 40.0)
+            colors::PANEL_IDLE
+        };
+        c[3] *= self.network_opacity;
+        if self.blur {
+            c[3] = -c[3].abs();
         }
+        c
     }
 
-    fn total_sidebar_height(&self) -> f32 {
-        let (_, tab_h) = self.vertical_tab_size();
-        let spacing = 10.0;
-        let step = if self.tabs_rotated { tab_h + spacing } else { 50.0 };
-        self.tab_y_offset + self.pages.len() as f32 * step - spacing
+    fn set_network_opacity(&mut self, opacity: f32) {
+        self.network_opacity = opacity;
     }
 
-    fn generate_tab_quads(&mut self) {
-        self.tab_text_quads.clear();
-        let (tab_w, tab_h) = self.vertical_tab_size();
-        let active_color = colors::paginator_tab_label_color();
-        let active_srgb = colors::to_srgb(active_color);
-        let active_r = (active_srgb[0] * 255.0) as u8;
-        let active_g = (active_srgb[1] * 255.0) as u8;
-        let active_b = (active_srgb[2] * 255.0) as u8;
-        let inactive_r = (active_r as f32 * 0.78) as u8;
-        let inactive_g = (active_g as f32 * 0.78) as u8;
-        let inactive_b = (active_b as f32 * 0.78) as u8;
+    fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
+        self.bounds = Some((bx, by, bw, bh));
+    }
 
-        for (i, page_name) in self.pages.iter().enumerate() {
-            let color = if self.selected_page == i {
-                [active_r, active_g, active_b]
-            } else {
-                [inactive_r, inactive_g, inactive_b]
-            };
-            let hex_color = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
+    fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
+        self.curved_circle = circle;
+    }
 
-            let trimmed = page_name.trim();
-            let has_icon = trimmed.find(' ').is_some();
-            let label_text = if let Some(space_idx) = trimmed.find(' ') {
-                trimmed.split_at(space_idx).1.trim()
-            } else {
-                trimmed
-            };
+    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;
+        }
+        if let Some((cx, cy, r)) = self.curved_circle {
+            let dx = px - cx;
+            let dy = py - cy;
+            return dx * dx + dy * dy <= r * r;
+        }
+        
+        let (x, y, w, h) = self.rect();
+        if px < x || px >= x + w || py < y || py >= y + h {
+            return false;
+        }
+        
+        let r = 12.0f32.min(w * 0.5).min(h * 0.5);
+        if r <= 0.1 {
+            return true;
+        }
+        
+        // Check corners
+        if px < x + r && py < y + r {
+            let dx = px - (x + r);
+            let dy = py - (y + r);
+            return dx * dx + dy * dy <= r * r;
+        }
+        if px >= x + w - r && py < y + r {
+            let dx = px - (x + w - r);
+            let dy = py - (y + r);
+            return dx * dx + dy * dy <= r * r;
+        }
+        if px >= x + w - r && py >= y + h - r {
+            let dx = px - (x + w - r);
+            let dy = py - (y + h - r);
+            return dx * dx + dy * dy <= r * r;
+        }
+        if px < x + r && py >= y + h - r {
+            let dx = px - (x + r);
+            let dy = py - (y + h - r);
+            return dx * dx + dy * dy <= r * r;
+        }
+        
+        true
+    }
 
-            let w_px = tab_w as u32;
-            let h_px = if has_icon { 80 } else { 120 };
+    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;
+        }
 
-            if w_px == 0 || h_px == 0 {
-                self.tab_text_quads.push(Vec::new());
-                continue;
-            }
+        if !self.visible {
+            return;
+        }
 
-            // Generate SVG string for the rotated text
-            let svg_data = format!(
-                r##"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
-  <text x="{}" y="{}" font-family="sans-serif" font-size="12" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
-</svg>"##,
-                w_px, h_px,
-                w_px as f32 / 2.0, h_px as f32 / 2.0,
-                hex_color,
-                w_px as f32 / 2.0, h_px as f32 / 2.0,
-                label_text
-            );
+        let padding_x = 20.0;
+        let padding_y = 20.0;
+        let left_x = x + padding_x;
+        let available_w = (w - 2.0 * padding_x).max(1.0);
+        let start_y = y + padding_y;
+        let available_h = (h - 2.0 * padding_y).max(1.0);
 
-            // Render SVG using resvg and tiny_skia
-            let opt = resvg::usvg::Options::default();
-            let fontdb = get_font_db();
-            
-            let mut page_quads = Vec::new();
-            if let Ok(tree) = resvg::usvg::Tree::from_data(svg_data.as_bytes(), &opt, fontdb) {
-                if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
-                    resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
-                    let pixels = pixmap.data();
-                    for row in 0..h_px {
-                        for col in 0..w_px {
-                            let idx = ((row * w_px + col) * 4) as usize;
-                            if idx + 3 < pixels.len() {
-                                let a = pixels[idx + 3] as f32 / 255.0;
-                                if a > 0.0 {
-                                    let r = pixels[idx] as f32 / 255.0;
-                                    let g = pixels[idx + 1] as f32 / 255.0;
-                                    let b = pixels[idx + 2] as f32 / 255.0;
-                                    page_quads.push((
-                                        col as f32,
-                                        row as f32,
-                                        1.0,
-                                        1.0,
-                                        [r, g, b, a],
-                                    ));
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/paginator_debug.txt") {
-                use std::io::Write;
-                let _ = writeln!(file, "Page {}: label='{}', w_px={}, h_px={}, quads_len={}", i, label_text, w_px, h_px, page_quads.len());
+        let center_x = left_x + available_w / 2.0;
+        let center_y = start_y + available_h / 2.0;
+        let aspect_ratio = available_w / available_h;
+
+        let mut active_widgets = Vec::new();
+        for &w_ptr in &self.children {
+            let w = unsafe { &*w_ptr };
+            if !w.layout_ignore() {
+                active_widgets.push(w_ptr);
             }
-            self.tab_text_quads.push(page_quads);
         }
-    }
 
-    fn update_target_pos(&mut self) {
-        if self.tabs_at_top {
-            let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
-            self.target_x = self.selected_page as f32 * tab_w;
-            if self.current_x.is_none() {
-                self.current_x = Some(self.target_x);
-            }
-        } else if self.tabs_rotated {
-            let (_, tab_h) = self.vertical_tab_size();
-            let spacing = 10.0;
-            let target = self.tab_y_offset + self.selected_page as f32 * (tab_h + spacing);
-            self.target_y = target;
-            if self.current_y.is_none() {
-                self.current_y = Some(target);
+        if self.column_layout {
+            let mut current_y = start_y;
+            let spacing = 12.0;
+            for &w_ptr in &active_widgets {
+                let w = unsafe { &mut *w_ptr };
+                let (_, _, ww, wh) = w.rect();
+                let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
+                let top = w.top_room();
+                let use_h = if wh > 0.0 { wh } else { 24.0 + top };
+
+                w.set_rect(left_x, current_y, use_w, use_h);
+                current_y += use_h + spacing;
             }
         } else {
-            let target = self.tab_y_offset + self.selected_page as f32 * 50.0;
-            self.target_y = target;
-            if self.current_y.is_none() {
-                self.current_y = Some(target);
+            let mut total_diagonal = 0.0;
+            let mut count = 0;
+            for &w_ptr in &active_widgets {
+                let w = unsafe { &*w_ptr };
+                let (_, _, ww, wh) = w.rect();
+                let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
+                let use_h = if wh > 0.0 { wh } else { 50.0 };
+                total_diagonal += (use_w * use_w + use_h * use_h).sqrt();
+                count += 1;
+            }
+            let avg_diagonal = if count > 0 { total_diagonal / count as f32 } else { 100.0 };
+            let base_spacing = (avg_diagonal * 0.55).max(60.0);
+
+            for (i, &w_ptr) in active_widgets.iter().enumerate() {
+                let w = unsafe { &mut *w_ptr };
+                let (_, _, ww, wh) = w.rect();
+                let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
+                let use_h = if wh > 0.0 { wh } else { 50.0 };
+
+                if i == 0 {
+                    w.set_rect(center_x - use_w / 2.0, center_y - use_h / 2.0, use_w, use_h);
+                } else {
+                    let mut ring = 1;
+                    let mut ring_start = 1;
+                    let mut placed = false;
+                    while !placed {
+                        let ring_capacity = ring * 6;
+                        if i < ring_start + ring_capacity {
+                            let pos_in_ring = i - ring_start;
+                            let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
+                            let radius = (ring as f32) * base_spacing;
+
+                            let x_offset = radius * angle.cos() * aspect_ratio;
+                            let y_offset = radius * angle.sin();
+
+                            w.set_rect(
+                                center_x + x_offset - use_w / 2.0,
+                                center_y + y_offset - use_h / 2.0,
+                                use_w,
+                                use_h,
+                            );
+                            placed = true;
+                        } else {
+                            ring_start += ring_capacity;
+                            ring += 1;
+                        }
+                    }
+                }
             }
         }
-        self.generate_tab_quads();
     }
 
-    pub fn selected_page(&self) -> usize {
-        self.selected_page
+    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
+        self.parent
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
+        self.parent = parent;
+    }
+
+    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
+        self.children.clone()
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Widget + 'static)) {
+        self.children.push(child);
+    }
+
+    fn clear_children(&mut self) {
+        self.children.clear();
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut quads = Vec::new();
+        let (px, py, pw, ph) = self.rect();
+        quads.push((px, py, pw, ph, self.color()));
+
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            let c = widget.color();
+            if c[3] > 0.0 {
+                let (wx, wy, ww, wh) = widget.rect();
+                quads.push((wx, wy, ww, wh, c));
+            }
+            quads.extend(widget.all_quads());
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible {
+            return Vec::new();
+        }
+        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],
+            });
+        }
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            labels.extend(widget.text_labels());
+        }
+        labels
+    }
+
+    fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut result = Vec::new();
+        if let Some(ref label) = self.base.label {
+            result.push((
+                TextLabel {
+                    text: label.clone(),
+                    x: self.base.x,
+                    y: self.base.y - 18.0,
+                    font_size: 12.0,
+                    color: [0x83, 0x83, 0x8a],
+                },
+                None,
+            ));
+        }
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            result.extend(widget.text_labels_with_bounds());
+        }
+        result
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        if !self.visible {
+            return false;
+        }
+        let mut changed = false;
+        for &widget_ptr in &self.children {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.is_dragging() {
+                if widget.drag_update(px, py) {
+                    changed = true;
+                }
+            } else if widget.cursor_moved(px, py) {
+                changed = true;
+            }
+        }
+        changed
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if !self.visible {
+            return false;
+        }
+        for &widget_ptr in self.children.iter().rev() {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.popover_rect().is_some() {
+                if widget.mouse_input(button, state, px, py) {
+                    return true;
+                }
+            }
+        }
+        for &widget_ptr in self.children.iter().rev() {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.mouse_input(button, state, px, py) {
+                return true;
+            }
+            if state == ElementState::Pressed && !widget.hit_test(px, py) {
+                widget.unfocus();
+            }
+        }
+
+        if button != MouseButton::Left { return false; }
+        match state {
+            ElementState::Pressed => {
+                if self.hit_test(px, py) {
+                    self.drag_begin(px, py);
+                    return true;
+                }
+            }
+            ElementState::Released => {
+                if self.dragging { self.drag_end(); return true; }
+            }
+        }
+        false
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+        if !self.visible {
+            return false;
+        }
+        for &widget_ptr in &self.children {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.keyboard_input(event) {
+                return true;
+            }
+        }
+        false
+    }
+
+    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
+        if !self.visible {
+            return false;
+        }
+        for &widget_ptr in self.children.iter().rev() {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.mouse_wheel(delta, px, py) {
+                return true;
+            }
+        }
+        false
+    }
+
+    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        if !self.visible {
+            return None;
+        }
+        for &widget_ptr in self.children.iter().rev() {
+            let widget = unsafe { &*widget_ptr };
+            if let Some(r) = widget.popover_rect() {
+                return Some(r);
+            }
+        }
+        None
+    }
+
+    fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+        if !self.visible {
+            return;
+        }
+        for &widget_ptr in self.children.iter().rev() {
+            let widget = unsafe { &*widget_ptr };
+            widget.render_popover(pc);
+        }
+    }
+
+    fn tick(&mut self, dt: f32) -> bool {
+        if !self.visible {
+            return false;
+        }
+        let mut changed = false;
+        for &widget_ptr in &self.children {
+            let widget = unsafe { &mut *widget_ptr };
+            if widget.tick(dt) {
+                changed = true;
+            }
+        }
+        changed
+    }
+
+    fn drag_update(&mut self, px: f32, py: f32) -> bool {
+        let nx = px - self.drag_ox;
+        let ny = py - self.drag_oy;
+        let (nx, ny) = if let Some((bx, by, bw, bh)) = self.bounds {
+            (nx.clamp(bx, bx + bw - self.base.w), ny.clamp(by, by + bh - self.base.h))
+        } else {
+            (nx, ny)
+        };
+        if (nx - self.base.x).abs() > 0.01 || (ny - self.base.y).abs() > 0.01 {
+            let dx = nx - self.base.x;
+            let dy = ny - self.base.y;
+            self.base.x = nx;
+            self.base.y = ny;
+            
+            for &child_ptr in &self.children {
+                unsafe {
+                    let (cx, cy, cw, ch) = (*child_ptr).rect();
+                    (*child_ptr).set_rect(cx + dx, cy + dy, cw, ch);
+                }
+            }
+            return true;
+        }
+        false
+    }
+
+    fn drag_begin(&mut self, px: f32, py: f32) {
+        self.dragging = true;
+        self.drag_ox = px - self.base.x;
+        self.drag_oy = py - self.base.y;
+        self.drag_start_x = self.base.x;
+        self.drag_start_y = self.base.y;
+    }
+
+    fn drag_end(&mut self) { self.dragging = false; }
+}
+
+unsafe impl Send for Plate {}
+unsafe impl Sync for Plate {}
+
+pub struct Paginator {
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
+    hovered: bool,
+    pub sidebar_menu: MenuBar,
+    pub plates: Vec<Plate>,
+    pub selected_page: usize,
+    pub page_changed: bool,
+    pub sidebar_label: Option<String>,
+    pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
+    pub sidebar_scroll_y: f32,
+    pub scale_factor: f32,
+    pub sidebar_mode: bool,
+    pub page_hidden: bool,
+    pub sidebar_w: f32,
+    pub pages: Vec<String>,
+    pub tabs_at_top: bool,
+    pub tab_y_offset: f32,
+    pub tabs_rotated: bool,
+    pub hovered_tab: Option<usize>,
+    pub pressed_tab: Option<usize>,
+    pub target_y: f32,
+    pub target_x: f32,
+    pub current_y: Option<f32>,
+    pub current_x: Option<f32>,
+    parent: Option<*mut (dyn Widget + 'static)>,
+}
+
+impl Paginator {
+    pub fn sidebar_w(&self) -> f32 {
+        self.sidebar_w
+    }
+
+    pub fn new(sidebar_w: f32, pages: Vec<String>) -> Self {
+        let num_pages = pages.len();
+        
+        let mut sidebar_menu = MenuBar::new(0.0, 0.0, sidebar_w, 0.0)
+            .with_vertical(true);
+        for page in &pages {
+            sidebar_menu = sidebar_menu.with_item(page, &[]);
+        }
+
+        let mut plates = Vec::new();
+        for _ in 0..num_pages {
+            let mut plate = Plate::new(0.0, 0.0, 0.0, 0.0);
+            plate.visible = false;
+            plates.push(plate);
+        }
+        if num_pages > 0 {
+            plates[0].visible = true;
+            sidebar_menu.menus[0].set_selected(true);
+        }
+
+        let mut pag = Self {
+            x: 0.0,
+            y: 0.0,
+            w: 0.0,
+            h: 0.0,
+            hovered: false,
+            sidebar_menu,
+            plates,
+            selected_page: 0,
+            page_changed: false,
+            sidebar_label: None,
+            tab_text_quads: Vec::new(),
+            sidebar_scroll_y: 0.0,
+            scale_factor: 1.0,
+            sidebar_mode: true,
+            page_hidden: false,
+            sidebar_w,
+            pages,
+            tabs_at_top: false,
+            tab_y_offset: 10.0,
+            tabs_rotated: true,
+            hovered_tab: None,
+            pressed_tab: None,
+            target_y: 10.0,
+            target_x: 0.0,
+            current_y: Some(10.0),
+            current_x: Some(0.0),
+            parent: None,
+        };
+        pag.update_target_pos();
+        pag
+    }
+
+    pub fn tab_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
+        if idx >= self.pages.len() {
+            return (0.0, 0.0, 0.0, 0.0);
+        }
+        if self.tabs_at_top {
+            let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
+            (self.x + idx as f32 * tab_w, self.y, tab_w, 40.0)
+        } else if self.tabs_rotated {
+            let (tab_w, tab_h) = self.vertical_tab_size();
+            let spacing = 10.0;
+            (
+                self.x + (self.sidebar_w - tab_w) / 2.0,
+                self.y + self.tab_y_offset + idx as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
+                tab_w,
+                tab_h,
+            )
+        } else {
+            let bw = self.sidebar_w - 10.0;
+            (self.x + 5.0, self.y + self.tab_y_offset + idx as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
+        }
+    }
+
+    pub fn tab_size(&self, idx: usize) -> (f32, f32) {
+        let r = self.tab_rect(idx);
+        (r.2, r.3)
+    }
+
+    pub fn with_column_layout(mut self, enabled: bool) -> Self {
+        for plate in &mut self.plates {
+            plate.column_layout = enabled;
+        }
+        self
+    }
+
+    pub fn with_sidebar_mode(mut self, enabled: bool) -> Self {
+        self.sidebar_mode = enabled;
+        self
+    }
+
+    pub fn with_sidebar_label(mut self, label: &str) -> Self {
+        self.sidebar_label = Some(label.to_string());
+        self
+    }
+
+    pub fn sidebar_label_height(&self) -> f32 {
+        if self.sidebar_label.is_some() {
+            24.0
+        } else {
+            0.0
+        }
+    }
+
+    pub fn is_page_hidden(&self) -> bool {
+        self.page_hidden
+    }
+
+    pub fn set_page_hidden(&mut self, hidden: bool) {
+        self.page_hidden = hidden;
+    }
+
+    pub fn set_sidebar_mode(&mut self, enabled: bool) {
+        self.sidebar_mode = enabled;
+    }
+
+    pub fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
+        if page_idx < self.plates.len() {
+            self.plates[page_idx].add_child(widget);
+            unsafe {
+                (*widget).set_parent(Some(&mut self.plates[page_idx] as *mut _));
+            }
+        }
+    }
+
+    pub fn clear_page_widgets(&mut self, page_idx: usize) {
+        if page_idx < self.plates.len() {
+            self.plates[page_idx].clear_children();
+        }
+    }
+
+    pub fn with_tabs_at_top(mut self, top: bool) -> Self {
+        self.tabs_at_top = top;
+        self.update_target_pos();
+        self
+    }
+
+    pub fn with_tab_y_offset(mut self, offset: f32) -> Self {
+        self.tab_y_offset = offset;
+        if self.current_y == Some(10.0) {
+            self.current_y = Some(offset);
+        }
+        self.update_target_pos();
+        self
+    }
+
+    pub fn with_tabs_rotated(mut self, rotated: bool) -> Self {
+        self.tabs_rotated = rotated;
+        self.update_target_pos();
+        self
+    }
+
+    pub fn selected_page(&self) -> usize {
+        self.selected_page
     }
 
     pub fn set_selected_page(&mut self, page: usize) {
-        if page < self.pages.len() {
+        if page < self.plates.len() {
             if self.selected_page != page {
+                self.plates[self.selected_page].visible = false;
                 self.selected_page = page;
+                self.plates[self.selected_page].visible = true;
                 self.update_target_pos();
+                
+                // Update MenuBar focus/selection
+                for (i, menu) in self.sidebar_menu.menus.iter_mut().enumerate() {
+                    menu.set_selected(i == page);
+                }
+            }
+        }
+    }
+
+    pub fn set_pages(&mut self, pages: Vec<String>) {
+        self.pages = pages.clone();
+        let num_pages = pages.len();
+        
+        let mut sidebar_menu = MenuBar::new(0.0, 0.0, self.sidebar_w, 0.0)
+            .with_vertical(true);
+        for page in &pages {
+            sidebar_menu = sidebar_menu.with_item(page, &[]);
+        }
+        self.sidebar_menu = sidebar_menu;
+
+        let mut plates = Vec::new();
+        for _ in 0..num_pages {
+            let mut plate = Plate::new(0.0, 0.0, 0.0, 0.0);
+            plate.visible = false;
+            plates.push(plate);
+        }
+        self.plates = plates;
+        if self.selected_page >= num_pages {
+            self.selected_page = 0;
+        }
+        if !self.plates.is_empty() {
+            self.plates[self.selected_page].visible = true;
+            self.sidebar_menu.menus[self.selected_page].set_selected(true);
+        }
+        self.update_target_pos();
+    }
+
+    pub fn set_scale_factor(&mut self, scale: f32) {
+        self.scale_factor = scale;
+    }
+
+    pub fn vertical_tab_size(&self) -> (f32, f32) {
+        if self.tabs_rotated {
+            ((self.sidebar_w - 16.0).clamp(24.0, 120.0), 120.0)
+        } else {
+            (self.sidebar_w - 10.0, 40.0)
+        }
+    }
+
+    fn total_sidebar_height(&self) -> f32 {
+        let (_, tab_h) = self.vertical_tab_size();
+        let spacing = 10.0;
+        let step = if self.tabs_rotated { tab_h + spacing } else { 50.0 };
+        self.tab_y_offset + self.pages.len() as f32 * step - spacing
+    }
+
+    fn update_target_pos(&mut self) {
+        if self.tabs_at_top {
+            let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
+            self.target_x = self.selected_page as f32 * tab_w;
+            if self.current_x.is_none() {
+                self.current_x = Some(self.target_x);
+            }
+        } else if self.tabs_rotated {
+            let (_, tab_h) = self.vertical_tab_size();
+            let spacing = 10.0;
+            let target = self.tab_y_offset + self.selected_page as f32 * (tab_h + spacing);
+            self.target_y = target;
+            if self.current_y.is_none() {
+                self.current_y = Some(target);
+            }
+        } else {
+            let target = self.tab_y_offset + self.selected_page as f32 * 50.0;
+            self.target_y = target;
+            if self.current_y.is_none() {
+                self.current_y = Some(target);
+            }
+        }
+        self.generate_tab_quads();
+    }
+
+    fn generate_tab_quads(&mut self) {
+        self.tab_text_quads.clear();
+        let (tab_w, tab_h) = self.vertical_tab_size();
+        let active_color = colors::paginator_tab_label_color();
+        let active_srgb = colors::to_srgb(active_color);
+        let active_r = (active_srgb[0] * 255.0) as u8;
+        let active_g = (active_srgb[1] * 255.0) as u8;
+        let active_b = (active_srgb[2] * 255.0) as u8;
+        let inactive_r = (active_r as f32 * 0.78) as u8;
+        let inactive_g = (active_g as f32 * 0.78) as u8;
+        let inactive_b = (active_b as f32 * 0.78) as u8;
+
+        for (i, page_name) in self.pages.iter().enumerate() {
+            let color = if self.selected_page == i {
+                [active_r, active_g, active_b]
+            } else {
+                [inactive_r, inactive_g, inactive_b]
+            };
+            let hex_color = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
+
+            let trimmed = page_name.trim();
+            let has_icon = trimmed.find(' ').is_some();
+            let label_text = if let Some(space_idx) = trimmed.find(' ') {
+                trimmed.split_at(space_idx).1.trim()
+            } else {
+                trimmed
+            };
+
+            let w_px = tab_w as u32;
+            let h_px = if has_icon { 80 } else { 120 };
+
+            if w_px == 0 || h_px == 0 {
+                self.tab_text_quads.push(Vec::new());
+                continue;
+            }
+
+            let svg_data = format!(
+                r##"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
+  <text x="{}" y="{}" font-family="sans-serif" font-size="12" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
+</svg>"##,
+                w_px, h_px,
+                w_px as f32 / 2.0, h_px as f32 / 2.0,
+                hex_color,
+                w_px as f32 / 2.0, h_px as f32 / 2.0,
+                label_text
+            );
+
+            let opt = resvg::usvg::Options::default();
+            let fontdb = get_font_db();
+            
+            let mut page_quads = Vec::new();
+            if let Ok(tree) = resvg::usvg::Tree::from_data(svg_data.as_bytes(), &opt, fontdb) {
+                if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
+                    resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
+                    let pixels = pixmap.data();
+                    for row in 0..h_px {
+                        for col in 0..w_px {
+                            let idx = ((row * w_px + col) * 4) as usize;
+                            if idx + 3 < pixels.len() {
+                                let a = pixels[idx + 3] as f32 / 255.0;
+                                if a > 0.0 {
+                                    let r = pixels[idx] as f32 / 255.0;
+                                    let g = pixels[idx + 1] as f32 / 255.0;
+                                    let b = pixels[idx + 2] as f32 / 255.0;
+                                    page_quads.push((
+                                        col as f32,
+                                        row as f32,
+                                        1.0,
+                                        1.0,
+                                        [r, g, b, a],
+                                    ));
+                                }
+                            }
+                        }
+                    }
+                }
             }
+            self.tab_text_quads.push(page_quads);
         }
     }
 }
 
 impl Widget for Paginator {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    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;
@@ -8786,57 +9473,38 @@ impl Widget for Paginator {
         self.h = h;
         self.update_target_pos();
 
-        // Perform relative wrapping layout for all page widgets
-        let padding_x = 20.0;
-        let gap_y = 16.0;
-        let gap_x = 20.0;
+        let self_ptr = self as *mut Paginator;
+        self.sidebar_menu.set_parent(Some(self_ptr));
+        for plate in &mut self.plates {
+            plate.set_parent(Some(self_ptr));
+        }
 
-        let (left_x, available_w, start_y) = if self.tabs_at_top {
-            (self.x + padding_x, self.w - 2.0 * padding_x, self.y + 40.0 + 20.0)
+        let tabs_at_top = self.tabs_at_top;
+        if tabs_at_top {
+            self.sidebar_menu.set_rect(self.x, self.y, self.w, 40.0);
+            for plate in &mut self.plates {
+                plate.set_rect(self.x, self.y + 40.0, self.w, (self.h - 40.0).max(0.0));
+            }
         } else {
-            (self.x + self.sidebar_w + padding_x, self.w - self.sidebar_w - 2.0 * padding_x, self.y + 20.0)
-        };
+            let sidebar_w = self.sidebar_w;
+            self.sidebar_menu.set_rect(self.x, self.y, sidebar_w, self.h);
+            for plate in &mut self.plates {
+                plate.set_rect(self.x + sidebar_w, self.y, (self.w - sidebar_w).max(0.0), self.h);
+            }
+        }
+    }
 
-        for page_idx in 0..self.page_widgets.len() {
-            let mut cur_y = start_y;
-            let widgets = &self.page_widgets[page_idx];
-            let mut i = 0;
-            while i < widgets.len() {
-                let w1_ptr = widgets[i];
-                let w1 = unsafe { &mut *w1_ptr };
-                let (_, _, w1_w, w1_h) = w1.rect();
-
-                if i + 1 < widgets.len() {
-                    let w2_ptr = widgets[i + 1];
-                    let w2 = unsafe { &mut *w2_ptr };
-                    let (_, _, w2_w, w2_h) = w2.rect();
-
-                    if w1_w > 0.0 && w2_w > 0.0 && w1_w + w2_w + gap_x <= available_w {
-                        let top_room = w1.top_room().max(w2.top_room());
-                        let widget_y = cur_y + top_room;
-
-                        w1.set_rect(left_x, widget_y, w1_w, w1_h);
-                        w2.set_rect(left_x + w1_w + gap_x, widget_y, w2_w, w2_h);
-
-                        cur_y = widget_y + w1_h.max(w2_h) + gap_y;
-                        i += 2;
-                        continue;
-                    }
-                }
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
 
-                let top_room = w1.top_room();
-                let widget_y = cur_y + top_room;
-                let use_w = if w1_w > 0.0 { w1_w.min(available_w) } else { available_w };
+    fn set_hovered(&mut self, v: bool) {
+        self.hovered = v;
+    }
 
-                w1.set_rect(left_x, widget_y, use_w, w1_h);
-                cur_y = widget_y + w1_h + gap_y;
-                i += 1;
-            }
-        }
+    fn hovered(&self) -> bool {
+        self.hovered
     }
-    fn color(&self) -> [f32; 4] { [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 highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
         if let Some(i) = self.hovered_tab {
@@ -8871,7 +9539,6 @@ impl Widget for Paginator {
                 let qy = by + scroll_offset;
                 let qh = bh;
 
-                // Clip to paginator Y bounds
                 let min_y = self.y;
                 let max_y = self.y + self.h;
                 let ry1 = qy.max(min_y);
@@ -8890,145 +9557,9 @@ impl Widget for Paginator {
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        if self.tabs_at_top {
-            // Draw top tab bar background
-            quads.push((self.x, self.y, self.w, 40.0, colors::sidebar_bg_color()));
-            // Draw main page background
-            quads.push((self.x, self.y + 40.0, self.w, self.h - 40.0, colors::page_low_color()));
-            // Divider line below tab bar
-            quads.push((self.x, self.y + 40.0 - 1.0, self.w, 1.0, [0.20, 0.20, 0.25, 0.6]));
-
-            // Sliding active tab indicator
-            if let Some(cx) = self.current_x {
-                let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
-                let bx = self.x + cx + 4.0;
-                let by = self.y + 4.0;
-                let bw = tab_w - 8.0;
-                let bh = 40.0 - 8.0;
-                
-                // Sliding capsule glow
-                let steps = 12;
-                let start_alpha = 0.01;
-                let c = 0.0006;
-                for i in 0..steps {
-                    let offset = i as f32 * 0.5;
-                    let rx = bx + offset;
-                    let ry = by + offset;
-                    let rw = bw - 2.0 * offset;
-                    let rh = bh - 2.0 * offset;
-                    if rw > 0.0 && rh > 0.0 {
-                        let step_alpha = start_alpha + c * (i * i) as f32;
-                        quads.push((rx, ry, rw, rh, [0.20, 0.40, 0.65, step_alpha]));
-                    }
-                }
-            }
-
-            // Tab button press/hover highlight overlays
-            for i in 0..self.pages.len() {
-                let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
-                let bx = self.x + i as f32 * tab_w + 4.0;
-                let by = self.y + 4.0;
-                let bw = tab_w - 8.0;
-                let bh = 40.0 - 8.0;
-
-                let c = if self.pressed_tab == Some(i) {
-                    [0.20, 0.20, 0.25, 0.25]
-                } else if self.hovered_tab == Some(i) {
-                    [0.20, 0.20, 0.25, 0.15]
-                } else {
-                    [0.0, 0.0, 0.0, 0.0]
-                };
-                
-                if c[3] > 0.0 {
-                    quads.push((bx, by, bw, bh, c));
-                }
-            }
-        } else {
-            // Vertical layout (old)
-            quads.push((self.x, self.y, self.sidebar_w, self.h, colors::sidebar_bg_color()));
-            quads.push((self.x + self.sidebar_w, self.y, self.w - self.sidebar_w, self.h, colors::page_low_color()));
-
-            let min_y = self.y;
-            let max_y = self.y + self.h;
-            let mut push_sidebar_quad = |qx: f32, qy: f32, qw: f32, qh: f32, qc: [f32; 4], q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
-                let ry1 = qy.max(min_y);
-                let ry2 = (qy + qh).min(max_y);
-                let rh = ry2 - ry1;
-                if rh > 0.0 {
-                    q.push((qx, ry1, qw, rh, qc));
-                }
-            };
-
-            if let Some(cy) = self.current_y {
-                let (bx, by, bw, bh) = if self.tabs_rotated {
-                    let (tab_w, tab_h) = self.vertical_tab_size();
-                    (
-                        self.x + (self.sidebar_w - tab_w) / 2.0,
-                        self.y + cy - self.sidebar_scroll_y,
-                        tab_w,
-                        tab_h,
-                    )
-                } else {
-                    let bw = self.sidebar_w - 10.0;
-                    (
-                        self.x + 5.0,
-                        self.y + cy - self.sidebar_scroll_y,
-                        bw,
-                        40.0,
-                    )
-                };
-                
-                let steps = 16;
-                let start_alpha = 0.008;
-                let c = 0.000516;
-                for i in 0..steps {
-                    let offset = i as f32 * 0.5;
-                    let rx = bx + offset;
-                    let ry = by + offset;
-                    let rw = bw - 2.0 * offset;
-                    let rh = bh - 2.0 * offset;
-                    if rw > 0.0 && rh > 0.0 {
-                        let step_alpha = start_alpha + c * (i * i) as f32;
-                        push_sidebar_quad(rx, ry, rw, rh, [0.20, 0.40, 0.65, step_alpha], &mut quads);
-                    }
-                }
-            }
-
-            for i in 0..self.pages.len() {
-                let (bx, by, bw, bh) = if self.tabs_rotated {
-                    let (tab_w, tab_h) = self.vertical_tab_size();
-                    let spacing = 10.0;
-                    (
-                        self.x + (self.sidebar_w - tab_w) / 2.0,
-                        self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
-                        tab_w,
-                        tab_h,
-                    )
-                } else {
-                    let bw = self.sidebar_w - 10.0;
-                    (
-                        self.x + 5.0,
-                        self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y,
-                        bw,
-                        40.0,
-                    )
-                };
-
-                let c = if self.pressed_tab == Some(i) {
-                    [0.20, 0.20, 0.25, 0.25]
-                } else if self.hovered_tab == Some(i) {
-                    [0.20, 0.20, 0.25, 0.15]
-                } else {
-                    [0.0, 0.0, 0.0, 0.0]
-                };
-                
-                if c[3] > 0.0 {
-                    push_sidebar_quad(bx, by, bw, bh, c, &mut quads);
-                }
-            }
-        }
-
         if self.tabs_rotated {
+            quads.push((self.x, self.y, self.sidebar_w, self.h, colors::sidebar_bg_color()));
+            
             let (tab_w, tab_h) = self.vertical_tab_size();
             let spacing = 10.0;
             let min_y = self.y;
@@ -9047,7 +9578,6 @@ impl Widget for Paginator {
                         let absolute_x = bx + qx;
                         let absolute_y = by + y_offset + qy;
                         
-                        // Clip vertical drawing bounds to paginator height
                         let ry1 = absolute_y.max(min_y);
                         let ry2 = (absolute_y + qh).min(max_y);
                         let rh = ry2 - ry1;
@@ -9057,60 +9587,36 @@ impl Widget for Paginator {
                     }
                 }
             }
+        } else {
+            quads.extend(self.sidebar_menu.extra_quads());
         }
 
-        // Delegate rendering to active page widgets
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &*widget_ptr };
-                let c = widget.color();
-                if c[3] > 0.0 {
-                    let (wx, wy, ww, wh) = widget.rect();
-                    quads.push((wx, wy, ww, wh, c));
-                }
-                quads.extend(widget.all_quads());
-            }
+        if self.selected_page < self.plates.len() {
+            quads.extend(self.plates[self.selected_page].extra_quads());
         }
         quads
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        let active_color = colors::paginator_tab_label_color();
-        let active_srgb = colors::to_srgb(active_color);
-        let active_r = (active_srgb[0] * 255.0) as u8;
-        let active_g = (active_srgb[1] * 255.0) as u8;
-        let active_b = (active_srgb[2] * 255.0) as u8;
-        let inactive_r = (active_r as f32 * 0.78) as u8;
-        let inactive_g = (active_g as f32 * 0.78) as u8;
-        let inactive_b = (active_b as f32 * 0.78) as u8;
-
-        for (i, page_name) in self.pages.iter().enumerate() {
-            let font_size = 12.0;
-            let est_w = TextLabel::estimate_width(page_name, font_size);
-            let color = if self.selected_page == i {
-                [active_r, active_g, active_b]
-            } else {
-                [inactive_r, inactive_g, inactive_b]
-            };
-
-            if self.tabs_at_top {
-                let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
-                let bx = self.x + i as f32 * tab_w;
-                let by = self.y;
-                let bw = tab_w;
-                let bh = 40.0;
+        if self.tabs_rotated {
+            let (tab_w, tab_h) = self.vertical_tab_size();
+            let spacing = 10.0;
+            let active_color = colors::paginator_tab_label_color();
+            let active_srgb = colors::to_srgb(active_color);
+            let active_r = (active_srgb[0] * 255.0) as u8;
+            let active_g = (active_srgb[1] * 255.0) as u8;
+            let active_b = (active_srgb[2] * 255.0) as u8;
+            let inactive_r = (active_r as f32 * 0.78) as u8;
+            let inactive_g = (active_g as f32 * 0.78) as u8;
+            let inactive_b = (active_b as f32 * 0.78) as u8;
 
-                labels.push(TextLabel {
-                    text: page_name.clone(),
-                    x: bx + (bw - est_w) / 2.0,
-                    y: by + (bh - font_size) / 2.0 - 1.0,
-                    font_size,
-                    color,
-                });
-            } else if self.tabs_rotated {
-                let (tab_w, tab_h) = self.vertical_tab_size();
-                let spacing = 10.0;
+            for (i, page_name) in self.pages.iter().enumerate() {
+                let color = if self.selected_page == i {
+                    [active_r, active_g, active_b]
+                } else {
+                    [inactive_r, inactive_g, inactive_b]
+                };
                 let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
                 let by = self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y;
                 let bw = tab_w;
@@ -9137,33 +9643,21 @@ impl Widget for Paginator {
                         }
                     }
                 }
-
-                // Rotated tab text is rendered as quads in generate_tab_quads and extra_quads
-            } else {
-                let bx = self.x + 5.0;
-                let by = self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y;
-                let bw = self.sidebar_w - 10.0;
-                let bh = 40.0;
-
-                let text_y = by + (bh - font_size) / 2.0 - 1.0;
-                if text_y >= self.y && text_y + font_size <= self.y + self.h {
-                    labels.push(TextLabel {
-                        text: page_name.clone(),
-                        x: bx + (bw - est_w) / 2.0,
-                        y: text_y,
-                        font_size,
-                        color,
-                    });
-                }
             }
+        } else {
+            labels.extend(self.sidebar_menu.text_labels());
         }
 
-        // Delegate labels to active page widgets
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &*widget_ptr };
-                labels.extend(widget.text_labels());
-            }
+        if self.selected_page < self.plates.len() {
+            labels.extend(self.plates[self.selected_page].text_labels());
+        }
+        labels
+    }
+
+    fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        let mut labels = Vec::new();
+        for l in self.text_labels() {
+            labels.push((l, None));
         }
         labels
     }
@@ -9198,17 +9692,12 @@ impl Widget for Paginator {
             changed = true;
         }
 
-        // Delegate to active page widgets
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.is_dragging() {
-                    if widget.drag_update(px, py) {
-                        changed = true;
-                    }
-                } else if widget.cursor_moved(px, py) {
-                    changed = true;
-                }
+        if self.sidebar_menu.cursor_moved(px, py) {
+            changed = true;
+        }
+        if self.selected_page < self.plates.len() {
+            if self.plates[self.selected_page].cursor_moved(px, py) {
+                changed = true;
             }
         }
 
@@ -9216,19 +9705,14 @@ impl Widget for Paginator {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
-        // First check popover clicks for active page widgets
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.popover_rect().is_some() {
-                    if widget.mouse_input(button, state, px, py) {
-                        return true;
-                    }
+        if self.selected_page < self.plates.len() {
+            if self.plates[self.selected_page].popover_rect().is_some() {
+                if self.plates[self.selected_page].mouse_input(button, state, px, py) {
+                    return true;
                 }
             }
         }
 
-        // Check if click is on paginator tabs
         let mut clicked_tab = false;
         if button == MouseButton::Left {
             match state {
@@ -9278,16 +9762,8 @@ impl Widget for Paginator {
                         };
                         if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
                             if self.selected_page != i {
-                                // Unfocus all widgets on the previously selected page
-                                if self.selected_page < self.page_widgets.len() {
-                                    for &widget_ptr in &self.page_widgets[self.selected_page] {
-                                        let widget = unsafe { &mut *widget_ptr };
-                                        widget.unfocus();
-                                    }
-                                }
-                                self.selected_page = i;
+                                self.set_selected_page(i);
                                 self.page_changed = true;
-                                self.update_target_pos();
                             }
                             clicked_tab = true;
                         }
@@ -9300,16 +9776,13 @@ impl Widget for Paginator {
             return true;
         }
 
-        // Delegate mouse input to widgets on active page
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.mouse_input(button, state, px, py) {
-                    return true;
-                }
-                if state == ElementState::Pressed && !widget.hit_test(px, py) {
-                    widget.unfocus();
-                }
+        if self.sidebar_menu.mouse_input(button, state, px, py) {
+            return true;
+        }
+
+        if self.selected_page < self.plates.len() {
+            if self.plates[self.selected_page].mouse_input(button, state, px, py) {
+                return true;
             }
         }
 
@@ -9317,12 +9790,12 @@ impl Widget for Paginator {
     }
 
     fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.keyboard_input(event) {
-                    return true;
-                }
+        if self.sidebar_menu.keyboard_input(event) {
+            return true;
+        }
+        if self.selected_page < self.plates.len() {
+            if self.plates[self.selected_page].keyboard_input(event) {
+                return true;
             }
         }
         false
@@ -9334,10 +9807,6 @@ impl Widget for Paginator {
             let by = self.y;
             let bw = self.sidebar_w;
             let bh = self.h;
-            if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/clear-scroll-debug.txt") {
-                use std::io::Write;
-                let _ = writeln!(file, "mouse_wheel check: px={}, py={}, bx={}, by={}, bw={}, bh={}, hover={}", px, py, bx, by, bw, bh, px >= bx && px <= bx + bw && py >= by && py <= by + bh);
-            }
             if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
                 let scroll_speed = 24.0;
                 let dy = match delta {
@@ -9346,10 +9815,6 @@ impl Widget for Paginator {
                 };
                 let old_scroll = self.sidebar_scroll_y;
                 let max_scroll = (self.total_sidebar_height() - self.h).max(0.0);
-                if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/clear-scroll-debug.txt") {
-                    use std::io::Write;
-                    let _ = writeln!(file, "mouse_wheel action: dy={}, old_scroll={}, total_h={}, h={}, max_scroll={}", dy, old_scroll, self.total_sidebar_height(), self.h, max_scroll);
-                }
                 self.sidebar_scroll_y = (self.sidebar_scroll_y + dy).clamp(0.0, max_scroll);
                 if (self.sidebar_scroll_y - old_scroll).abs() > 0.01 {
                     self.update_target_pos();
@@ -9358,35 +9823,34 @@ impl Widget for Paginator {
             }
         }
 
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.mouse_wheel(delta, px, py) {
-                    return true;
-                }
+        if self.sidebar_menu.mouse_wheel(delta, px, py) {
+            return true;
+        }
+
+        if self.selected_page < self.plates.len() {
+            if self.plates[self.selected_page].mouse_wheel(delta, px, py) {
+                return true;
             }
         }
         false
     }
 
     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &*widget_ptr };
-                if let Some(r) = widget.popover_rect() {
-                    return Some(r);
-                }
+        if let Some(r) = self.sidebar_menu.popover_rect() {
+            return Some(r);
+        }
+        if self.selected_page < self.plates.len() {
+            if let Some(r) = self.plates[self.selected_page].popover_rect() {
+                return Some(r);
             }
         }
         None
     }
 
     fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &*widget_ptr };
-                widget.render_popover(pc);
-            }
+        self.sidebar_menu.render_popover(pc);
+        if self.selected_page < self.plates.len() {
+            self.plates[self.selected_page].render_popover(pc);
         }
     }
 
@@ -9399,7 +9863,9 @@ impl Widget for Paginator {
         }
     }
 
-    fn value(&self) -> i32 { self.selected_page as i32 }
+    fn value(&self) -> i32 {
+        self.selected_page as i32
+    }
 
     fn tick(&mut self, dt: f32) -> bool {
         let mut changed = false;
@@ -9426,24 +9892,91 @@ impl Widget for Paginator {
             }
         }
 
-        // Delegate tick to active page widgets
-        if self.selected_page < self.page_widgets.len() {
-            for &widget_ptr in &self.page_widgets[self.selected_page] {
-                let widget = unsafe { &mut *widget_ptr };
-                if widget.tick(dt) {
-                    changed = true;
-                }
+        let self_ptr = self as *mut Paginator;
+        self.sidebar_menu.set_parent(Some(self_ptr));
+        for plate in &mut self.plates {
+            plate.set_parent(Some(self_ptr));
+        }
+
+        if self.sidebar_menu.tick(dt) {
+            changed = true;
+        }
+
+        for plate in &mut self.plates {
+            if plate.tick(dt) {
+                changed = true;
             }
         }
 
         changed
     }
 
-    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
-    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
-    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
-    fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
-    fn clear_children(&mut self) { self.children.clear(); }
+    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
+        self.parent
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
+        self.parent = parent;
+    }
+
+    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
+        let mut list = Vec::new();
+        list.push(&self.sidebar_menu as *const dyn Widget as *mut dyn Widget);
+        for plate in &self.plates {
+            list.push(plate as *const dyn Widget as *mut dyn Widget);
+        }
+        list
+    }
+
+    fn add_child(&mut self, _child: *mut (dyn Widget + 'static)) {}
+    fn clear_children(&mut self) {}
+
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        self.sidebar_menu.set_modifiers(ctrl, shift, alt);
+        if self.selected_page < self.plates.len() {
+            self.plates[self.selected_page].set_modifiers(ctrl, shift, alt);
+        }
+    }
+    fn menu_names(&self) -> Vec<String> {
+        self.pages.clone()
+    }
+    fn selected_page(&self) -> usize {
+        self.selected_page()
+    }
+    fn set_selected_page(&mut self, page: usize) {
+        self.set_selected_page(page);
+    }
+    fn is_page_hidden(&self) -> bool {
+        self.is_page_hidden()
+    }
+    fn set_page_hidden(&mut self, hidden: bool) {
+        self.set_page_hidden(hidden);
+    }
+    fn set_pages(&mut self, pages: Vec<String>) {
+        self.set_pages(pages);
+    }
+    fn sidebar_w(&self) -> f32 {
+        self.sidebar_w()
+    }
+    fn set_sidebar_mode(&mut self, enabled: bool) {
+        self.set_sidebar_mode(enabled);
+    }
+    fn set_sidebar_label(&mut self, label: Option<String>) {
+        self.sidebar_label = label;
+        self.update_target_pos();
+    }
+    fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
+        self.add_widget_to_page(page_idx, widget);
+    }
+    fn clear_page_widgets(&mut self, page_idx: usize) {
+        self.clear_page_widgets(page_idx);
+    }
+    fn menu_items_list(&self) -> Vec<Vec<String>> {
+        self.sidebar_menu.menu_items_list()
+    }
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+        self.sidebar_menu.menu_checked_list()
+    }
 }
 
 unsafe impl Send for Paginator {}
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index afbafcc..dfc790a 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -59,6 +59,8 @@ pub struct JsonLayoutWidget {
     pub widgets: Vec<JsonWidget>,
     pub paginator: Option<Paginator>,
     pub dragging_slider_idx: Option<usize>,
+    pub page_scroll_y: Vec<f32>,
+    pub page_total_heights: Vec<f32>,
 }
 
 impl JsonLayoutWidget {
@@ -230,6 +232,8 @@ impl JsonLayoutWidget {
             widgets,
             paginator,
             dragging_slider_idx: None,
+            page_scroll_y: vec![0.0; 16],
+            page_total_heights: vec![0.0; 16],
         }
     }
 
@@ -269,7 +273,8 @@ impl JsonLayoutWidget {
                 top_room = sl.top_room();
             }
 
-            w_state.y = by + *current_y + top_room;
+            let scroll_offset = self.page_scroll_y.get(p_idx).cloned().unwrap_or(0.0);
+            w_state.y = by + *current_y + top_room - scroll_offset;
             w_state.w = usable_w;
 
             if let Some(cb) = &mut w_state.checkbox {
@@ -301,6 +306,13 @@ impl JsonLayoutWidget {
 
             *current_y += top_room + w_state.h + spacing;
         }
+
+        // Store total height of each page (adding a little padding at the end)
+        for (i, &height) in page_current_y.iter().enumerate() {
+            if i < self.page_total_heights.len() {
+                self.page_total_heights[i] = height + 4.0;
+            }
+        }
     }
 }
 
@@ -354,35 +366,66 @@ impl Widget for JsonLayoutWidget {
         }
 
         let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+        let (bx, by, bw, bh) = self.rect();
+        let has_paginator = self.paginator.is_some();
+        let pad_x = if has_paginator { 76.0 } else { 16.0 };
+        let min_x = bx + pad_x - 4.0;
+        let max_x = bx + bw;
+        let min_y = by;
+        let max_y = by + bh;
+
+        let push_clipped = |qx: f32, qy: f32, qw: f32, qh: f32, qc: [f32; 4], q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
+            let rx1 = qx.max(min_x);
+            let ry1 = qy.max(min_y);
+            let rx2 = (qx + qw).min(max_x);
+            let ry2 = (qy + qh).min(max_y);
+            let rw = rx2 - rx1;
+            let rh = ry2 - ry1;
+            if rw > 0.0 && rh > 0.0 {
+                q.push((rx1, ry1, rw, rh, qc));
+            }
+        };
 
         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());
+                push_clipped(cb.rect().0, cb.rect().1, cb.rect().2, cb.rect().3, cb.color(), &mut quads);
+                for q in cb.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+                }
                 if let Some(hq) = cb.highlight_quad() {
-                    quads.push(hq);
+                    push_clipped(hq.0, hq.1, hq.2, hq.3, hq.4, &mut quads);
                 }
             } 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());
+                push_clipped(btn.rect().0, btn.rect().1, btn.rect().2, btn.rect().3, btn.color(), &mut quads);
+                for q in btn.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+                }
                 if let Some(hq) = btn.highlight_quad() {
-                    quads.push(hq);
+                    push_clipped(hq.0, hq.1, hq.2, hq.3, hq.4, &mut quads);
                 }
             } 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());
+                push_clipped(lbl.rect().0, lbl.rect().1, lbl.rect().2, lbl.rect().3, lbl.color(), &mut quads);
+                for q in lbl.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut 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());
+                push_clipped(sb.rect().0, sb.rect().1, sb.rect().2, sb.rect().3, sb.color(), &mut quads);
+                for q in sb.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut 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());
+                push_clipped(cs.rect().0, cs.rect().1, cs.rect().2, cs.rect().3, cs.color(), &mut quads);
+                for q in cs.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut 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());
+                push_clipped(sl.rect().0, sl.rect().1, sl.rect().2, sl.rect().3, sl.color(), &mut quads);
+                for q in sl.extra_quads() {
+                    push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+                }
             }
         }
         quads
@@ -419,6 +462,50 @@ impl Widget for JsonLayoutWidget {
         labels
     }
 
+    fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        let mut labels = Vec::new();
+        let (bx, by, bw, bh) = self.rect();
+        if let Some(paginator) = &self.paginator {
+            for l in paginator.text_labels() {
+                labels.push((l, None));
+            }
+        }
+
+        let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+        let has_paginator = self.paginator.is_some();
+        let pad_x = if has_paginator { 76.0 } else { 16.0 };
+        let content_bounds = Some([bx + pad_x - 4.0, by, bx + bw, by + bh]);
+
+        for w in &self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            let w_labels = if let Some(_cb) = &w.checkbox {
+                if let Some(tl) = &w.label_text {
+                    vec![tl.clone()]
+                } else {
+                    Vec::new()
+                }
+            } else if let Some(btn) = &w.button {
+                btn.text_labels()
+            } else if let Some(lbl) = &w.label {
+                lbl.text_labels()
+            } else if let Some(sb) = &w.spinbox {
+                sb.text_labels()
+            } else if let Some(cs) = &w.color_selector {
+                cs.text_labels()
+            } else if let Some(sl) = &w.slider {
+                sl.text_labels()
+            } else {
+                Vec::new()
+            };
+            for l in w_labels {
+                labels.push((l, content_bounds));
+            }
+        }
+        labels
+    }
+
     fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
         let mut changed = false;
         if let Some(paginator) = &mut self.paginator {
@@ -572,4 +659,29 @@ impl Widget for JsonLayoutWidget {
         }
         false
     }
+
+    fn mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, px: f32, py: f32) -> bool {
+        let (bx, by, bw, bh) = self.rect();
+        if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
+            let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+            if active_page < self.page_total_heights.len() {
+                let total_height = self.page_total_heights[active_page];
+                let visible_h = bh;
+                let max_scroll_y = (total_height - visible_h).max(0.0);
+                if max_scroll_y > 0.0 {
+                    let scroll_amount = match delta {
+                        crate::widget::MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
+                        crate::widget::MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
+                    };
+                    let old_scroll = self.page_scroll_y[active_page];
+                    self.page_scroll_y[active_page] = (old_scroll + scroll_amount).clamp(0.0, max_scroll_y);
+                    if (self.page_scroll_y[active_page] - old_scroll).abs() > 0.01 {
+                        self.layout_children();
+                        return true;
+                    }
+                }
+            }
+        }
+        false
+    }
 }