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

commita73f9c2821156209c51a481f579111f5b217d55c
parent0311afa81a
authorLucas Galante <[email protected]>
date2026-05-29 17:37
Update system configuration and interface modules

 Cargo.lock    |   11 +
 Cargo.toml    |    1 +
 src/layout.rs |    4 +-
 src/main.rs   |  106 ++-
 src/widget.rs | 2607 +++++++++++++++++++++++++++++++++++++--------------------
 5 files changed, 1790 insertions(+), 939 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index ce63757..22dc4d0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -177,6 +177,7 @@ dependencies = [
  "pollster",
  "raw-window-handle",
  "resvg",
+ "serde",
  "smithay-client-toolkit",
  "tokio",
  "wayland-client",
@@ -1228,6 +1229,16 @@ version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
 
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
 [[package]]
 name = "serde_core"
 version = "1.0.228"
diff --git a/Cargo.toml b/Cargo.toml
index 0669d1f..e555819 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,4 +20,5 @@ calloop-wayland-source = "0.3.0"
 wayland-client = { version = "0.31", features = ["system"] }
 xkeysym = "0.2"
 resvg = "0.41.0"
+serde = { version = "1.0", features = ["derive"] }
 
diff --git a/src/layout.rs b/src/layout.rs
index 5bf2e89..136ecf5 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -14,8 +14,8 @@ pub fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y:
         pc.rect(qc, qx, qy, qw, qh);
     }
     // Render hover highlight on top of widget surfaces
-    if let Some(hc) = w.hover_highlight() {
-        pc.rect(hc, x, y, ww, wh);
+    if let Some((hx, hy, hw, hh, hc)) = w.hover_highlight_quad() {
+        pc.rect(hc, hx, hy, hw, hh);
     }
     let font_opt = w.widget_font();
     for label in w.text_labels() {
diff --git a/src/main.rs b/src/main.rs
index 6af9574..1f37072 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use clear_ui::widget::{
-    Button, Checkbox, ContentBg, Header, Panel, ProgressBar, Sidebar, Slider, Spinbox, StatusBar,
-    TextLabel, Toggle, Widget,
+    Button, Checkbox, ContentBg, Header, Panel, ProgressBar, RangeSlider, Sidebar, Slider, Spinbox,
+    StatusBar, TextLabel, Toggle, Widget,
 };
 
 use glyphon::{
@@ -17,7 +17,7 @@ use smithay_client_toolkit::{
     output::{OutputHandler, OutputState},
     seat::{
         keyboard::KeyboardHandler,
-        pointer::PointerHandler,
+        pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
         Capability, SeatHandler, SeatState,
     },
     shell::{
@@ -248,8 +248,9 @@ impl State {
             Box::new(Toggle::new()),
             Box::new(ProgressBar::new(0.65)),
             Box::new(Slider::new()),
-            Box::new(StatusBar::new()),
             Box::new(Spinbox::new(0, -10, 10, 1)),
+            Box::new(RangeSlider::new()),
+            Box::new(StatusBar::new()),
         ];
 
         let positions = demo_positions(sw, sh);
@@ -311,7 +312,10 @@ impl State {
         let sw = self.width;
         let sh = self.height;
         let mut verts = Vec::new();
-        for w in &self.widgets {
+        let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
+        draw_order.sort_by_key(|&i| self.widgets[i].z_index());
+        for &i in &draw_order {
+            let w = &self.widgets[i];
             verts.extend(widget_vertices(w.as_ref(), sw, sh));
             for (qx, qy, qw, qh, qc) in w.extra_quads() {
                 verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
@@ -403,10 +407,23 @@ impl State {
 
         let mut widget_buffers: Vec<Buffer> = Vec::new();
         let mut widget_labels: Vec<TextLabel> = Vec::new();
-        for w in self.widgets.iter() {
+        for (i, w) in self.widgets.iter().enumerate() {
             for label in w.text_labels() {
-                widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
-                widget_labels.push(label);
+                let mut covered = false;
+                for (pi, pw) in self.widgets.iter().enumerate() {
+                    if pi != i {
+                        if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
+                            if label.is_covered_by(px, py, pw_val, ph) {
+                                covered = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+                if !covered {
+                    widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+                    widget_labels.push(label);
+                }
             }
         }
 
@@ -487,7 +504,7 @@ impl State {
                             r: 0.05,
                             g: 0.05,
                             b: 0.08,
-                            a: 0.92,
+                            a: 0.20,
                         }),
                         store: wgpu::StoreOp::Store,
                     },
@@ -529,7 +546,8 @@ fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
         (162.0, 410.0, 160.0, 24.0),        // 11 progress_bar
         (70.0, 444.0, 300.0, 32.0),         // 12 slider
         (70.0, 486.0, 120.0, 32.0),         // 13 spinbox
-        (0.0, sh - 28.0, sw, 28.0),         // 14 status_bar
+        (70.0, 528.0, 300.0, 32.0),         // 14 range_slider
+        (0.0, sh - 28.0, sw, 28.0),         // 15 status_bar
     ]
 }
 
@@ -565,7 +583,7 @@ struct AppState {
     output_state: OutputState,
 
     seats: Vec<wl_seat::WlSeat>,
-    pointer: Option<wl_pointer::WlPointer>,
+    pointer: Option<ThemedPointer>,
     keyboard: Option<wl_keyboard::WlKeyboard>,
 
     window: Option<XdgWindow>,
@@ -663,8 +681,15 @@ impl SeatHandler for AppState {
         capability: Capability,
     ) {
         if capability == Capability::Pointer && self.pointer.is_none() {
-            let pointer = self.seat_state.get_pointer(qh, &seat).unwrap();
-            self.pointer = Some(pointer);
+            let surface = self.compositor_state.create_surface(qh);
+            let themed_pointer = self.seat_state.get_pointer_with_theme(
+                qh,
+                &seat,
+                self.shm_state.wl_shm(),
+                surface,
+                ThemeSpec::System,
+            ).unwrap();
+            self.pointer = Some(themed_pointer);
         }
         if capability == Capability::Keyboard && self.keyboard.is_none() {
             let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
@@ -715,7 +740,11 @@ impl PointerHandler for AppState {
             }
 
             match &event.kind {
-                PointerEventKind::Enter { .. } => {}
+                PointerEventKind::Enter { .. } => {
+                    if let Some(ref themed_pointer) = self.pointer {
+                        let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
+                    }
+                }
                 PointerEventKind::Leave { .. } => {}
                 PointerEventKind::Motion { .. } => {
                     if let Some(state) = &mut self.state {
@@ -758,32 +787,39 @@ impl PointerHandler for AppState {
                                 changed = true;
                             }
                         } else {
-                            if btn == clear_ui::widget::MouseButton::Left {
-                                if let Some(old) = st.focused_widget.take() {
-                                    st.widgets[old].unfocus();
-                                }
-                            }
+                            let mut clicked_idx = None;
                             for i in (0..st.widgets.len()).rev() {
                                 if st.widgets[i].hit_test(st.cursor_x, st.cursor_y) {
-                                    if st.widgets[i].mouse_input(
-                                        btn,
-                                        clear_ui::widget::ElementState::Pressed,
-                                        st.cursor_x,
-                                        st.cursor_y,
-                                    ) {
-                                        changed = true;
-                                    }
-                                    if btn == clear_ui::widget::MouseButton::Left && st.widgets[i].draggable() {
-                                        st.widgets[i].drag_begin(st.cursor_x, st.cursor_y);
-                                        st.drag_widget = Some(i);
-                                    }
-                                    if btn == clear_ui::widget::MouseButton::Left {
-                                        st.widgets[i].focus();
-                                        st.focused_widget = Some(i);
-                                    }
+                                    clicked_idx = Some(i);
                                     break;
                                 }
                             }
+                            if btn == clear_ui::widget::MouseButton::Left {
+                                if let Some(old) = st.focused_widget {
+                                    if Some(old) != clicked_idx {
+                                        st.widgets[old].unfocus();
+                                        st.focused_widget = None;
+                                    }
+                                }
+                            }
+                            if let Some(i) = clicked_idx {
+                                if st.widgets[i].mouse_input(
+                                    btn,
+                                    clear_ui::widget::ElementState::Pressed,
+                                    st.cursor_x,
+                                    st.cursor_y,
+                                ) {
+                                    changed = true;
+                                }
+                                if btn == clear_ui::widget::MouseButton::Left && st.widgets[i].draggable() {
+                                    st.widgets[i].drag_begin(st.cursor_x, st.cursor_y);
+                                    st.drag_widget = Some(i);
+                                }
+                                if btn == clear_ui::widget::MouseButton::Left {
+                                    st.widgets[i].focus();
+                                    st.focused_widget = Some(i);
+                                }
+                            }
                         }
                         if changed {
                             st.upload_vertices();
diff --git a/src/widget.rs b/src/widget.rs
index 2282928..4305876 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -466,26 +466,140 @@ pub struct TextLabel {
     pub color: [u8; 3],
 }
 
+impl TextLabel {
+    pub fn is_covered_by(&self, px: f32, py: f32, pw: f32, ph: f32) -> bool {
+        let text_w = self.text.chars().count() as f32 * self.font_size * 0.65;
+        let x_overlap = self.x <= px + pw && (self.x + text_w) >= px;
+        let y_overlap = self.y <= py + ph && (self.y + self.font_size) >= py;
+        x_overlap && y_overlap
+    }
+}
+
+
+#[derive(Debug, Clone)]
+pub struct WidgetBase {
+    pub x: f32,
+    pub y: f32,
+    pub w: f32,
+    pub h: f32,
+    pub label: Option<String>,
+    pub hovered: bool,
+    pub row_x: f32,
+    pub row_w: f32,
+}
+
+impl WidgetBase {
+    pub fn new() -> Self {
+        Self {
+            x: 0.0,
+            y: 0.0,
+            w: 0.0,
+            h: 0.0,
+            label: None,
+            hovered: false,
+            row_x: 0.0,
+            row_w: 0.0,
+        }
+    }
+}
+
 pub trait Widget {
-    fn rect(&self) -> (f32, f32, f32, f32);
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32);
-    fn set_row_rect(&mut self, _x: f32, _w: f32) {}
+    fn base(&self) -> Option<&WidgetBase> { None }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { None }
+
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        if let Some(b) = self.base() {
+            (b.x, b.y, b.w, b.h)
+        } else {
+            (0.0, 0.0, 0.0, 0.0)
+        }
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        if let Some(b) = self.base_mut() {
+            b.x = x;
+            b.y = y;
+            b.w = w;
+            b.h = h;
+        }
+    }
+
+    fn set_row_rect(&mut self, x: f32, w: f32) {
+        if let Some(b) = self.base_mut() {
+            b.row_x = x;
+            b.row_w = w;
+        }
+    }
 
     fn hit_test(&self, px: f32, py: f32) -> bool {
         let (x, y, w, h) = self.rect();
-        px >= x && px <= x + w && py >= y && py <= y + h
+        let (hx, hw) = if let Some(b) = self.base() {
+            if b.row_w > 0.0 {
+                (b.row_x, b.row_w)
+            } else {
+                (x, w)
+            }
+        } else {
+            (x, w)
+        };
+        let top = self.top_room();
+        let hy = y - top;
+        let hh = h + top;
+        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        if self.base().is_some() {
+            let was = self.hovered();
+            let is_hit = self.hit_test(px, py);
+            self.set_hovered(is_hit);
+            was != is_hit
+        } else {
+            false
+        }
     }
 
-    fn cursor_moved(&mut self, _px: f32, _py: f32) -> bool { false }
     fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32) -> bool { false }
     fn mouse_wheel(&mut self, _delta: &MouseScrollDelta, _px: f32, _py: f32) -> bool { false }
 
-    fn set_hovered(&mut self, _hovered: bool) {}
-    fn hovered(&self) -> bool { false }
+    fn set_hovered(&mut self, hovered: bool) {
+        if let Some(b) = self.base_mut() {
+            b.hovered = hovered;
+        }
+    }
+
+    fn hovered(&self) -> bool {
+        if let Some(b) = self.base() {
+            b.hovered
+        } else {
+            false
+        }
+    }
+
     fn hover_highlight(&self) -> Option<[f32; 4]> {
         if self.hovered() { Some([1.0, 1.0, 1.0, 0.06]) } else { None }
     }
 
+    fn hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+        if let Some(b) = self.base() {
+            if self.hovered() {
+                let hx = if b.row_w > 0.0 { b.row_x } else { b.x };
+                let hw = if b.row_w > 0.0 { b.row_w } else { b.w };
+                let top_offset = self.top_room();
+                let hy = b.y - top_offset;
+                let hh = b.h + top_offset;
+                Some((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]))
+            } else {
+                None
+            }
+        } else if let Some(hc) = self.hover_highlight() {
+            let (x, y, w, h) = self.rect();
+            Some((x, y, w, h, hc))
+        } else {
+            None
+        }
+    }
+
     fn color(&self) -> [f32; 4];
 
     fn is_dragging(&self) -> bool { false }
@@ -496,15 +610,42 @@ pub trait Widget {
     fn draggable(&self) -> bool { false }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
-    fn text_labels(&self) -> Vec<TextLabel> { Vec::new() }
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if let Some(b) = self.base() {
+            if let Some(ref label) = b.label {
+                return vec![TextLabel {
+                    text: label.clone(),
+                    x: b.x,
+                    y: b.y - 18.0,
+                    font_size: 12.0,
+                    color: [0x83, 0x83, 0x8a],
+                }];
+            }
+        }
+        Vec::new()
+    }
     fn widget_font(&self) -> Option<String> { None }
     fn value(&self) -> i32 { 0 }
-    fn top_room(&self) -> f32 { 0.0 }
+    fn top_room(&self) -> f32 {
+        if let Some(b) = self.base() {
+            if b.label.is_some() {
+                return 18.0;
+            }
+        }
+        0.0
+    }
+    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { None }
+    fn set_text(&mut self, text: &str) {
+        if let Some(b) = self.base_mut() {
+            b.label = Some(text.to_string());
+        }
+    }
 
     fn set_drag_bounds(&mut self, _bx: f32, _by: f32, _bw: f32, _bh: f32) {}
 
     fn focus(&mut self) {}
     fn unfocus(&mut self) {}
+    fn set_selected(&mut self, _selected: bool) {}
     fn keyboard_input(&mut self, _event: &KeyEvent) -> bool { false }
 
     fn menu_click(&mut self) -> Option<(usize, usize)> { None }
@@ -543,6 +684,7 @@ pub trait Widget {
     fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { vec![] }
     fn add_child(&mut self, _child: *mut (dyn Widget + 'static)) {}
     fn clear_children(&mut self) {}
+    fn z_index(&self) -> i32 { 0 }
 }
 
 #[derive(Clone)]
@@ -1165,23 +1307,24 @@ impl Widget for Canvas {
 
 const DROPDOWN_ITEM_H: f32 = 22.0;
 const CHECKBOX_WIDTH: f32 = 16.0;
-
 pub struct MenuBar {
     x: f32, y: f32, w: f32, h: f32,
     hovering: bool,
     pub title: String,
+    pub menus: Vec<Box<Menu>>,
     pub menu_items: Vec<String>,
     pub vertical_items: Vec<String>,
     pub menu_dropdowns: Vec<Vec<String>>,
-    menu_dropdown_checked: Vec<Vec<Option<bool>>>,
-    hovered_menu: Option<usize>,
-    open_menu: Option<usize>,
-    hovered_dropdown: Option<usize>,
-    clicked_dropdown: Option<(usize, usize)>,
-    was_open: Option<usize>,
+    pub menu_dropdown_checked: Vec<Vec<Option<bool>>>,
+    pub hovered_menu: Option<usize>,
+    pub open_menu: Option<usize>,
+    pub hovered_dropdown: Option<usize>,
+    pub clicked_dropdown: Option<(usize, usize)>,
+    pub was_open: Option<usize>,
     pub vertical: bool,
     pub visible: bool,
     pub focused: bool,
+    pub z_level: i32,
 }
 
 impl MenuBar {
@@ -1189,6 +1332,7 @@ impl MenuBar {
         Self {
             x, y, w, h, hovering: false,
             title: String::new(),
+            menus: Vec::new(),
             menu_items: Vec::new(),
             vertical_items: Vec::new(),
             menu_dropdowns: Vec::new(),
@@ -1201,10 +1345,10 @@ impl MenuBar {
             vertical: false,
             visible: true,
             focused: false,
+            z_level: 100,
         }
     }
 
-
     pub fn with_title(mut self, title: &str) -> Self {
         self.title = title.to_string();
         self
@@ -1215,6 +1359,11 @@ impl MenuBar {
         self.vertical_items.push(label.to_string());
         self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
         self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+        let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+        let mut menu = Menu::new(label, label, &item_strs);
+        menu.vertical = self.vertical;
+        self.menus.push(Box::new(menu));
         self
     }
 
@@ -1223,11 +1372,24 @@ impl MenuBar {
         self.vertical_items.push(vertical_label.to_string());
         self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
         self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+        let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+        let mut menu = Menu::new(horizontal_label, vertical_label, &item_strs);
+        menu.vertical = self.vertical;
+        self.menus.push(Box::new(menu));
         self
     }
 
     pub fn with_vertical(mut self, vertical: bool) -> Self {
         self.vertical = vertical;
+        for menu in &mut self.menus {
+            menu.vertical = vertical;
+        }
+        self
+    }
+
+    pub fn with_z_index(mut self, z: i32) -> Self {
+        self.z_level = z;
         self
     }
 
@@ -1242,60 +1404,18 @@ impl MenuBar {
     fn item_h_vertical(&self) -> f32 {
         24.0
     }
-
-    fn dropdown_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
-        if self.vertical {
-            let iy = self.item_y_vertical(idx);
-            let dx = self.x + self.w;
-            let dy = self.y + iy;
-            let mut dw = 80.0f32;
-            if let Some(items) = self.menu_dropdowns.get(idx) {
-                for (i, item) in items.iter().enumerate() {
-                    let mut tw = item.len() as f32 * 7.5 + 16.0;
-                    if self.menu_dropdown_checked.get(idx)
-                        .and_then(|m| m.get(i))
-                        .is_some()
-                    {
-                        tw += CHECKBOX_WIDTH;
-                    }
-                    dw = dw.max(tw);
-                }
-            }
-            let dh = self.menu_dropdowns.get(idx).map_or(0.0, |items| items.len() as f32 * DROPDOWN_ITEM_H);
-            (dx, dy, dw, dh)
-        } else {
-            let ix = menu_item_x(&self.title, &self.menu_items, idx);
-            let iw = menu_item_w(&self.menu_items, idx);
-            let dx = self.x + ix;
-            let dy = self.y + self.h;
-            let mut dw = iw;
-            if let Some(items) = self.menu_dropdowns.get(idx) {
-                for (i, item) in items.iter().enumerate() {
-                    let mut tw = item.len() as f32 * 7.5 + 16.0;
-                    if self.menu_dropdown_checked.get(idx)
-                        .and_then(|m| m.get(i))
-                        .is_some()
-                    {
-                        tw += CHECKBOX_WIDTH;
-                    }
-                    dw = dw.max(tw);
-                }
-            }
-            let dh = self.menu_dropdowns.get(idx).map_or(0.0, |items| items.len() as f32 * DROPDOWN_ITEM_H);
-            (dx, dy, dw, dh)
-        }
-    }
 }
+
 impl Widget for MenuBar {
     fn rect(&self) -> (f32, f32, f32, f32) {
         if !self.visible {
             return (0.0, 0.0, 0.0, 0.0);
         }
         if self.vertical {
-            let total_h = if self.menu_items.is_empty() {
+            let total_h = if self.menus.is_empty() {
                 self.h
             } else {
-                let last_idx = self.menu_items.len() - 1;
+                let last_idx = self.menus.len() - 1;
                 self.item_y_vertical(last_idx) + self.item_h_vertical()
             };
             (self.x, self.y, self.w, total_h)
@@ -1303,7 +1423,40 @@ impl Widget for MenuBar {
             (self.x, self.y, self.w, self.h)
         }
     }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.x = x;
+        self.y = y;
+        self.w = w;
+        self.h = h;
+
+        let parent_ptr = self as *mut MenuBar as *mut (dyn Widget + 'static);
+
+        if self.vertical {
+            let mut cy = 8.0;
+            if !self.title.is_empty() {
+                cy += 24.0;
+            }
+            for menu in &mut self.menus {
+                let ih = 24.0;
+                menu.set_rect(x, y + cy, w, ih);
+                menu.set_parent(Some(parent_ptr));
+                cy += ih;
+            }
+        } else {
+            let mut cx = 8.0;
+            if !self.title.is_empty() {
+                cx += self.title.len() as f32 * 7.5 + 24.0;
+            }
+            for menu in &mut self.menus {
+                let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
+                menu.set_rect(x + cx, y, iw, h);
+                menu.set_parent(Some(parent_ptr));
+                cx += iw;
+            }
+        }
+    }
+
     fn color(&self) -> [f32; 4] {
         if !self.visible {
             [0.0, 0.0, 0.0, 0.0]
@@ -1313,8 +1466,14 @@ impl Widget for MenuBar {
             colors::PANEL_MENU_BG
         }
     }
-    fn set_hovered(&mut self, v: bool) { self.hovering = v; }
-    fn hovered(&self) -> bool { self.hovering }
+
+    fn set_hovered(&mut self, v: bool) {
+        self.hovering = v;
+    }
+
+    fn hovered(&self) -> bool {
+        self.hovering
+    }
 
     fn hit_test(&self, px: f32, py: f32) -> bool {
         if !self.visible {
@@ -1324,9 +1483,8 @@ impl Widget for MenuBar {
         if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
             return true;
         }
-        if let Some(idx) = self.open_menu {
-            let (dx, dy, dw, dh) = self.dropdown_rect(idx);
-            if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+        for menu in &self.menus {
+            if menu.hit_test(px, py) {
                 return true;
             }
         }
@@ -1337,134 +1495,75 @@ impl Widget for MenuBar {
         if !self.visible {
             return false;
         }
-        self.was_open = None;
-        let was = self.hovering;
-        self.hovering = self.hit_test(px, py);
-        let old_menu = self.hovered_menu;
-        let old_dd = self.hovered_dropdown;
+        let (rx, ry, rw, rh) = self.rect();
+        self.set_rect(rx, ry, rw, rh);
+
+        let mut changed = false;
         self.hovered_menu = None;
-        self.hovered_dropdown = None;
-
-        if self.hovering {
-            if self.vertical {
-                if px >= self.x && px < self.x + self.w {
-                    let ly = py - self.y;
-                    for i in 0..self.menu_items.len() {
-                        let iy = self.item_y_vertical(i);
-                        let ih = self.item_h_vertical();
-                        if ly >= iy && ly < iy + ih {
-                            self.hovered_menu = Some(i);
-                            break;
-                        }
-                    }
-                }
-            } else {
-                if py >= self.y && py < self.y + self.h {
-                    let lx = px - self.x;
-                    for i in 0..self.menu_items.len() {
-                        let ix = menu_item_x(&self.title, &self.menu_items, i);
-                        if lx >= ix && lx < ix + menu_item_w(&self.menu_items, i) {
-                            self.hovered_menu = Some(i);
-                            break;
-                        }
-                    }
-                }
+        for (idx, menu) in self.menus.iter_mut().enumerate() {
+            if menu.cursor_moved(px, py) {
+                changed = true;
             }
-            if let Some(idx) = self.open_menu {
-                let (dx, dy, dw, dh) = self.dropdown_rect(idx);
-                if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
-                    let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
-                    if let Some(items) = self.menu_dropdowns.get(idx) {
-                        if di < items.len() {
-                            self.hovered_dropdown = Some(di);
-                        }
-                    }
-                }
+            if menu.hovered() {
+                self.hovered_menu = Some(idx);
             }
         }
-
-        was != self.hovering || old_menu != self.hovered_menu || old_dd != self.hovered_dropdown
+        changed
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if !self.visible {
             return false;
         }
-        if button != MouseButton::Left || state != ElementState::Pressed { return false; }
-        if !self.hit_test(px, py) { return false; }
+        let (rx, ry, rw, rh) = self.rect();
+        self.set_rect(rx, ry, rw, rh);
 
-        if self.vertical {
-            if px >= self.x && px < self.x + self.w {
-                let ly = py - self.y;
-                for i in 0..self.menu_items.len() {
-                    let iy = self.item_y_vertical(i);
-                    let ih = self.item_h_vertical();
-                    if ly >= iy && ly < iy + ih {
-                        if self.was_open == Some(i) || self.open_menu == Some(i) {
-                            self.open_menu = None;
-                            self.was_open = None;
-                        } else {
-                            self.open_menu = Some(i);
-                            self.was_open = None;
-                        }
-                        return true;
-                    }
-                }
-                self.was_open = None;
-                self.open_menu = None;
-                return true;
-            }
-        } else {
-            if py >= self.y && py < self.y + self.h {
-                let lx = px - self.x;
-                for i in 0..self.menu_items.len() {
-                    let ix = menu_item_x(&self.title, &self.menu_items, i);
-                    if lx >= ix && lx < ix + menu_item_w(&self.menu_items, i) {
-                        if self.was_open == Some(i) || self.open_menu == Some(i) {
-                            self.open_menu = None;
-                            self.was_open = None;
-                        } else {
-                            self.open_menu = Some(i);
-                            self.was_open = None;
-                        }
-                        return true;
-                    }
-                }
-                self.was_open = None;
-                self.open_menu = None;
-                return true;
+        let mut changed = false;
+        for menu in &mut self.menus {
+            if menu.mouse_input(button, state, px, py) {
+                changed = true;
             }
         }
+        changed
+    }
 
-        if let Some(idx) = self.open_menu {
-            let (dx, dy, dw, dh) = self.dropdown_rect(idx);
-            if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
-                let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
-                if let Some(items) = self.menu_dropdowns.get(idx) {
-                    if di < items.len() {
-                        self.clicked_dropdown = Some((idx, di));
-                        self.open_menu = None;
-                        return true;
-                    }
+    fn focus(&mut self) {
+        if self.is_menu_open() {
+            for menu in &mut self.menus {
+                if menu.is_menu_open() {
+                    menu.focus();
+                    return;
                 }
             }
         }
-
-        false
-    }
-
-    fn focus(&mut self) {
         self.focused = true;
         focus::set_focused(self);
     }
 
     fn unfocus(&mut self) {
         self.focused = false;
-        self.was_open = self.open_menu.take();
+        focus::clear_if_matches(self);
+        for menu in &mut self.menus {
+            menu.unfocus();
+        }
+    }
+
+    fn set_selected(&mut self, selected: bool) {
+        self.focused = selected;
+        if !selected {
+            for menu in &mut self.menus {
+                menu.set_selected(false);
+            }
+        }
     }
 
     fn menu_click(&mut self) -> Option<(usize, usize)> {
-        self.clicked_dropdown.take()
+        for (idx, menu) in self.menus.iter_mut().enumerate() {
+            if let Some((_, item_idx)) = menu.menu_click() {
+                return Some((idx, item_idx));
+            }
+        }
+        None
     }
 
     fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
@@ -1473,6 +1572,9 @@ impl Widget for MenuBar {
                 menu[item_idx] = Some(checked);
             }
         }
+        if let Some(menu) = self.menus.get_mut(menu_idx) {
+            menu.set_item_checked(0, item_idx, checked);
+        }
     }
 
     fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
@@ -1480,34 +1582,27 @@ impl Widget for MenuBar {
             self.menu_dropdowns[menu_idx] = items.to_vec();
             self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
         }
+        if let Some(menu) = self.menus.get_mut(menu_idx) {
+            menu.items = items.to_vec();
+            menu.item_checked = vec![Some(false); items.len()];
+        }
+    }
+
+    fn is_menu_bar(&self) -> bool {
+        self.visible
     }
 
-    fn is_menu_bar(&self) -> bool { self.visible }
-    fn is_menu_open(&self) -> bool { self.visible && self.open_menu.is_some() }
+    fn is_menu_open(&self) -> bool {
+        self.visible && self.menus.iter().any(|m| m.is_menu_open())
+    }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         if !self.visible {
             return Vec::new();
         }
         let mut quads = Vec::new();
-        if let Some(idx) = self.hovered_menu {
-            if self.vertical {
-                let iy = self.item_y_vertical(idx);
-                let ih = self.item_h_vertical();
-                quads.push((self.x, self.y + iy, self.w, ih, colors::PANEL_MENU_HOVER));
-            } else {
-                let ix = menu_item_x(&self.title, &self.menu_items, idx);
-                quads.push((self.x + ix, self.y, menu_item_w(&self.menu_items, idx), self.h, colors::PANEL_MENU_HOVER));
-            }
-        }
-        if let Some(idx) = self.open_menu {
-            let (dx, dy, dw, dh) = self.dropdown_rect(idx);
-            if dh > 0.0 {
-                quads.push((dx, dy, dw, dh, colors::PANEL_MENU_BG));
-                if let Some(di) = self.hovered_dropdown {
-                    quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
-                }
-            }
+        for menu in &self.menus {
+            quads.extend(menu.extra_quads());
         }
         quads
     }
@@ -1526,59 +1621,33 @@ impl Widget for MenuBar {
                 color: [0xaa, 0xaa, 0xbb],
             });
         }
-        if self.vertical {
-            for (i, item) in self.vertical_items.iter().enumerate() {
-                let iy = self.item_y_vertical(i);
-                labels.push(TextLabel {
-                    text: item.clone(),
-                    x: self.x + 8.0,
-                    y: self.y + iy + 5.0,
-                    font_size: 12.0,
-                    color: [0xcc, 0xcc, 0xd4],
-                });
-            }
-        } else {
-            for (i, item) in self.menu_items.iter().enumerate() {
-                labels.push(TextLabel {
-                    text: item.clone(),
-                    x: self.x + menu_item_x(&self.title, &self.menu_items, i) + 8.0,
-                    y: self.y + 7.0,
-                    font_size: 12.0,
-                    color: [0xcc, 0xcc, 0xd4],
-                });
-            }
-        }
-        if let Some(idx) = self.open_menu {
-            if let Some(items) = self.menu_dropdowns.get(idx) {
-                let (dx, dy, _, _) = self.dropdown_rect(idx);
-                for (i, item) in items.iter().enumerate() {
-                    let checked = self.menu_dropdown_checked.get(idx)
-                        .and_then(|m| m.get(i))
-                        .and_then(|&v| v);
-                    let prefix = match checked {
-                        Some(true) => "\u{2713} ",
-                        Some(false) => "  ",
-                        None => "",
-                    };
-                    labels.push(TextLabel {
-                        text: format!("{}{}", prefix, item),
-                        x: dx + 8.0,
-                        y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
-                        font_size: 12.0,
-                        color: [0xcc, 0xcc, 0xd4],
-                    });
-                }
-            }
+        for menu in &self.menus {
+            labels.extend(menu.text_labels());
         }
         labels
     }
 
     fn set_visible(&mut self, visible: bool) {
         self.visible = visible;
+        for menu in &mut self.menus {
+            menu.set_visible(visible);
+        }
     }
+
     fn visible(&self) -> bool {
         self.visible
     }
+
+    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
+        self.menus.iter().map(|m| {
+            let ptr: *const dyn Widget = &**m as &dyn Widget;
+            ptr as *mut (dyn Widget + 'static)
+        }).collect()
+    }
+
+    fn z_index(&self) -> i32 {
+        self.z_level
+    }
 }
 
 impl Drop for MenuBar {
@@ -1587,44 +1656,346 @@ impl Drop for MenuBar {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ButtonKind {
-    Primary,
-    Reset,
-    ListRow,
-    CopyIcon,
-}
-
 #[derive(Debug, Clone)]
-pub struct Button {
+pub struct Menu {
     x: f32, y: f32, w: f32, h: f32,
+    pub title: String,
+    pub vertical_title: String,
+    pub items: Vec<String>,
+    pub item_checked: Vec<Option<bool>>,
+    pub open: bool,
+    pub vertical: bool,
+    pub focused: bool,
     hovering: bool,
-    pressed: bool,
-    just_clicked: bool,
-    kind: ButtonKind,
-    pub label: Option<String>,
-    pub selected: bool,
+    hovered_item: Option<usize>,
+    clicked_item: Option<usize>,
+    was_open: Option<usize>,
+    pub parent: Option<*mut (dyn Widget + 'static)>,
+    pub children: Vec<*mut (dyn Widget + 'static)>,
 }
 
-impl Button {
-    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Primary, label: None, selected: false }
+impl Menu {
+    pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
+        Self {
+            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            title: title.to_string(),
+            vertical_title: vertical_title.to_string(),
+            items: items.to_vec(),
+            item_checked: vec![None; items.len()],
+            open: false,
+            vertical: false,
+            focused: false,
+            hovering: false,
+            hovered_item: None,
+            clicked_item: None,
+            was_open: None,
+            parent: None,
+            children: Vec::new(),
+        }
+    }
+
+    pub fn active_title(&self) -> &str {
+        if self.vertical {
+            &self.vertical_title
+        } else {
+            &self.title
+        }
+    }
+
+    fn dropdown_rect(&self) -> (f32, f32, f32, f32) {
+        let dh = self.items.len() as f32 * DROPDOWN_ITEM_H;
+        let mut max_len = 0;
+        for item in &self.items {
+            max_len = max_len.max(item.len());
+        }
+        let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
+        let dx = if self.vertical {
+            self.x + self.w
+        } else {
+            self.x
+        };
+        let dy = if self.vertical {
+            self.y
+        } else {
+            self.y + self.h
+        };
+        (dx, dy, dw, dh)
+    }
+}
+
+impl Widget for Menu {
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.x, self.y, self.w, self.h)
+    }
+
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.x = x;
+        self.y = y;
+        self.w = w;
+        self.h = h;
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn set_hovered(&mut self, v: bool) {
+        self.hovering = v;
+    }
+
+    fn hovered(&self) -> bool {
+        self.hovering
+    }
+
+    fn hit_test(&self, px: f32, py: f32) -> bool {
+        let (rx, ry, rw, rh) = self.rect();
+        if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
+            return true;
+        }
+        if self.open {
+            let (dx, dy, dw, dh) = self.dropdown_rect();
+            if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+                return true;
+            }
+        }
+        false
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        self.was_open = None;
+        let was_hovering = self.hovering;
+        self.hovering = self.hit_test(px, py);
+        let old_item = self.hovered_item;
+        self.hovered_item = None;
+
+        if self.open {
+            let (dx, dy, dw, dh) = self.dropdown_rect();
+            if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+                let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+                if di < self.items.len() {
+                    self.hovered_item = Some(di);
+                }
+            }
+        }
+
+        was_hovering != self.hovering || old_item != self.hovered_item
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left || state != ElementState::Pressed {
+            return false;
+        }
+        if !self.hit_test(px, py) {
+            return false;
+        }
+
+        let (rx, ry, rw, rh) = self.rect();
+        if px >= rx && px < rx + rw && py >= ry && py < ry + rh {
+            if self.was_open == Some(0) || self.open {
+                self.open = false;
+                self.was_open = None;
+            } else {
+                self.open = true;
+                self.was_open = None;
+                focus::set_focused(self);
+            }
+            return true;
+        }
+
+        if self.open {
+            let (dx, dy, dw, dh) = self.dropdown_rect();
+            if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+                let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+                if di < self.items.len() {
+                    self.clicked_item = Some(di);
+                    self.open = false;
+                    return true;
+                }
+            }
+        }
+
+        false
+    }
+
+    fn focus(&mut self) {
+        self.open = true;
+        self.focused = true;
+        focus::set_focused(self);
+    }
+
+    fn unfocus(&mut self) {
+        if self.open {
+            self.was_open = Some(0);
+        }
+        self.open = false;
+        self.focused = false;
+        focus::clear_if_matches(self);
+        self.hovered_item = None;
+    }
+
+    fn set_selected(&mut self, selected: bool) {
+        self.focused = selected;
+        if !selected {
+            self.open = false;
+            self.was_open = None;
+            self.hovered_item = None;
+            focus::clear_if_matches(self);
+        }
+    }
+
+    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);
+        }
+    }
+
+    fn is_menu_open(&self) -> bool {
+        self.open
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if self.hovering && !self.open {
+            quads.push((self.x, self.y, self.w, self.h, colors::PANEL_MENU_HOVER));
+        }
+        if self.open {
+            let (dx, dy, dw, dh) = self.dropdown_rect();
+            if dh > 0.0 {
+                quads.push((dx, dy, dw, dh, colors::PANEL_MENU_BG));
+                if let Some(di) = self.hovered_item {
+                    quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
+                }
+            }
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        labels.push(TextLabel {
+            text: self.active_title().to_string(),
+            x: self.x + 8.0,
+            y: self.y + 7.0,
+            font_size: 12.0,
+            color: [0xcc, 0xcc, 0xd4],
+        });
+        if self.open {
+            let (dx, dy, _, _) = self.dropdown_rect();
+            for (i, item) in self.items.iter().enumerate() {
+                let checked = self.item_checked.get(i).and_then(|&v| v);
+                let prefix = match checked {
+                    Some(true) => "\u{2713} ",
+                    Some(false) => "  ",
+                    None => "",
+                };
+                labels.push(TextLabel {
+                    text: format!("{}{}", prefix, item),
+                    x: dx + 8.0,
+                    y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
+                    font_size: 12.0,
+                    color: [0xcc, 0xcc, 0xd4],
+                });
+            }
+        }
+        labels
+    }
+
+    fn set_visible(&mut self, visible: bool) {
+        self.hovering = false;
+        if !visible {
+            self.open = false;
+            self.was_open = None;
+            self.hovered_item = None;
+            focus::clear_if_matches(self);
+        }
+    }
+
+    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
+        self.parent
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
+        self.parent = parent;
+    }
+
+    fn z_index(&self) -> i32 {
+        100
+    }
+}
+
+unsafe impl Send for Menu {}
+unsafe impl Sync for Menu {}
+
+impl Drop for Menu {
+    fn drop(&mut self) {
+        focus::clear_if_matches(self);
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ButtonKind {
+    Primary,
+    Reset,
+    ListRow,
+    CopyIcon,
+}
+
+#[derive(Debug, Clone)]
+pub struct Button {
+    base: WidgetBase,
+    pressed: bool,
+    just_clicked: bool,
+    kind: ButtonKind,
+    pub selected: bool,
+}
+
+impl Button {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+        Self {
+            base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            pressed: false,
+            just_clicked: false,
+            kind: ButtonKind::Primary,
+            selected: false,
+        }
     }
 
     pub fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Reset, label: None, selected: false }
+        Self {
+            base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            pressed: false,
+            just_clicked: false,
+            kind: ButtonKind::Reset,
+            selected: false,
+        }
     }
 
     pub fn new_list_row(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::ListRow, label: None, selected: false }
+        Self {
+            base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            pressed: false,
+            just_clicked: false,
+            kind: ButtonKind::ListRow,
+            selected: false,
+        }
     }
 
     pub fn new_copy_icon(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::CopyIcon, label: None, selected: false }
+        Self {
+            base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            pressed: false,
+            just_clicked: false,
+            kind: ButtonKind::CopyIcon,
+            selected: false,
+        }
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.label = Some(label.to_string());
+        self.base.label = Some(label.to_string());
         self
     }
 
@@ -1635,53 +2006,48 @@ impl Button {
 }
 
 impl Widget for Button {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_hovered(&mut self, v: bool) { self.hovering = v; }
-    fn hovered(&self) -> bool { self.hovering }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn top_room(&self) -> f32 { 0.0 }
+    fn hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+
     fn color(&self) -> [f32; 4] {
         match self.kind {
             ButtonKind::Primary => {
                 if self.pressed { colors::BUTTON_PRESS }
-                else if self.hovering { colors::BUTTON_HOVER }
+                else if self.base.hovered { colors::BUTTON_HOVER }
                 else { colors::BUTTON_IDLE }
             }
             ButtonKind::Reset => {
                 if self.pressed { colors::RESET_BTN_PRESS }
-                else if self.hovering { colors::RESET_BTN_HOVER }
+                else if self.base.hovered { colors::RESET_BTN_HOVER }
                 else { colors::RESET_BTN_IDLE }
             }
             ButtonKind::ListRow => {
                 if self.selected {
                     if self.pressed { [0.30, 0.52, 0.78, 0.6] }
-                    else if self.hovering { [0.30, 0.52, 0.78, 0.5] }
+                    else if self.base.hovered { [0.30, 0.52, 0.78, 0.5] }
                     else { [0.20, 0.40, 0.65, 0.4] }
                 } else {
                     if self.pressed { [0.20, 0.20, 0.25, 0.25] }
-                    else if self.hovering { [0.20, 0.20, 0.25, 0.15] }
+                    else if self.base.hovered { [0.20, 0.20, 0.25, 0.15] }
                     else { [0.0, 0.0, 0.0, 0.0] }
                 }
             }
             ButtonKind::CopyIcon => {
                 if self.selected {
                     if self.pressed { [0.30, 0.52, 0.78, 0.5] }
-                    else if self.hovering { [0.30, 0.52, 0.78, 0.5] }
+                    else if self.base.hovered { [0.30, 0.52, 0.78, 0.5] }
                     else { [0.20, 0.40, 0.65, 0.2] }
                 } else {
                     if self.pressed { [0.20, 0.20, 0.25, 0.25] }
-                    else if self.hovering { [0.20, 0.20, 0.25, 0.25] }
+                    else if self.base.hovered { [0.20, 0.20, 0.25, 0.25] }
                     else { [0.0, 0.0, 0.0, 0.0] }
                 }
             }
         }
     }
 
-    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovering;
-        self.hovering = self.hit_test(px, py);
-        was != self.hovering
-    }
-
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left { return false; }
         match state {
@@ -1709,7 +2075,7 @@ impl Widget for Button {
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        if let Some(ref label) = self.label {
+        if let Some(ref label) = self.base.label {
             let font_size = 12.0;
             let est_w = if label == "📋" {
                 12.0
@@ -1725,14 +2091,17 @@ impl Widget for Button {
             };
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.x + (self.w - est_w) / 2.0,
-                y: self.y + (self.h - font_size) / 2.0 - 1.0,
+                x: self.base.x + (self.base.w - est_w) / 2.0,
+                y: self.base.y + (self.base.h - font_size) / 2.0 - 1.0,
                 font_size,
                 color,
             });
         }
         labels
     }
+    fn set_selected(&mut self, selected: bool) {
+        self.selected = selected;
+    }
 }
 
 pub enum PageButton {
@@ -1758,8 +2127,7 @@ impl Widget for Sidebar {
 }
 
 pub struct Panel {
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
+    base: WidgetBase,
     dragging: bool,
     drag_ox: f32, drag_oy: f32,
     drag_start_x: f32, drag_start_y: f32,
@@ -1768,7 +2136,29 @@ pub struct Panel {
 
 impl Panel {
     pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, hovered: false, dragging: false, drag_ox: 0.0, drag_oy: 0.0, drag_start_x: 0.0, drag_start_y: 0.0, bounds: None }
+        Self {
+            base: WidgetBase {
+                x,
+                y,
+                w,
+                h,
+                label: None,
+                hovered: false,
+                row_x: 0.0,
+                row_w: 0.0,
+            },
+            dragging: false,
+            drag_ox: 0.0,
+            drag_oy: 0.0,
+            drag_start_x: 0.0,
+            drag_start_y: 0.0,
+            bounds: None,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
     }
 
     pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
@@ -1777,11 +2167,12 @@ impl Panel {
 }
 
 impl Widget for Panel {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn top_room(&self) -> f32 { 0.0 }
+    fn hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+
     fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
 
     fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
         self.bounds = Some((bx, by, bw, bh));
@@ -1810,13 +2201,13 @@ impl Widget for Panel {
         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.w), ny.clamp(by, by + bh - self.h))
+            (nx.clamp(bx, bx + bw - self.base.w), ny.clamp(by, by + bh - self.base.h))
         } else {
             (nx, ny)
         };
-        if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
-            self.x = nx;
-            self.y = ny;
+        if (nx - self.base.x).abs() > 0.01 || (ny - self.base.y).abs() > 0.01 {
+            self.base.x = nx;
+            self.base.y = ny;
             return true;
         }
         false
@@ -1824,10 +2215,10 @@ impl Widget for Panel {
 
     fn drag_begin(&mut self, px: f32, py: f32) {
         self.dragging = true;
-        self.drag_ox = px - self.x;
-        self.drag_oy = py - self.y;
-        self.drag_start_x = self.x;
-        self.drag_start_y = self.y;
+        self.drag_ox = px - self.base.x;
+        self.drag_oy = py - self.base.y;
+        self.drag_start_x = self.base.x;
+        self.drag_start_y = self.base.y;
     }
 
     fn drag_end(&mut self) { self.dragging = false; }
@@ -2036,30 +2427,31 @@ fn menu_item_w(menu_items: &[String], idx: usize) -> f32 {
 }
 
 pub struct Checkbox {
-    x: f32, y: f32, w: f32, h: f32,
-    hovering: bool,
+    base: WidgetBase,
     checked: bool,
     just_clicked: bool,
 }
 
 impl Checkbox {
     pub fn new() -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, checked: false, just_clicked: false }
+        Self {
+            base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            checked: false,
+            just_clicked: false,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
     }
 }
 
 impl Widget for Checkbox {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn color(&self) -> [f32; 4] { colors::CHECKBOX_BG }
-    fn set_hovered(&mut self, v: bool) { self.hovering = v; }
-    fn hovered(&self) -> bool { self.hovering }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
-    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovering;
-        self.hovering = self.hit_test(px, py);
-        was != self.hovering
-    }
+    fn color(&self) -> [f32; 4] { if self.checked { colors::CHECKBOX_CHECKED } else { colors::CHECKBOX_BG } }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left { return false; }
@@ -2076,34 +2468,46 @@ impl Widget for Checkbox {
         false
     }
 
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if self.checked {
+            let (x, y, w, h) = self.rect();
+            let pad_x = w * 0.25;
+            let pad_y = h * 0.25;
+            quads.push((x + pad_x, y + pad_y, w - 2.0 * pad_x, h - 2.0 * pad_y, [1.0, 1.0, 1.0, 0.9]));
+        }
+        quads
+    }
+
     fn take_click(&mut self) -> bool {
         if self.just_clicked { self.just_clicked = false; true } else { false }
     }
+    fn value(&self) -> i32 { if self.checked { 1 } else { 0 } }
 }
 
 #[derive(Debug, Clone)]
 pub struct Toggle {
-    x: f32, y: f32, w: f32, h: f32,
-    hovering: bool,
+    base: WidgetBase,
     toggled: bool,
     just_toggled: bool,
-    label: Option<String>,
-    row_x: f32,
-    row_w: f32,
 }
 
 impl Toggle {
     pub fn new() -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, toggled: false, just_toggled: false, label: None, row_x: 0.0, row_w: 0.0 }
+        Self {
+            base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            toggled: false,
+            just_toggled: false,
+        }
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.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.label = Some(label.to_string());
     }
 
     pub fn set_toggled(&mut self, v: bool) {
@@ -2116,32 +2520,10 @@ impl Toggle {
 }
 
 impl Widget for Toggle {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
-    fn color(&self) -> [f32; 4] { if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF } }
-    fn set_hovered(&mut self, v: bool) { self.hovering = v; }
-    fn hovered(&self) -> bool { self.hovering }
-
-    fn hit_test(&self, px: f32, py: f32) -> bool {
-        let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        let (hy, hh) = if self.label.is_some() {
-            (y - 18.0, h + 18.0)
-        } else {
-            (y, h)
-        };
-        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
-    }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
-    fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
-
-    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovering;
-        self.hovering = self.hit_test(px, py);
-        was != self.hovering
-    }
+    fn color(&self) -> [f32; 4] { if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF } }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left { return false; }
@@ -2159,62 +2541,37 @@ impl Widget for Toggle {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
         let bg = if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF };
-        quads.push((self.x, self.y, self.w, self.h, bg));
-        if self.hovering {
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            let (hy, hh) = if self.label.is_some() {
-                (self.y - 18.0, self.h + 18.0)
-            } else {
-                (self.y, self.h)
-            };
-            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
-        }
-        quads
+        vec![(self.base.x, self.base.y, self.base.w, self.base.h, bg)]
     }
 
     fn take_click(&mut self) -> bool {
         if self.just_toggled { self.just_toggled = false; true } else { false }
     }
-
-    fn text_labels(&self) -> Vec<TextLabel> {
-        if let Some(ref label) = self.label {
-            vec![TextLabel {
-                text: label.clone(),
-                x: self.x,
-                y: self.y - 18.0,
-                font_size: 12.0,
-                color: [0x83, 0x83, 0x8a],
-            }]
-        } else {
-            Vec::new()
-        }
-    }
 }
 
 #[derive(Debug, Clone)]
 pub struct Label {
-    x: f32, y: f32, w: f32, h: f32,
-    text: String,
+    base: WidgetBase,
     font_size: f32,
     color: [u8; 3],
-    hovered: bool,
-    row_x: f32,
-    row_w: f32,
 }
 
 impl Label {
     pub fn new(text: &str) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
-            text: text.to_string(),
+            base: WidgetBase {
+                x: 0.0,
+                y: 0.0,
+                w: 0.0,
+                h: 0.0,
+                label: Some(text.to_string()),
+                hovered: false,
+                row_x: 0.0,
+                row_w: 0.0,
+            },
             font_size: 12.0,
             color: [0x83, 0x83, 0x8a],
-            hovered: false,
-            row_x: 0.0,
-            row_w: 0.0,
         }
     }
 
@@ -2229,7 +2586,7 @@ impl Label {
     }
 
     pub fn set_text(&mut self, text: &str) {
-        self.text = text.to_string();
+        self.base.label = Some(text.to_string());
     }
 
     pub fn set_color(&mut self, color: [u8; 3]) {
@@ -2238,41 +2595,17 @@ impl Label {
 }
 
 impl Widget for Label {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
-    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
-
-    fn hit_test(&self, px: f32, py: f32) -> bool {
-        let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        px >= hx && px <= hx + hw && py >= y && py <= y + h
-    }
-
-    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovered;
-        self.hovered = self.hit_test(px, py);
-        was != self.hovered
-    }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn top_room(&self) -> f32 { 0.0 }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        if self.hovered {
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            quads.push((hx, self.y, hw, self.h, [1.0, 1.0, 1.0, 0.06]));
-        }
-        quads
-    }
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
 
     fn text_labels(&self) -> Vec<TextLabel> {
         vec![TextLabel {
-            text: self.text.clone(),
-            x: self.x,
-            y: self.y + (self.h - self.font_size) / 2.0,
+            text: self.base.label.clone().unwrap_or_default(),
+            x: self.base.x,
+            y: self.base.y + (self.base.h - self.font_size) / 2.0,
             font_size: self.font_size,
             color: self.color,
         }]
@@ -2369,8 +2702,7 @@ impl Widget for Svg {
 }
 
 pub struct Slider {
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
+    base: WidgetBase,
     dragging: bool,
     value: f32,
     drag_offset: f32,
@@ -2379,7 +2711,18 @@ pub struct Slider {
 
 impl Slider {
     pub fn new() -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, dragging: false, value: 0.5, drag_offset: 0.0, scroll_enabled: false }
+        Self {
+            base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            dragging: false,
+            value: 0.5,
+            drag_offset: 0.0,
+            scroll_enabled: false,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
     }
 
     pub fn with_scroll(mut self, enabled: bool) -> Self {
@@ -2393,18 +2736,17 @@ impl Slider {
 }
 
 impl Widget for Slider {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+
     fn color(&self) -> [f32; 4] { colors::SLIDER_TRACK }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
 
     fn draggable(&self) -> bool { true }
     fn is_dragging(&self) -> bool { self.dragging }
 
     fn drag_update(&mut self, px: f32, _py: f32) -> bool {
         let (sx, _, sw, _) = self.rect();
-        let thumb_size = self.h * 0.9;
+        let thumb_size = self.base.h * 0.9;
         let range = sw - thumb_size;
         let raw = (px - self.drag_offset - sx) / range;
         let new_val = raw.clamp(0.0, 1.0);
@@ -2417,8 +2759,8 @@ impl Widget for Slider {
 
     fn drag_begin(&mut self, px: f32, _py: f32) {
         self.dragging = true;
-        let thumb_size = self.h * 0.9;
-        let thumb_x = self.x + self.value * (self.w - thumb_size);
+        let thumb_size = self.base.h * 0.9;
+        let thumb_x = self.base.x + self.value * (self.base.w - thumb_size);
         self.drag_offset = px - thumb_x;
     }
 
@@ -2445,9 +2787,9 @@ impl Widget for Slider {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let thumb_size = self.h * 0.9;
-        let thumb_x = self.x + self.value * (self.w - thumb_size);
-        let thumb_y = self.y + (self.h - thumb_size) / 2.0;
+        let thumb_size = self.base.h * 0.9;
+        let thumb_x = self.base.x + self.value * (self.base.w - thumb_size);
+        let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
         let thumb_color = if self.dragging {
             colors::SLIDER_THUMB_DRAG
         } else {
@@ -2455,26 +2797,189 @@ impl Widget for Slider {
         };
         vec![(thumb_x, thumb_y, thumb_size, thumb_size, thumb_color)]
     }
+    fn value(&self) -> i32 { (self.value * 100.0) as i32 }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ActiveThumb {
+    Low,
+    High,
+}
+
+pub struct RangeSlider {
+    base: WidgetBase,
+    value_low: f32,
+    value_high: f32,
+    active_thumb: Option<ActiveThumb>,
+    drag_offset: f32,
+}
+
+impl RangeSlider {
+    pub fn new() -> Self {
+        Self {
+            base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            value_low: 0.2,
+            value_high: 0.8,
+            active_thumb: None,
+            drag_offset: 0.0,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
+    }
+
+    pub fn with_values(mut self, low: f32, high: f32) -> Self {
+        self.value_low = low.clamp(0.0, 1.0);
+        self.value_high = high.clamp(self.value_low, 1.0);
+        self
+    }
+
+    pub fn set_values(&mut self, low: f32, high: f32) {
+        self.value_low = low.clamp(0.0, 1.0);
+        self.value_high = high.clamp(self.value_low, 1.0);
+    }
+
+    pub fn values(&self) -> (f32, f32) {
+        (self.value_low, self.value_high)
+    }
+}
+
+impl Widget for RangeSlider {
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+
+    fn color(&self) -> [f32; 4] { colors::SLIDER_TRACK }
+
+    fn draggable(&self) -> bool { true }
+    fn is_dragging(&self) -> bool { self.active_thumb.is_some() }
+
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        let Some(active) = self.active_thumb else { return false; };
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        if range <= 0.0 { return false; }
+        
+        let new_val = ((px - self.drag_offset - self.base.x) / range).clamp(0.0, 1.0);
+        match active {
+            ActiveThumb::Low => {
+                let constrained = new_val.min(self.value_high);
+                if (constrained - self.value_low).abs() > 0.001 {
+                    self.value_low = constrained;
+                    return true;
+                }
+            }
+            ActiveThumb::High => {
+                let constrained = new_val.max(self.value_low);
+                if (constrained - self.value_high).abs() > 0.001 {
+                    self.value_high = constrained;
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    fn drag_begin(&mut self, px: f32, _py: f32) {
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        let thumb_low_x = self.base.x + self.value_low * range;
+        let thumb_high_x = self.base.x + self.value_high * range;
+        let center_low = thumb_low_x + thumb_size / 2.0;
+        let center_high = thumb_high_x + thumb_size / 2.0;
+
+        let active = if (self.value_low - self.value_high).abs() < 0.001 {
+            if px < center_low {
+                ActiveThumb::Low
+            } else {
+                ActiveThumb::High
+            }
+        } else {
+            let dist_low = (px - center_low).abs();
+            let dist_high = (px - center_high).abs();
+            if dist_low < dist_high {
+                ActiveThumb::Low
+            } else {
+                ActiveThumb::High
+            }
+        };
+
+        self.active_thumb = Some(active);
+        let active_x = match active {
+            ActiveThumb::Low => thumb_low_x,
+            ActiveThumb::High => thumb_high_x,
+        };
+        self.drag_offset = px - active_x;
+    }
+
+    fn drag_end(&mut self) {
+        self.active_thumb = None;
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let thumb_size = self.base.h * 0.9;
+        let range = self.base.w - thumb_size;
+        let thumb_low_x = self.base.x + self.value_low * range;
+        let thumb_high_x = self.base.x + self.value_high * range;
+        
+        let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
+        
+        // Highlighted track segment
+        let highlight_x = thumb_low_x + thumb_size / 2.0;
+        let highlight_w = thumb_high_x - thumb_low_x;
+        let highlight_y = self.base.y + self.base.h * 0.35;
+        let highlight_h = self.base.h * 0.3;
+        
+        let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+
+        let high_color = if self.active_thumb == Some(ActiveThumb::High) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+
+        vec![
+            (highlight_x, highlight_y, highlight_w, highlight_h, colors::PROGRESS_FILL),
+            (thumb_low_x, thumb_y, thumb_size, thumb_size, low_color),
+            (thumb_high_x, thumb_y, thumb_size, thumb_size, high_color),
+        ]
+    }
+
+    fn value(&self) -> i32 {
+        ((self.value_low * 100.0) as i32) | (((self.value_high * 100.0) as i32) << 16)
+    }
 }
 
 pub struct ProgressBar {
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
+    base: WidgetBase,
     value: f32,
 }
 
 impl ProgressBar {
     pub fn new(value: f32) -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, value }
+        Self {
+            base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+            value,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
     }
 }
 
 impl Widget for ProgressBar {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+    fn hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+
     fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
 }
 
 pub struct StatusBar {
@@ -2562,18 +3067,14 @@ impl Widget for Splitter {
 
 #[derive(Debug, Clone)]
 pub struct Spinbox {
-    x: f32, y: f32, w: f32, h: f32,
+    base: WidgetBase,
     pub value: i32,
     min: i32, max: i32, step: i32,
     editing: bool,
     edit_buffer: String,
-    hovered: bool,
     hover_dec: bool,
     hover_inc: bool,
-    label: Option<String>,
     unit: Option<String>,
-    row_x: f32,
-    row_w: f32,
     pub decimals: u32,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
@@ -2581,16 +3082,30 @@ pub struct Spinbox {
 
 impl Spinbox {
     pub fn new(value: i32, min: i32, max: i32, step: i32) -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value, min, max, step, editing: false, edit_buffer: String::new(), hovered: false, hover_dec: false, hover_inc: false, label: None, unit: None, row_x: 0.0, row_w: 0.0, decimals: 0, parent: None, children: Vec::new() }
+        Self {
+            base: WidgetBase::new(),
+            value,
+            min,
+            max,
+            step,
+            editing: false,
+            edit_buffer: String::new(),
+            hover_dec: false,
+            hover_inc: false,
+            unit: None,
+            decimals: 0,
+            parent: None,
+            children: Vec::new(),
+        }
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.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.label = Some(label.to_string());
     }
 
     pub fn with_unit(mut self, unit: &str) -> Self {
@@ -2609,45 +3124,31 @@ impl Spinbox {
 }
 
 impl Widget for Spinbox {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+
     fn color(&self) -> [f32; 4] { colors::SPINBOX_BG }
     fn value(&self) -> i32 { self.value }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
     fn widget_font(&self) -> Option<String> { Some("monospace".to_string()) }
 
-    fn hit_test(&self, px: f32, py: f32) -> bool {
-        let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        let (hy, hh) = if self.label.is_some() {
-            (y - 20.0, h + 20.0)
-        } else {
-            (y, h)
-        };
-        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
-    }
 
-    fn top_room(&self) -> f32 { if self.label.is_some() { 16.0 } else { 0.0 } }
 
     fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovered;
-        self.hovered = self.hit_test(px, py);
-        if !self.hovered {
+        let was = self.base.hovered;
+        self.base.hovered = self.hit_test(px, py);
+        if !self.base.hovered {
             let changed = self.hover_dec || self.hover_inc;
             self.hover_dec = false;
             self.hover_inc = false;
-            return changed || was != self.hovered;
+            return changed || was != self.base.hovered;
         }
-        let split = self.x + self.w * 0.55;
-        let hd = px >= split && px < split + self.w * 0.225;
-        let hi = px >= split + self.w * 0.225;
+        let split = self.base.x + self.base.w * 0.55;
+        let hd = px >= split && px < split + self.base.w * 0.225;
+        let hi = px >= split + self.base.w * 0.225;
         let changed = hd != self.hover_dec || hi != self.hover_inc;
         self.hover_dec = hd;
         self.hover_inc = hi;
-        changed || was != self.hovered
+        changed || was != self.base.hovered
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
@@ -2655,11 +3156,11 @@ impl Widget for Spinbox {
         if !self.hit_test(px, py) { return false; }
         match state {
             ElementState::Pressed => {
-                let split = self.x + self.w * 0.55;
-                if px >= split && px < split + self.w * 0.225 {
+                let split = self.base.x + self.base.w * 0.55;
+                if px >= split && px < split + self.base.w * 0.225 {
                     self.value = (self.value - self.step).max(self.min);
                     true
-                } else if px >= split + self.w * 0.225 {
+                } else if px >= split + self.base.w * 0.225 {
                     self.value = (self.value + self.step).min(self.max);
                     true
                 } else if px < split {
@@ -2747,24 +3248,10 @@ impl Widget for Spinbox {
         }
     }
 
-    fn hover_highlight(&self) -> Option<[f32; 4]> {
-        None // rendered in extra_quads to cover label area
-    }
-
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        if self.hovered {
-            let (hy, hh) = if self.label.is_some() {
-                (self.y - 20.0, self.h + 20.0)
-            } else {
-                (self.y, self.h)
-            };
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
-        }
-        let split = self.x + self.w * 0.55;
-        let btn_w = self.w * 0.225;
+        let split = self.base.x + self.base.w * 0.55;
+        let btn_w = self.base.w * 0.225;
         let inc_col = if self.hover_inc { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
         let dec_col = if self.hover_dec { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
         
@@ -2774,27 +3261,27 @@ impl Widget for Spinbox {
             colors::SPINBOX_DISPLAY
         };
         
-        quads.push((self.x, self.y, self.w * 0.55, self.h, display_bg));
-        quads.push((split, self.y, btn_w, self.h, dec_col));
-        quads.push((split + btn_w, self.y, btn_w, self.h, inc_col));
+        quads.push((self.base.x, self.base.y, self.base.w * 0.55, self.base.h, display_bg));
+        quads.push((split, self.base.y, btn_w, self.base.h, dec_col));
+        quads.push((split + btn_w, self.base.y, btn_w, self.base.h, inc_col));
         
         if self.editing {
             let border_color = [0.20, 0.50, 0.85, 1.0]; // Bright focused blue border
             // Top border
-            quads.push((self.x, self.y, self.w * 0.55, 1.0, border_color));
+            quads.push((self.base.x, self.base.y, self.base.w * 0.55, 1.0, border_color));
             // Bottom border
-            quads.push((self.x, self.y + self.h - 1.0, self.w * 0.55, 1.0, border_color));
+            quads.push((self.base.x, self.base.y + self.base.h - 1.0, self.base.w * 0.55, 1.0, border_color));
             // Left border
-            quads.push((self.x, self.y, 1.0, self.h, border_color));
+            quads.push((self.base.x, self.base.y, 1.0, self.base.h, border_color));
             // Right border
-            quads.push((self.x + self.w * 0.55 - 1.0, self.y, 1.0, self.h, border_color));
+            quads.push((self.base.x + self.base.w * 0.55 - 1.0, self.base.y, 1.0, self.base.h, border_color));
 
             // Caret cursor
             let char_width = 8.4;
-            let cursor_x = self.x + 4.0 + (self.edit_buffer.len() as f32 * char_width);
+            let cursor_x = self.base.x + 4.0 + (self.edit_buffer.len() as f32 * char_width);
             let max_cursor_x = split - 4.0;
             let final_cursor_x = cursor_x.min(max_cursor_x);
-            let cursor_y = self.y + (self.h - 14.0) / 2.0;
+            let cursor_y = self.base.y + (self.base.h - 14.0) / 2.0;
             quads.push((final_cursor_x, cursor_y, 1.5, 14.0, [0.80, 0.80, 0.85, 1.0]));
         }
         
@@ -2803,11 +3290,11 @@ impl Widget for Spinbox {
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        if let Some(ref label) = self.label {
+        if let Some(ref label) = self.base.label {
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.x + 4.0,
-                y: self.y - 16.0,
+                x: self.base.x + 4.0,
+                y: self.base.y - 18.0,
                 font_size: 12.0,
                 color: [0x83, 0x83, 0x8a],
             });
@@ -2822,31 +3309,32 @@ impl Widget for Spinbox {
         };
         labels.push(TextLabel {
             text: value_text,
-            x: self.x + 4.0,
-            y: self.y + (self.h - 14.0) / 2.0 - 2.0,
+            x: self.base.x + 4.0,
+            y: self.base.y + (self.base.h - 14.0) / 2.0 - 2.0,
             font_size: 14.0,
             color: [0xcc, 0xcc, 0xd4],
         });
         if let Some(ref unit) = self.unit {
             labels.push(TextLabel {
                 text: unit.clone(),
-                x: self.x + 4.0 + 36.0,
-                y: self.y + (self.h - 11.0) / 2.0 - 2.0,
+                x: self.base.x + 4.0 + 36.0,
+                y: self.base.y + (self.base.h - 11.0) / 2.0 - 2.0,
                 font_size: 11.0,
                 color: [0x73, 0x73, 0x7a],
             });
         }
+        let split = self.base.x + self.base.w * 0.55;
         labels.push(TextLabel {
             text: "-".to_string(),
-            x: self.x + self.w * 0.6625 - 4.0,
-            y: self.y + (self.h - 12.0) / 2.0 - 2.0,
+            x: self.base.x + self.base.w * 0.6625 - 4.0,
+            y: self.base.y + (self.base.h - 12.0) / 2.0 - 2.0,
             font_size: 12.0,
             color: [0xcc, 0xcc, 0xd4],
         });
         labels.push(TextLabel {
             text: "+".to_string(),
-            x: self.x + self.w * 0.8875 - 4.0,
-            y: self.y + (self.h - 12.0) / 2.0 - 2.0,
+            x: self.base.x + self.base.w * 0.8875 - 4.0,
+            y: self.base.y + (self.base.h - 12.0) / 2.0 - 2.0,
             font_size: 12.0,
             color: [0xcc, 0xcc, 0xd4],
         });
@@ -2868,15 +3356,11 @@ impl Drop for Spinbox {
 
 #[derive(Debug, Clone)]
 pub struct ColorSelector {
-    x: f32, y: f32, w: f32, h: f32,
+    base: WidgetBase,
     pub color: [u8; 3],
     just_clicked: bool,
-    hovered: bool,
     editing: bool,
     edit_buffer: String,
-    label: Option<String>,
-    row_x: f32,
-    row_w: f32,
     pub command: String,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
@@ -2885,15 +3369,11 @@ pub struct ColorSelector {
 impl ColorSelector {
     pub fn new(color: [u8; 3]) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            base: WidgetBase::new(),
             color,
             just_clicked: false,
-            hovered: false,
             editing: false,
             edit_buffer: String::new(),
-            label: None,
-            row_x: 0.0,
-            row_w: 0.0,
             command: "clear-color-interface".to_string(),
             parent: None,
             children: Vec::new(),
@@ -2901,7 +3381,7 @@ impl ColorSelector {
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.label = Some(label.to_string());
+        self.base.label = Some(label.to_string());
         self
     }
 
@@ -2912,25 +3392,10 @@ impl ColorSelector {
 }
 
 impl Widget for ColorSelector {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
-    fn hit_test(&self, px: f32, py: f32) -> bool {
-        let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        let (hy, hh) = if self.label.is_some() {
-            (y - 20.0, h + 20.0)
-        } else {
-            (y, h)
-        };
-        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
-    }
 
-    fn top_room(&self) -> f32 { if self.label.is_some() { 16.0 } else { 0.0 } }
 
     fn color(&self) -> [f32; 4] {
         colors::to_linear([
@@ -2941,17 +3406,11 @@ impl Widget for ColorSelector {
         ])
     }
 
-    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was = self.hovered;
-        self.hovered = self.hit_test(px, py);
-        was != self.hovered
-    }
-
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left { return false; }
         if state != ElementState::Pressed { return false; }
         if !self.hit_test(px, py) { return false; }
-        if px >= self.x + self.w * 0.65 {
+        if px >= self.base.x + self.base.w * 0.65 {
             self.just_clicked = true;
             let hex = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
             if let Ok(output) = std::process::Command::new(&self.command)
@@ -3031,41 +3490,27 @@ impl Widget for ColorSelector {
         }
     }
 
-    fn hover_highlight(&self) -> Option<[f32; 4]> {
-        None // rendered in extra_quads to cover label area
-    }
-
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        if self.hovered {
-            let (hy, hh) = if self.label.is_some() {
-                (self.y - 20.0, self.h + 20.0)
-            } else {
-                (self.y, self.h)
-            };
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
-        }
-        let pick_x = self.x + self.w * 0.65;
-        let pick_w = self.w * 0.35;
+        let pick_x = self.base.x + self.base.w * 0.65;
+        let pick_w = self.base.w * 0.35;
         let linear_c = colors::to_linear([
             self.color[0] as f32 / 255.0,
             self.color[1] as f32 / 255.0,
             self.color[2] as f32 / 255.0,
             1.0,
         ]);
-        quads.push((pick_x, self.y, pick_w, self.h, linear_c));
+        quads.push((pick_x, self.base.y, pick_w, self.base.h, linear_c));
         quads
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        if let Some(ref label) = self.label {
+        if let Some(ref label) = self.base.label {
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.x + 4.0,
-                y: self.y - 16.0,
+                x: self.base.x + 4.0,
+                y: self.base.y - 18.0,
                 font_size: 12.0,
                 color: [0x83, 0x83, 0x8a],
             });
@@ -3073,8 +3518,8 @@ impl Widget for ColorSelector {
         let hex = if self.editing { self.edit_buffer.clone() } else { format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]) };
         labels.push(TextLabel {
             text: hex,
-            x: self.x + 4.0,
-            y: self.y + 3.0,
+            x: self.base.x + 4.0,
+            y: self.base.y + 3.0,
             font_size: 12.0,
             color: [0xcc, 0xcc, 0xd4],
         });
@@ -3786,6 +4231,71 @@ impl Drop for ScrollBox {
 mod tests {
     use super::*;
 
+    #[test]
+    fn test_rangeslider_interaction() {
+        let mut rs = RangeSlider::new();
+        rs.set_rect(10.0, 10.0, 200.0, 20.0);
+
+        // Low value: 0.2, High value: 0.8
+        let (low, high) = rs.values();
+        assert_eq!(low, 0.2);
+        assert_eq!(high, 0.8);
+
+        // Thumb size = h * 0.9 = 18.0
+        // Range = w - thumb_size = 200.0 - 18.0 = 182.0
+        // Thumb low center: x + 0.2 * 182.0 + 9.0 = 10.0 + 36.4 + 9.0 = 55.4
+        // Thumb high center: x + 0.8 * 182.0 + 9.0 = 10.0 + 145.6 + 9.0 = 164.6
+
+        // 1. Drag Low thumb from 0.2 to 0.45
+        // Click at px = 55.4 (center of low thumb)
+        rs.drag_begin(55.4, 20.0);
+        assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
+
+        // Drag to px = 100.9 (new low value = (100.9 - offset(9.0) - 10.0) / 182.0 = 81.9 / 182.0 = 0.45)
+        let changed = rs.drag_update(100.9, 20.0);
+        assert!(changed);
+        assert!((rs.values().0 - 0.45).abs() < 0.01);
+        assert_eq!(rs.values().1, 0.8); // High value unchanged
+
+        rs.drag_end();
+        assert_eq!(rs.active_thumb, None);
+
+        // 2. Drag High thumb from 0.8 to 0.6
+        // Click at px = 164.6 (center of high thumb)
+        rs.drag_begin(164.6, 20.0);
+        assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
+
+        // Drag to px = 128.2 (new high value = (128.2 - offset(9.0) - 10.0) / 182.0 = 109.2 / 182.0 = 0.6)
+        let changed = rs.drag_update(128.2, 20.0);
+        assert!(changed);
+        assert!((rs.values().1 - 0.6).abs() < 0.01);
+
+        rs.drag_end();
+    }
+
+    #[test]
+    fn test_rangeslider_overlap() {
+        let mut rs = RangeSlider::new().with_values(0.5, 0.5);
+        rs.set_rect(10.0, 10.0, 200.0, 20.0);
+
+        // Both low and high are 0.5. Thumb center = 10.0 + 0.5 * 182.0 + 9.0 = 110.0
+        // Click to the left of center should select Low thumb
+        rs.drag_begin(109.0, 20.0);
+        assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
+        rs.drag_end();
+
+        // Click to the right of center should select High thumb
+        rs.drag_begin(111.0, 20.0);
+        assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
+        rs.drag_end();
+
+        // Drag Low thumb past High value (0.5). It should be constrained to 0.5
+        rs.drag_begin(110.0, 20.0); // selects low
+        rs.drag_update(150.0, 20.0); // drag past high
+        assert_eq!(rs.values().0, 0.5); // constrained
+        rs.drag_end();
+    }
+
 
     #[test]
     fn test_node_toggle_geometry_visibility() {
@@ -4436,16 +4946,12 @@ unsafe impl Sync for ScrollingList {}
 
 #[derive(Debug, Clone)]
 pub struct Dropdown {
-    x: f32, y: f32, w: f32, h: f32,
+    base: WidgetBase,
     pub options: Vec<String>,
     pub selected: usize,
     pub open: bool,
-    hovered: bool,
     hovered_item: Option<usize>,
     just_changed: bool,
-    label: Option<String>,
-    row_x: f32,
-    row_w: f32,
     pub parent: Option<*mut (dyn Widget + 'static)>,
     pub children: Vec<*mut (dyn Widget + 'static)>,
 }
@@ -4453,28 +4959,24 @@ pub struct Dropdown {
 impl Dropdown {
     pub fn new(options: Vec<String>, selected: usize) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            base: WidgetBase::new(),
             options,
             selected,
             open: false,
-            hovered: false,
             hovered_item: None,
             just_changed: false,
-            label: None,
-            row_x: 0.0,
-            row_w: 0.0,
             parent: None,
             children: Vec::new(),
         }
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.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.label = Some(label.to_string());
     }
 
     pub fn take_change(&mut self) -> bool {
@@ -4486,15 +4988,15 @@ impl Dropdown {
     pub fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
         if !self.open { return; }
         
-        let dy = self.y + self.h;
+        let dy = self.base.y + self.base.h;
         let dh = self.options.len() as f32 * 24.0;
         
-        pc.rect([0.22, 0.22, 0.28, 1.0], self.x, dy, self.w, dh); // border
-        pc.rect([0.06, 0.06, 0.09, 1.0], self.x + 1.0, dy + 1.0, self.w - 2.0, dh - 2.0); // bg
+        pc.rect([0.22, 0.22, 0.28, 1.0], self.base.x, dy, self.base.w, dh); // border
+        pc.rect([0.06, 0.06, 0.09, 1.0], self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0); // bg
         
         if let Some(h_idx) = self.hovered_item {
             let iy = dy + h_idx as f32 * 24.0;
-            pc.rect([0.20, 0.40, 0.65, 0.6], self.x + 2.0, iy + 2.0, self.w - 4.0, 20.0);
+            pc.rect([0.20, 0.40, 0.65, 0.6], self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0);
         }
         
         for (idx, opt) in self.options.iter().enumerate() {
@@ -4509,7 +5011,7 @@ impl Dropdown {
             
             pc.text(
                 opt,
-                self.x + 8.0,
+                self.base.x + 8.0,
                 iy,
                 12.0,
                 [
@@ -4530,11 +5032,8 @@ impl Default for Dropdown {
 }
 
 impl Widget for Dropdown {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
 
     fn color(&self) -> [f32; 4] {
         [0.08, 0.08, 0.12, 1.0]
@@ -4542,13 +5041,11 @@ impl Widget for Dropdown {
 
     fn hit_test(&self, px: f32, py: f32) -> bool {
         let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        let (hy, hh) = if self.label.is_some() {
-            (y - 18.0, h + 18.0)
-        } else {
-            (y, h)
-        };
+        let hx = if self.base.row_w > 0.0 { self.base.row_x } else { x };
+        let hw = if self.base.row_w > 0.0 { self.base.row_w } else { w };
+        let top = self.top_room();
+        let hy = y - top;
+        let hh = h + top;
         if self.open {
             let dy = y + h;
             let dh = self.options.len() as f32 * 24.0;
@@ -4560,13 +5057,11 @@ impl Widget for Dropdown {
         }
     }
 
-    fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
-
     fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
-        let was_hovered = self.hovered;
+        let was_hovered = self.base.hovered;
         let was_hovered_item = self.hovered_item;
         
-        self.hovered = self.hit_test(px, py);
+        self.base.hovered = self.hit_test(px, py);
         self.hovered_item = None;
 
         if self.open {
@@ -4581,14 +5076,14 @@ impl Widget for Dropdown {
             }
         }
 
-        self.hovered != was_hovered || self.hovered_item != was_hovered_item
+        self.base.hovered != was_hovered || self.hovered_item != was_hovered_item
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left || state != ElementState::Pressed { return false; }
 
         let (x, y, w, h) = self.rect();
-        let (hy, hh) = if self.label.is_some() {
+        let (hy, hh) = if self.base.label.is_some() {
             (y - 18.0, h + 18.0)
         } else {
             (y, h)
@@ -4687,29 +5182,32 @@ impl Widget for Dropdown {
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
-        
-        if self.hovered && !self.open {
-            let (hy, hh) = if self.label.is_some() {
-                (self.y - 18.0, self.h + 18.0)
-            } else {
-                (self.y, self.h)
-            };
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.04]));
-        }
 
         let bg_color = [0.08, 0.08, 0.12, 1.0];
         let border_color = if self.open {
             [0.30, 0.50, 0.32, 1.0]
-        } else if self.hovered {
+        } else if self.base.hovered {
             [0.25, 0.25, 0.35, 1.0]
         } else {
             [0.18, 0.18, 0.24, 1.0]
         };
 
-        quads.push((self.x, self.y, self.w, self.h, border_color));
-        quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, bg_color));
+        quads.push((self.base.x, self.base.y, self.base.w, self.base.h, border_color));
+        quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, bg_color));
+
+        if self.open {
+            let dy = self.base.y + self.base.h;
+            let dh = self.options.len() as f32 * 24.0;
+            // Border
+            quads.push((self.base.x, dy, self.base.w, dh, [0.22, 0.22, 0.28, 1.0]));
+            // BG
+            quads.push((self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0, [0.06, 0.06, 0.09, 1.0]));
+
+            if let Some(h_idx) = self.hovered_item {
+                let iy = dy + h_idx as f32 * 24.0;
+                quads.push((self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0, [0.20, 0.40, 0.65, 0.6]));
+            }
+        }
 
         quads
     }
@@ -4717,11 +5215,11 @@ impl Widget for Dropdown {
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
         
-        if let Some(ref label) = self.label {
+        if let Some(ref label) = self.base.label {
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.x + 4.0,
-                y: self.y - 14.0,
+                x: self.base.x + 4.0,
+                y: self.base.y - 14.0,
                 font_size: 11.0,
                 color: [0x83, 0x83, 0x8a],
             });
@@ -4730,20 +5228,41 @@ impl Widget for Dropdown {
         let selected_text = self.options.get(self.selected).cloned().unwrap_or_default();
         labels.push(TextLabel {
             text: selected_text,
-            x: self.x + 8.0,
-            y: self.y + (self.h - 12.0) / 2.0,
+            x: self.base.x + 8.0,
+            y: self.base.y + (self.base.h - 12.0) / 2.0,
             font_size: 12.0,
             color: [0xdd, 0xdd, 0xe2],
         });
 
         labels.push(TextLabel {
             text: "â–¼".to_string(),
-            x: self.x + self.w - 18.0,
-            y: self.y + (self.h - 10.0) / 2.0,
+            x: self.base.x + self.base.w - 18.0,
+            y: self.base.y + (self.base.h - 10.0) / 2.0,
             font_size: 10.0,
             color: [0x83, 0x83, 0x8a],
         });
 
+        if self.open {
+            let dy = self.base.y + self.base.h;
+            for (idx, opt) in self.options.iter().enumerate() {
+                let iy = dy + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
+                let text_color = if self.hovered_item == Some(idx) {
+                    [0xff, 0xff, 0xff]
+                } else if self.selected == idx {
+                    [0x3a, 0x9a, 0xff]
+                } else {
+                    [0xcc, 0xcc, 0xd4]
+                };
+                labels.push(TextLabel {
+                    text: opt.clone(),
+                    x: self.base.x + 8.0,
+                    y: iy,
+                    font_size: 12.0,
+                    color: text_color,
+                });
+            }
+        }
+
         labels
     }
 
@@ -4752,6 +5271,15 @@ impl Widget for Dropdown {
     fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
     fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
     fn clear_children(&mut self) { self.children.clear(); }
+    fn value(&self) -> i32 { self.selected as i32 }
+    fn take_click(&mut self) -> bool { self.take_change() }
+    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        if self.open {
+            Some((self.base.x, self.base.y + self.base.h, self.base.w, self.options.len() as f32 * 24.0))
+        } else {
+            None
+        }
+    }
 }
 
 unsafe impl Send for Dropdown {}
@@ -4761,15 +5289,11 @@ unsafe impl Sync for Dropdown {}
 
 #[derive(Debug, Clone)]
 pub struct TextBox {
-    x: f32, y: f32, w: f32, h: f32,
+    base: WidgetBase,
     pub text: String,
     pub editing: bool,
     pub edit_buffer: String,
-    hovered: bool,
     just_changed: bool,
-    label: Option<String>,
-    row_x: f32,
-    row_w: f32,
     pub disabled: bool,
     pub all_selected: bool,
     pub cursor_idx: usize,
@@ -4787,15 +5311,11 @@ pub struct TextBox {
 impl TextBox {
     pub fn new(text: String) -> Self {
         Self {
-            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            base: WidgetBase::new(),
             text,
             editing: false,
             edit_buffer: String::new(),
-            hovered: false,
             just_changed: false,
-            label: None,
-            row_x: 0.0,
-            row_w: 0.0,
             disabled: false,
             all_selected: false,
             cursor_idx: 0,
@@ -4817,12 +5337,12 @@ impl TextBox {
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
-        self.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.label = Some(label.to_string());
     }
 
     pub fn take_change(&mut self) -> bool {
@@ -4878,10 +5398,9 @@ impl TextBox {
             self.cursor_idx = start;
             self.select_anchor = None;
             self.all_selected = false;
-            true
-        } else {
-            false
+            return true;
         }
+        false
     }
 
     pub fn paste_from_clipboard(&mut self) -> bool {
@@ -4928,60 +5447,55 @@ impl Default for TextBox {
 }
 
 impl Widget for TextBox {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
+
+    fn 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.x = x;
-        self.y = y;
-        self.w = if let Some(explicit_w) = self.width {
+        let final_w = if let Some(explicit_w) = self.width {
             explicit_w
         } else if let Some(max_w) = self.max_width {
             w.min(max_w)
         } else {
             w
         };
-        self.h = h;
+        if let Some(b) = self.base_mut() {
+            b.x = x;
+            b.y = y;
+            b.w = final_w;
+            b.h = h;
+        }
     }
     fn set_row_rect(&mut self, x: f32, w: f32) {
-        self.row_x = x;
-        self.row_w = if let Some(explicit_w) = self.width {
+        let final_w = if let Some(explicit_w) = self.width {
             explicit_w
         } else if let Some(max_w) = self.max_width {
             w.min(max_w)
         } else {
             w
         };
+        if let Some(b) = self.base_mut() {
+            b.row_x = x;
+            b.row_w = final_w;
+        }
     }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
 
     fn color(&self) -> [f32; 4] {
         [0.10, 0.10, 0.16, 1.0]
     }
 
-    fn hit_test(&self, px: f32, py: f32) -> bool {
-        let (x, y, w, h) = self.rect();
-        let hx = if self.row_w > 0.0 { self.row_x } else { x };
-        let hw = if self.row_w > 0.0 { self.row_w } else { w };
-        let (hy, hh) = if self.label.is_some() {
-            (y - 18.0, h + 18.0)
-        } else {
-            (y, h)
-        };
-        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
-    }
 
-    fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
 
     fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
         if self.disabled {
-            let was = self.hovered;
-            self.hovered = false;
+            let was = self.base.hovered;
+            self.base.hovered = false;
             return was;
         }
         let mut changed = false;
         if self.dragging && self.editing {
             let char_width = 7.2;
-            let drag_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+            let drag_idx = (((px - (self.base.x + 8.0)) / char_width).round() as isize)
                 .max(0)
                 .min(self.edit_buffer.chars().count() as isize) as usize;
             if self.cursor_idx != drag_idx {
@@ -4994,9 +5508,9 @@ impl Widget for TextBox {
                 changed = true;
             }
         }
-        let was = self.hovered;
-        self.hovered = self.hit_test(px, py);
-        if was != self.hovered {
+        let was = self.base.hovered;
+        self.base.hovered = self.hit_test(px, py);
+        if was != self.base.hovered {
             changed = true;
         }
         changed
@@ -5014,7 +5528,7 @@ impl Widget for TextBox {
     fn drag_update(&mut self, px: f32, _py: f32) -> bool {
         if self.disabled || !self.editing { return false; }
         let char_width = 7.2;
-        let drag_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+        let drag_idx = (((px - (self.base.x + 8.0)) / char_width).round() as isize)
             .max(0)
             .min(self.edit_buffer.chars().count() as isize) as usize;
         if self.cursor_idx != drag_idx {
@@ -5033,152 +5547,78 @@ impl Widget for TextBox {
         self.dragging = false;
     }
 
+    fn value(&self) -> i32 { 0 }
+
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if self.disabled { return false; }
-        
-        let (x, y, w, h) = self.rect();
-        let (hy, hh) = if self.label.is_some() {
-            (y - 18.0, h + 18.0)
-        } else {
-            (y, h)
-        };
-        let is_inside = px >= x && px <= x + w && py >= hy && py <= hy + hh;
-
-        if button == MouseButton::Right {
-            if state == ElementState::Pressed {
-                if !is_inside {
-                    return false;
-                }
-                
-                // Focus the TextBox if it wasn't editing yet
+        if button != MouseButton::Left { return false; }
+        if !self.hit_test(px, py) { return false; }
+        match state {
+            ElementState::Pressed => {
                 if !self.editing {
-                    self.editing = true;
-                    self.edit_buffer = self.text.clone();
-                    let len = self.edit_buffer.chars().count();
-                    self.cursor_idx = len;
-                    self.select_anchor = Some(0);
-                    self.all_selected = len > 0;
-                    self.just_focused = true;
-                    focus::set_focused(self);
-                }
-
-                // Place cursor under click if clicked inside the content bounds
-                if px >= x && px <= x + w && py >= y && py <= y + h {
+                    self.focus();
+                } else {
                     let char_width = 7.2;
-                    let click_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+                    let idx = (((px - (self.base.x + 8.0)) / char_width).round() as isize)
                         .max(0)
                         .min(self.edit_buffer.chars().count() as isize) as usize;
-                    self.cursor_idx = click_idx;
-                    self.select_anchor = Some(click_idx);
+                    self.cursor_idx = idx;
+                    self.select_anchor = Some(idx);
                     self.all_selected = false;
                 }
-                
-                // Spawn context menu
-                let options = vec![
-                    "Cut".to_string(),
-                    "Copy".to_string(),
-                    "Paste".to_string(),
-                    "Select All".to_string(),
-                ];
-                context_menu::show(px, py, options, self as *mut TextBox);
-                return true;
+                true
             }
-            return false;
-        }
-
-        if button != MouseButton::Left { return false; }
-        
-        let (x, y, w, h) = self.rect();
-        let (hy, hh) = if self.label.is_some() {
-            (y - 18.0, h + 18.0)
-        } else {
-            (y, h)
-        };
-        let is_inside = px >= x && px <= x + w && py >= hy && py <= hy + hh;
-
-        if state == ElementState::Pressed {
-            if !is_inside {
-                if self.editing {
-                    self.unfocus();
-                    return true;
+            ElementState::Released => {
+                if self.dragging {
+                    self.drag_end();
                 }
-                return false;
-            }
-            
-            let char_width = 7.2;
-            let click_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
-                .max(0)
-                .min(self.edit_buffer.chars().count() as isize) as usize;
-            
-            if self.editing {
-                self.cursor_idx = click_idx;
-                self.select_anchor = Some(click_idx);
-                self.all_selected = false;
-                self.just_focused = false;
-                self.drag_start_idx = Some(click_idx);
-            } else {
-                self.editing = true;
-                self.edit_buffer = self.text.clone();
-                let len = self.edit_buffer.chars().count();
-                self.cursor_idx = len;
-                self.select_anchor = Some(0);
-                self.all_selected = len > 0;
-                self.just_focused = true;
-                self.drag_start_idx = Some(click_idx);
-                focus::set_focused(self);
-            }
-            true
-        } else if state == ElementState::Released {
-            if is_inside && self.editing && self.just_focused {
-                let len = self.edit_buffer.chars().count();
-                self.cursor_idx = len;
-                self.select_anchor = Some(0);
-                self.all_selected = len > 0;
+                if self.select_anchor == Some(self.cursor_idx) {
+                    self.select_anchor = None;
+                }
+                true
             }
-            self.just_focused = false;
-            self.drag_start_idx = None;
-            true
-        } else {
-            false
         }
     }
 
     fn focus(&mut self) {
         if self.disabled { return; }
-        if !self.editing {
-            self.editing = true;
-            self.edit_buffer = self.text.clone();
-            let len = self.edit_buffer.chars().count();
-            self.cursor_idx = len;
-            self.select_anchor = Some(0);
-            self.all_selected = len > 0;
-            self.just_focused = true;
-            focus::set_focused(self);
-        }
+        self.editing = true;
+        self.edit_buffer = self.text.clone();
+        let len = self.edit_buffer.chars().count();
+        self.cursor_idx = len;
+        self.select_anchor = Some(0);
+        self.all_selected = len > 0;
+        self.just_focused = true;
+        focus::set_focused(self);
     }
 
     fn unfocus(&mut self) {
         if self.editing {
             self.editing = false;
-            self.all_selected = false;
-            self.select_anchor = None;
-            self.dragging = false;
-            self.just_focused = false;
-            self.drag_start_idx = None;
             if self.text != self.edit_buffer {
                 self.text = self.edit_buffer.clone();
                 self.just_changed = true;
             }
+            self.select_anchor = None;
+            self.all_selected = false;
         }
     }
 
     fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
-        if self.disabled { return false; }
-        if !self.editing { return false; }
+        if !self.editing || self.disabled { return false; }
         if event.state != ElementState::Pressed { return false; }
-        let char_count = self.edit_buffer.chars().count();
+        
+        let control = event.ctrl;
+        
         match &event.logical_key {
             Key::Named(NamedKey::Backspace) => {
+                if self.all_selected {
+                    self.edit_buffer.clear();
+                    self.cursor_idx = 0;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                    return true;
+                }
                 let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
                 let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
                 if start != end {
@@ -5193,24 +5633,26 @@ impl Widget for TextBox {
                     self.edit_buffer = new_buf;
                     self.cursor_idx = start;
                     self.select_anchor = None;
-                    self.all_selected = false;
-                } else if self.cursor_idx > 0 {
-                    let chars: Vec<char> = self.edit_buffer.chars().collect();
-                    let mut new_buf = String::new();
-                    for i in 0..(self.cursor_idx - 1) {
-                        new_buf.push(chars[i]);
-                    }
-                    for i in self.cursor_idx..chars.len() {
-                        new_buf.push(chars[i]);
-                    }
-                    self.edit_buffer = new_buf;
+                    return true;
+                }
+                if self.cursor_idx > 0 {
+                    let mut chars: Vec<char> = self.edit_buffer.chars().collect();
+                    chars.remove(self.cursor_idx - 1);
+                    self.edit_buffer = chars.into_iter().collect();
                     self.cursor_idx -= 1;
                     self.select_anchor = None;
-                    self.all_selected = false;
+                    return true;
                 }
-                true
+                false
             }
             Key::Named(NamedKey::Delete) => {
+                if self.all_selected {
+                    self.edit_buffer.clear();
+                    self.cursor_idx = 0;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                    return true;
+                }
                 let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
                 let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
                 if start != end {
@@ -5225,160 +5667,157 @@ impl Widget for TextBox {
                     self.edit_buffer = new_buf;
                     self.cursor_idx = start;
                     self.select_anchor = None;
-                    self.all_selected = false;
-                } else if self.cursor_idx < char_count {
-                    let chars: Vec<char> = self.edit_buffer.chars().collect();
-                    let mut new_buf = String::new();
-                    for i in 0..self.cursor_idx {
-                        new_buf.push(chars[i]);
-                    }
-                    for i in (self.cursor_idx + 1)..chars.len() {
-                        new_buf.push(chars[i]);
-                    }
-                    self.edit_buffer = new_buf;
+                    return true;
+                }
+                if self.cursor_idx < self.edit_buffer.chars().count() {
+                    let mut chars: Vec<char> = self.edit_buffer.chars().collect();
+                    chars.remove(self.cursor_idx);
+                    self.edit_buffer = chars.into_iter().collect();
                     self.select_anchor = None;
-                    self.all_selected = false;
+                    return true;
                 }
-                true
+                false
             }
             Key::Named(NamedKey::ArrowLeft) => {
-                if event.shift {
+                let shift = event.shift;
+                if shift {
                     if self.select_anchor.is_none() {
                         self.select_anchor = Some(self.cursor_idx);
                     }
                     if self.cursor_idx > 0 {
                         self.cursor_idx -= 1;
+                        true
+                    } else {
+                        false
                     }
                 } else {
-                    let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                    let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                    if start != end {
-                        self.cursor_idx = start;
+                    if let Some(anchor) = self.select_anchor {
+                        self.cursor_idx = anchor.min(self.cursor_idx);
+                        self.select_anchor = None;
+                        self.all_selected = false;
+                        true
                     } else if self.cursor_idx > 0 {
                         self.cursor_idx -= 1;
+                        true
+                    } else {
+                        false
                     }
-                    self.select_anchor = None;
                 }
-                let len = self.edit_buffer.chars().count();
-                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                self.all_selected = start == 0 && end == len && len > 0;
-                true
             }
             Key::Named(NamedKey::ArrowRight) => {
-                if event.shift {
+                let shift = event.shift;
+                if shift {
                     if self.select_anchor.is_none() {
                         self.select_anchor = Some(self.cursor_idx);
                     }
-                    if self.cursor_idx < char_count {
+                    if self.cursor_idx < self.edit_buffer.chars().count() {
                         self.cursor_idx += 1;
+                        true
+                    } else {
+                        false
                     }
                 } else {
-                    let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                    let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                    if start != end {
-                        self.cursor_idx = end;
-                    } else if self.cursor_idx < char_count {
+                    if let Some(anchor) = self.select_anchor {
+                        self.cursor_idx = anchor.max(self.cursor_idx);
+                        self.select_anchor = None;
+                        self.all_selected = false;
+                        true
+                    } else if self.cursor_idx < self.edit_buffer.chars().count() {
                         self.cursor_idx += 1;
+                        true
+                    } else {
+                        false
+                    }
+                }
+            }
+            Key::Named(NamedKey::ArrowUp) => {
+                let shift = event.shift;
+                if shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
+                    }
+                } else {
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                }
+                self.cursor_idx = 0;
+                true
+            }
+            Key::Named(NamedKey::ArrowDown) => {
+                let shift = event.shift;
+                if shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
                     }
+                } else {
                     self.select_anchor = None;
+                    self.all_selected = false;
                 }
-                let len = self.edit_buffer.chars().count();
-                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                self.all_selected = start == 0 && end == len && len > 0;
+                self.cursor_idx = self.edit_buffer.chars().count();
                 true
             }
-            Key::Named(NamedKey::ArrowUp) | Key::Named(NamedKey::Home) => {
-                if event.shift {
+            Key::Named(NamedKey::Home) => {
+                let shift = event.shift;
+                if shift {
                     if self.select_anchor.is_none() {
                         self.select_anchor = Some(self.cursor_idx);
                     }
-                    self.cursor_idx = 0;
                 } else {
-                    self.cursor_idx = 0;
                     self.select_anchor = None;
+                    self.all_selected = false;
                 }
-                let len = self.edit_buffer.chars().count();
-                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                self.all_selected = start == 0 && end == len && len > 0;
+                self.cursor_idx = 0;
                 true
             }
-            Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::End) => {
-                if event.shift {
+            Key::Named(NamedKey::End) => {
+                let shift = event.shift;
+                if shift {
                     if self.select_anchor.is_none() {
                         self.select_anchor = Some(self.cursor_idx);
                     }
-                    self.cursor_idx = char_count;
                 } else {
-                    self.cursor_idx = char_count;
                     self.select_anchor = None;
+                    self.all_selected = false;
                 }
-                let len = self.edit_buffer.chars().count();
-                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
-                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                self.all_selected = start == 0 && end == len && len > 0;
+                self.cursor_idx = self.edit_buffer.chars().count();
                 true
             }
             Key::Named(NamedKey::Enter) => {
-                self.text = self.edit_buffer.clone();
-                self.editing = false;
-                self.all_selected = false;
-                self.select_anchor = None;
-                self.just_changed = true;
+                self.unfocus();
                 true
             }
             Key::Named(NamedKey::Escape) => {
                 self.editing = false;
-                self.all_selected = false;
+                self.edit_buffer = self.text.clone();
                 self.select_anchor = None;
+                self.all_selected = false;
                 true
             }
-            Key::Character(ref s) if event.ctrl => {
-                match s.as_str() {
-                    "c" | "C" => {
-                        self.copy_selection();
-                        true
-                    }
-                    "x" | "X" => {
-                        if self.cut_selection() {
-                            self.just_changed = true;
-                        }
-                        true
-                    }
-                    "v" | "V" => {
-                        if self.paste_from_clipboard() {
-                            self.just_changed = true;
-                        }
-                        true
-                    }
-                    "a" | "A" => {
-                        self.select_all();
-                        true
-                    }
-                    _ => false,
-                }
+            Key::Character(ref ch_str) if control && (ch_str == "a" || ch_str == "A") => {
+                self.cursor_idx = self.edit_buffer.chars().count();
+                self.select_anchor = Some(0);
+                self.all_selected = true;
+                true
             }
-            _ => {
-                if event.ctrl {
-                    return false;
-                }
-                if let Some(text) = &event.text {
+            Key::Character(ref ch_str) if control && (ch_str == "c" || ch_str == "C") => {
+                self.copy_selection();
+                true
+            }
+            Key::Character(ref ch_str) if control && (ch_str == "x" || ch_str == "X") => {
+                self.cut_selection();
+                true
+            }
+            Key::Character(ref ch_str) if control && (ch_str == "v" || ch_str == "V") => {
+                if let Some(pasted) = clipboard::read_from_clipboard() {
                     let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
                     let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-                    let chars: Vec<char> = self.edit_buffer.chars().collect();
                     let mut new_buf = String::new();
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
                     for i in 0..start {
                         new_buf.push(chars[i]);
                     }
-                    let mut inserted_count = 0;
-                    for ch in text.chars() {
-                        if !ch.is_control() {
-                            new_buf.push(ch);
-                            inserted_count += 1;
-                        }
-                    }
+                    let inserted_count = pasted.chars().count();
+                    new_buf.push_str(&pasted);
                     for i in end..chars.len() {
                         new_buf.push(chars[i]);
                     }
@@ -5392,30 +5831,40 @@ impl Widget for TextBox {
                 }
                 true
             }
+            _ => {
+                if let Some(text) = &event.text {
+                    if !control {
+                        let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                        let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                        let mut new_buf = String::new();
+                        let chars: Vec<char> = self.edit_buffer.chars().collect();
+                        for i in 0..start {
+                            new_buf.push(chars[i]);
+                        }
+                        let inserted_count = text.chars().count();
+                        new_buf.push_str(text);
+                        for i in end..chars.len() {
+                            new_buf.push(chars[i]);
+                        }
+                        self.edit_buffer = new_buf;
+                        self.cursor_idx = start + inserted_count;
+                        self.select_anchor = None;
+                        self.all_selected = false;
+                        return true;
+                    }
+                }
+                false
+            }
         }
     }
 
-    fn hover_highlight(&self) -> Option<[f32; 4]> {
-        None
-    }
-
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
         if self.disabled {
-            quads.push((self.x, self.y, self.w, self.h, [0.12, 0.12, 0.16, 1.0]));
-            quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, [0.06, 0.06, 0.08, 1.0]));
+            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, [0.12, 0.12, 0.16, 1.0]));
+            quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, [0.06, 0.06, 0.08, 1.0]));
             return quads;
         }
-        if self.hovered {
-            let (hy, hh) = if self.label.is_some() {
-                (self.y - 18.0, self.h + 18.0)
-            } else {
-                (self.y, self.h)
-            };
-            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
-            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
-            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
-        }
         let bg_color = if self.editing {
             [0.06, 0.10, 0.18, 1.0]
         } else {
@@ -5423,13 +5872,13 @@ impl Widget for TextBox {
         };
         let border_color = if self.editing {
             [0.20, 0.50, 0.85, 1.0]
-        } else if self.hovered {
+        } else if self.base.hovered {
             [0.25, 0.25, 0.35, 1.0]
         } else {
             [0.18, 0.18, 0.24, 1.0]
         };
-        quads.push((self.x, self.y, self.w, self.h, border_color));
-        quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, bg_color));
+        quads.push((self.base.x, self.base.y, self.base.w, self.base.h, border_color));
+        quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, bg_color));
 
         if self.editing {
             let char_width = 7.2;
@@ -5437,22 +5886,22 @@ impl Widget for TextBox {
             let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
             
             if start != end {
-                let highlight_x = self.x + 8.0 + (start as f32 * char_width);
-                let max_x = self.x + self.w - 6.0;
+                let highlight_x = self.base.x + 8.0 + (start as f32 * char_width);
+                let max_x = self.base.x + self.base.w - 6.0;
                 let highlight_w = ((end - start) as f32 * char_width).min(max_x - highlight_x).max(0.0);
                 quads.push((
                     highlight_x,
-                    self.y + (self.h - 16.0) / 2.0,
+                    self.base.y + (self.base.h - 16.0) / 2.0,
                     highlight_w,
                     16.0,
                     [0.20, 0.40, 0.75, 0.4],
                 ));
             }
 
-            let cursor_x = self.x + 8.0 + (self.cursor_idx as f32 * char_width);
-            let max_cursor_x = self.x + self.w - 6.0;
+            let cursor_x = self.base.x + 8.0 + (self.cursor_idx as f32 * char_width);
+            let max_cursor_x = self.base.x + self.base.w - 6.0;
             let final_cursor_x = cursor_x.min(max_cursor_x);
-            let cursor_y = self.y + (self.h - 14.0) / 2.0;
+            let cursor_y = self.base.y + (self.base.h - 14.0) / 2.0;
             quads.push((final_cursor_x, cursor_y, 1.5, 14.0, [0.80, 0.80, 0.85, 1.0]));
         }
 
@@ -5461,11 +5910,11 @@ impl Widget for TextBox {
 
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        if let Some(ref label) = self.label {
+        if let Some(ref label) = self.base.label {
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.x + 4.0,
-                y: self.y - 14.0,
+                x: self.base.x + 4.0,
+                y: self.base.y - 14.0,
                 font_size: 11.0,
                 color: [0x83, 0x83, 0x8a],
             });
@@ -5480,8 +5929,8 @@ impl Widget for TextBox {
         }
         labels.push(TextLabel {
             text: val_text,
-            x: self.x + 8.0,
-            y: self.y + (self.h - 12.0) / 2.0,
+            x: self.base.x + 8.0,
+            y: self.base.y + (self.base.h - 12.0) / 2.0,
             font_size: 12.0,
             color: if self.disabled {
                 [0x53, 0x53, 0x5a]
@@ -5512,4 +5961,358 @@ impl Drop for TextBox {
 unsafe impl Send for TextBox {}
 unsafe impl Sync for TextBox {}
 
+pub struct Paginator {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    sidebar_w: f32,
+    pages: Vec<String>,
+    selected_page: usize,
+    hovered_tab: Option<usize>,
+    pressed_tab: Option<usize>,
+    page_changed: bool,
+    parent: Option<*mut (dyn Widget + 'static)>,
+    children: Vec<*mut (dyn Widget + 'static)>,
+}
+
+impl Paginator {
+    pub fn new(sidebar_w: f32, pages: Vec<String>) -> Self {
+        Self {
+            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            hovered: false,
+            sidebar_w,
+            pages,
+            selected_page: 0,
+            hovered_tab: None,
+            pressed_tab: None,
+            page_changed: false,
+            parent: None,
+            children: Vec::new(),
+        }
+    }
+
+    pub fn selected_page(&self) -> usize {
+        self.selected_page
+    }
+
+    pub fn set_selected_page(&mut self, page: usize) {
+        if page < self.pages.len() {
+            self.selected_page = page;
+        }
+    }
+}
+
+impl Widget for Paginator {
+    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        quads.push((self.x, self.y, self.sidebar_w, self.h, colors::SIDEBAR_BG));
+        quads.push((self.x + self.sidebar_w, self.y, self.w - self.sidebar_w, self.h, colors::CONTENT_BG));
+
+        for i in 0..self.pages.len() {
+            let bx = self.x + 5.0;
+            let by = self.y + 10.0 + i as f32 * 50.0;
+            let bw = self.sidebar_w - 10.0;
+            let bh = 40.0;
+
+            let c = if self.selected_page == i {
+                [0.20, 0.40, 0.65, 0.4]
+            } else if self.pressed_tab == Some(i) {
+                [0.20, 0.20, 0.25, 0.25]
+            } else if self.hovered_tab == Some(i) {
+                [0.20, 0.20, 0.25, 0.15]
+            } else {
+                [0.0, 0.0, 0.0, 0.0]
+            };
+            
+            if c[3] > 0.0 {
+                quads.push((bx, by, bw, bh, c));
+            }
+        }
+
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        for (i, page_name) in self.pages.iter().enumerate() {
+            let bx = self.x + 5.0;
+            let by = self.y + 10.0 + i as f32 * 50.0;
+            let bw = self.sidebar_w - 10.0;
+            let bh = 40.0;
+
+            let font_size = 12.0;
+            let est_w = page_name.len() as f32 * 6.5;
+            let color = if self.selected_page == i {
+                [230, 230, 242]
+            } else {
+                [178, 178, 191]
+            };
+
+            labels.push(TextLabel {
+                text: page_name.clone(),
+                x: bx + (bw - est_w) / 2.0,
+                y: by + (bh - font_size) / 2.0 - 1.0,
+                font_size,
+                color,
+            });
+        }
+        labels
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        let was_hovered_tab = self.hovered_tab;
+        self.hovered_tab = None;
+        for i in 0..self.pages.len() {
+            let bx = self.x + 5.0;
+            let by = self.y + 10.0 + i as f32 * 50.0;
+            let bw = self.sidebar_w - 10.0;
+            let bh = 40.0;
+            if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
+                self.hovered_tab = Some(i);
+                break;
+            }
+        }
+        was_hovered_tab != self.hovered_tab
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left { return false; }
+        match state {
+            ElementState::Pressed => {
+                self.pressed_tab = None;
+                for i in 0..self.pages.len() {
+                    let bx = self.x + 5.0;
+                    let by = self.y + 10.0 + i as f32 * 50.0;
+                    let bw = self.sidebar_w - 10.0;
+                    let bh = 40.0;
+                    if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
+                        self.pressed_tab = Some(i);
+                        return true;
+                    }
+                }
+            }
+            ElementState::Released => {
+                if let Some(i) = self.pressed_tab.take() {
+                    let bx = self.x + 5.0;
+                    let by = self.y + 10.0 + i as f32 * 50.0;
+                    let bw = self.sidebar_w - 10.0;
+                    let bh = 40.0;
+                    if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
+                        if self.selected_page != i {
+                            self.selected_page = i;
+                            self.page_changed = true;
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+        false
+    }
+
+    fn take_click(&mut self) -> bool {
+        if self.page_changed {
+            self.page_changed = false;
+            true
+        } else {
+            false
+        }
+    }
+
+    fn value(&self) -> i32 { self.selected_page as i32 }
+
+    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
+    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
+    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
+    fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
+    fn clear_children(&mut self) { self.children.clear(); }
+}
+
+unsafe impl Send for Paginator {}
+unsafe impl Sync for Paginator {}
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
+pub struct Finger {
+    pub slot: usize,
+    pub x: f32,
+    pub y: f32,
+}
+
+#[derive(Debug, Clone)]
+pub struct Trackpad {
+    base: WidgetBase,
+    pub fingers: Vec<Finger>,
+}
+
+impl Trackpad {
+    pub fn new() -> Self {
+        Self {
+            base: WidgetBase {
+                x: 0.0,
+                y: 0.0,
+                w: 0.0,
+                h: 0.0,
+                label: None,
+                hovered: false,
+                row_x: 0.0,
+                row_w: 0.0,
+            },
+            fingers: Vec::new(),
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.base.label = Some(label.to_string());
+        self
+    }
+
+    pub fn set_fingers(&mut self, fingers: Vec<Finger>) {
+        self.fingers = fingers;
+    }
+}
+
+impl Widget for Trackpad {
+    fn base(&self) -> Option<&WidgetBase> {
+        Some(&self.base)
+    }
+
+    fn base_mut(&mut self) -> Option<&mut WidgetBase> {
+        Some(&mut self.base)
+    }
+
+    fn color(&self) -> [f32; 4] {
+        [0.11, 0.11, 0.16, 0.85]
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (x, y, w, h) = self.rect();
+        let mut quads = Vec::new();
+
+        // 1. Background
+        quads.push((x, y, w, h, [0.11, 0.11, 0.16, 0.85]));
+
+        // 2. Borders
+        let border_color = [0.28, 0.28, 0.38, 1.0];
+        quads.push((x, y, w, 1.0, border_color));             // Top
+        quads.push((x, y + h - 1.0, w, 1.0, border_color));     // Bottom
+        quads.push((x, y, 1.0, h, border_color));             // Left
+        quads.push((x + w - 1.0, y, 1.0, h, border_color));     // Right
+
+        // 3. Fingers
+        for finger in &self.fingers {
+            let rx = finger.x.clamp(0.0, 1.0);
+            let ry = finger.y.clamp(0.0, 1.0);
+            let fx = x + rx * w;
+            let fy = y + ry * h;
+            let dot_size = 12.0;
+
+            // Render glow (outer light blue rectangle)
+            quads.push((
+                fx - (dot_size + 6.0) / 2.0,
+                fy - (dot_size + 6.0) / 2.0,
+                dot_size + 6.0,
+                dot_size + 6.0,
+                [0.35, 0.55, 0.95, 0.4],
+            ));
+            // Render core (solid blue/purple rectangle)
+            quads.push((
+                fx - dot_size / 2.0,
+                fy - dot_size / 2.0,
+                dot_size,
+                dot_size,
+                [0.45, 0.65, 1.0, 1.0],
+            ));
+        }
+
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let (x, y, _w, h) = self.rect();
+        let mut labels = Vec::new();
+
+        // Render "Touchpad Area" label
+        labels.push(TextLabel {
+            text: "Touchpad Area".to_string(),
+            x: x + 12.0,
+            y: y + h - 22.0,
+            font_size: 11.0,
+            color: [0x73, 0x73, 0x8c],
+        });
+
+        // Optional widget-base label on top
+        if let Some(ref label) = self.base.label {
+            labels.push(TextLabel {
+                text: label.clone(),
+                x,
+                y: y - 18.0,
+                font_size: 12.0,
+                color: [0x83, 0x83, 0x8a],
+            });
+        }
+
+        labels
+    }
+
+    fn top_room(&self) -> f32 {
+        if self.base.label.is_some() {
+            16.0
+        } else {
+            0.0
+        }
+    }
+
+    fn draggable(&self) -> bool { true }
+    fn is_dragging(&self) -> bool { !self.fingers.is_empty() }
+
+    fn drag_begin(&mut self, px: f32, py: f32) {
+        let (x, y, w, h) = self.rect();
+        if w > 0.0 && h > 0.0 {
+            let rx = ((px - x) / w).clamp(0.0, 1.0);
+            let ry = ((py - y) / h).clamp(0.0, 1.0);
+            self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
+        }
+    }
+
+    fn drag_update(&mut self, px: f32, py: f32) -> bool {
+        let (x, y, w, h) = self.rect();
+        if w > 0.0 && h > 0.0 {
+            let rx = ((px - x) / w).clamp(0.0, 1.0);
+            let ry = ((py - y) / h).clamp(0.0, 1.0);
+            self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
+            true
+        } else {
+            false
+        }
+    }
+
+    fn drag_end(&mut self) {
+        self.fingers.clear();
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button == MouseButton::Left {
+            let (x, y, w, h) = self.rect();
+            if px >= x && px <= x + w && py >= y && py <= y + h {
+                if state == ElementState::Pressed {
+                    let rx = ((px - x) / w).clamp(0.0, 1.0);
+                    let ry = ((py - y) / h).clamp(0.0, 1.0);
+                    self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
+                    return true;
+                } else {
+                    self.fingers.clear();
+                    return true;
+                }
+            }
+        }
+        false
+    }
+}
+
+