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

commitdf641ab5ff277f142aff7d2d84f67350f4fe7ab9
parent64761df486
authorLucas Galante <[email protected]>
date2026-06-10 22:00
Add gap width spinbox, layout improvements, and switcher/page/layer container widgets

 src/backend/window_runner.rs           |  51 ++-
 src/context.rs                         |  38 +-
 src/widget/container/breadcrumb.rs     |  25 +-
 src/widget/container/content_bg.rs     |  23 +-
 src/widget/container/layer.rs          | 170 ++++++++
 src/widget/container/menu.rs           | 685 +++++++++++++++++----------------
 src/widget/container/mod.rs            |   6 +
 src/widget/container/page.rs           | 153 ++++++++
 src/widget/container/paginator.rs      | 187 ++++++---
 src/widget/container/parameters_bg.rs  | 238 ++++++++----
 src/widget/container/plate.rs          | 226 +++++++----
 src/widget/container/scrolling_list.rs |   5 +
 src/widget/container/spreadsheet.rs    |  25 +-
 src/widget/container/switcher.rs       | 513 ++++++++++++++++++++++++
 src/widget/core.rs                     |  72 ++--
 src/widget/display/float3.rs           |   7 +
 src/widget/display/graph.rs            | 172 +++++----
 src/widget/display/node.rs             |  30 +-
 src/widget/display/serialize.rs        |  19 +-
 src/widget/input/button.rs             |  28 +-
 src/widget/input/button_strip.rs       | 198 +++++++++-
 src/widget/input/checkbox.rs           |  35 ++
 src/widget/input/color_selector.rs     |  34 ++
 src/widget/input/dropdown.rs           |  46 +++
 src/widget/input/slider.rs             |  47 +++
 src/widget/input/spinbox.rs            |  48 +++
 src/widget/input/text_box.rs           |  51 +++
 src/widget/mod.rs                      | 189 +++++----
 28 files changed, 2529 insertions(+), 792 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 2f9d4d5..e4ac1e0 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -440,6 +440,7 @@ pub fn push_plate_bevel_vertices(
     clip_circle: [f32; 3],
     out: &mut Vec<Vertex>,
 ) {
+    let r = r.min(ww * 0.5).min(h * 0.5);
     let highlight_color = [1.0, 1.0, 1.0, 0.15];
     let shadow_color = [0.0, 0.0, 0.0, 0.25];
 
@@ -491,6 +492,52 @@ pub fn push_plate_bevel_vertices(
     );
 }
 
+pub fn push_plate_solid_border_vertices(
+    x: f32, y: f32, ww: f32, h: f32,
+    r: f32,
+    t: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    clip_circle: [f32; 3],
+    out: &mut Vec<Vertex>,
+) {
+    let r = r.min(ww * 0.5).min(h * 0.5);
+    out.extend_from_slice(&quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, color, clip_circle));
+    out.extend_from_slice(&quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, color, clip_circle));
+    out.extend_from_slice(&quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, color, clip_circle));
+    out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, color, clip_circle));
+
+    let segments = 16;
+
+    push_arc_background_vertices(
+        x + r, y + r, r, t,
+        std::f32::consts::PI, 1.5 * std::f32::consts::PI,
+        sw, sh, color, segments, clip_circle,
+        out,
+    );
+
+    push_arc_background_vertices(
+        x + ww - r, y + r, r, t,
+        1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
+        sw, sh, color, segments, clip_circle,
+        out,
+    );
+
+    push_arc_background_vertices(
+        x + ww - r, y + h - r, r, t,
+        0.0, 0.5 * std::f32::consts::PI,
+        sw, sh, color, segments, clip_circle,
+        out,
+    );
+
+    push_arc_background_vertices(
+        x + r, y + h - r, r, t,
+        0.5 * std::f32::consts::PI, std::f32::consts::PI,
+        sw, sh, color, segments, clip_circle,
+        out,
+    );
+}
+
 pub fn widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
     let mut verts = Vec::new();
     push_widget_vertices(w, sw, sh, clip_circle, &mut verts);
@@ -506,8 +553,8 @@ pub fn push_widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, cl
         out.extend_from_slice(&quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle));
     }
 
-    if w.is_plate() {
-        push_plate_bevel_vertices(x, y, ww, h, 12.0, 1.5, sw, sh, clip_circle, out);
+    if let Some((color, thickness)) = w.solid_border() {
+        push_plate_solid_border_vertices(x, y, ww, h, 12.0, thickness, sw, sh, color, clip_circle, out);
     }
 }
 
diff --git a/src/context.rs b/src/context.rs
index 681b90c..476ca22 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -339,34 +339,54 @@ impl UiContext {
 
     // --- Context Menu ---
     pub fn is_context_menu_visible(&self) -> bool {
-        self.context_menu.visible
+        crate::widget::context_menu::is_visible()
     }
 
-    pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
-        self.context_menu.show(x, y, options, target);
+    pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
+        crate::widget::context_menu::show(x, y, options, target);
+    }
+
+    pub fn handle_right_click(&mut self, target: *mut (dyn Element + 'static), px: f32, py: f32) {
+        let name = unsafe { (*target).type_name() };
+        let label = unsafe { (*target).label() };
+        let header = if let Some(lbl) = label {
+            format!("[{}]: {}", name, lbl)
+        } else {
+            format!("[{}]", name)
+        };
+
+        let options = if name == "TextBox" {
+            vec![header, "Cut".to_string(), "Copy".to_string(), "Paste".to_string(), "Select All".to_string()]
+        } else {
+            vec![header, "Copy".to_string(), "Paste".to_string()]
+        };
+
+        let scroll_y = crate::widget::hover_animation::get_scroll_offset();
+        let adjusted_py = py - scroll_y;
+        crate::widget::context_menu::show(px, adjusted_py, options, target);
     }
 
     pub fn hide_context_menu(&mut self) {
-        self.context_menu.hide();
+        crate::widget::context_menu::hide();
     }
 
     pub fn hit_test_context_menu(&self, px: f32, py: f32) -> bool {
-        self.context_menu.hit_test(px, py)
+        crate::widget::context_menu::hit_test(px, py)
     }
 
     pub fn cursor_moved_context_menu(&mut self, px: f32, py: f32) -> bool {
-        self.context_menu.cursor_moved(px, py)
+        crate::widget::context_menu::cursor_moved(px, py)
     }
 
     pub fn mouse_input_context_menu(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
-        self.context_menu.mouse_input(button, state, px, py)
+        crate::widget::context_menu::mouse_input(button, state, px, py)
     }
 
     pub fn context_menu_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        self.context_menu.extra_quads()
+        crate::widget::context_menu::extra_quads()
     }
 
     pub fn context_menu_labels(&self) -> Vec<crate::widget::display::TextLabel> {
-        self.context_menu.text_labels()
+        crate::widget::context_menu::text_labels()
     }
 }
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index a2f381d..604961e 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -68,20 +68,8 @@ impl Element for Breadcrumb {
         false
     }
 
-    fn set_path(&mut self, segments: &[String]) {
-        let mut s = Vec::with_capacity(segments.len().max(1));
-        if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
-            s.push("/".to_string());
-        } else {
-            s.push("/".to_string());
-            for name in segments {
-                s.push(format!(" \u{203A} {}", name));
-            }
-        }
-        self.path = s;
-    }
-
-    fn path_click(&mut self) -> Option<usize> { self.clicked_seg.take() }
+    fn as_path_controller(&self) -> Option<&dyn PathController> { Some(self) }
+    fn as_path_controller_mut(&mut self) -> Option<&mut dyn PathController> { Some(self) }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
@@ -113,3 +101,12 @@ impl Element for Breadcrumb {
         labels
     }
 }
+
+impl PathController for Breadcrumb {
+    fn set_path(&mut self, segments: &[String]) {
+        self.path = segments.to_vec();
+    }
+    fn path_click(&mut self) -> Option<usize> {
+        self.clicked_seg.take()
+    }
+}
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 5e53ad8..1d11bc2 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -33,10 +33,8 @@ impl Element for ContentBg {
     fn hovered(&self) -> bool { self.hovered }
     fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
 
-    fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
-    fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
-    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
-    fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+    fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
+    fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
@@ -234,3 +232,20 @@ impl Element for ContentBg {
         quads
     }
 }
+
+impl GraphController for ContentBg {
+    fn set_nodes(&mut self, _nodes: &[GraphNode]) {}
+    fn get_nodes(&self) -> Vec<GraphNode> { Vec::new() }
+    fn selected_node(&self) -> Option<usize> { None }
+    fn set_selected_node(&mut self, _idx: Option<usize>) {}
+    fn double_clicked_node(&self) -> Option<usize> { None }
+    fn clear_double_clicked_node(&mut self) {}
+    fn set_grid_snap_enabled(&mut self, _enabled: bool) {}
+    fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { None }
+    fn set_grid_snap(&mut self, _gx: f32, _gy: f32) {}
+    fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+    fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+    fn grid_origin(&self) -> (f32, f32) { (self.grid_origin_x, self.grid_origin_y) }
+    fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+}
diff --git a/src/widget/container/layer.rs b/src/widget/container/layer.rs
new file mode 100644
index 0000000..b49e760
--- /dev/null
+++ b/src/widget/container/layer.rs
@@ -0,0 +1,170 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+
+#[derive(Debug, Clone)]
+pub struct Layer {
+    pub base: Widget,
+    pub children: Vec<*mut (dyn Element + 'static)>,
+    pub parent: Option<*mut (dyn Element + 'static)>,
+    pub visible: bool,
+    pub padding: Option<f32>,
+}
+
+impl Layer {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+        Self {
+            base: Widget::new_rect(x, y, w, h),
+            children: Vec::new(),
+            parent: None,
+            visible: true,
+            padding: None,
+        }
+    }
+
+    pub fn with_padding(mut self, padding: f32) -> Self {
+        self.padding = Some(padding);
+        self
+    }
+}
+
+impl Element for Layer {
+    fn base(&self) -> Option<&Widget> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
+    fn as_any(&self) -> &dyn std::any::Any { self }
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    }
+
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.base.x, self.base.y, self.base.w, self.base.h)
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.base.x = x;
+        self.base.y = y;
+        self.base.w = w;
+        self.base.h = h;
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn visible(&self) -> bool {
+        self.visible
+    }
+
+    fn set_visible(&mut self, visible: bool) {
+        self.visible = visible;
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+        let id = self.base.id();
+        let self_ptr = self as *mut Layer as *mut (dyn Element + 'static);
+        self.children.push(child);
+        unsafe {
+            let c_id = (*child).base().map(|b| b.id()).unwrap();
+            ctx.register_widget(c_id, child);
+            ctx.link_ids(id, c_id);
+            (*child).set_parent(Some(self_ptr), ctx);
+        }
+    }
+
+    fn clear_children(&mut self, ctx: &mut UiContext) {
+        self.children.clear();
+        let id = self.base.id();
+        ctx.clear_children_ids(id);
+    }
+
+    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        self.children.clone()
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+        self.parent = parent;
+    }
+
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.parent
+    }
+
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut quads = Vec::new();
+        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(ctx));
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut labels = Vec::new();
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            labels.extend(widget.text_labels());
+        }
+        labels
+    }
+
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut result = Vec::new();
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            result.extend(widget.text_labels_with_bounds(ctx));
+        }
+        result
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut result = Vec::new();
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            result.extend(widget.text_labels_with_font_and_bounds(ctx));
+        }
+        result
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut result = Vec::new();
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            result.extend(widget.get_text_items());
+        }
+        result
+    }
+
+    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+        if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+            return false;
+        }
+        for &child_ptr in &self.children {
+            let widget = unsafe { &*child_ptr };
+            if widget.hit_test(px, py, ctx) {
+                return true;
+            }
+        }
+        false
+    }
+}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 4e52ed3..f4270b4 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -3,7 +3,12 @@ use crate::widget::*;
 use crate::widget::display::{make_widget_text_buffer, TextLabel};
 
 pub struct MenuBar {
-    pub base: Plate,
+    pub base: Widget,
+    pub visible: bool,
+    pub network_opacity: f32,
+    pub curved_circle: Option<(f32, f32, f32)>,
+    pub blur: bool,
+    pub color: Option<[f32; 4]>,
     pub title: String,
     pub menus: ButtonStrip,
     pub menu_items: Vec<String>,
@@ -26,25 +31,40 @@ pub struct MenuBar {
     pub context_hovered_item: Option<usize>,
     pub context_title_hovered: bool,
     pub context_item_bufs: Vec<glyphon::Buffer>,
+    pub parent: Option<*mut (dyn Element + 'static)>,
+    pub page_hidden: bool,
+    pub layout_dirty: bool,
+    pub on_context_change_cb: Option<Box<dyn Fn(usize) + Send + Sync>>,
+    pub on_menu_click_cb: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
 }
 
 impl MenuBar {
     pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
-        self.base.curved_circle = circle;
+        self.curved_circle = circle;
     }
 
     pub fn set_network_opacity(&mut self, opacity: f32) {
-        self.base.set_network_opacity(opacity);
+        self.network_opacity = opacity;
     }
 
-    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        let mut base_plate = Plate::new(x, y, w, h)
-            .with_color(colors::PANEL_MENU_BG)
-            .with_draggable(false);
-        base_plate.blur = true;
+    pub fn with_color(mut self, color: [f32; 4]) -> Self {
+        self.color = Some(color);
+        self
+    }
 
+    pub fn with_blur(mut self, blur: bool) -> Self {
+        self.blur = blur;
+        self
+    }
+
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
         Self {
-            base: base_plate,
+            base: Widget::new_rect(x, y, w, h),
+            visible: true,
+            network_opacity: 1.0,
+            curved_circle: None,
+            blur: false,
+            color: None,
             title: String::new(),
             menus: ButtonStrip::new(x, y, w, h),
             menu_items: Vec::new(),
@@ -67,18 +87,34 @@ impl MenuBar {
             context_hovered_item: None,
             context_title_hovered: false,
             context_item_bufs: Vec::new(),
+            parent: None,
+            page_hidden: false,
+            layout_dirty: true,
+            on_context_change_cb: None,
+            on_menu_click_cb: None,
         }
     }
 
+    pub fn on_context_change<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+        self.on_context_change_cb = Some(Box::new(cb));
+        self
+    }
+
+    pub fn on_menu_click<F: Fn(usize, usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+        self.on_menu_click_cb = Some(Box::new(cb));
+        self
+    }
+
     pub fn with_label(mut self, label: &str) -> Self {
         self.label = Some(label.to_string());
-        self.base.base.label = Some(label.to_string());
+        self.base.label = Some(label.to_string());
         self
     }
 
     pub fn set_label(&mut self, label: &str) {
         self.label = Some(label.to_string());
-        self.base.base.label = Some(label.to_string());
+        self.base.label = Some(label.to_string());
+        self.layout_dirty = true;
     }
 
     pub fn with_context_options(mut self, options: Vec<String>, selected: usize) -> Self {
@@ -118,19 +154,19 @@ impl MenuBar {
             display_title.push_str(" ▼");
         }
 
-        if let Some((_ccx, _ccy, _ccr)) = self.base.curved_circle {
+        if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
             if let Some((tx, ty)) = self.title_pos {
                 let title_w = display_title.len() as f32 * char_w + 24.0;
-                (tx, ty, title_w, self.base.base.h)
+                (tx, ty, title_w, self.base.h)
             } else {
-                (self.base.base.x, self.base.base.y, display_title.len() as f32 * char_w + 24.0, self.base.base.h)
+                (self.base.x, self.base.y, display_title.len() as f32 * char_w + 24.0, self.base.h)
             }
         } else if self.vertical {
-            let mut cy = 8.0;
+            let mut cy = 16.0;
             if let Some(ref label) = self.label {
                 let line_height = font_size * 1.2;
                 let label_h = label.chars().count() as f32 * line_height;
-                cy += label_h + 8.0;
+                cy += label_h + 20.0;
             }
             let line_height = font_size * 1.2;
             let mut display_title_vertical = self.title.clone();
@@ -138,7 +174,7 @@ impl MenuBar {
                 display_title_vertical.push_str("▼");
             }
             let title_h = display_title_vertical.chars().count() as f32 * line_height;
-            (self.base.base.x, self.base.base.y + cy, self.base.base.w, title_h)
+            (self.base.x, self.base.y + cy, self.base.w, title_h)
         } else {
             let mut start_x = 8.0;
             if self.center_items {
@@ -147,12 +183,12 @@ impl MenuBar {
                 for btn_label in &self.menus.buttons {
                     total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
                 }
-                if self.base.base.w > total_width {
-                    start_x = (self.base.base.w - total_width) / 2.0;
+                if self.base.w > total_width {
+                    start_x = (self.base.w - total_width) / 2.0;
                 }
             }
             let title_w = display_title.len() as f32 * char_w + 24.0;
-            (self.base.base.x + start_x, self.base.base.y, title_w, self.base.base.h)
+            (self.base.x + start_x, self.base.y, title_w, self.base.h)
         }
     }
 
@@ -219,6 +255,7 @@ impl MenuBar {
             &self.menu_items
         };
         self.menus.buttons = src.clone();
+        self.menus.generate_rotated_labels();
     }
 
     pub fn with_vertical(mut self, vertical: bool) -> Self {
@@ -235,14 +272,23 @@ impl MenuBar {
 }
 
 impl Element for MenuBar {
-    fn as_any(&self) -> &dyn std::any::Any { self }
-    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
-    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
-        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    crate::impl_widget_base!(MenuBar);
+
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        self.menus.set_modifiers(ctrl, shift, alt);
+    }
+
+    fn layout_ignore(&self) -> bool {
+        true
+    }
+
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.parent
     }
 
-    fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
-    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+        self.parent = parent;
+    }
 
     fn label(&self) -> Option<String> {
         self.label.clone()
@@ -250,11 +296,12 @@ impl Element for MenuBar {
 
     fn set_text(&mut self, text: &str) {
         self.label = Some(text.to_string());
-        self.base.base.label = Some(text.to_string());
+        self.base.label = Some(text.to_string());
+        self.layout_dirty = true;
     }
 
     fn rect(&self) -> (f32, f32, f32, f32) {
-        if !self.base.visible {
+        if !self.visible {
             return (0.0, 0.0, 0.0, 0.0);
         }
         if self.vertical {
@@ -263,7 +310,7 @@ impl Element for MenuBar {
                 let font_size = 12.0;
                 let line_height = font_size * 1.2;
                 let label_h = label.chars().count() as f32 * line_height;
-                h += label_h + 8.0;
+                h += label_h + 20.0;
             }
             if !self.title.is_empty() {
                 let font_size = 12.0;
@@ -273,17 +320,35 @@ impl Element for MenuBar {
                     display_title.push_str("▼");
                 }
                 let title_h = display_title.chars().count() as f32 * line_height;
-                h += title_h + 8.0;
+                h += title_h + 36.0;
             }
             let (_, _, _, menus_h) = self.menus.rect();
-            (self.base.base.x, self.base.base.y, self.base.base.w, h + menus_h)
+            (self.base.x, self.base.y, self.base.w, h + menus_h)
         } else {
-            (self.base.base.x, self.base.base.y, self.base.base.w, self.base.base.h)
+            (self.base.x, self.base.y, self.base.w, self.base.h)
         }
     }
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        self.base.set_rect(x, y, w, h);
+        if self.base.x == x && self.base.y == y && self.base.w == w && self.base.h == h && !self.layout_dirty {
+            return;
+        }
+        self.layout_dirty = false;
+        let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
+            let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+            let cx = x.clamp(px, px + pw);
+            let cy = y.clamp(py, py + ph);
+            let cw = w.min(px + pw - cx);
+            let ch = h.min(py + ph - cy);
+            (cx, cy, cw, ch)
+        } else {
+            (x, y, w, h)
+        };
+
+        self.base.x = clamped_x;
+        self.base.y = clamped_y;
+        self.base.w = clamped_w;
+        self.base.h = clamped_h;
         let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
 
         let font_setting = crate::layout::menubar_font();
@@ -292,12 +357,12 @@ impl Element for MenuBar {
         let char_w = 7.5 * (font_size / 12.0);
 
         if self.vertical {
-            let mut cy = 8.0;
+            let mut cy = 16.0;
             if let Some(ref label) = self.label {
                 let font_size = 12.0;
                 let line_height = font_size * 1.2;
                 let label_h = label.chars().count() as f32 * line_height;
-                cy += label_h + 8.0;
+                cy += label_h + 20.0;
             }
             if !self.title.is_empty() {
                 let font_size = 12.0;
@@ -307,10 +372,12 @@ impl Element for MenuBar {
                     display_title.push_str("▼");
                 }
                 let title_h = display_title.chars().count() as f32 * line_height;
-                cy += title_h + 8.0;
+                cy += title_h + 36.0;
             }
+            let menus_y = (clamped_y + cy).clamp(clamped_y, clamped_y + clamped_h);
+            let menus_h = (clamped_h - cy).min(clamped_y + clamped_h - menus_y).max(0.0);
             self.menus.vertical = true;
-            self.menus.set_rect(x, y + cy, w, (h - cy).max(0.0));
+            self.menus.set_rect(clamped_x, menus_y, clamped_w, menus_h);
             let mut dummy = crate::context::UiContext::new();
             self.menus.set_parent(Some(parent_ptr), &mut dummy);
         } else {
@@ -330,8 +397,8 @@ impl Element for MenuBar {
                     btn_strip_w += btn_label.len() as f32 * char_w + 2.0 * padding_x;
                 }
                 total_width += btn_strip_w;
-                if self.base.base.w > total_width {
-                    cx = (self.base.base.w - total_width) / 2.0;
+                if self.base.w > total_width {
+                    cx = (self.base.w - total_width) / 2.0;
                 }
             }
             if !self.title.is_empty() {
@@ -345,38 +412,29 @@ impl Element for MenuBar {
             for btn_label in &self.menus.buttons {
                 btn_strip_w += btn_label.len() as f32 * char_w + 2.0 * padding_x;
             }
+            let menus_x = (clamped_x + cx).clamp(clamped_x, clamped_x + clamped_w);
+            let menus_w = btn_strip_w.min(clamped_x + clamped_w - menus_x);
             self.menus.vertical = false;
-            self.menus.set_rect(x + cx, y, btn_strip_w, h);
+            self.menus.set_rect(menus_x, clamped_y, menus_w, clamped_h);
             let mut dummy = crate::context::UiContext::new();
             self.menus.set_parent(Some(parent_ptr), &mut dummy);
         }
     }
 
     fn color(&self) -> [f32; 4] {
-        if !self.base.visible {
-            [0.0, 0.0, 0.0, 0.0]
-        } else if self.focused {
-            let mut c = colors::PANEL_MENU_FOCUSED;
-            c[3] *= self.base.network_opacity;
-            if self.base.blur {
-                c[3] = -c[3].abs();
-            }
-            c
-        } else {
-            self.base.color()
-        }
+        [0.0, 0.0, 0.0, 0.0]
     }
 
     fn set_hovered(&mut self, v: bool) {
-        self.base.base.hovered = v;
+        self.base.hovered = v;
     }
 
     fn hovered(&self) -> bool {
-        self.base.base.hovered
+        self.base.hovered
     }
 
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
-        if !self.base.visible {
+        if !self.visible {
             return false;
         }
         if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
@@ -398,7 +456,7 @@ impl Element for MenuBar {
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if !self.base.visible {
+        if !self.visible {
             return false;
         }
         let (rx, ry, rw, rh) = self.rect();
@@ -439,7 +497,7 @@ impl Element for MenuBar {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if !self.base.visible {
+        if !self.visible {
             return false;
         }
         if button != MouseButton::Left {
@@ -460,6 +518,9 @@ impl Element for MenuBar {
                         self.context_just_changed = true;
                         self.context_dropdown_open = false;
                         self.unfocus();
+                        if let Some(ref cb) = self.on_context_change_cb {
+                            cb(di);
+                        }
                         return true;
                     }
                 }
@@ -570,69 +631,33 @@ impl Element for MenuBar {
         }
     }
 
-    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
-        self.menus.set_modifiers(ctrl, shift, alt);
-    }
-
-    fn menu_names(&self) -> Vec<String> {
-        self.menus.buttons.clone()
-    }
-
-    fn menu_click(&mut self) -> Option<(usize, usize)> {
-        if let Some(idx) = self.menus.take_click() {
-            return Some((idx, 0));
-        }
-        None
-    }
-
-    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
-        if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
-            if item_idx < menu.len() {
-                menu[item_idx] = Some(checked);
-            }
-        }
-    }
-
-    fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
-        if menu_idx < self.menu_dropdowns.len() {
-            self.menu_dropdowns[menu_idx] = items.to_vec();
-            self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
-        }
-    }
-
-    fn is_menu_bar(&self) -> bool {
-        self.base.visible
-    }
-
-    fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
-        None
-    }
-
-    fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
-
-    fn is_menu_open(&self) -> bool {
-        self.context_dropdown_open
-    }
+    fn as_page_selector(&self) -> Option<&dyn PageSelector> { Some(self) }
+    fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { Some(self) }
+    fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
 
     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        if !self.base.visible {
+        if !self.visible {
             return Vec::new();
         }
-        let mut quads = Vec::new();
-        quads.extend(self.base.all_quads(ctx));
-        quads.extend(self.menus.all_quads(ctx));
+        let mut quads = self.extra_quads();
+        if let Some(hq) = self.menus.highlight_quad(ctx) {
+            if hq.4 != colors::HIGHLIGHT_SECONDARY {
+                quads.push(hq);
+            }
+        }
         quads
     }
 
     fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
-        if !self.base.visible {
+        if !self.visible {
             return Vec::new();
         }
         self.menus.extra_arcs()
     }
 
     fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
-        if !self.base.visible {
+        if !self.visible {
             return;
         }
         let current_font = crate::layout::menubar_font();
@@ -649,7 +674,7 @@ impl Element for MenuBar {
             if !self.context_options.is_empty() {
                 display_title.push_str(" ▼");
             }
-            if let Some((_ccx, _ccy, _ccr)) = self.base.curved_circle {
+            if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
                 if self.curved_title_char_bufs.len() != display_title.chars().count() {
                     let font_fam_clone = font_fam.clone();
                     self.curved_title_char_bufs = display_title.chars()
@@ -689,111 +714,11 @@ impl Element for MenuBar {
     }
 
     fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
-        if !self.base.visible {
-            return Vec::new();
-        }
-        let mut items = Vec::new();
-        let label_color = crate::colors::paginator_tab_label_color();
-        let srgb = crate::colors::to_srgb(label_color);
-        let color = glyphon::Color::rgb(
-            (srgb[0] * 255.0) as u8,
-            (srgb[1] * 255.0) as u8,
-            (srgb[2] * 255.0) as u8,
-        );
-
-        let font_setting = crate::layout::menubar_font();
-        let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
-        let font_size = font_size_opt.unwrap_or(12.0);
-        let char_w = 7.5 * (font_size / 12.0);
-
-        let padding_x = crate::layout::paginator_tab_padding_x();
-        if let Some((ccx, ccy, ccr)) = self.base.curved_circle {
-            let r_mid = ccr - self.base.base.h / 2.0;
-            let mut total_width = 8.0;
-            let mut display_title = self.title.clone();
-            if !self.context_options.is_empty() {
-                display_title.push_str(" ▼");
-            }
-            if !self.title.is_empty() {
-                total_width += display_title.len() as f32 * char_w + 24.0;
-            }
-            for btn_label in &self.menus.buttons {
-                total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
-            }
-            let total_angular_width = total_width / r_mid;
-            let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
-            let current_angle = start_angle;
-
-            if !self.title.is_empty() {
-                let title_w = display_title.len() as f32 * char_w + 24.0;
-                let dtheta_title = title_w / r_mid;
-
-                let char_widths: Vec<f32> = display_title.chars().map(|c| {
-                    TextLabel::estimate_width(&c.to_string(), font_size)
-                }).collect();
-                let total_chars_width: f32 = char_widths.iter().sum();
-                let mid_angle = (current_angle + current_angle + dtheta_title) / 2.0;
-                let angular_width = total_chars_width / r_mid;
-                let text_start_angle = mid_angle - angular_width / 2.0;
-                let mut cur_char_angle = text_start_angle;
-
-                for (char_idx, c_buf) in self.curved_title_char_bufs.iter().enumerate() {
-                    if char_idx < char_widths.len() {
-                        let cw = char_widths[char_idx];
-                        let dtheta = cw / r_mid;
-                        let char_center_angle = cur_char_angle + dtheta / 2.0;
-
-                        let tx = ccx + r_mid * char_center_angle.cos() - cw / 2.0;
-                        let ty = ccy + r_mid * char_center_angle.sin() - font_size / 2.0;
-
-                        items.push((c_buf, tx, ty, color));
-                        cur_char_angle += dtheta;
-                    }
-                }
-            }
-        } else if !self.vertical {
-            let mut start_x = 8.0;
-            let mut display_title = self.title.clone();
-            if !self.context_options.is_empty() {
-                display_title.push_str(" ▼");
-            }
-            if self.center_items {
-                let mut total_width = 8.0;
-                if !self.title.is_empty() {
-                    total_width += display_title.len() as f32 * char_w + 24.0;
-                }
-                for btn_label in &self.menus.buttons {
-                    total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
-                }
-                if self.base.base.w > total_width {
-                    start_x = (self.base.base.w - total_width) / 2.0;
-                }
-            }
-            if let Some(ref title_buf) = self.title_buf {
-                let text_y = self.base.base.y + (self.base.base.h - font_size) / 2.0;
-                items.push((title_buf, self.base.base.x + start_x, text_y, color));
-            }
-        }
-
-        if self.context_dropdown_open {
-            if let Some((dx, dy, _, _)) = self.context_popover_rect() {
-                for (i, item_buf) in self.context_item_bufs.iter().enumerate() {
-                    items.push((
-                        item_buf,
-                        dx + 8.0,
-                        dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
-                        color,
-                    ));
-                }
-            }
-        }
-
-        items.extend(self.menus.get_text_items());
-        items
+        Vec::new()
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
-        if !self.base.visible {
+        if !self.visible {
             return Vec::new();
         }
         let label_color = crate::colors::paginator_tab_label_color();
@@ -810,11 +735,11 @@ impl Element for MenuBar {
             if self.vertical {
                 let font_size = 12.0;
                 let line_height = font_size * 1.2;
-                let start_y = self.base.base.y + 8.0;
+                let start_y = self.base.y + 16.0;
                 for (i, c) in label.chars().enumerate() {
                     let char_str = c.to_string();
                     let char_w = TextLabel::estimate_width(&char_str, font_size);
-                    let x_pos = self.base.base.x + (self.base.base.w - char_w) / 2.0;
+                    let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
                     let y_pos = start_y + i as f32 * line_height;
                     labels.push(TextLabel {
                         text: char_str,
@@ -837,8 +762,8 @@ impl Element for MenuBar {
             display_title.push_str(" ▼");
         }
 
-        if let Some((ccx, ccy, ccr)) = self.base.curved_circle {
-            let r_mid = ccr - self.base.base.h / 2.0;
+        if let Some((ccx, ccy, ccr)) = self.curved_circle {
+            let r_mid = ccr - self.base.h / 2.0;
             let mut total_width = 8.0;
             if !self.title.is_empty() {
                 total_width += display_title.len() as f32 * char_w + 24.0;
@@ -863,16 +788,16 @@ impl Element for MenuBar {
             }
         } else if self.vertical {
             if !self.title.is_empty() {
-                let mut start_y = self.base.base.y + 8.0;
+                let mut start_y = self.base.y + 16.0;
                 if let Some(ref label) = self.label {
                     let font_size = 12.0;
                     let line_height = font_size * 1.2;
                     let label_h = label.chars().count() as f32 * line_height;
-                    start_y += label_h + 8.0;
+                    start_y += label_h + 20.0;
                 }
                 let line_height = font_size * 1.2;
                 let char_w = TextLabel::estimate_width("o", font_size);
-                let x_pos = self.base.base.x + (self.base.base.w - char_w) / 2.0;
+                let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
                 let mut display_title_vertical = self.title.clone();
                 if !self.context_options.is_empty() {
                     display_title_vertical.push_str("▼");
@@ -899,15 +824,15 @@ impl Element for MenuBar {
                 for btn_label in &self.menus.buttons {
                     total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
                 }
-                if self.base.base.w > total_width {
-                    start_x = (self.base.base.w - total_width) / 2.0;
+                if self.base.w > total_width {
+                    start_x = (self.base.w - total_width) / 2.0;
                 }
             }
             if !self.title.is_empty() {
-                let text_y = self.base.base.y + (self.base.base.h - font_size) / 2.0;
+                let text_y = self.base.y + (self.base.h - font_size) / 2.0;
                 labels.push(TextLabel {
                     text: display_title,
-                    x: self.base.base.x + start_x,
+                    x: self.base.x + start_x,
                     y: text_y,
                     font_size,
                     color: text_color,
@@ -936,12 +861,15 @@ impl Element for MenuBar {
     }
 
     fn set_visible(&mut self, visible: bool) {
-        self.base.set_visible(visible);
-        self.menus.set_visible(visible);
+        if self.visible != visible {
+            self.visible = visible;
+            self.menus.set_visible(visible);
+            self.layout_dirty = true;
+        }
     }
 
     fn visible(&self) -> bool {
-        self.base.visible()
+        self.visible
     }
 
     fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
@@ -953,24 +881,17 @@ impl Element for MenuBar {
         self.z_level
     }
 
-    fn set_center_items(&mut self, center: bool) {
-        self.center_items = center;
-    }
 
-    fn menu_items_list(&self) -> Vec<Vec<String>> {
-        self.menus.buttons.iter().map(|_| Vec::new()).collect()
-    }
-
-    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
-        self.menus.buttons.iter().map(|_| Vec::new()).collect()
-    }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        if !self.base.visible {
+        if !self.visible {
             return Vec::new();
         }
         let mut quads = Vec::new();
 
+        // Draw main background
+        quads.push((self.base.x, self.base.y, self.base.w, self.base.h, colors::sidebar_bg_color()));
+
         // 1. Highlight the title on hover or open
         if !self.context_options.is_empty() {
             let tr = self.title_rect();
@@ -999,14 +920,8 @@ impl Element for MenuBar {
         Some(crate::layout::menubar_font())
     }
 
-    fn take_context_change(&mut self) -> Option<usize> {
-        self.take_context_change()
-    }
-
-    fn set_context_selected(&mut self, selected: usize) {
-        self.set_context_selected(selected);
-    }
-}impl Drop for MenuBar {
+}
+impl Drop for MenuBar {
     fn drop(&mut self) {
         focus::clear_if_matches(self);
     }
@@ -1224,20 +1139,8 @@ impl Element for Menu {
         }
     }
 
-    fn menu_click(&mut self) -> Option<(usize, usize)> {
-        self.clicked_item.take().map(|i| (0, i))
-    }
-
-    fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
-        if item_idx < self.item_checked.len() {
-            self.item_checked[item_idx] = Some(checked);
-            self.item_bufs.clear();
-        }
-    }
-
-    fn is_menu_open(&self) -> bool {
-        self.open
-    }
+    fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
 
     fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{
         None
@@ -1364,18 +1267,6 @@ impl Element for Menu {
         100
     }
 
-    fn menu_items(&self) -> Vec<String> {
-        self.items.clone()
-    }
-
-    fn menu_item_checked(&self) -> Vec<Option<bool>> {
-        self.item_checked.clone()
-    }
-
-    fn is_vertical(&self) -> bool {
-        self.vertical
-    }
-
     fn focused(&self, ctx: &UiContext) -> bool {
         self.base.focused
     }
@@ -1430,77 +1321,219 @@ impl Element for Menu {
     }
 
     fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
-        let mut items = Vec::new();
-        let label_color = crate::colors::paginator_tab_label_color();
-        let srgb = crate::colors::to_srgb(label_color);
-        let color = glyphon::Color::rgb(
-            (srgb[0] * 255.0) as u8,
-            (srgb[1] * 255.0) as u8,
-            (srgb[2] * 255.0) as u8,
-        );
+        Vec::new()
+    }
 
-        let font_setting = crate::layout::menubar_font();
-        let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
-        let font_size = font_size_opt.unwrap_or(12.0);
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::menubar_font())
+    }
+}
 
-        if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
-            let r_mid = r - thickness / 2.0;
-            let active_title = self.active_title();
-            
-            let char_widths: Vec<f32> = active_title.chars().map(|c| {
-                TextLabel::estimate_width(&c.to_string(), font_size)
-            }).collect();
-            let total_chars_width: f32 = char_widths.iter().sum();
-            
-            let angular_width = total_chars_width / r_mid;
-            let text_start_angle = (start_angle + end_angle) / 2.0 - angular_width / 2.0;
-            let mut cur_char_angle = text_start_angle;
-            
-            for (char_idx, c_buf) in self.curved_char_bufs.iter().enumerate() {
-                if char_idx < char_widths.len() {
-                    let cw = char_widths[char_idx];
-                    let dtheta = cw / r_mid;
-                    let char_center_angle = cur_char_angle + dtheta / 2.0;
-                    
-                    let tx = cx + r_mid * char_center_angle.cos() - cw / 2.0;
-                    let ty = cy + r_mid * char_center_angle.sin() - font_size / 2.0;
-                    
-                    items.push((c_buf, tx, ty, color));
-                    cur_char_angle += dtheta;
-                }
-            }
-        } else if !self.vertical {
-            if let Some(ref title_buf) = self.title_buf {
-                let text_y = self.base.y + (self.base.h - font_size) / 2.0;
-                items.push((title_buf, self.base.x + crate::layout::paginator_tab_padding_x(), text_y, color));
+unsafe impl Send for Menu {}
+unsafe impl Sync for Menu {}
+
+impl Drop for Menu {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
+impl MenuController for Menu {
+    fn menu_click(&mut self) -> Option<(usize, usize)> {
+        self.clicked_item.take().map(|i| (0, i))
+    }
+    fn trigger_menu_click(&mut self, _menu_idx: usize, item_idx: usize) {
+        if item_idx < self.items.len() {
+            self.clicked_item = Some(item_idx);
+        }
+    }
+    fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
+        if item_idx < self.item_checked.len() {
+            self.item_checked[item_idx] = Some(checked);
+            self.item_bufs.clear();
+        }
+    }
+    fn set_menu_items(&mut self, _menu_idx: usize, items: &[String]) {
+        self.items = items.to_vec();
+        self.item_checked = vec![None; items.len()];
+        self.item_bufs.clear();
+    }
+    fn is_menu_bar(&self) -> bool { false }
+    fn is_menu_open(&self) -> bool { self.open }
+    fn menu_items(&self) -> Vec<String> { self.items.clone() }
+    fn menu_item_checked(&self) -> Vec<Option<bool>> { self.item_checked.clone() }
+    fn is_vertical(&self) -> bool { self.vertical }
+    fn menu_names(&self) -> Vec<String> { vec![self.active_title().to_string()] }
+    fn menu_items_list(&self) -> Vec<Vec<String>> { vec![self.items.clone()] }
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> { vec![self.item_checked.clone()] }
+    fn take_context_change(&mut self) -> Option<usize> { None }
+    fn set_context_selected(&mut self, _selected: usize) {}
+    fn set_center_items(&mut self, _center: bool) {}
+    fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
+}
+
+
+impl MenuController for MenuBar {
+    fn menu_click(&mut self) -> Option<(usize, usize)> {
+        if let Some(idx) = self.menus.take_click() {
+            if let Some(ref cb) = self.on_menu_click_cb {
+                cb(idx, 0);
             }
+            return Some((idx, 0));
         }
+        None
+    }
 
-        if self.open {
-            let (dx, dy, _, _) = self.dropdown_rect();
-            for (i, item_buf) in self.item_bufs.iter().enumerate() {
-                items.push((
-                    item_buf,
-                    dx + 8.0,
-                    dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
-                    color,
-                ));
+    fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
+
+    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+        if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
+            if item_idx < menu.len() {
+                menu[item_idx] = Some(checked);
             }
         }
+    }
 
-        items
+    fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+        if menu_idx < self.menu_dropdowns.len() {
+            self.menu_dropdowns[menu_idx] = items.to_vec();
+            self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
+            self.layout_dirty = true;
+        }
     }
 
-    fn widget_font(&self) -> Option<String> {
-        Some(crate::layout::menubar_font())
+    fn is_menu_bar(&self) -> bool {
+        self.visible
+    }
+
+    fn is_menu_open(&self) -> bool {
+        self.context_dropdown_open
+    }
+
+    fn menu_items(&self) -> Vec<String> {
+        self.menu_items.clone()
+    }
+
+    fn menu_item_checked(&self) -> Vec<Option<bool>> {
+        self.menu_dropdown_checked.iter().flatten().copied().collect()
+    }
+
+    fn is_vertical(&self) -> bool {
+        self.vertical
+    }
+
+    fn menu_names(&self) -> Vec<String> {
+        self.menus.buttons.clone()
+    }
+
+    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()
+    }
+
+    fn take_context_change(&mut self) -> Option<usize> {
+        self.take_context_change()
+    }
+
+    fn set_context_selected(&mut self, selected: usize) {
+        self.set_context_selected(selected);
+    }
+
+    fn set_center_items(&mut self, center: bool) {
+        self.center_items = center;
+    }
+
+    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+        if !self.visible {
+            return None;
+        }
+        for i in 0..self.menus.buttons.len() {
+            let r = self.menus.item_rect(i);
+            if px >= r.0 && px < r.0 + r.2 && py >= r.1 && py < r.1 + r.3 {
+                let title = self.menus.buttons[i].clone();
+                let mut formatted_items = Vec::new();
+                if let Some(items) = self.menu_dropdowns.get(i) {
+                    for (item_idx, item) in items.iter().enumerate() {
+                        let checked = self.menu_dropdown_checked.get(i)
+                            .and_then(|menu| menu.get(item_idx))
+                            .and_then(|&v| v);
+                        let prefix = match checked {
+                            Some(true) => "✓ ",
+                            Some(false) => "  ",
+                            None => "",
+                        };
+                        formatted_items.push(format!("{}{}", prefix, item));
+                    }
+                }
+                return Some((i, title, formatted_items, r.0, r.1, r.2, r.3));
+            }
+        }
+        None
     }
 }
 
-unsafe impl Send for Menu {}
-unsafe impl Sync for Menu {}
+impl PageSelector for MenuBar {
+    fn selected_page(&self) -> usize {
+        self.menus.selected.unwrap_or(0)
+    }
 
-impl Drop for Menu {
-    fn drop(&mut self) {
-        focus::clear_if_matches(self);
+    fn set_selected_page(&mut self, page: usize) {
+        self.menus.set_selected(Some(page));
     }
+
+    fn is_page_hidden(&self) -> bool {
+        self.page_hidden
+    }
+
+    fn set_page_hidden(&mut self, hidden: bool) {
+        self.page_hidden = hidden;
+    }
+
+    fn set_pages(&mut self, pages: Vec<String>) {
+        self.menu_items = pages.clone();
+        self.vertical_items = pages.clone();
+        self.menus.buttons = pages;
+        self.menus.generate_rotated_labels();
+    }
+
+    fn set_pages_with_items(&mut self, pages: Vec<String>, items: Vec<Vec<String>>) {
+        self.menu_items = pages.clone();
+        self.vertical_items = pages.clone();
+        self.menu_dropdowns = items;
+        self.menu_dropdown_checked = vec![vec![None; 0]; self.menu_dropdowns.len()];
+        self.menus.buttons = pages;
+        self.menus.generate_rotated_labels();
+    }
+
+    fn sidebar_w(&self) -> f32 {
+        let padding_x = crate::layout::paginator_tab_padding_x();
+        let margin_x = crate::layout::paginator_tab_margin_x();
+        if self.vertical {
+            (12.0 + 2.0 * padding_x).max(24.0) + 2.0 * margin_x
+        } else {
+            let items = &self.menu_items;
+            let max_req_w = items.iter()
+                .map(|p| p.len() as f32 * 7.5 + 2.0 * padding_x)
+                .max_by(|a, b| a.partial_cmp(b).unwrap())
+                .unwrap_or(0.0);
+            max_req_w.max(24.0) + 2.0 * margin_x
+        }
+    }
+
+    fn set_sidebar_mode(&mut self, _enabled: bool) {}
+
+    fn set_sidebar_label(&mut self, label: Option<String>) {
+        self.label = label.clone();
+        self.base.label = label.clone();
+        if self.vertical {
+            self.title = label.unwrap_or_default();
+            self.label = None;
+        }
+    }
+
+    fn add_widget_to_page(&mut self, _page_idx: usize, _widget: *mut (dyn Element + 'static), _ctx: &mut UiContext) {}
+    fn clear_page_widgets(&mut self, _page_idx: usize, _ctx: &mut UiContext) {}
 }
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 9bbff51..fc48ecd 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -10,6 +10,9 @@ pub mod scroll_box;
 pub mod scrolling_list;
 pub mod plate;
 pub mod paginator;
+pub mod switcher;
+pub mod layer;
+pub mod page;
 
 pub use container::Container;
 pub use header::Header;
@@ -23,3 +26,6 @@ pub use scroll_box::ScrollBox;
 pub use scrolling_list::ScrollingList;
 pub use plate::Plate;
 pub use paginator::Paginator;
+pub use switcher::Switcher;
+pub use layer::Layer;
+pub use page::Page;
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
new file mode 100644
index 0000000..2b70760
--- /dev/null
+++ b/src/widget/container/page.rs
@@ -0,0 +1,153 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+use super::layer::Layer;
+
+pub struct Page {
+    pub base: Layer,
+    pub visible: bool,
+    pub owned_children: Vec<Box<dyn Element>>,
+}
+
+impl std::fmt::Debug for Page {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Page")
+            .field("base", &self.base)
+            .field("visible", &self.visible)
+            .finish()
+    }
+}
+
+impl Page {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+        Self {
+            base: Layer::new(x, y, w, h),
+            visible: true,
+            owned_children: Vec::new(),
+        }
+    }
+
+    pub fn add_child_owned(&mut self, child: Box<dyn Element>, ctx: &mut UiContext) {
+        let ptr = &*child as *const (dyn Element + 'static) as *mut (dyn Element + 'static);
+        self.owned_children.push(child);
+        self.add_child(ptr, ctx);
+    }
+}
+
+impl Element for Page {
+    fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
+    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+    fn as_any(&self) -> &dyn std::any::Any { self }
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    }
+
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        self.base.rect()
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.base.set_rect(x, y, w, h);
+        if !self.visible {
+            return;
+        }
+        let padding_x = 8.0;
+        let padding_y = 10.0;
+        let left_x = x + padding_x;
+        let available_w = (w - 2.0 * padding_x).max(1.0);
+        let mut current_y = y + padding_y;
+        let spacing = 8.0;
+
+        for &child_ptr in &self.base.children {
+            let child = unsafe { &mut *child_ptr };
+            let (_, _, _, ch) = child.rect();
+            let use_h = if ch > 0.0 { ch } else { 42.0 };
+            child.set_rect(left_x, current_y, available_w, use_h);
+            current_y += use_h + spacing;
+        }
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn visible(&self) -> bool {
+        self.visible
+    }
+
+    fn set_visible(&mut self, visible: bool) {
+        self.visible = visible;
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+        let id = self.base.base.id();
+        let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
+        self.base.children.push(child);
+        unsafe {
+            let c_id = (*child).base().map(|b| b.id()).unwrap();
+            ctx.register_widget(c_id, child);
+            ctx.link_ids(id, c_id);
+            (*child).set_parent(Some(self_ptr), ctx);
+        }
+    }
+
+    fn clear_children(&mut self, ctx: &mut UiContext) {
+        self.base.clear_children(ctx);
+        self.owned_children.clear();
+    }
+
+    fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        self.base.children(ctx)
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
+        self.base.set_parent(parent, ctx);
+    }
+
+    fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.base.parent(ctx)
+    }
+
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        self.base.all_quads(ctx)
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible {
+            return Vec::new();
+        }
+        self.base.text_labels()
+    }
+
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        self.base.text_labels_with_bounds(ctx)
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        self.base.text_labels_with_font_and_bounds(ctx)
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        self.base.get_text_items()
+    }
+
+    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        self.base.hit_test(px, py, ctx)
+    }
+}
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index f3e1d1a..280ca93 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -36,6 +36,8 @@ pub struct Paginator {
     pub context_options: Vec<String>,
     pub context_selected: usize,
     pub context_just_changed: bool,
+    pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
+    pub on_page_changed_cb: Option<Box<dyn Fn(usize) + Send + Sync>>,
 }
 
 impl Paginator {
@@ -91,6 +93,8 @@ impl Paginator {
             context_options: Vec::new(),
             context_selected: 0,
             context_just_changed: false,
+            tab_quads_cache: std::collections::HashMap::new(),
+            on_page_changed_cb: None,
         };
 
         for page in &pages {
@@ -241,6 +245,11 @@ impl Paginator {
         self
     }
 
+    pub fn on_page_changed<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+        self.on_page_changed_cb = Some(Box::new(cb));
+        self
+    }
+
     pub fn selected_page(&self) -> usize {
         self.selected_page
     }
@@ -255,6 +264,10 @@ impl Paginator {
                 
                 // Update MenuBar focus/selection
                 self.sidebar_menu.menus.set_selected(Some(page));
+
+                if let Some(ref cb) = self.on_page_changed_cb {
+                    cb(page);
+                }
             }
         }
     }
@@ -398,6 +411,12 @@ impl Paginator {
                 continue;
             }
 
+            let cache_key = format!("{}:{}:{}:{:?}:{}:{}", trimmed, w_px, h_px, color, font_fam, scale);
+            if let Some(cached_quads) = self.tab_quads_cache.get(&cache_key) {
+                self.tab_text_quads.push(cached_quads.clone());
+                continue;
+            }
+
             let svg_data = format!(
                 r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
   <text x="{}" y="{}" font-family="{}" font-size="{}" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
@@ -446,6 +465,7 @@ impl Paginator {
             if !page_quads.is_empty() {
                 eprintln!("DEBUG_PAGINATOR_EXAMPLES: {:?}", &page_quads[..5.min(page_quads.len())]);
             }
+            self.tab_quads_cache.insert(cache_key, page_quads.clone());
             self.tab_text_quads.push(page_quads);
         }
     }
@@ -490,6 +510,13 @@ impl Element for Paginator {
         }
     }
 
+    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 color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
@@ -518,10 +545,6 @@ impl Element for Paginator {
         px >= x && px <= x + w && py >= y && py <= y + h
     }
 
-    fn is_menu_open(&self) -> bool {
-        self.sidebar_menu.is_menu_open()
-    }
-
     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{
         if let Some(i) = self.hovered_tab {
             if self.tabs_at_top {
@@ -961,15 +984,56 @@ impl Element for Paginator {
     fn add_child(&mut self, _child: *mut (dyn Element + 'static), ctx: &mut UiContext) {}
     fn clear_children(&mut self, ctx: &mut UiContext) {}
 
-    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
-        self.sidebar_menu.set_modifiers(ctrl, shift, alt);
+    fn as_page_selector(&self) -> Option<&dyn PageSelector> { Some(self) }
+    fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { Some(self) }
+    fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
+
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        self.sidebar_menu.prepare_text(fs);
+        for plate in &mut self.plates {
+            plate.prepare_text(fs);
+        }
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let mut result = Vec::new();
+        if self.tabs_rotated {
+            let font = self.widget_font();
+            for l in self.text_labels() {
+                result.push((l, font.clone(), None));
+            }
+        } else {
+            result.extend(self.sidebar_menu.text_labels_with_font_and_bounds(ctx));
+        }
+
         if self.selected_page < self.plates.len() {
-            self.plates[self.selected_page].set_modifiers(ctrl, shift, alt);
+            result.extend(self.plates[self.selected_page].text_labels_with_font_and_bounds(ctx));
         }
+        result
     }
-    fn menu_names(&self) -> Vec<String> {
-        self.pages.clone()
+
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::menubar_font())
+    }
+
+    fn focus(&mut self) {
+        self.sidebar_menu.focus();
+    }
+
+    fn unfocus(&mut self) {
+        self.sidebar_menu.unfocus();
     }
+
+    fn focused(&self, ctx: &UiContext) -> bool {
+        self.sidebar_menu.focused(ctx)
+    }
+}
+
+unsafe impl Send for Paginator {}
+unsafe impl Sync for Paginator {}
+
+impl PageSelector for Paginator {
     fn selected_page(&self) -> usize {
         self.selected_page()
     }
@@ -1011,21 +1075,9 @@ impl Element for Paginator {
     fn clear_page_widgets(&mut self, page_idx: usize, ctx: &mut UiContext) {
         self.clear_page_widgets(page_idx, ctx);
     }
-    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()
-    }
-
-    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
-        if self.sidebar_mode {
-            self.sidebar_menu.get_menu_items_at(px, py)
-        } else {
-            None
-        }
-    }
+}
 
+impl MenuController for Paginator {
     fn menu_click(&mut self) -> Option<(usize, usize)> {
         if self.sidebar_mode {
             self.sidebar_menu.menu_click()
@@ -1034,32 +1086,56 @@ impl Element for Paginator {
         }
     }
 
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
-        self.sidebar_menu.prepare_text(fs);
-        for plate in &mut self.plates {
-            plate.prepare_text(fs);
-        }
+    fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+        self.sidebar_menu.trigger_menu_click(menu_idx, item_idx);
     }
 
-    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        let mut result = Vec::new();
-        if self.tabs_rotated {
-            let font = self.widget_font();
-            for l in self.text_labels() {
-                result.push((l, font.clone(), None));
-            }
-        } else {
-            result.extend(self.sidebar_menu.text_labels_with_font_and_bounds(ctx));
-        }
+    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+        self.sidebar_menu.set_item_checked(menu_idx, item_idx, checked);
+    }
 
-        if self.selected_page < self.plates.len() {
-            result.extend(self.plates[self.selected_page].text_labels_with_font_and_bounds(ctx));
+    fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+        self.sidebar_menu.set_menu_items(menu_idx, items);
+    }
+
+    fn is_menu_bar(&self) -> bool {
+        true
+    }
+
+    fn is_menu_open(&self) -> bool {
+        self.sidebar_menu.is_menu_open()
+    }
+
+    fn menu_items(&self) -> Vec<String> {
+        self.sidebar_menu.menu_items()
+    }
+
+    fn menu_item_checked(&self) -> Vec<Option<bool>> {
+        self.sidebar_menu.menu_item_checked()
+    }
+
+    fn is_vertical(&self) -> bool {
+        self.sidebar_menu.is_vertical()
+    }
+
+    fn menu_names(&self) -> Vec<String> {
+        self.pages.clone()
+    }
+
+    fn menu_items_list(&self) -> Vec<Vec<String>> {
+        if let Some(c) = self.sidebar_menu.as_menu_controller() {
+            c.menu_items_list()
+        } else {
+            vec![]
         }
-        result
     }
 
-    fn widget_font(&self) -> Option<String> {
-        Some(crate::layout::menubar_font())
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+        if let Some(c) = self.sidebar_menu.as_menu_controller() {
+            c.menu_checked_list()
+        } else {
+            vec![]
+        }
     }
 
     fn take_context_change(&mut self) -> Option<usize> {
@@ -1075,26 +1151,21 @@ impl Element for Paginator {
         self.set_context_selected(selected);
     }
 
-    fn focus(&mut self) {
-        self.sidebar_menu.focus();
-    }
-
-    fn unfocus(&mut self) {
-        self.sidebar_menu.unfocus();
-    }
-
-    fn focused(&self, ctx: &UiContext) -> bool {
-        self.sidebar_menu.focused(ctx)
+    fn set_center_items(&mut self, center: bool) {
+        if let Some(c) = self.sidebar_menu.as_menu_controller_mut() {
+            c.set_center_items(center);
+        }
     }
 
-    fn is_menu_bar(&self) -> bool {
-        true
+    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+        if let Some(c) = self.sidebar_menu.as_menu_controller() {
+            c.get_menu_items_at(px, py)
+        } else {
+            None
+        }
     }
 }
 
-unsafe impl Send for Paginator {}
-unsafe impl Sync for Paginator {}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index d493c2c..064a625 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -150,6 +150,36 @@ impl ParametersBg {
                     font_size: 12.0,
                     color: [0xee, 0xee, 0xf0],
                 });
+            } else if ptype.starts_with("choice") {
+                labels.push(TextLabel {
+                    text: name.clone(),
+                    x: self.base.x + 8.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xaa, 0xaa, 0xbb],
+                });
+                labels.push(TextLabel {
+                    text: value.clone(),
+                    x: self.base.x + 106.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xee, 0xee, 0xf0],
+                });
+            } else if ptype == "button" {
+                labels.push(TextLabel {
+                    text: name.clone(),
+                    x: self.base.x + 8.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xaa, 0xaa, 0xbb],
+                });
+                labels.push(TextLabel {
+                    text: "Trigger".to_string(),
+                    x: self.base.x + 106.0,
+                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+                    font_size: 12.0,
+                    color: [0xee, 0xee, 0xf0],
+                });
             } else {
                 labels.push(TextLabel {
                     text: format!("{}: {}", name, value),
@@ -270,81 +300,8 @@ impl Element for ParametersBg {
         px >= self.base.x && px <= self.base.x + self.base.w && py >= self.base.y && py <= self.base.y + self.base.h
     }
 
-    fn set_display_params(&mut self, params: &[(String, String, String)]) {
-        let mut layout_changed = self.display_params.len() != params.len();
-        if !layout_changed {
-            for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
-                if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
-                    layout_changed = true;
-                    break;
-                }
-            }
-        }
-
-        if layout_changed {
-            self.display_params = params.to_vec();
-            self.focused_param = None;
-            self.sliders = self.display_params.iter().map(|p| {
-                if p.2.starts_with("slider") {
-                    let val = p.1.parse::<f32>().unwrap_or(0.0);
-                    let (min, max) = parse_slider_range(&p.2);
-                    let t = if max - min != 0.0 {
-                        ((val - min) / (max - min)).clamp(0.0, 1.0)
-                    } else {
-                        0.0
-                    };
-                    Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
-                } else {
-                    None
-                }
-            }).collect();
-            self.float3s = self.display_params.iter().map(|p| {
-                if p.2.starts_with("float3") {
-                    let (min, max) = parse_slider_range(&p.2);
-                    let vals = parse_float3_value(&p.1, min, max);
-                    Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
-                } else {
-                    None
-                }
-            }).collect();
-            self.spinboxes = self.display_params.iter().map(|p| {
-                if p.2.starts_with("spinbox") {
-                    let (min, max, step) = parse_spinbox_range(&p.2);
-                    let val = p.1.parse::<i32>().unwrap_or(min);
-                    Some(Spinbox::new(val, min, max, step))
-                } else {
-                    None
-                }
-            }).collect();
-        } else {
-            for (i, p_new) in params.iter().enumerate() {
-                if Some(i) != self.focused_param && Some(i) != self.dragging_param {
-                    self.display_params[i].1 = p_new.1.clone();
-                    if let Some(ref mut s) = self.sliders[i] {
-                        let val = p_new.1.parse::<f32>().unwrap_or(0.0);
-                        let (min, max) = parse_slider_range(&p_new.2);
-                        let t = if max - min != 0.0 {
-                            ((val - min) / (max - min)).clamp(0.0, 1.0)
-                        } else {
-                            0.0
-                        };
-                        s.set_value(t);
-                    } else if let Some(ref mut f) = self.float3s[i] {
-                        let (min, max) = parse_slider_range(&p_new.2);
-                        let vals = parse_float3_value(&p_new.1, min, max);
-                        f.set_values(vals);
-                    } else if let Some(ref mut sb) = self.spinboxes[i] {
-                        if !sb.editing {
-                            let (min, _max, _step) = parse_spinbox_range(&p_new.2);
-                            let val = p_new.1.parse::<i32>().unwrap_or(min);
-                            sb.value = val;
-                        }
-                    }
-                }
-            }
-        }
-        self.update_slider_rects();
-    }
+    fn as_param_controller(&self) -> Option<&dyn ParamController> { Some(self) }
+    fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { Some(self) }
 
     fn unfocus(&mut self) {
         if let Some(idx) = self.focused_param {
@@ -382,10 +339,6 @@ impl Element for ParametersBg {
         }
     }
 
-    fn node_params(&self) -> Vec<(String, String, String)> {
-        self.display_params.clone()
-    }
-
     fn draggable(&self) -> bool {
         self.dragging_param.is_some() 
             || self.display_params.iter().any(|p| p.2.starts_with("slider") || p.2.starts_with("float3"))
@@ -569,6 +522,29 @@ impl Element for ParametersBg {
                         clicked_any_focusable = true;
                         break;
                     }
+                } else if p.2.starts_with("choice") {
+                    let box_x = self.base.x + 100.0;
+                    let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                    let r = rects[i];
+                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
+                        if let Some(options_str) = p.2.strip_prefix("choice:") {
+                            let options: Vec<&str> = options_str.split(',').collect();
+                            if !options.is_empty() {
+                                let cur_idx = options.iter().position(|&o| o == p.1).unwrap_or(0);
+                                let next_idx = (cur_idx + 1) % options.len();
+                                p.1 = options[next_idx].to_string();
+                                return true;
+                            }
+                        }
+                    }
+                } else if p.2 == "button" {
+                    let box_x = self.base.x + 100.0;
+                    let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                    let r = rects[i];
+                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
+                        p.1 = "clicked".to_string();
+                        return true;
+                    }
                 } else if p.2.starts_with("spinbox") {
                     if let Some(sb) = &mut self.spinboxes[i] {
                         if sb.mouse_input(button, state, px, py, ctx) {
@@ -906,6 +882,28 @@ impl Element for ParametersBg {
                 quads.push((bx, by + bh - border_t, bw, border_t, border_color));
                 quads.push((bx, by, border_t, bh, border_color));
                 quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+            } else if p.2.starts_with("choice") {
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
+                let border_color = [0.20, 0.20, 0.25, 1.0];
+                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
+                let border_t = 1.0;
+                quads.push((bx, by, bw, border_t, border_color));
+                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+                quads.push((bx, by, border_t, bh, border_color));
+                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+            } else if p.2 == "button" {
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                quads.push((box_x, r.1, box_w, r.3, [0.15, 0.22, 0.38, 1.0]));
+                let border_color = [0.25, 0.35, 0.58, 1.0];
+                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
+                let border_t = 1.0;
+                quads.push((bx, by, bw, border_t, border_color));
+                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+                quads.push((bx, by, border_t, bh, border_color));
+                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
             } else if p.2.starts_with("spinbox") {
                 if let Some(sb) = &self.spinboxes[i] {
                     quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
@@ -1026,3 +1024,85 @@ fn collect_child_quads(widget: &dyn Element) -> Vec<(f32, f32, f32, f32, [f32; 4
     quads
 }
 
+impl ParamController for ParametersBg {
+    fn node_params(&self) -> Vec<(String, String, String)> {
+        self.display_params.clone()
+    }
+
+    fn set_display_params(&mut self, params: &[(String, String, String)]) {
+        let mut layout_changed = self.display_params.len() != params.len();
+        if !layout_changed {
+            for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
+                if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
+                    layout_changed = true;
+                    break;
+                }
+            }
+        }
+
+        if layout_changed {
+            self.display_params = params.to_vec();
+            self.focused_param = None;
+            self.sliders = self.display_params.iter().map(|p| {
+                if p.2.starts_with("slider") {
+                    let val = p.1.parse::<f32>().unwrap_or(0.0);
+                    let (min, max) = parse_slider_range(&p.2);
+                    let t = if max - min != 0.0 {
+                        ((val - min) / (max - min)).clamp(0.0, 1.0)
+                    } else {
+                        0.0
+                    };
+                    Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
+                } else {
+                    None
+                }
+            }).collect();
+            self.float3s = self.display_params.iter().map(|p| {
+                if p.2.starts_with("float3") {
+                    let (min, max) = parse_slider_range(&p.2);
+                    let vals = parse_float3_value(&p.1, min, max);
+                    Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
+                } else {
+                    None
+                }
+            }).collect();
+            self.spinboxes = self.display_params.iter().map(|p| {
+                if p.2.starts_with("spinbox") {
+                    let (min, max, step) = parse_spinbox_range(&p.2);
+                    let val = p.1.parse::<i32>().unwrap_or(min);
+                    Some(Spinbox::new(val, min, max, step))
+                } else {
+                    None
+                }
+            }).collect();
+        } else {
+            for (i, p_new) in params.iter().enumerate() {
+                if Some(i) != self.focused_param && Some(i) != self.dragging_param {
+                    self.display_params[i].1 = p_new.1.clone();
+                    if let Some(ref mut s) = self.sliders[i] {
+                        let val = p_new.1.parse::<f32>().unwrap_or(0.0);
+                        let (min, max) = parse_slider_range(&p_new.2);
+                        let t = if max - min != 0.0 {
+                            ((val - min) / (max - min)).clamp(0.0, 1.0)
+                        } else {
+                            0.0
+                        };
+                        s.set_value(t);
+                    } else if let Some(ref mut f) = self.float3s[i] {
+                        let (min, max) = parse_slider_range(&p_new.2);
+                        let vals = parse_float3_value(&p_new.1, min, max);
+                        f.set_values(vals);
+                    } else if let Some(ref mut sb) = self.spinboxes[i] {
+                        if !sb.editing {
+                            let (min, _max, _step) = parse_spinbox_range(&p_new.2);
+                            let val = p_new.1.parse::<i32>().unwrap_or(min);
+                            sb.value = val;
+                        }
+                    }
+                }
+            }
+        }
+        self.update_slider_rects();
+    }
+}
+
diff --git a/src/widget/container/plate.rs b/src/widget/container/plate.rs
index cfab8ef..d883eaa 100644
--- a/src/widget/container/plate.rs
+++ b/src/widget/container/plate.rs
@@ -4,7 +4,7 @@ use crate::widget::display::TextLabel;
 
 #[derive(Debug, Clone)]
 pub struct Plate {
-    pub base: Widget,
+    pub base: Layer,
     pub dragging: bool,
     pub drag_ox: f32,
     pub drag_oy: f32,
@@ -15,11 +15,12 @@ pub struct Plate {
     pub curved_circle: Option<(f32, f32, f32)>,
     pub network_opacity: f32,
     pub blur: bool,
-    pub children: Vec<*mut (dyn Element + 'static)>,
-    pub parent: Option<*mut (dyn Element + 'static)>,
     pub visible: bool,
     pub column_layout: bool,
     pub draggable: bool,
+    pub solid_border: Option<([f32; 4], f32)>,
+    pub selected: bool,
+    pub padding: Option<f32>,
 }
 
 impl Plate {
@@ -33,7 +34,7 @@ impl Plate {
 
     pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
         Self {
-            base: Widget::new_rect(x, y, w, h),
+            base: Layer::new(x, y, w, h),
             dragging: false,
             drag_ox: 0.0,
             drag_oy: 0.0,
@@ -44,11 +45,12 @@ impl Plate {
             curved_circle: None,
             network_opacity: 1.0,
             blur: true,
-            children: Vec::new(),
-            parent: None,
             visible: true,
             column_layout: false,
             draggable: true,
+            solid_border: None,
+            selected: false,
+            padding: None,
         }
     }
 
@@ -58,7 +60,7 @@ impl Plate {
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
+        self.base.base.label = Some(label.to_string());
         self
     }
 
@@ -72,19 +74,48 @@ impl Plate {
         self
     }
 
+    pub fn with_solid_border(mut self, color: [f32; 4], thickness: f32) -> Self {
+        self.solid_border = Some((color, thickness));
+        self
+    }
+
+    pub fn with_padding(mut self, padding: f32) -> Self {
+        self.padding = Some(padding);
+        self
+    }
+
     pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
         self.bounds = Some((bx, by, bw, bh));
     }
 }
 
 impl Element for Plate {
-    crate::impl_widget_base!(Plate);
+    fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
+    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+    fn as_any(&self) -> &dyn std::any::Any { self }
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    }
+
     fn is_plate(&self) -> bool { true }
     fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
-    fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+    fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        if self.selected {
+            Some((colors::active_theme().primary_accent, 1.5))
+        } else {
+            self.solid_border
+        }
+    }
+
+    fn set_selected(&mut self, selected: bool) {
+        self.selected = selected;
+    }
 
     fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             unsafe {
                 (*child_ptr).set_modifiers(ctrl, shift, alt);
             }
@@ -97,11 +128,7 @@ impl Element for Plate {
 
     fn set_visible(&mut self, visible: bool) {
         self.visible = visible;
-        for &child_ptr in &self.children {
-            unsafe {
-                (*child_ptr).set_visible(visible);
-            }
-        }
+        self.base.visible = visible;
     }
 
     fn color(&self) -> [f32; 4] {
@@ -175,30 +202,40 @@ impl Element for Plate {
     }
 
     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;
-        }
+        let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.base.parent {
+            let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+            let cx = x.clamp(px, px + pw.max(0.0));
+            let cy = y.clamp(py, py + ph.max(0.0));
+            let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
+            let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
+            (cx, cy, cw, ch)
+        } else {
+            (x, y, w, h)
+        };
 
-        if !self.visible {
+        self.base.base.x = clamped_x;
+        self.base.base.y = clamped_y;
+        self.base.base.w = clamped_w;
+        self.base.base.h = clamped_h;
+
+        if !self.base.visible {
             return;
         }
 
-        let padding_x = crate::layout::plate_padding();
-        let padding_y = crate::layout::plate_padding();
-        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);
+        let pad = self.padding.unwrap_or_else(|| crate::layout::plate_padding());
+        let padding_x = pad;
+        let padding_y = pad;
+        let left_x = clamped_x + padding_x;
+        let available_w = (clamped_w - 2.0 * padding_x).max(1.0);
+        let start_y = clamped_y + padding_y;
+        let available_h = (clamped_h - 2.0 * padding_y).max(1.0);
 
         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 {
+        for &w_ptr in &self.base.children {
             let w = unsafe { &*w_ptr };
             if !w.layout_ignore() {
                 active_widgets.push(w_ptr);
@@ -215,8 +252,12 @@ impl Element for Plate {
                 let top = crate::widget::label_offset(w);
                 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;
+                let max_y = clamped_y + clamped_h - padding_y;
+                let active_y = current_y.min(max_y);
+                let active_h = use_h.min(max_y - active_y);
+
+                w.set_rect(left_x, active_y, use_w, active_h);
+                current_y += active_h + spacing;
             }
         } else {
             let mut total_diagonal = 0.0;
@@ -239,7 +280,11 @@ impl Element for Plate {
                 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);
+                    let cx = (center_x - use_w / 2.0).clamp(left_x, (left_x + available_w - use_w).max(left_x));
+                    let cy = (center_y - use_h / 2.0).clamp(start_y, (start_y + available_h - use_h).max(start_y));
+                    let cw = use_w.min(clamped_x + clamped_w - padding_x - cx);
+                    let ch = use_h.min(clamped_y + clamped_h - padding_y - cy);
+                    w.set_rect(cx, cy, cw, ch);
                 } else {
                     let mut ring = 1;
                     let mut ring_start = 1;
@@ -254,12 +299,15 @@ impl Element for Plate {
                             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,
-                            );
+                            let raw_x = center_x + x_offset - use_w / 2.0;
+                            let raw_y = center_y + y_offset - use_h / 2.0;
+
+                            let cx = raw_x.clamp(left_x, (left_x + available_w - use_w).max(left_x));
+                            let cy = raw_y.clamp(start_y, (start_y + available_h - use_h).max(start_y));
+                            let cw = use_w.min(clamped_x + clamped_w - padding_x - cx);
+                            let ch = use_h.min(clamped_y + clamped_h - padding_y - cy);
+
+                            w.set_rect(cx, cy, cw, ch);
                             placed = true;
                         } else {
                             ring_start += ring_capacity;
@@ -271,13 +319,13 @@ impl Element for Plate {
         }
     }
 
-    fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
-        self.parent
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.base.parent
     }
 
     fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
-        self.parent = parent;
-        let id = self.base.id();
+        self.base.parent = parent;
+        let id = self.base.base.id();
         if let Some(p_ptr) = parent {
             if let Some(p_base) = unsafe { (*p_ptr).base() } {
                 let p_id = p_base.id();
@@ -291,24 +339,27 @@ impl Element for Plate {
     }
 
     fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
-        self.children.clone()
+        self.base.children.clone()
     }
 
     fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
-        self.children.push(child);
-        let id = self.base.id();
+        self.base.children.push(child);
+        let id = self.base.base.id();
+        let self_ptr = self.as_ptr();
         if let Some(c_base) = unsafe { (*child).base() } {
             let c_id = c_base.id();
-            let self_ptr = self.as_ptr();
             ctx.register_widget(id, self_ptr);
             ctx.register_widget(c_id, child);
             ctx.link_ids(id, c_id);
         }
+        unsafe {
+            (*child).set_parent(Some(self_ptr), ctx);
+        }
     }
 
     fn clear_children(&mut self, ctx: &mut UiContext) {
-        self.children.clear();
-        let id = self.base.id();
+        self.base.children.clear();
+        let id = self.base.base.id();
         ctx.clear_children_ids(id);
     }
 
@@ -320,7 +371,7 @@ impl Element for Plate {
         let (px, py, pw, ph) = self.rect();
         quads.push((px, py, pw, ph, self.color()));
 
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             let widget = unsafe { &*child_ptr };
             let c = widget.color();
             if c[3] > 0.0 {
@@ -337,16 +388,16 @@ impl Element for Plate {
             return Vec::new();
         }
         let mut labels = Vec::new();
-        if let Some(ref label) = self.base.label {
+        if let Some(ref label) = self.base.base.label {
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.base.x,
-                y: self.base.y - (12.0 + crate::layout::label_margin()),
+                x: self.base.base.x,
+                y: self.base.base.y - (12.0 + crate::layout::label_margin()),
                 font_size: 12.0,
                 color: [0x83, 0x83, 0x8a],
             });
         }
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             let widget = unsafe { &*child_ptr };
             labels.extend(widget.text_labels());
         }
@@ -358,19 +409,19 @@ impl Element for Plate {
             return Vec::new();
         }
         let mut result = Vec::new();
-        if let Some(ref label) = self.base.label {
+        if let Some(ref label) = self.base.base.label {
             result.push((
                 TextLabel {
                     text: label.clone(),
-                    x: self.base.x,
-                    y: self.base.y - (12.0 + crate::layout::label_margin()),
+                    x: self.base.base.x,
+                    y: self.base.base.y - (12.0 + crate::layout::label_margin()),
                     font_size: 12.0,
                     color: [0x83, 0x83, 0x8a],
                 },
                 None,
             ));
         }
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             let widget = unsafe { &*child_ptr };
             result.extend(widget.text_labels_with_bounds(ctx));
         }
@@ -382,12 +433,12 @@ impl Element for Plate {
             return Vec::new();
         }
         let mut result = Vec::new();
-        if let Some(ref label) = self.base.label {
+        if let Some(ref label) = self.base.base.label {
             result.push((
                 TextLabel {
                     text: label.clone(),
-                    x: self.base.x,
-                    y: self.base.y - (12.0 + crate::layout::label_margin()),
+                    x: self.base.base.x,
+                    y: self.base.base.y - (12.0 + crate::layout::label_margin()),
                     font_size: 12.0,
                     color: [0x83, 0x83, 0x8a],
                 },
@@ -395,31 +446,42 @@ impl Element for Plate {
                 None,
             ));
         }
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             let widget = unsafe { &*child_ptr };
             result.extend(widget.text_labels_with_font_and_bounds(ctx));
         }
         result
     }
 
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut items = Vec::new();
+        for &child_ptr in &self.base.children {
+            let widget = unsafe { &*child_ptr };
+            items.extend(widget.get_text_items());
+        }
+        items
+    }
+
     fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
         if !self.visible {
             return;
         }
-        for &child_ptr in &self.children {
+        for &child_ptr in &self.base.children {
             unsafe {
                 (*child_ptr).prepare_text(fs);
             }
         }
     }
 
-
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         if !self.visible {
             return false;
         }
         let mut changed = false;
-        for &widget_ptr in &self.children {
+        for &widget_ptr in &self.base.children {
             let widget = unsafe { &mut *widget_ptr };
             if widget.is_dragging() {
                 if widget.drag_update(px, py) {
@@ -436,7 +498,7 @@ impl Element for Plate {
         if !self.visible {
             return false;
         }
-        for &widget_ptr in self.children.iter().rev() {
+        for &widget_ptr in self.base.children.iter().rev() {
             let widget = unsafe { &mut *widget_ptr };
             if widget.popover_rect().is_some() {
                 if widget.mouse_input(button, state, px, py, ctx) {
@@ -444,7 +506,7 @@ impl Element for Plate {
                 }
             }
         }
-        for &widget_ptr in self.children.iter().rev() {
+        for &widget_ptr in self.base.children.iter().rev() {
             let widget = unsafe { &mut *widget_ptr };
             if widget.mouse_input(button, state, px, py, ctx) {
                 return true;
@@ -474,7 +536,7 @@ impl Element for Plate {
         if !self.visible {
             return false;
         }
-        for &widget_ptr in &self.children {
+        for &widget_ptr in &self.base.children {
             let widget = unsafe { &mut *widget_ptr };
             if widget.keyboard_input(event, ctx) {
                 return true;
@@ -487,7 +549,7 @@ impl Element for Plate {
         if !self.visible {
             return false;
         }
-        for &widget_ptr in self.children.iter().rev() {
+        for &widget_ptr in self.base.children.iter().rev() {
             let widget = unsafe { &mut *widget_ptr };
             if widget.mouse_wheel(delta, px, py, ctx) {
                 return true;
@@ -500,7 +562,7 @@ impl Element for Plate {
         if !self.visible {
             return None;
         }
-        for &widget_ptr in self.children.iter().rev() {
+        for &widget_ptr in self.base.children.iter().rev() {
             let widget = unsafe { &*widget_ptr };
             if let Some(r) = widget.popover_rect() {
                 return Some(r);
@@ -513,7 +575,7 @@ impl Element for Plate {
         if !self.visible {
             return;
         }
-        for &widget_ptr in self.children.iter().rev() {
+        for &widget_ptr in self.base.children.iter().rev() {
             let widget = unsafe { &*widget_ptr };
             widget.render_popover(pc);
         }
@@ -524,7 +586,7 @@ impl Element for Plate {
             return false;
         }
         let mut changed = false;
-        for &widget_ptr in &self.children {
+        for &widget_ptr in &self.base.children {
             let widget = unsafe { &mut *widget_ptr };
             if widget.tick(dt, ctx) {
                 changed = true;
@@ -537,17 +599,17 @@ impl Element for Plate {
         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))
+            (nx.clamp(bx, bx + bw - self.base.base.w), ny.clamp(by, by + bh - self.base.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;
+        if (nx - self.base.base.x).abs() > 0.01 || (ny - self.base.base.y).abs() > 0.01 {
+            let dx = nx - self.base.base.x;
+            let dy = ny - self.base.base.y;
+            self.base.base.x = nx;
+            self.base.base.y = ny;
             
-            for &child_ptr in &self.children {
+            for &child_ptr in &self.base.children {
                 unsafe {
                     let (cx, cy, cw, ch) = (*child_ptr).rect();
                     (*child_ptr).set_rect(cx + dx, cy + dy, cw, ch);
@@ -560,10 +622,10 @@ impl Element for Plate {
 
     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;
+        self.drag_ox = px - self.base.base.x;
+        self.drag_oy = py - self.base.base.y;
+        self.drag_start_x = self.base.base.x;
+        self.drag_start_y = self.base.base.y;
     }
 
     fn drag_end(&mut self) { self.dragging = false; }
diff --git a/src/widget/container/scrolling_list.rs b/src/widget/container/scrolling_list.rs
index 50e5552..2475122 100644
--- a/src/widget/container/scrolling_list.rs
+++ b/src/widget/container/scrolling_list.rs
@@ -104,6 +104,11 @@ impl Element for ScrollingList {
     fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) { self.scroll_box.add_child(child, ctx); }
     fn clear_children(&mut self, ctx: &mut UiContext) { self.scroll_box.clear_children(ctx); }
 
+    fn as_scroll_controller(&self) -> Option<&dyn ScrollController> { Some(self) }
+    fn as_scroll_controller_mut(&mut self) -> Option<&mut dyn ScrollController> { Some(self) }
+}
+
+impl ScrollController for ScrollingList {
     fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
         self.update_bounds(count, viewport_y, viewport_h);
     }
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 22e7957..c0f1d42 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -91,16 +91,8 @@ impl Element for Spreadsheet {
         self.visible
     }
 
-    fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
-        self.headers = headers;
-        self.rows = rows;
-        
-        // Clamp scroll_y to new bounds
-        let content_h = self.rows.len() as f32 * 24.0;
-        let visible_h = (self.h - 24.0).max(0.0);
-        let max_scroll_y = (content_h - visible_h).max(0.0);
-        self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
-    }
+    fn as_spreadsheet_controller(&self) -> Option<&dyn SpreadsheetController> { Some(self) }
+    fn as_spreadsheet_controller_mut(&mut self) -> Option<&mut dyn SpreadsheetController> { Some(self) }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         let was_hovered = self.hovered;
@@ -395,3 +387,16 @@ impl Element for Spreadsheet {
         labels
     }
 }
+
+impl SpreadsheetController for Spreadsheet {
+    fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
+        self.headers = headers;
+        self.rows = rows;
+        
+        // Clamp scroll_y to new bounds
+        let content_h = self.rows.len() as f32 * 24.0;
+        let visible_h = (self.h - 24.0).max(0.0);
+        let max_scroll_y = (content_h - visible_h).max(0.0);
+        self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
+    }
+}
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
new file mode 100644
index 0000000..9de8822
--- /dev/null
+++ b/src/widget/container/switcher.rs
@@ -0,0 +1,513 @@
+use crate::widget::*;
+use crate::widget::display::TextLabel;
+
+#[derive(Debug, Clone)]
+pub struct Switcher {
+    pub base: Widget,
+    pub parent: Option<*mut (dyn Element + 'static)>,
+    pub children: Vec<*mut (dyn Element + 'static)>,
+    pub active_index: Option<usize>,
+    pub visible: bool,
+}
+
+impl Switcher {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+        Self {
+            base: Widget::new_rect(x, y, w, h),
+            parent: None,
+            children: Vec::new(),
+            active_index: None,
+            visible: true,
+        }
+    }
+
+    pub fn set_active_index(&mut self, index: Option<usize>) {
+        self.active_index = index;
+        for (i, &child_ptr) in self.children.iter().enumerate() {
+            unsafe {
+                (*child_ptr).set_visible(self.active_index == Some(i));
+            }
+        }
+    }
+
+    pub fn active_index(&self) -> Option<usize> {
+        self.active_index
+    }
+}
+
+impl Element for Switcher {
+    crate::impl_widget_base!(Switcher);
+
+    fn visible(&self) -> bool {
+        self.visible
+    }
+
+    fn set_visible(&mut self, visible: bool) {
+        self.visible = visible;
+    }
+
+    fn color(&self) -> [f32; 4] {
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).color() };
+            }
+        }
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.parent
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+        self.parent = parent;
+    }
+
+    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        self.children.clone()
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+        self.children.push(child);
+        let id = self.base.id();
+        let self_ptr = self.as_ptr();
+        if let Some(c_base) = unsafe { (*child).base() } {
+            let c_id = c_base.id();
+            ctx.register_widget(id, self_ptr);
+            ctx.register_widget(c_id, child);
+            ctx.link_ids(id, c_id);
+        }
+        unsafe {
+            (*child).set_parent(Some(self_ptr), ctx);
+        }
+        // Sync visibility of newly added child
+        let idx = self.children.len() - 1;
+        unsafe {
+            (*child).set_visible(self.active_index == Some(idx));
+        }
+    }
+
+    fn clear_children(&mut self, ctx: &mut UiContext) {
+        self.children.clear();
+        let id = self.base.id();
+        ctx.clear_children_ids(id);
+    }
+
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.base.x, self.base.y, self.base.w, self.base.h)
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
+            let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+            let cx = x.clamp(px, px + pw.max(0.0));
+            let cy = y.clamp(py, py + ph.max(0.0));
+            let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
+            let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
+            (cx, cy, cw, ch)
+        } else {
+            (x, y, w, h)
+        };
+
+        self.base.x = clamped_x;
+        self.base.y = clamped_y;
+        self.base.w = clamped_w;
+        self.base.h = clamped_h;
+
+        if !self.visible {
+            return;
+        }
+
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                unsafe {
+                    (*self.children[idx]).set_rect(clamped_x, clamped_y, clamped_w, clamped_h);
+                }
+            }
+        }
+    }
+
+    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).hit_test(px, py, ctx) };
+            }
+        }
+        false
+    }
+
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        let mut quads = Vec::new();
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                let widget = unsafe { &*self.children[idx] };
+                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(ctx));
+            }
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).text_labels() };
+            }
+        }
+        Vec::new()
+    }
+
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).text_labels_with_bounds(ctx) };
+            }
+        }
+        Vec::new()
+    }
+
+    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).text_labels_with_font_and_bounds(ctx) };
+            }
+        }
+        Vec::new()
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).get_text_items() };
+            }
+        }
+        Vec::new()
+    }
+
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        if !self.visible {
+            return;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                unsafe {
+                    (*self.children[idx]).prepare_text(fs);
+                }
+            }
+        }
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                let widget = unsafe { &mut *self.children[idx] };
+                if widget.is_dragging() {
+                    return widget.drag_update(px, py);
+                } else {
+                    return widget.cursor_moved(px, py, ctx);
+                }
+            }
+        }
+        false
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                let widget = unsafe { &mut *self.children[idx] };
+                if widget.popover_rect().is_some() {
+                    if widget.mouse_input(button, state, px, py, ctx) {
+                        return true;
+                    }
+                }
+                if widget.mouse_input(button, state, px, py, ctx) {
+                    return true;
+                }
+                if state == ElementState::Pressed && !widget.hit_test(px, py, ctx) {
+                    widget.unfocus();
+                }
+            }
+        }
+        false
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).keyboard_input(event, ctx) };
+            }
+        }
+        false
+    }
+
+    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).mouse_wheel(delta, px, py, ctx) };
+            }
+        }
+        false
+    }
+
+    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        if !self.visible {
+            return None;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).popover_rect() };
+            }
+        }
+        None
+    }
+
+    fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+        if !self.visible {
+            return;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                unsafe {
+                    (*self.children[idx]).render_popover(pc);
+                }
+            }
+        }
+    }
+
+    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
+        if !self.visible {
+            return false;
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).tick(dt, ctx) };
+            }
+        }
+        false
+    }
+
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).rounded_corners() };
+            }
+        }
+        (false, false, false, false)
+    }
+
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).solid_border() };
+            }
+        }
+        None
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).extra_quads() };
+            }
+        }
+        Vec::new()
+    }
+
+    fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+        if !self.visible {
+            return Vec::new();
+        }
+        if let Some(idx) = self.active_index {
+            if idx < self.children.len() {
+                return unsafe { (*self.children[idx]).extra_arcs() };
+            }
+        }
+        Vec::new()
+    }
+
+    fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
+}
+
+impl MenuController for Switcher {
+    fn menu_click(&mut self) -> Option<(usize, usize)> {
+        let idx = self.active_index?;
+        let child_ptr = *self.children.get(idx)?;
+        unsafe { &mut *child_ptr }.as_menu_controller_mut()?.menu_click()
+    }
+    fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+                    mc.trigger_menu_click(menu_idx, item_idx);
+                }
+            }
+        }
+    }
+    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+                    mc.set_item_checked(menu_idx, item_idx, checked);
+                }
+            }
+        }
+    }
+    fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+                    mc.set_menu_items(menu_idx, items);
+                }
+            }
+        }
+    }
+    fn is_menu_bar(&self) -> bool {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.is_menu_bar();
+                }
+            }
+        }
+        false
+    }
+    fn is_menu_open(&self) -> bool {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.is_menu_open();
+                }
+            }
+        }
+        false
+    }
+    fn menu_items(&self) -> Vec<String> {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.menu_items();
+                }
+            }
+        }
+        Vec::new()
+    }
+    fn menu_item_checked(&self) -> Vec<Option<bool>> {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.menu_item_checked();
+                }
+            }
+        }
+        Vec::new()
+    }
+    fn is_vertical(&self) -> bool {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.is_vertical();
+                }
+            }
+        }
+        false
+    }
+    fn menu_names(&self) -> Vec<String> {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.menu_names();
+                }
+            }
+        }
+        Vec::new()
+    }
+    fn menu_items_list(&self) -> Vec<Vec<String>> {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.menu_items_list();
+                }
+            }
+        }
+        Vec::new()
+    }
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+                    return mc.menu_checked_list();
+                }
+            }
+        }
+        Vec::new()
+    }
+    fn take_context_change(&mut self) -> Option<usize> {
+        let idx = self.active_index?;
+        let child_ptr = *self.children.get(idx)?;
+        unsafe { &mut *child_ptr }.as_menu_controller_mut()?.take_context_change()
+    }
+    fn set_context_selected(&mut self, selected: usize) {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+                    mc.set_context_selected(selected);
+                }
+            }
+        }
+    }
+    fn set_center_items(&mut self, center: bool) {
+        if let Some(idx) = self.active_index {
+            if let Some(child) = self.children.get(idx) {
+                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+                    mc.set_center_items(center);
+                }
+            }
+        }
+    }
+    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+        let idx = self.active_index?;
+        let child_ptr = *self.children.get(idx)?;
+        unsafe { &*child_ptr }.as_menu_controller()?.get_menu_items_at(px, py)
+    }
+}
+
+unsafe impl Send for Switcher {}
+unsafe impl Sync for Switcher {}
diff --git a/src/widget/core.rs b/src/widget/core.rs
index a88d662..aade366 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -411,27 +411,33 @@ pub mod clipboard {
     }
 
     pub fn read_from_clipboard() -> Option<String> {
-        if let Ok(output) = std::process::Command::new("wl-paste")
+        match std::process::Command::new("wl-paste")
             .arg("-n")
             .output()
         {
-            if output.status.success() {
-                if let Ok(text) = String::from_utf8(output.stdout) {
-                    return Some(text);
+            Ok(output) => {
+                if output.status.success() {
+                    if let Ok(text) = String::from_utf8(output.stdout) {
+                        return Some(text);
+                    }
                 }
             }
+            Err(_) => {}
         }
-        if let Ok(output) = std::process::Command::new("xclip")
+        match std::process::Command::new("xclip")
             .arg("-selection")
             .arg("clipboard")
             .arg("-o")
             .output()
         {
-            if output.status.success() {
-                if let Ok(text) = String::from_utf8(output.stdout) {
-                    return Some(text);
+            Ok(output) => {
+                if output.status.success() {
+                    if let Ok(text) = String::from_utf8(output.stdout) {
+                        return Some(text);
+                    }
                 }
             }
+            Err(_) => {}
         }
         None
     }
@@ -450,7 +456,7 @@ pub mod context_menu {
         pub visible: bool,
         pub options: Vec<String>,
         pub hovered_item: Option<usize>,
-        pub target: Option<*mut TextBox>,
+        pub target: Option<*mut (dyn Element + 'static)>,
     }
 
     impl ContextMenuState {
@@ -467,11 +473,13 @@ pub mod context_menu {
             }
         }
 
-        pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+        pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
             self.x = x;
             self.y = y;
             self.options = options;
             self.h = self.options.len() as f32 * 24.0;
+            let max_len = self.options.iter().map(|s| s.len()).max().unwrap_or(0);
+            self.w = ((max_len as f32 * 7.5) + 24.0).max(120.0);
             self.visible = true;
             self.hovered_item = None;
             self.target = Some(target);
@@ -493,7 +501,7 @@ pub mod context_menu {
             self.hovered_item = None;
             if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
                 let idx = ((py - self.y) / 24.0) as usize;
-                if idx < self.options.len() {
+                if idx < self.options.len() && idx > 0 {
                     self.hovered_item = Some(idx);
                 }
             }
@@ -513,28 +521,26 @@ pub mod context_menu {
             if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
                 let idx = ((py - self.y) / 24.0) as usize;
                 if idx < self.options.len() {
-                    let opt = self.options[idx].clone();
-                    if let Some(target_ptr) = self.target {
-                        unsafe {
-                            let target = &mut *target_ptr;
-                            match opt.as_str() {
-                                "Cut" => {
-                                    if target.cut_selection() {
-                                        target.just_changed = true;
+                    if idx > 0 {
+                        let opt = self.options[idx].clone();
+                        if let Some(target_ptr) = self.target {
+                            unsafe {
+                                let target = &mut *target_ptr;
+                                match opt.as_str() {
+                                    "Cut" => {
+                                        let _ = target.cut_selection();
                                     }
-                                }
-                                "Copy" => {
-                                    target.copy_selection();
-                                }
-                                "Paste" => {
-                                    if target.paste_from_clipboard() {
-                                        target.just_changed = true;
+                                    "Copy" => {
+                                        target.copy_selection();
                                     }
+                                    "Paste" => {
+                                        let _ = target.paste_from_clipboard();
+                                    }
+                                    "Select All" => {
+                                        target.select_all();
+                                    }
+                                    _ => {}
                                 }
-                                "Select All" => {
-                                    target.select_all();
-                                }
-                                _ => {}
                             }
                         }
                     }
@@ -570,7 +576,9 @@ pub mod context_menu {
 
             for (idx, opt) in self.options.iter().enumerate() {
                 let iy = self.y + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
-                let text_color = if self.hovered_item == Some(idx) {
+                let text_color = if idx == 0 {
+                    [0x70, 0x70, 0x78]
+                } else if self.hovered_item == Some(idx) {
                     [0xff, 0xff, 0xff]
                 } else {
                     [0xcc, 0xcc, 0xd4]
@@ -596,7 +604,7 @@ pub mod context_menu {
         CONTEXT_MENU.with(|m| m.borrow().visible)
     }
 
-    pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+    pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
         CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, target));
     }
 
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index f0be9f6..6c2a3a0 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -307,3 +307,10 @@ impl Element for Float3 {
         labels
     }
 }
+
+impl Drop for Float3 {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 7a394fd..b363541 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -4,6 +4,8 @@ use crate::widget::display::TextLabel;
 
 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
 pub struct GraphNode {
+    #[serde(default)]
+    pub id: String,
     pub name: String,
     pub position: (f32, f32), // (column, row)
     pub parameters: Vec<(String, String, String)>, // (name, value, type)
@@ -22,6 +24,7 @@ pub struct Graph {
     skipped_col_w: f32,
     nodes: Vec<GraphNode>,
     selected_idx: Option<usize>,
+    selected_id: Option<String>,
     double_clicked_idx: Option<usize>,
     double_click_timer: Option<(std::time::Instant, usize)>,
     grid_snap_enabled: bool,
@@ -29,6 +32,7 @@ pub struct Graph {
 
     // For dragging a node
     dragging_idx: Option<usize>,
+    dragging_id: Option<String>,
     drag_ox: f32,
     drag_oy: f32,
     pub(crate) drag_node_pos: Option<(f32, f32)>,
@@ -69,11 +73,13 @@ impl Graph {
             skipped_col_w: 37.5,
             nodes: Vec::new(),
             selected_idx: None,
+            selected_id: None,
             double_clicked_idx: None,
             double_click_timer: None,
             grid_snap_enabled: false,
             node_geom_toggled: None,
             dragging_idx: None,
+            dragging_id: None,
             drag_ox: 0.0,
             drag_oy: 0.0,
             drag_node_pos: None,
@@ -103,7 +109,9 @@ impl Graph {
 
     pub fn toggle_rect(&self, idx: usize) -> Option<(f32, f32, f32, f32)> {
         let (nx, ny, nw, nh) = self.node_rect(idx)?;
-        Some((nx + nw - 30.0, ny + (nh - 18.0) / 2.0, 18.0, 18.0))
+        let scale_f = nw / 80.0;
+        let size = (18.0 * scale_f).clamp(6.0, 50.0);
+        Some((nx + nw - size - 6.0 * scale_f, ny + (nh - size) / 2.0, size, size))
     }
 
     fn find_empty_cell(&self, start_x: f32, start_y: f32, skip_idx: Option<usize>) -> (f32, f32) {
@@ -153,39 +161,23 @@ impl Element for Graph {
         px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
     }
 
-    fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
-    fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
-    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
-    fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
-
-    fn set_nodes(&mut self, nodes: &[GraphNode]) {
-        self.nodes = nodes.to_vec();
-        if let Some(sel) = self.selected_idx {
-            if sel >= self.nodes.len() {
-                self.selected_idx = None;
-            }
-        }
-    }
-    fn get_nodes(&self) -> Vec<GraphNode> { self.nodes.clone() }
-    fn selected_node(&self) -> Option<usize> { self.selected_idx }
-    fn set_selected_node(&mut self, idx: Option<usize>) { self.selected_idx = idx; }
-    fn double_clicked_node(&self) -> Option<usize> { self.double_clicked_idx }
-    fn clear_double_clicked_node(&mut self) { self.double_clicked_idx = None; }
-    fn set_grid_snap_enabled(&mut self, enabled: bool) { self.grid_snap_enabled = enabled; }
-    fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { self.node_geom_toggled.take() }
+    fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
+    fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
         for (i, node) in self.nodes.iter().enumerate() {
-            if let Some((nx, ny, _nw, nh)) = self.node_rect(i) {
-                let lx = nx + 8.0;
-                let ly = ny + (nh - 12.0) / 2.0;
+            if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+                let scale_f = nw / 80.0;
+                let font_size = (14.0 * scale_f).clamp(6.0, 48.0);
+                let lx = nx + nw + 8.0 * scale_f;
+                let ly = ny + (nh - font_size) / 2.0;
                 if lx >= self.x && lx < self.x + self.w && ly >= self.y && ly < self.y + self.h {
                     labels.push(TextLabel {
                         text: node.name.clone(),
                         x: lx,
                         y: ly,
-                        font_size: 14.0,
+                        font_size,
                         color: [0xcc, 0xcc, 0xd4],
                     });
                 }
@@ -248,6 +240,7 @@ impl Element for Graph {
         } else {
             self.dragging_idx = None;
         }
+        self.dragging_id = None;
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
@@ -286,7 +279,9 @@ impl Element for Graph {
                             }
                             self.double_click_timer = Some((now, i));
                             self.selected_idx = Some(i);
+                            self.selected_id = Some(self.nodes[i].id.clone());
                             self.dragging_idx = Some(i);
+                            self.dragging_id = Some(self.nodes[i].id.clone());
                             self.drag_ox = px - nx;
                             self.drag_oy = py - ny;
                             self.drag_node_pos = Some((nx, ny));
@@ -296,6 +291,7 @@ impl Element for Graph {
                     }
                 }
                 self.selected_idx = None;
+                self.selected_id = None;
                 false
             }
             ElementState::Released => {
@@ -327,8 +323,9 @@ impl Element for Graph {
         };
 
         // Draw connection wires
+        let scale_f = self.grid_size_x / 80.0;
         let wire_color = [0.0, 0.75, 1.0, 0.7]; // Vibrant cyan glow
-        let wire_thickness = 3.0;
+        let wire_thickness = (3.0 * scale_f).clamp(1.0, 15.0);
         for i in 0..self.nodes.len() {
             let node = &self.nodes[i];
             if let Some((_, input_name, _)) = node.parameters.iter().find(|(name, _, _)| name.eq_ignore_ascii_case("input")) {
@@ -386,59 +383,28 @@ impl Element for Graph {
             let step_x = self.grid_size_x + self.skipped_col_w;
 
             if step_y >= 4.0 && step_x >= 4.0 {
-                let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
-                let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
-                let ry_start = ry_start.max(-100_000);
-                let ry_end = ry_end.min(100_000);
+                let ry_start = (((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1).max(-100_000);
+                let ry_end = (((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1).min(100_000);
 
-                let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
-                let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
-                let cx_start = cx_start.max(-100_000);
-                let cx_end = cx_end.min(100_000);
+                let cx_start = (((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1).max(-100_000);
+                let cx_end = (((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1).min(100_000);
 
                 // Draw gap color as solid background color of grid
-                quads.push((self.x, self.y, self.w, self.h, [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity]));
+                push_clipped(self.x, self.y, self.w, self.h, [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity], &mut quads);
 
                 // Draw filled cells with cell color
                 for r in ry_start..=ry_end {
-                    let y1 = self.grid_origin_y + (r as f32) * step_y;
+                    let y1 = (self.grid_origin_y + (r as f32) * step_y).round();
                     for c in cx_start..=cx_end {
-                        let x1 = self.grid_origin_x + (c as f32) * step_x;
-                        let cell_x = x1.max(self.x);
-                        let cell_y = y1.max(self.y);
-                        let cell_w = (x1 + self.grid_size_x).min(self.x + self.w) - cell_x;
-                        let cell_h = (y1 + self.grid_size_y).min(self.y + self.h) - cell_y;
-                        if cell_w > 0.0 && cell_h > 0.0 {
-                            quads.push((cell_x, cell_y, cell_w, cell_h, [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity]));
-                        }
-                    }
-                }
-
-                let grid_line_color = [0.18, 0.18, 0.22, 0.40];
-
-                for k in ry_start..=ry_end {
-                    let y1 = self.grid_origin_y + (k as f32) * step_y;
-                    let y2 = y1 + self.grid_size_y;
-                    if y1 < self.y + self.h {
-                        if y1 >= self.y {
-                            quads.push((self.x, y1, self.w, 1.0, grid_line_color));
-                        }
-                        if y2 >= self.y && y2 < self.y + self.h {
-                            quads.push((self.x, y2, self.w, 1.0, grid_line_color));
-                        }
-                    }
-                }
-
-                for k in cx_start..=cx_end {
-                    let x1 = self.grid_origin_x + (k as f32) * step_x;
-                    let x2 = x1 + self.grid_size_x;
-                    if x1 < self.x + self.w {
-                        if x1 >= self.x {
-                            quads.push((x1, self.y, 1.0, self.h, grid_line_color));
-                        }
-                        if x2 >= self.x && x2 < self.x + self.w {
-                            quads.push((x2, self.y, 1.0, self.h, grid_line_color));
-                        }
+                        let x1 = (self.grid_origin_x + (c as f32) * step_x).round();
+                        push_clipped(
+                            x1,
+                            y1,
+                            self.grid_size_x.round(),
+                            self.grid_size_y.round(),
+                            [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity],
+                            &mut quads,
+                        );
                     }
                 }
             }
@@ -446,6 +412,7 @@ impl Element for Graph {
 
         for i in 0..self.nodes.len() {
             if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+                let scale_f = nw / 80.0;
                 let bg_color = if self.dragging_idx == Some(i) {
                     colors::node_drag_color()
                 } else if self.selected_idx == Some(i) {
@@ -464,7 +431,7 @@ impl Element for Graph {
                     push_clipped(tx, ty, tw, th, btn_color, &mut quads);
 
                     if self.nodes[i].geom_visible {
-                        let inset = 3.0;
+                        let inset = 3.0 * scale_f;
                         push_clipped(tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON, &mut quads);
                     }
                 }
@@ -474,9 +441,7 @@ impl Element for Graph {
         quads
     }
 
-    fn grid_origin(&self) -> (f32, f32) {
-        (self.grid_origin_x, self.grid_origin_y)
-    }
+
 
     fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         if self.hit_test(px, py, ctx) {
@@ -497,3 +462,58 @@ impl Element for Graph {
         }
     }
 }
+
+impl Drop for Graph {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
+impl GraphController for Graph {
+    fn set_nodes(&mut self, nodes: &[GraphNode]) {
+        self.nodes = nodes.to_vec();
+        
+        // Sync selected_idx from selected_id
+        if let Some(ref id) = self.selected_id {
+            self.selected_idx = self.nodes.iter().position(|n| n.id == *id);
+            if self.selected_idx.is_none() {
+                self.selected_id = None;
+            }
+        } else {
+            self.selected_idx = None;
+        }
+
+        // Sync dragging_idx from dragging_id
+        if let Some(ref id) = self.dragging_id {
+            self.dragging_idx = self.nodes.iter().position(|n| n.id == *id);
+            if self.dragging_idx.is_none() {
+                self.dragging_id = None;
+                self.drag_node_pos = None;
+            }
+        } else {
+            self.dragging_idx = None;
+            self.drag_node_pos = None;
+        }
+
+        self.double_clicked_idx = None;
+        self.double_click_timer = None;
+        self.toggle_hovered_idx = None;
+    }
+    fn get_nodes(&self) -> Vec<GraphNode> { self.nodes.clone() }
+    fn selected_node(&self) -> Option<usize> { self.selected_idx }
+    fn set_selected_node(&mut self, idx: Option<usize>) {
+        self.selected_idx = idx;
+        self.selected_id = idx.and_then(|i| self.nodes.get(i).map(|n| n.id.clone()));
+    }
+    fn double_clicked_node(&self) -> Option<usize> { self.double_clicked_idx }
+    fn clear_double_clicked_node(&mut self) { self.double_clicked_idx = None; }
+    fn set_grid_snap_enabled(&mut self, enabled: bool) { self.grid_snap_enabled = enabled; }
+    fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { self.node_geom_toggled.take() }
+    fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+    fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+    fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+    fn grid_origin(&self) -> (f32, f32) { (self.grid_origin_x, self.grid_origin_y) }
+    fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+}
+
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index 57a545c..4d541be 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -45,6 +45,10 @@ impl Node {
     pub(crate) fn toggle_rect(&self) -> (f32, f32, f32, f32) {
         (self.x + self.w - 30.0, self.y + (self.h - 18.0) / 2.0, 18.0, 18.0)
     }
+
+    pub fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_snap_x = gx; self.grid_snap_y = gy; }
+    pub fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+    pub fn set_node_name(&mut self, name: &str) { self.name = name.to_string(); }
 }
 
 impl Element for Node {
@@ -64,16 +68,16 @@ impl Element for Node {
     }
     fn unfocus(&mut self) { self.selected = false; }
 
-    fn node_params(&self) -> Vec<(String, String, String)> { self.parameters.clone() }
-    fn set_display_params(&mut self, params: &[(String, String, String)]) {
-        self.parameters = params.to_vec();
-    }
+    fn as_param_controller(&self) -> Option<&dyn ParamController> { Some(self) }
+    fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { Some(self) }
+    fn as_geom_controller(&self) -> Option<&dyn GeomController> { Some(self) }
+    fn as_geom_controller_mut(&mut self) -> Option<&mut dyn GeomController> { Some(self) }
 
     fn text_labels(&self) -> Vec<TextLabel> {
         vec![TextLabel {
             text: self.name.clone(),
-            x: self.x + 8.0,
-            y: self.y + (self.h - 12.0) / 2.0,
+            x: self.x + self.w + 8.0,
+            y: self.y + (self.h - 14.0) / 2.0,
             font_size: 14.0,
             color: [0xcc, 0xcc, 0xd4],
         }]
@@ -119,10 +123,6 @@ impl Element for Node {
     fn is_dragging(&self) -> bool { self.dragging }
     fn draggable(&self) -> bool { !self.toggle_hovered }
 
-    fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_snap_x = gx; self.grid_snap_y = gy; }
-    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
-    fn set_node_name(&mut self, name: &str) { self.name = name.to_string(); }
-
     fn drag_update(&mut self, px: f32, py: f32) -> bool {
         let nx = px - self.drag_ox;
         let ny = py - self.drag_oy;
@@ -177,6 +177,16 @@ impl Element for Node {
         quads
     }
 
+}
+
+impl ParamController for Node {
+    fn node_params(&self) -> Vec<(String, String, String)> { self.parameters.clone() }
+    fn set_display_params(&mut self, params: &[(String, String, String)]) {
+        self.parameters = params.to_vec();
+    }
+}
+
+impl GeomController for Node {
     fn set_geom_visible(&mut self, visible: bool) { self.geom_visible = visible; }
     fn geom_visible(&self) -> bool { self.geom_visible }
     fn take_geom_toggle(&mut self) -> bool { std::mem::take(&mut self.geom_toggled) }
diff --git a/src/widget/display/serialize.rs b/src/widget/display/serialize.rs
index 131d5d4..b9cc436 100644
--- a/src/widget/display/serialize.rs
+++ b/src/widget/display/serialize.rs
@@ -19,20 +19,27 @@ fn serialize_single_widget(w: &dyn Element, json: &mut String) {
     // Handle children
     let dummy = crate::context::UiContext::new();
     let children = w.children(&dummy);
-    let menu_items = w.menu_items();
+    let mut menu_items = Vec::new();
+    let mut is_menu_open = false;
+    let mut is_vertical = false;
+    let mut checked_states = Vec::new();
+    if let Some(mc) = w.as_menu_controller() {
+        menu_items = mc.menu_items();
+        is_menu_open = mc.is_menu_open();
+        is_vertical = mc.is_vertical();
+        checked_states = mc.menu_item_checked();
+    }
 
-    if type_name == "Menu" && w.is_menu_open() && !menu_items.is_empty() {
+    if type_name == "Menu" && is_menu_open && !menu_items.is_empty() {
         json.push_str(",\"children\":[");
         let mut max_len = 0;
         for item in &menu_items {
             max_len = max_len.max(item.len());
         }
         let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
-        let vertical = w.is_vertical();
-        let dx = if vertical { x + width } else { x };
-        let dy = if vertical { y } else { y + height };
+        let dx = if is_vertical { x + width } else { x };
+        let dy = if is_vertical { y } else { y + height };
 
-        let checked_states = w.menu_item_checked();
         for (i, item) in menu_items.iter().enumerate() {
             if i > 0 {
                 json.push(',');
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index b1acc6e..9132191 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -9,13 +9,27 @@ pub enum ButtonKind {
     CopyIcon,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct Button {
     base: Widget,
     pressed: bool,
     just_clicked: bool,
     kind: ButtonKind,
     pub selected: bool,
+    pub on_click_cb: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
+}
+
+impl std::fmt::Debug for Button {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Button")
+            .field("base", &self.base)
+            .field("pressed", &self.pressed)
+            .field("just_clicked", &self.just_clicked)
+            .field("kind", &self.kind)
+            .field("selected", &self.selected)
+            .field("on_click_cb", &self.on_click_cb.as_ref().map(|_| "<callback>"))
+            .finish()
+    }
 }
 
 impl Button {
@@ -26,6 +40,7 @@ impl Button {
             just_clicked: false,
             kind: ButtonKind::Primary,
             selected: false,
+            on_click_cb: None,
         }
     }
 
@@ -36,6 +51,7 @@ impl Button {
             just_clicked: false,
             kind: ButtonKind::Reset,
             selected: false,
+            on_click_cb: None,
         }
     }
 
@@ -46,6 +62,7 @@ impl Button {
             just_clicked: false,
             kind: ButtonKind::ListRow,
             selected: false,
+            on_click_cb: None,
         }
     }
 
@@ -56,6 +73,7 @@ impl Button {
             just_clicked: false,
             kind: ButtonKind::CopyIcon,
             selected: false,
+            on_click_cb: None,
         }
     }
 
@@ -68,6 +86,11 @@ impl Button {
         self.selected = selected;
         self
     }
+
+    pub fn on_click<F: Fn() + Send + Sync + 'static>(mut self, cb: F) -> Self {
+        self.on_click_cb = Some(std::sync::Arc::new(cb));
+        self
+    }
 }
 
 impl Element for Button {
@@ -123,6 +146,9 @@ impl Element for Button {
             ElementState::Released => {
                 if self.pressed && self.hit_test(px, py, ctx) {
                     self.just_clicked = true;
+                    if let Some(ref cb) = self.on_click_cb {
+                        cb();
+                    }
                 }
                 let was = self.pressed;
                 self.pressed = false;
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 6e9af66..8eccb87 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -1,5 +1,6 @@
 use crate::colors;
 use crate::widget::*;
+use crate::widget::input::get_font_db;
 
 #[derive(Debug, Clone)]
 pub struct ButtonStrip {
@@ -10,6 +11,8 @@ pub struct ButtonStrip {
     pub just_clicked: Option<usize>,
     pub hovered_idx: Option<usize>,
     pub pressed_idx: Option<usize>,
+    pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
+    pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
 }
 
 impl ButtonStrip {
@@ -22,21 +25,26 @@ impl ButtonStrip {
             just_clicked: None,
             hovered_idx: None,
             pressed_idx: None,
+            tab_text_quads: Vec::new(),
+            tab_quads_cache: std::collections::HashMap::new(),
         }
     }
 
     pub fn with_buttons(mut self, buttons: Vec<String>) -> Self {
         self.buttons = buttons;
+        self.generate_rotated_labels();
         self
     }
 
     pub fn with_selected(mut self, selected: Option<usize>) -> Self {
         self.selected = selected;
+        self.generate_rotated_labels();
         self
     }
 
     pub fn with_vertical(mut self, vertical: bool) -> Self {
         self.vertical = vertical;
+        self.generate_rotated_labels();
         self
     }
 
@@ -45,7 +53,10 @@ impl ButtonStrip {
     }
 
     pub fn set_selected(&mut self, selected: Option<usize>) {
-        self.selected = selected;
+        if self.selected != selected {
+            self.selected = selected;
+            self.generate_rotated_labels();
+        }
     }
 
     pub fn take_click(&mut self) -> Option<usize> {
@@ -54,6 +65,109 @@ impl ButtonStrip {
 
     pub fn add_button(&mut self, label: &str) {
         self.buttons.push(label.to_string());
+        self.generate_rotated_labels();
+    }
+
+    pub fn generate_rotated_labels(&mut self) {
+        self.tab_text_quads.clear();
+        if !self.vertical || self.buttons.is_empty() {
+            return;
+        }
+
+        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;
+
+        let (font_fam, font_size) = crate::layout::menubar_font_parsed();
+        let scale = crate::scale::scale_factor().max(1.0);
+
+        for (i, page_name) in self.buttons.iter().enumerate() {
+            let color = if self.selected == Some(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 r = self.item_rect(i);
+            let padding_y = crate::layout::paginator_tab_padding_y();
+            let y_offset = if has_icon { 2.0 * padding_y + 12.0 } else { 0.0 };
+            let usable_h = (r.3 - y_offset).max(1.0);
+
+            let w_px = (r.2 * scale) as u32;
+            let h_px = (usable_h * scale) as u32;
+
+            if w_px == 0 || h_px == 0 {
+                self.tab_text_quads.push(Vec::new());
+                continue;
+            }
+
+            let cache_key = format!("{}:{}:{}:{:?}:{}:{}", trimmed, w_px, h_px, color, font_fam, scale);
+            if let Some(cached_quads) = self.tab_quads_cache.get(&cache_key) {
+                self.tab_text_quads.push(cached_quads.clone());
+                continue;
+            }
+
+            let svg_data = format!(
+                r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
+  <text x="{}" y="{}" font-family="{}" font-size="{}" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
+</svg>"##,
+                w_px, h_px,
+                r.2, usable_h,
+                r.2 / 2.0, usable_h / 2.0,
+                font_fam,
+                font_size,
+                hex_color,
+                r.2 / 2.0, usable_h / 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) / a).min(1.0);
+                                    let g = ((pixels[idx + 1] as f32 / 255.0) / a).min(1.0);
+                                    let b = ((pixels[idx + 2] as f32 / 255.0) / a).min(1.0);
+                                    page_quads.push((
+                                        col as f32 / scale,
+                                        row as f32 / scale,
+                                        1.2 / scale,
+                                        1.2 / scale,
+                                        [r, g, b, a],
+                                    ));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            self.tab_quads_cache.insert(cache_key, page_quads.clone());
+            self.tab_text_quads.push(page_quads);
+        }
     }
 
     pub fn item_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
@@ -63,8 +177,14 @@ impl ButtonStrip {
         let n = self.buttons.len() as f32;
         let (x, y, w, h) = self.rect();
         if self.vertical {
-            let btn_h = h / n;
-            (x, y + idx as f32 * btn_h, w, btn_h)
+            let spacing = 8.0;
+            let max_btn_h = if n > 1.0 {
+                (h - spacing * (n - 1.0)) / n
+            } else {
+                h
+            };
+            let btn_h = max_btn_h.min(144.0).max(0.0);
+            (x, y + idx as f32 * (btn_h + spacing), w, btn_h)
         } else {
             let btn_w = w / n;
             (x + idx as f32 * btn_w, y, btn_w, h)
@@ -75,6 +195,16 @@ impl ButtonStrip {
 impl Element for ButtonStrip {
     crate::impl_widget_base!(ButtonStrip);
 
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        if self.base.x != x || self.base.y != y || self.base.w != w || self.base.h != h {
+            self.base.x = x;
+            self.base.y = y;
+            self.base.w = w;
+            self.base.h = h;
+            self.generate_rotated_labels();
+        }
+    }
+
     fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
         None
     }
@@ -106,6 +236,12 @@ impl Element for ButtonStrip {
                         if self.selected != Some(pressed) {
                             self.selected = Some(pressed);
                             self.just_clicked = Some(pressed);
+                            self.generate_rotated_labels();
+                            changed = true;
+                        } else {
+                            self.selected = None;
+                            self.just_clicked = Some(pressed);
+                            self.generate_rotated_labels();
                             changed = true;
                         }
                     }
@@ -144,6 +280,30 @@ impl Element for ButtonStrip {
             if bg_color != [0.0, 0.0, 0.0, 0.0] {
                 quads.push((r.0, r.1, r.2, r.3, bg_color));
             }
+
+            if self.vertical {
+                if i < self.tab_text_quads.len() {
+                    let min_y = self.base.y;
+                    let max_y = self.base.y + self.base.h;
+                    let page_name = &self.buttons[i];
+                    let trimmed = page_name.trim();
+                    let has_icon = trimmed.find(' ').is_some();
+                    let padding_y = crate::layout::paginator_tab_padding_y();
+                    let y_offset = if has_icon { 2.0 * padding_y + 12.0 } else { 0.0 };
+
+                    for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
+                        let absolute_x = r.0 + qx;
+                        let absolute_y = r.1 + y_offset + qy;
+                        
+                        let ry1 = absolute_y.max(min_y);
+                        let ry2 = (absolute_y + qh).min(max_y);
+                        let rh = ry2 - ry1;
+                        if rh > 0.0 {
+                            quads.push((absolute_x, ry1, qw, rh, qc));
+                        }
+                    }
+                }
+            }
         }
         quads
     }
@@ -159,20 +319,23 @@ impl Element for ButtonStrip {
                 [0xa8, 0xa8, 0xb3]
             };
             if self.vertical {
-                let line_height = font_size * 1.2;
-                let label_len = btn_label.chars().count() as f32;
-                let total_h = label_len * line_height;
-                let start_y = r.1 + (r.3 - total_h) / 2.0;
-                let char_w = TextLabel::estimate_width("o", font_size);
-                let x_pos = r.0 + (r.2 - char_w) / 2.0;
-                for (char_idx, c) in btn_label.chars().enumerate() {
-                    labels.push(TextLabel {
-                        text: c.to_string(),
-                        x: x_pos,
-                        y: start_y + char_idx as f32 * line_height,
-                        font_size,
-                        color,
-                    });
+                let trimmed = btn_label.trim();
+                if let Some(space_idx) = trimmed.find(' ') {
+                    let (icon, _) = trimmed.split_at(space_idx);
+                    let icon = icon.trim();
+                    if !icon.is_empty() {
+                        let icon_font_size = 14.0;
+                        let est_icon_w = TextLabel::estimate_width(icon, icon_font_size);
+                        let padding_y = crate::layout::paginator_tab_padding_y();
+                        let icon_y = r.1 + (padding_y - 2.0).max(0.0);
+                        labels.push(TextLabel {
+                            text: icon.to_string(),
+                            x: r.0 + (r.2 - est_icon_w) / 2.0,
+                            y: icon_y,
+                            font_size: icon_font_size,
+                            color,
+                        });
+                    }
                 }
             } else {
                 let est_w = TextLabel::estimate_width(btn_label, font_size);
@@ -216,6 +379,7 @@ impl Element for ButtonStrip {
         if Some(next) != self.selected {
             self.selected = Some(next);
             self.just_clicked = Some(next);
+            self.generate_rotated_labels();
             return true;
         }
         false
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index ac9dfa5..44f8e25 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -5,6 +5,7 @@ pub struct Checkbox {
     base: Widget,
     checked: bool,
     just_clicked: bool,
+    pub just_changed: bool,
 }
 
 impl Checkbox {
@@ -13,6 +14,7 @@ impl Checkbox {
             base: Widget::new(),
             checked: false,
             just_clicked: false,
+            just_changed: false,
         }
     }
 
@@ -33,6 +35,33 @@ impl Checkbox {
 impl Element for Checkbox {
     crate::impl_widget_base!(Checkbox);
 
+    fn get_value_string(&self) -> Option<String> {
+        Some(self.checked.to_string())
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let val_trimmed = val.trim().to_lowercase();
+        let new_checked = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
+            true
+        } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
+            false
+        } else {
+            return false;
+        };
+        if self.checked != new_checked {
+            self.checked = new_checked;
+            self.just_changed = true;
+            return true;
+        }
+        false
+    }
+
+    fn take_change(&mut self) -> bool {
+        let ret = self.just_changed;
+        self.just_changed = false;
+        ret
+    }
+
     fn color(&self) -> [f32; 4] {
         let (_, _, w, _) = self.rect();
         if w > 30.0 {
@@ -51,6 +80,12 @@ impl Element for Checkbox {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         match state {
             ElementState::Released => {
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 7e68a84..5328a50 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -15,6 +15,7 @@ pub struct ColorSelector {
     pub children: Vec<*mut (dyn Element + 'static)>,
     child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
     pub editor_state: TextEditorState,
+    pub just_changed: bool,
 }
 
 impl Clone for ColorSelector {
@@ -32,6 +33,7 @@ impl Clone for ColorSelector {
             children: self.children.clone(),
             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
             editor_state: self.editor_state.clone(),
+            just_changed: self.just_changed,
         }
     }
 }
@@ -51,6 +53,7 @@ impl ColorSelector {
             children: Vec::new(),
             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
             editor_state: TextEditorState::new(String::new()),
+            just_changed: false,
         }
     }
 
@@ -73,6 +76,31 @@ impl ColorSelector {
 impl Element for ColorSelector {
     crate::impl_widget_base!(ColorSelector);
 
+    fn get_value_string(&self) -> Option<String> {
+        Some(format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]))
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        if let Some(c) = parse_hex(val) {
+            if self.color != c {
+                self.color = c;
+                self.just_changed = true;
+                if self.editing {
+                    self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+                    self.cursor_idx = self.edit_buffer.chars().count();
+                }
+                return true;
+            }
+        }
+        false
+    }
+
+    fn take_change(&mut self) -> bool {
+        let ret = self.just_changed;
+        self.just_changed = false;
+        ret
+    }
+
     fn preferred_height(&self) -> Option<f32> {
         Some(crate::layout::color_selector_height())
     }
@@ -95,6 +123,12 @@ impl Element for ColorSelector {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         if state != ElementState::Pressed { return false; }
         if !self.hit_test(px, py, ctx) { return false; }
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 007bfc6..f3fa55d 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -122,6 +122,39 @@ impl Default for Dropdown {
 impl Element for Dropdown {
     crate::impl_widget_base!(Dropdown);
 
+    fn get_value_string(&self) -> Option<String> {
+        self.options.get(self.selected).cloned()
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let val_trimmed = val.trim();
+        for (idx, opt) in self.options.iter().enumerate() {
+            if opt.eq_ignore_ascii_case(val_trimmed) {
+                if self.selected != idx {
+                    self.selected = idx;
+                    self.just_changed = true;
+                    return true;
+                }
+                return false;
+            }
+        }
+        if let Ok(idx) = val_trimmed.parse::<usize>() {
+            if idx < self.options.len() {
+                if self.selected != idx {
+                    self.selected = idx;
+                    self.just_changed = true;
+                    return true;
+                }
+                return false;
+            }
+        }
+        false
+    }
+
+    fn take_change(&mut self) -> bool {
+        self.take_change()
+    }
+
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
@@ -175,6 +208,12 @@ impl Element for Dropdown {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left || state != ElementState::Pressed { return false; }
 
         let (x, y, w, h) = self.rect();
@@ -333,6 +372,13 @@ impl Element for Dropdown {
     }
 }
 
+impl Drop for Dropdown {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
+
 unsafe impl Send for Dropdown {}
 unsafe impl Sync for Dropdown {}
 
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 881f586..5b88ae6 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -14,6 +14,7 @@ pub struct Slider {
     min: f32,
     max: f32,
     pub editor_state: TextEditorState,
+    pub just_changed: bool,
 }
 
 impl Slider {
@@ -30,6 +31,7 @@ impl Slider {
             min: 0.0,
             max: 1.0,
             editor_state: TextEditorState::new(String::new()),
+            just_changed: false,
         }
     }
 
@@ -105,6 +107,38 @@ impl Slider {
 impl Element for Slider {
     crate::impl_widget_base!(Slider);
 
+    fn get_value_string(&self) -> Option<String> {
+        let scaled_val = self.min + self.value * (self.max - self.min);
+        Some(format!("{:.2}", scaled_val))
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        if let Ok(new_val) = val.trim().parse::<f32>() {
+            let old_val = self.value;
+            let range = self.max - self.min;
+            if range != 0.0 {
+                self.value = ((new_val - self.min) / range).clamp(0.0, 1.0);
+            } else {
+                self.value = 0.0;
+            }
+            if (self.value - old_val).abs() > 0.0001 {
+                self.just_changed = true;
+                if self.editing {
+                    let scaled_val = self.min + self.value * (self.max - self.min);
+                    self.edit_buffer = format!("{:.2}", scaled_val);
+                }
+                return true;
+            }
+        }
+        false
+    }
+
+    fn take_change(&mut self) -> bool {
+        let ret = self.just_changed;
+        self.just_changed = false;
+        ret
+    }
+
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
@@ -198,6 +232,12 @@ impl Element for Slider {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         
         let top = self.base.label_offset();
@@ -390,6 +430,13 @@ impl Element for Slider {
     fn value(&self) -> i32 { (self.value * 100.0) as i32 }
 }
 
+impl Drop for Slider {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum ActiveThumb {
     Low,
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 375ccf1..46ac008 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -16,6 +16,7 @@ pub struct Spinbox {
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub children: Vec<*mut (dyn Element + 'static)>,
     pub editor_state: TextEditorState,
+    pub just_changed: bool,
 }
 
 impl Spinbox {
@@ -36,6 +37,7 @@ impl Spinbox {
             parent: None,
             children: Vec::new(),
             editor_state: TextEditorState::new(String::new()),
+            just_changed: false,
         }
     }
 
@@ -66,6 +68,46 @@ impl Spinbox {
 impl Element for Spinbox {
     crate::impl_widget_base!(Spinbox);
 
+    fn get_value_string(&self) -> Option<String> {
+        if self.decimals > 0 {
+            let divisor = 10.0f32.powi(self.decimals as i32);
+            Some(format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize))
+        } else {
+            Some(self.value.to_string())
+        }
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let val = val.trim();
+        let old_val = self.value;
+        if self.decimals > 0 {
+            if let Ok(val_f) = val.parse::<f32>() {
+                let divisor = 10.0f32.powi(self.decimals as i32);
+                self.value = (val_f * divisor).round() as i32;
+                self.value = self.value.clamp(self.min, self.max);
+            }
+        } else {
+            if let Ok(val_i) = val.parse::<i32>() {
+                self.value = val_i.clamp(self.min, self.max);
+            }
+        }
+        if self.value != old_val {
+            self.just_changed = true;
+            if self.editing {
+                self.edit_buffer = self.get_value_string().unwrap_or_default();
+                self.cursor_idx = self.edit_buffer.chars().count();
+            }
+            return true;
+        }
+        false
+    }
+
+    fn take_change(&mut self) -> bool {
+        let ret = self.just_changed;
+        self.just_changed = false;
+        ret
+    }
+
     fn preferred_height(&self) -> Option<f32> {
         Some(crate::layout::spinbox_height())
     }
@@ -93,6 +135,12 @@ impl Element for Spinbox {
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         if !self.hit_test(px, py, ctx) { return false; }
         match state {
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 5bb2136..fdd58c3 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -323,6 +323,51 @@ impl Default for TextBox {
 impl Element for TextBox {
     crate::impl_widget_base!(TextBox);
 
+    fn get_value_string(&self) -> Option<String> {
+        Some(self.text.clone())
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let val_str = val.to_string();
+        if self.text != val_str {
+            self.text = val_str.clone();
+            self.edit_buffer = val_str;
+            self.just_changed = true;
+            self.sync_editor_state();
+            true
+        } else {
+            false
+        }
+    }
+
+    fn take_change(&mut self) -> bool {
+        self.take_change()
+    }
+
+    fn cut_selection(&mut self) -> bool {
+        let res = self.cut_selection();
+        if res {
+            self.just_changed = true;
+        }
+        res
+    }
+
+    fn copy_selection(&self) {
+        self.copy_selection();
+    }
+
+    fn paste_from_clipboard(&mut self) -> bool {
+        let res = self.paste_from_clipboard();
+        if res {
+            self.just_changed = true;
+        }
+        res
+    }
+
+    fn select_all(&mut self) {
+        self.select_all();
+    }
+
     fn preferred_height(&self) -> Option<f32> {
         Some(crate::layout::textbox_height())
     }
@@ -446,6 +491,12 @@ impl Element for TextBox {
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         if self.disabled { return false; }
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.hit_test(px, py, ctx) {
+                ctx.handle_right_click(self.as_ptr(), px, py);
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         if !self.hit_test(px, py, ctx) { return false; }
         match state {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index edaa801..5133165 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -179,7 +179,35 @@ pub trait Element {
         }
     }
 
-    fn label(&self) -> Option<String> { None }
+    fn label(&self) -> Option<String> {
+        self.base().and_then(|b| b.label.clone())
+    }
+
+    fn get_value_string(&self) -> Option<String> { None }
+    fn set_value_string(&mut self, _val: &str) -> bool { false }
+    fn take_change(&mut self) -> bool { false }
+
+    fn cut_selection(&mut self) -> bool {
+        if let Some(val) = self.get_value_string() {
+            clipboard::copy_to_clipboard(&val);
+            self.set_value_string("")
+        } else {
+            false
+        }
+    }
+    fn copy_selection(&self) {
+        if let Some(val) = self.get_value_string() {
+            clipboard::copy_to_clipboard(&val);
+        }
+    }
+    fn paste_from_clipboard(&mut self) -> bool {
+        if let Some(text) = clipboard::read_from_clipboard() {
+            self.set_value_string(&text)
+        } else {
+            false
+        }
+    }
+    fn select_all(&mut self) {}
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         if let Some(b) = self.base_mut() {
@@ -278,6 +306,7 @@ pub trait Element {
     }
 
     fn color(&self) -> [f32; 4];
+    fn solid_border(&self) -> Option<([f32; 4], f32)> { None }
 
     fn is_dragging(&self) -> bool { false }
     fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
@@ -365,48 +394,27 @@ pub trait Element {
     fn set_selected(&mut self, _selected: bool) {}
     fn keyboard_input(&mut self, _event: &KeyEvent, _ctx: &mut UiContext) -> bool { false }
 
-    fn menu_click(&mut self) -> Option<(usize, usize)> { None }
-    fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
-    fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
-    fn set_item_checked(&mut self, _menu_idx: usize, _item_idx: usize, _checked: bool) {}
-    fn set_menu_items(&mut self, _menu_idx: usize, _items: &[String]) {}
-    fn is_menu_bar(&self) -> bool { false }
-    fn is_menu_open(&self) -> bool { false }
-    fn set_grid_snap(&mut self, _gx: f32, _gy: f32) {}
-    fn set_node_name(&mut self, _name: &str) {}
     fn set_visible(&mut self, _visible: bool) {}
     fn visible(&self) -> bool { true }
-    fn set_path(&mut self, _segments: &[String]) {}
-    fn path_click(&mut self) -> Option<usize> { None }
-
-    fn node_params(&self) -> Vec<(String, String, String)> { vec![] }
-    fn set_display_params(&mut self, _params: &[(String, String, String)]) {}
-
-    fn set_config_toggle(&mut self, _id: usize, _val: bool) {}
-    fn take_config_toggle(&mut self) -> Option<(usize, bool)> { None }
-    fn set_show_network_grid(&mut self, _show: bool) {}
-    fn set_config_spin(&mut self, _id: usize, _val: f32) {}
-    fn take_config_spin(&mut self) -> Option<(usize, f32)> { None }
-    fn set_grid_sizes(&mut self, _gx: f32, _gy: f32) {}
-    fn set_grid_origin(&mut self, _ox: f32, _oy: f32) {}
-    fn grid_origin(&self) -> (f32, f32) { (0.0, 0.0) }
-    fn set_skipped_sizes(&mut self, _row_h: f32, _col_w: f32) {}
-    fn set_palette_state(&mut self, _visible: bool, _query: &str, _items: &[String], _selected: usize) {}
-
-    fn set_geom_visible(&mut self, _visible: bool) {}
-    fn geom_visible(&self) -> bool { true }
-    fn take_geom_toggle(&mut self) -> bool { false }
-    fn set_spreadsheet_data(&mut self, _headers: Vec<String>, _rows: Vec<Vec<String>>) {}
     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
+    fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
 
-    fn set_nodes(&mut self, _nodes: &[GraphNode]) {}
-    fn get_nodes(&self) -> Vec<GraphNode> { vec![] }
-    fn selected_node(&self) -> Option<usize> { None }
-    fn set_selected_node(&mut self, _idx: Option<usize>) {}
-    fn double_clicked_node(&self) -> Option<usize> { None }
-    fn clear_double_clicked_node(&mut self) {}
-    fn set_grid_snap_enabled(&mut self, _enabled: bool) {}
-    fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { None }
+    fn as_page_selector(&self) -> Option<&dyn PageSelector> { None }
+    fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { None }
+    fn as_menu_controller(&self) -> Option<&dyn MenuController> { None }
+    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { None }
+    fn as_graph_controller(&self) -> Option<&dyn GraphController> { None }
+    fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { None }
+    fn as_spreadsheet_controller(&self) -> Option<&dyn SpreadsheetController> { None }
+    fn as_spreadsheet_controller_mut(&mut self) -> Option<&mut dyn SpreadsheetController> { None }
+    fn as_path_controller(&self) -> Option<&dyn PathController> { None }
+    fn as_path_controller_mut(&mut self) -> Option<&mut dyn PathController> { None }
+    fn as_param_controller(&self) -> Option<&dyn ParamController> { None }
+    fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { None }
+    fn as_geom_controller(&self) -> Option<&dyn GeomController> { None }
+    fn as_geom_controller_mut(&mut self) -> Option<&mut dyn GeomController> { None }
+    fn as_scroll_controller(&self) -> Option<&dyn ScrollController> { None }
+    fn as_scroll_controller_mut(&mut self) -> Option<&mut dyn ScrollController> { None }
 
     fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
         let base = self.base()?;
@@ -465,36 +473,10 @@ pub trait Element {
     }
 
     fn z_index(&self) -> i32 { 0 }
-    fn set_center_items(&mut self, _center: bool) {}
-    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 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 set_pages_with_items(&mut self, _pages: Vec<String>, _items: Vec<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 Element + 'static), _ctx: &mut UiContext) {}
-    fn clear_page_widgets(&mut self, _page_idx: usize, _ctx: &mut UiContext) {}
-    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 }
-
-    fn take_context_change(&mut self) -> Option<usize> { None }
-    fn set_context_selected(&mut self, _selected: usize) {}
 }
 
 pub trait Control: Element {
@@ -542,7 +524,7 @@ pub use self::input::{
 pub use self::container::{
     Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
     ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
-    Paginator
+    Paginator, Switcher, Layer, Page
 };
 pub use self::display::{
     TextLabel, Label, SectionHeader, StyledLabel, TextItem, Svg, UsageBar,
@@ -551,6 +533,81 @@ pub use self::display::{
     DotStatus, PreviewLayoutMode, Sidebar, Panel, serialize_widgets
 };
 
+pub trait PageSelector {
+    fn selected_page(&self) -> usize;
+    fn set_selected_page(&mut self, page: usize);
+    fn is_page_hidden(&self) -> bool;
+    fn set_page_hidden(&mut self, hidden: bool);
+    fn set_pages(&mut self, pages: Vec<String>);
+    fn set_pages_with_items(&mut self, pages: Vec<String>, items: Vec<Vec<String>>);
+    fn sidebar_w(&self) -> f32;
+    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 Element + 'static), ctx: &mut UiContext);
+    fn clear_page_widgets(&mut self, page_idx: usize, ctx: &mut UiContext);
+}
+
+pub trait MenuController {
+    fn menu_click(&mut self) -> Option<(usize, usize)>;
+    fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize);
+    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool);
+    fn set_menu_items(&mut self, menu_idx: usize, items: &[String]);
+    fn is_menu_bar(&self) -> bool;
+    fn is_menu_open(&self) -> bool;
+    fn menu_items(&self) -> Vec<String>;
+    fn menu_item_checked(&self) -> Vec<Option<bool>>;
+    fn is_vertical(&self) -> bool;
+    fn menu_names(&self) -> Vec<String>;
+    fn menu_items_list(&self) -> Vec<Vec<String>>;
+    fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>>;
+    fn take_context_change(&mut self) -> Option<usize>;
+    fn set_context_selected(&mut self, selected: usize);
+    fn set_center_items(&mut self, center: bool);
+    fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)>;
+}
+
+pub trait GraphController {
+    fn set_nodes(&mut self, nodes: &[GraphNode]);
+    fn get_nodes(&self) -> Vec<GraphNode>;
+    fn selected_node(&self) -> Option<usize>;
+    fn set_selected_node(&mut self, idx: Option<usize>);
+    fn double_clicked_node(&self) -> Option<usize>;
+    fn clear_double_clicked_node(&mut self);
+    fn set_grid_snap_enabled(&mut self, enabled: bool);
+    fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)>;
+    fn set_grid_snap(&mut self, gx: f32, gy: f32);
+    fn set_grid_sizes(&mut self, gx: f32, gy: f32);
+    fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32);
+    fn set_grid_origin(&mut self, ox: f32, oy: f32);
+    fn grid_origin(&self) -> (f32, f32);
+    fn set_show_network_grid(&mut self, show: bool);
+}
+
+pub trait SpreadsheetController {
+    fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>);
+}
+
+pub trait PathController {
+    fn set_path(&mut self, segments: &[String]);
+    fn path_click(&mut self) -> Option<usize>;
+}
+
+pub trait ParamController {
+    fn node_params(&self) -> Vec<(String, String, String)>;
+    fn set_display_params(&mut self, params: &[(String, String, String)]);
+}
+
+pub trait GeomController {
+    fn set_geom_visible(&mut self, visible: bool);
+    fn geom_visible(&self) -> bool;
+    fn take_geom_toggle(&mut self) -> bool;
+}
+
+pub trait ScrollController {
+    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>;
+}
+
 pub fn label_offset(w: &dyn Element) -> f32 {
     let name = w.type_name();
     if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" {