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

commit5760c1228d1df63098ec47230d300858fd7dee7d
parent8bcc0c39eb
authorLucas Galante <[email protected]>
date2026-05-21 19:08
Add Breadcrumb widget, Section layout helper, and unit tests

- Add new Breadcrumb widget for path navigation and rendering
- Add Section layout helper to layout.rs for bordered, labeled content sections
- Refactor Column and Row layout helpers to remove direct RenderTarget lifetimes
- Update UI colors in color.rs (node colors, canvas, viewport, panel menus, and splitters)
- Add unit tests for Node geometry toggle visibility and MenuBar orientation labels
- Ignore .antigravitycli/ folder

 .gitignore    |    2 +
 Cargo.lock    |   20 -
 src/color.rs  |   16 +-
 src/layout.rs |  108 +++--
 src/main.rs   |    2 +-
 src/widget.rs | 1225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 6 files changed, 1278 insertions(+), 95 deletions(-)

diff --git a/.gitignore b/.gitignore
index ea8c4bf..62737aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
 /target
+.antigravitycli/
+
diff --git a/Cargo.lock b/Cargo.lock
index 55463fd..cfd45fe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -238,7 +238,6 @@ dependencies = [
  "bytemuck",
  "glyphon",
  "pollster",
- "taffy",
  "tokio",
  "wgpu",
  "winit",
@@ -657,12 +656,6 @@ dependencies = [
  "bitflags 2.11.1",
 ]
 
-[[package]]
-name = "grid"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5"
-
 [[package]]
 name = "hashbrown"
 version = "0.15.5"
@@ -1621,7 +1614,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
 dependencies = [
  "serde_core",
- "serde_derive",
 ]
 
 [[package]]
@@ -1831,18 +1823,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "taffy"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859"
-dependencies = [
- "arrayvec",
- "grid",
- "serde",
- "slotmap",
-]
-
 [[package]]
 name = "termcolor"
 version = "1.4.1"
diff --git a/src/color.rs b/src/color.rs
index 3dd05c3..5650a2a 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -1,9 +1,12 @@
 pub const HEADER_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
 pub const HEADER_ACCENT: [f32; 4] = [0.60, 0.40, 0.20, 1.0];
 pub const SIDEBAR_BG: [f32; 4] = [0.10, 0.10, 0.13, 1.0];
-pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 1.0];
+pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 0.2];
 pub const PANEL_IDLE: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
 pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
+pub const NODE_IDLE: [f32; 4] = [0.10, 0.45, 0.70, 1.0];
+pub const NODE_SELECTED: [f32; 4] = [0.20, 0.65, 0.90, 1.0];
+pub const NODE_DRAG: [f32; 4] = [0.30, 0.80, 1.00, 1.0];
 pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
 pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 1.0];
 pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 1.0];
@@ -29,6 +32,17 @@ pub const SPINBOX_BUTTON: [f32; 4] = [0.25, 0.25, 0.32, 1.0];
 pub const SPINBOX_BUTTON_HOVER: [f32; 4] = [0.35, 0.35, 0.42, 1.0];
 pub const SPINBOX_DISPLAY: [f32; 4] = [0.12, 0.12, 0.16, 1.0];
 
+pub const CANVAS_BG: [f32; 4] = [0.05, 0.05, 0.10, 1.0];
+pub const VIEWPORT_BG: [f32; 4] = [0.08, 0.08, 0.12, 0.25];
+pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
+
+pub const PANEL_MENU_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+pub const PANEL_MENU_HOVER: [f32; 4] = [0.18, 0.18, 0.25, 1.0];
+
+pub const SPLITTER_IDLE: [f32; 4] = [0.20, 0.20, 0.27, 1.0];
+pub const SPLITTER_HOVER: [f32; 4] = [0.40, 0.40, 0.50, 1.0];
+pub const SPLITTER_DRAG: [f32; 4] = [0.50, 0.50, 0.60, 1.0];
+
 pub const TEXT_FG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
 pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
diff --git a/src/layout.rs b/src/layout.rs
index 8a167c6..f63ad43 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -5,11 +5,15 @@ pub trait RenderTarget {
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
 }
 
-fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
+pub fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
     w.set_rect(x, y, ww, wh);
     for (qx, qy, qw, qh, qc) in w.extra_quads() {
         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);
+    }
     for label in w.text_labels() {
         pc.text(
             &label.text,
@@ -26,8 +30,7 @@ fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32
     }
 }
 
-pub struct Column<'a> {
-    pc: &'a mut dyn RenderTarget,
+pub struct Column {
     ox: f32,
     oy: f32,
     cx: f32,
@@ -35,57 +38,63 @@ pub struct Column<'a> {
     pub cw: f32,
 }
 
-impl<'a> Column<'a> {
-    pub fn new(pc: &'a mut dyn RenderTarget, ox: f32, oy: f32, cx: f32, cy: f32, cw: f32) -> Self {
-        Self { pc, ox, oy, cx, y: cy, cw }
+impl Column {
+    pub fn new(ox: f32, oy: f32, cx: f32, cy: f32, cw: f32) -> Self {
+        Self { ox, oy, cx, y: cy, cw }
     }
 
-    fn ax(&self, x_off: f32) -> f32 {
+    pub fn ax(&self, x_off: f32) -> f32 {
         self.ox + self.cx + x_off
     }
 
-    fn ay(&self) -> f32 {
+    pub fn ay(&self) -> f32 {
         self.oy + self.y
     }
 
+    pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+        pc.rect(color, self.ax(x_off), self.ay(), w, h);
+        self.y += h;
+    }
+
+    pub fn advance(&mut self, dy: f32) {
+        self.y += dy;
+    }
+
     pub fn spacing(&mut self, dy: f32) {
         self.y += dy;
     }
 
-    pub fn separator(&mut self) {
+    pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
         let x = self.ax(8.0);
         let y = self.ay();
-        self.pc
-            .rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
+        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
         self.y += 8.0;
     }
 
-    pub fn header(&mut self, text: &str, x_off: f32) {
+    pub fn header(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32) {
         let x = self.ax(x_off);
         let y = self.ay();
-        self.pc
-            .text(text, x, y, 14.0, [0.83, 0.83, 0.83, 1.0]);
+        pc.text(text, x, y, 14.0, [0.83, 0.83, 0.83, 1.0]);
         self.y += 22.0;
     }
 
-    pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+    pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
         let x = self.ax(x_off);
         let y = self.ay() + y_off;
-        self.pc
-            .text(text, x, y, font_size, color);
+        pc.text(text, x, y, font_size, color);
     }
 
-    pub fn widget<T: Widget>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+    pub fn widget<T: Widget>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
         let x = self.ax(x_off);
         let y = self.ay();
-        render_widget(self.pc, w, x, y, ww, wh);
-        self.y += wh;
+        render_widget(pc, w, x, y, ww, wh);
+        self.y += wh + w.top_room();
     }
 
-    pub fn row<F: FnOnce(&mut Row)>(&mut self, h: f32, f: F) {
+    pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
         let row_y = self.ay();
         let mut row = Row {
-            pc: &mut *self.pc,
+            pc: &mut *pc,
             base_x: self.ox + self.cx,
             y: row_y,
         };
@@ -110,3 +119,58 @@ impl<'a> Row<'a> {
         render_widget(self.pc, w, self.base_x + x_off, self.y, ww, wh);
     }
 }
+
+pub struct Section {
+    left: f32,
+    top: f32,
+    pub content_y: f32,
+    pub cw: f32,
+}
+
+impl Section {
+    pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
+        pc.text(label, left + 12.0, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
+        pc.rect([0.18, 0.18, 0.27, 1.0], left + 8.0, top + 22.0, cw - 16.0, 1.0);
+        Self { left, top, content_y: top + 40.0, cw }
+    }
+
+    pub fn ax(&self, x_off: f32) -> f32 { self.left + x_off }
+
+    pub fn ay(&self) -> f32 { self.content_y }
+
+    pub fn spacing(&mut self, dy: f32) { self.content_y += dy; }
+
+    pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+        pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
+    }
+
+    pub fn widget<T: Widget>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+        render_widget(pc, w, self.ax(x_off), self.ay(), ww, wh);
+        self.content_y += wh + w.top_room();
+    }
+
+    pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
+        let x = self.ax(8.0);
+        let y = self.ay();
+        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
+        self.content_y += 8.0;
+    }
+
+    pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+        pc.rect(color, self.ax(x_off), self.ay(), w, h);
+        self.content_y += h;
+    }
+
+    pub fn finish(&mut self, pc: &mut dyn RenderTarget) -> f32 {
+        let border: [f32; 4] = [0.25, 0.25, 0.35, 1.0];
+        let x = self.left + 8.0;
+        let y = self.top + 22.0;
+        let w = self.cw - 16.0;
+        let h = self.content_y - y;
+        pc.rect(border, x, y, w, 1.0);
+        pc.rect(border, x, y + h + 4.0, w, 1.0);
+        pc.rect(border, x, y, 1.0, h + 4.0);
+        pc.rect(border, x + w - 1.0, y, 1.0, h + 4.0);
+        self.content_y + 12.0
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 5b69fc1..48040ca 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ use winit::event::{ElementState, MouseButton, WindowEvent};
 use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
 use winit::window::{Window, WindowAttributes};
 
-use clear_ui::widget::{Button, Checkbox, ContentBg, Header, Panel, ProgressBar, Sidebar, Slider, Spinbox, StatusBar, Toggle, Widget};
+use clear_ui::widget::{Button, Checkbox, ContentBg, Header, Panel, ProgressBar, Sidebar, Slider, Spinbox, StatusBar, TextLabel, Toggle, Widget};
 
 use glyphon::{
     Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
diff --git a/src/widget.rs b/src/widget.rs
index 45af781..79a11f8 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -23,6 +23,12 @@ pub trait Widget {
     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 set_hovered(&mut self, _hovered: bool) {}
+    fn hovered(&self) -> bool { false }
+    fn hover_highlight(&self) -> Option<[f32; 4]> {
+        if self.hovered() { Some([1.0, 1.0, 1.0, 0.06]) } else { None }
+    }
+
     fn color(&self) -> [f32; 4];
 
     fn is_dragging(&self) -> bool { false }
@@ -35,38 +41,549 @@ pub trait Widget {
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
     fn text_labels(&self) -> Vec<TextLabel> { Vec::new() }
     fn value(&self) -> i32 { 0 }
+    fn top_room(&self) -> f32 { 0.0 }
+
+    fn set_drag_bounds(&mut self, _bx: f32, _by: f32, _bw: f32, _bh: f32) {}
 
     fn focus(&mut self) {}
     fn unfocus(&mut self) {}
     fn keyboard_input(&mut self, _event: &KeyEvent) -> bool { false }
+
+    fn menu_click(&mut self) -> Option<(usize, usize)> { None }
+    fn set_item_checked(&mut self, _menu_idx: usize, _item_idx: usize, _checked: bool) {}
+    fn set_grid_snap(&mut self, _gx: f32, _gy: f32) {}
+    fn set_node_name(&mut self, _name: &str) {}
+    fn set_visible(&mut self, _visible: bool) {}
+    fn visible(&self) -> bool { true }
+    fn set_path(&mut self, _segments: &[String]) {}
+    fn path_click(&mut self) -> Option<usize> { None }
+
+    fn node_params(&self) -> Vec<(String, String)> { vec![] }
+    fn set_display_params(&mut self, _params: &[(String, String)]) {}
+
+    fn set_config_toggle(&mut self, _id: usize, _val: bool) {}
+    fn take_config_toggle(&mut self) -> Option<(usize, bool)> { None }
+    fn set_show_network_grid(&mut self, _show: bool) {}
+    fn set_config_spin(&mut self, _id: usize, _val: f32) {}
+    fn take_config_spin(&mut self) -> Option<(usize, f32)> { None }
+    fn set_grid_sizes(&mut self, _gx: f32, _gy: f32) {}
+    fn set_grid_origin(&mut self, _ox: f32, _oy: f32) {}
+    fn set_palette_state(&mut self, _visible: bool, _query: &str, _items: &[String], _selected: usize) {}
+
+    fn set_geom_visible(&mut self, _visible: bool) {}
+    fn geom_visible(&self) -> bool { true }
+    fn take_geom_toggle(&mut self) -> bool { false }
 }
 
 pub struct Header {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
 }
 
 impl Header {
-    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
 }
 
 impl Widget for Header {
     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::HEADER_BG }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
 }
 
 pub struct ContentBg {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    show_network_grid: bool,
+    grid_size_x: f32,
+    grid_size_y: f32,
+    grid_origin_x: f32,
+    grid_origin_y: f32,
 }
 
 impl ContentBg {
-    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+    pub fn new() -> Self {
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 75.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0 }
+    }
 }
 
 impl Widget for ContentBg {
     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::CONTENT_BG }
+    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 { false }
+
+    fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+    fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
+            return vec![];
+        }
+        let mut quads = Vec::new();
+        let grid_color = [0.25, 0.25, 0.32, 1.0];
+
+        let start_y = self.y;
+        let diff_y = start_y - self.grid_origin_y;
+        let k_y = (diff_y / self.grid_size_y).floor();
+        let mut y = self.grid_origin_y + k_y * self.grid_size_y;
+        while y < self.y + self.h {
+            if y >= self.y {
+                quads.push((self.x, y, self.w, 1.0, grid_color));
+            }
+            y += self.grid_size_y;
+        }
+
+        let start_x = self.x;
+        let diff_x = start_x - self.grid_origin_x;
+        let k_x = (diff_x / self.grid_size_x).floor();
+        let mut x = self.grid_origin_x + k_x * self.grid_size_x;
+        while x < self.x + self.w {
+            if x >= self.x {
+                quads.push((x, self.y, 1.0, self.h, grid_color));
+            }
+            x += self.grid_size_x;
+        }
+        quads
+    }
+}
+
+pub struct ViewportBg {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+}
+
+impl ViewportBg {
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Widget for ViewportBg {
+    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::VIEWPORT_BG }
+    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 { false }
+}
+
+pub struct ParametersBg {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    display_params: Vec<(String, String)>,
+}
+
+impl ParametersBg {
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, display_params: Vec::new() }
+    }
+}
+
+impl Widget for ParametersBg {
+    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn color(&self) -> [f32; 4] { colors::PARAM_BG }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+    fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
+
+    fn set_display_params(&mut self, params: &[(String, String)]) {
+        self.display_params = params.to_vec();
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        self.display_params.iter().enumerate().map(|(i, (name, value))| {
+            TextLabel {
+                text: format!("{}: {}", name, value),
+                x: self.x + 8.0,
+                y: self.y + 30.0 + (i as f32) * 20.0,
+                font_size: 12.0,
+                color: [0xaa, 0xaa, 0xbb],
+            }
+        }).collect()
+    }
+}
+
+pub struct Canvas {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+}
+
+impl Canvas {
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Widget for Canvas {
+    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 hit_test(&self, _px: f32, _py: f32) -> bool { false }
+}
+
+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 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 vertical: bool,
+}
+
+impl MenuBar {
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+        Self {
+            x, y, w, h, hovering: false,
+            title: String::new(),
+            menu_items: Vec::new(),
+            vertical_items: Vec::new(),
+            menu_dropdowns: Vec::new(),
+            menu_dropdown_checked: Vec::new(),
+            hovered_menu: None,
+            open_menu: None,
+            hovered_dropdown: None,
+            clicked_dropdown: None,
+            was_open: None,
+            vertical: false,
+        }
+    }
+
+    pub fn with_title(mut self, title: &str) -> Self {
+        self.title = title.to_string();
+        self
+    }
+
+    pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
+        self.menu_items.push(label.to_string());
+        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()]);
+        self
+    }
+
+    pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
+        self.menu_items.push(horizontal_label.to_string());
+        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()]);
+        self
+    }
+
+    pub fn with_vertical(mut self, vertical: bool) -> Self {
+        self.vertical = vertical;
+        self
+    }
+
+    fn item_y_vertical(&self, idx: usize) -> f32 {
+        let mut y = 8.0;
+        if !self.title.is_empty() {
+            y += 24.0;
+        }
+        y + idx as f32 * 24.0
+    }
+
+    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.vertical {
+            let total_h = if self.menu_items.is_empty() {
+                self.h
+            } else {
+                let last_idx = self.menu_items.len() - 1;
+                self.item_y_vertical(last_idx) + self.item_h_vertical()
+            };
+            (self.x, self.y, self.w, total_h)
+        } else {
+            (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::PANEL_MENU_BG }
+    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 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 {
+                return true;
+            }
+        }
+        false
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        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;
+        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;
+                        }
+                    }
+                }
+            }
+            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);
+                        }
+                    }
+                }
+            }
+        }
+
+        was != self.hovering || old_menu != self.hovered_menu || old_dd != self.hovered_dropdown
+    }
+
+    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; }
+
+        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.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.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;
+            }
+        }
+
+        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;
+                    }
+                }
+            }
+        }
+
+        false
+    }
+
+    fn unfocus(&mut self) {
+        self.was_open = self.open_menu.take();
+    }
+
+    fn menu_click(&mut self) -> Option<(usize, usize)> {
+        self.clicked_dropdown.take()
+    }
+
+    fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+        if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
+            if item_idx < menu.len() {
+                menu[item_idx] = Some(checked);
+            }
+        }
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        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));
+                }
+            }
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        if !self.title.is_empty() {
+            labels.push(TextLabel {
+                text: self.title.clone(),
+                x: self.x + 8.0,
+                y: self.y + 7.0,
+                font_size: 12.0,
+                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),
+                    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],
+                    });
+                }
+            }
+        }
+        labels
+    }
 }
 
 pub enum ButtonKind {
@@ -95,6 +612,8 @@ 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 color(&self) -> [f32; 4] {
         match self.kind {
             ButtonKind::Primary => {
@@ -149,28 +668,37 @@ pub enum PageButton {
 
 pub struct Sidebar {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
 }
 
 impl Sidebar {
-    pub fn new(w: f32) -> Self { Self { x: 0.0, y: 0.0, w, h: 0.0 } }
+    pub fn new(w: f32) -> Self { Self { x: 0.0, y: 0.0, w, h: 0.0, hovered: false } }
 }
 
 impl Widget for Sidebar {
     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::SIDEBAR_BG }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
 }
 
 pub struct Panel {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
     dragging: bool,
     drag_ox: f32, drag_oy: f32,
     drag_start_x: f32, drag_start_y: f32,
+    bounds: Option<(f32, f32, f32, f32)>,
 }
 
 impl Panel {
     pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self { x, y, w, h, dragging: false, drag_ox: 0.0, drag_oy: 0.0, drag_start_x: 0.0, drag_start_y: 0.0 }
+        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 }
+    }
+
+    pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
+        self.bounds = Some((bx, by, bw, bh));
     }
 }
 
@@ -178,6 +706,12 @@ 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 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));
+    }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
         if button != MouseButton::Left { return false; }
@@ -201,6 +735,11 @@ impl Widget for Panel {
     fn drag_update(&mut self, px: f32, py: f32) -> bool {
         let nx = px - self.drag_ox;
         let ny = py - self.drag_oy;
+        let (nx, ny) = if let Some((bx, by, bw, bh)) = self.bounds {
+            (nx.clamp(bx, bx + bw - self.w), ny.clamp(by, by + bh - self.h))
+        } else {
+            (nx, ny)
+        };
         if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
             self.x = nx;
             self.y = ny;
@@ -220,6 +759,196 @@ impl Widget for Panel {
     fn drag_end(&mut self) { self.dragging = false; }
 }
 
+pub struct Node {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    selected: bool,
+    dragging: bool,
+    drag_ox: f32, drag_oy: f32,
+    bounds: Option<(f32, f32, f32, f32)>,
+    grid_snap_x: f32,
+    grid_snap_y: f32,
+    grid_origin_x: f32,
+    grid_origin_y: f32,
+    name: String,
+    pub parameters: Vec<(String, String)>,
+    geom_visible: bool,
+    geom_toggled: bool,
+    toggle_hovered: bool,
+}
+
+impl Node {
+    pub fn new(x: f32, y: f32, w: f32, h: f32, name: &str) -> Self {
+        Self {
+            x, y, w, h,
+            hovered: false, selected: false, dragging: false, drag_ox: 0.0, drag_oy: 0.0,
+            bounds: None, grid_snap_x: 0.0, grid_snap_y: 0.0,
+            grid_origin_x: 0.0, grid_origin_y: 0.0,
+            name: name.to_string(), parameters: Vec::new(),
+            geom_visible: true, geom_toggled: false, toggle_hovered: false,
+        }
+    }
+
+    pub fn with_params(mut self, params: &[(&str, &str)]) -> Self {
+        self.parameters = params.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
+        self
+    }
+
+    pub fn with_grid_snap(mut self, gx: f32, gy: f32) -> Self {
+        self.grid_snap_x = gx;
+        self.grid_snap_y = gy;
+        self
+    }
+
+    fn toggle_rect(&self) -> (f32, f32, f32, f32) {
+        (self.x + self.w - 30.0, self.y + (self.h - 18.0) / 2.0, 18.0, 18.0)
+    }
+}
+
+impl Widget for Node {
+    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] {
+        if self.dragging { colors::NODE_DRAG }
+        else if self.selected { colors::NODE_SELECTED }
+        else { colors::NODE_IDLE }
+    }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    fn focus(&mut self) { self.selected = true; }
+    fn unfocus(&mut self) { self.selected = false; }
+
+    fn node_params(&self) -> Vec<(String, String)> { self.parameters.clone() }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        vec![TextLabel {
+            text: self.name.clone(),
+            x: self.x + 8.0,
+            y: self.y + (self.h - 12.0) / 2.0,
+            font_size: 14.0,
+            color: [0xcc, 0xcc, 0xd4],
+        }]
+    }
+
+    fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
+        self.bounds = Some((bx, by, bw, bh));
+    }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        let was_hovered = self.hovered;
+        self.hovered = self.hit_test(px, py);
+
+        let was_toggle_hovered = self.toggle_hovered;
+        let (tx, ty, tw, th) = self.toggle_rect();
+        self.toggle_hovered = px >= tx && px < tx + tw && py >= ty && py < ty + th;
+
+        was_hovered != self.hovered || was_toggle_hovered != self.toggle_hovered
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if button != MouseButton::Left { return false; }
+        match state {
+            ElementState::Pressed => {
+                if self.hit_test(px, py) {
+                    let (tx, ty, tw, th) = self.toggle_rect();
+                    if px >= tx && px < tx + tw && py >= ty && py < ty + th {
+                        self.geom_visible = !self.geom_visible;
+                        self.geom_toggled = true;
+                        return true;
+                    }
+                    self.drag_begin(px, py);
+                    return true;
+                }
+            }
+            ElementState::Released => {
+                if self.dragging { self.drag_end(); return true; }
+            }
+        }
+        false
+    }
+
+    fn is_dragging(&self) -> bool { self.dragging }
+    fn draggable(&self) -> bool { !self.toggle_hovered }
+
+    fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_snap_x = gx; self.grid_snap_y = gy; }
+    fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+    fn set_node_name(&mut self, name: &str) { self.name = name.to_string(); }
+
+    fn drag_update(&mut self, px: f32, py: f32) -> bool {
+        let nx = px - self.drag_ox;
+        let ny = py - self.drag_oy;
+        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))
+        } else {
+            (nx, ny)
+        };
+        let nx = if self.grid_snap_x > 0.0 {
+            let relative = nx - self.grid_origin_x;
+            let snapped = (relative / self.grid_snap_x).round() * self.grid_snap_x;
+            snapped + self.grid_origin_x
+        } else { nx };
+        let ny = if self.grid_snap_y > 0.0 {
+            let relative = ny - self.grid_origin_y;
+            let snapped = (relative / self.grid_snap_y).round() * self.grid_snap_y;
+            snapped + self.grid_origin_y
+        } else { ny };
+        if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
+            self.x = nx;
+            self.y = ny;
+            return true;
+        }
+        false
+    }
+
+    fn drag_begin(&mut self, px: f32, py: f32) {
+        self.dragging = true;
+        self.drag_ox = px - self.x;
+        self.drag_oy = py - self.y;
+    }
+
+    fn drag_end(&mut self) { self.dragging = false; }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (tx, ty, tw, th) = self.toggle_rect();
+        let bg_color = if self.toggle_hovered {
+            colors::TOGGLE_HOVER
+        } else {
+            colors::TOGGLE_OFF
+        };
+
+        let mut quads = vec![
+            (tx, ty, tw, th, bg_color)
+        ];
+
+        if self.geom_visible {
+            let inset = 3.0;
+            quads.push((tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON));
+        }
+
+        quads
+    }
+
+    fn set_geom_visible(&mut self, visible: bool) { self.geom_visible = visible; }
+    fn geom_visible(&self) -> bool { self.geom_visible }
+    fn take_geom_toggle(&mut self) -> bool { std::mem::take(&mut self.geom_toggled) }
+}
+
+fn menu_item_x(title: &str, menu_items: &[String], idx: usize) -> f32 {
+    let mut x = 8.0;
+    if !title.is_empty() {
+        x += title.len() as f32 * 7.5 + 24.0;
+    }
+    for i in 0..idx {
+        x += menu_items[i].len() as f32 * 7.5 + 16.0;
+    }
+    x
+}
+
+fn menu_item_w(menu_items: &[String], idx: usize) -> f32 {
+    menu_items[idx].len() as f32 * 7.5 + 16.0
+}
+
 pub struct Checkbox {
     x: f32, y: f32, w: f32, h: f32,
     hovering: bool,
@@ -237,6 +966,8 @@ 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 cursor_moved(&mut self, px: f32, py: f32) -> bool {
         let was = self.hovering;
@@ -264,16 +995,31 @@ impl Widget for Checkbox {
     }
 }
 
+#[derive(Debug, Clone)]
 pub struct Toggle {
     x: f32, y: f32, w: f32, h: f32,
     hovering: bool,
     toggled: bool,
     just_toggled: bool,
+    label: Option<String>,
 }
 
 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 }
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, toggled: false, just_toggled: false, label: None }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.label = Some(label.to_string());
+        self
+    }
+
+    pub fn set_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    pub fn set_toggled(&mut self, v: bool) {
+        self.toggled = v;
     }
 }
 
@@ -281,6 +1027,10 @@ 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 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 top_room(&self) -> f32 { 0.0 }
 
     fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
         let was = self.hovering;
@@ -302,10 +1052,39 @@ impl Widget for Toggle {
         }
         false
     }
+
+    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 && self.label.is_some() {
+            quads.push((self.x - 87.0, self.y, 87.0 + self.w, self.h, [1.0, 1.0, 1.0, 0.06]));
+        }
+        quads
+    }
+
+    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 - 85.0,
+                y: self.y + (self.h - 12.0) / 2.0,
+                font_size: 12.0,
+                color: [0x83, 0x83, 0x8a],
+            }]
+        } else {
+            Vec::new()
+        }
+    }
 }
 
 pub struct Slider {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
     dragging: bool,
     value: f32,
     drag_offset: f32,
@@ -313,7 +1092,7 @@ pub struct Slider {
 
 impl Slider {
     pub fn new() -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, dragging: false, value: 0.5, drag_offset: 0.0 }
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, dragging: false, value: 0.5, drag_offset: 0.0 }
     }
 }
 
@@ -321,6 +1100,8 @@ 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 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 }
@@ -350,12 +1131,13 @@ impl Widget for Slider {
 
 pub struct ProgressBar {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
     value: f32,
 }
 
 impl ProgressBar {
     pub fn new(value: f32) -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value }
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, value }
     }
 }
 
@@ -363,38 +1145,128 @@ 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 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 {
     x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
 }
 
 impl StatusBar {
-    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
 }
 
 impl Widget for StatusBar {
     fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
     fn color(&self) -> [f32; 4] { colors::STATUS_BG }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+}
+
+pub struct Splitter {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    dragging: bool,
+    drag_ox: f32,
+}
+
+impl Splitter {
+    pub fn new(w: f32) -> Self {
+        Self { x: 0.0, y: 0.0, w, h: 0.0, hovered: false, dragging: false, drag_ox: 0.0 }
+    }
+}
+
+impl Widget for Splitter {
+    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] {
+        if self.dragging { colors::SPLITTER_DRAG }
+        else if self.hovered { colors::SPLITTER_HOVER }
+        else { colors::SPLITTER_IDLE }
+    }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    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; }
+        match state {
+            ElementState::Pressed => {
+                if self.hit_test(px, py) {
+                    self.drag_begin(px, py);
+                    return true;
+                }
+            }
+            ElementState::Released => {
+                if self.dragging { self.drag_end(); return true; }
+            }
+        }
+        false
+    }
+
+    fn is_dragging(&self) -> bool { self.dragging }
+    fn draggable(&self) -> bool { true }
+
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        let new_x = px - self.drag_ox;
+        if (new_x - self.x).abs() > 0.5 {
+            self.x = new_x;
+            return true;
+        }
+        false
+    }
+
+    fn drag_begin(&mut self, px: f32, _py: f32) {
+        self.dragging = true;
+        self.drag_ox = px - self.x;
+    }
+
+    fn drag_end(&mut self) { self.dragging = false; }
 }
 
 #[derive(Debug, Clone)]
 pub struct Spinbox {
     x: f32, y: f32, w: f32, h: f32,
     pub value: i32,
-    min: i32,
-    max: i32,
-    step: 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>,
 }
 
 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(), hover_dec: false, hover_inc: false }
+        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 }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.label = Some(label.to_string());
+        self
+    }
+
+    pub fn set_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    pub fn with_unit(mut self, unit: &str) -> Self {
+        self.unit = Some(unit.to_string());
+        self
+    }
+
+    pub fn set_unit(&mut self, unit: &str) {
+        self.unit = Some(unit.to_string());
     }
 }
 
@@ -403,13 +1275,19 @@ impl Widget for Spinbox {
     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::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 top_room(&self) -> f32 { if self.label.is_some() { 16.0 } else { 0.0 } }
 
     fn cursor_moved(&mut self, px: f32, _py: f32) -> bool {
-        if !self.hit_test(px, _py) {
+        let was = self.hovered;
+        self.hovered = self.hit_test(px, _py);
+        if !self.hovered {
             let changed = self.hover_dec || self.hover_inc;
             self.hover_dec = false;
             self.hover_inc = false;
-            return changed;
+            return changed || was != self.hovered;
         }
         let split = self.x + self.w * 0.55;
         let hd = px >= split && px < split + self.w * 0.225;
@@ -417,7 +1295,7 @@ impl Widget for Spinbox {
         let changed = hd != self.hover_dec || hi != self.hover_inc;
         self.hover_dec = hd;
         self.hover_inc = hi;
-        changed
+        changed || was != self.hovered
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
@@ -427,10 +1305,10 @@ impl Widget for Spinbox {
             ElementState::Pressed => {
                 let split = self.x + self.w * 0.55;
                 if px >= split && px < split + self.w * 0.225 {
-                    self.value = (self.value + self.step).min(self.max);
+                    self.value = (self.value - self.step).max(self.min);
                     true
                 } else if px >= split + self.w * 0.225 {
-                    self.value = (self.value - self.step).max(self.min);
+                    self.value = (self.value + self.step).min(self.max);
                     true
                 } else if px < split {
                     self.editing = true;
@@ -496,59 +1374,90 @@ 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)
+            };
+            quads.push((self.x, hy, self.w, 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 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 };
-        vec![
-            (self.x, self.y, self.w * 0.55, self.h, colors::SPINBOX_DISPLAY),
-            (split, self.y, btn_w, self.h, inc_col),
-            (split + btn_w, self.y, btn_w, self.h, dec_col),
-        ]
+        quads.push((self.x, self.y, self.w * 0.55, self.h, colors::SPINBOX_DISPLAY));
+        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
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
-        let value_text = if self.editing { self.edit_buffer.clone() } else { self.value.to_string() };
-        vec![
-            TextLabel {
-                text: value_text,
+        let mut labels = Vec::new();
+        if let Some(ref label) = self.label {
+            labels.push(TextLabel {
+                text: label.clone(),
                 x: self.x + 4.0,
-                y: self.y + 2.0,
-                font_size: 14.0,
-                color: [0xcc, 0xcc, 0xd4],
-            },
-            TextLabel {
-                text: "+".to_string(),
-                x: self.x + self.w * 0.6625 - 4.0,
-                y: self.y + 2.0,
-                font_size: 12.0,
-                color: [0xcc, 0xcc, 0xd4],
-            },
-            TextLabel {
-                text: "-".to_string(),
-                x: self.x + self.w * 0.8875 - 4.0,
-                y: self.y + 2.0,
+                y: self.y - 16.0,
                 font_size: 12.0,
-                color: [0xcc, 0xcc, 0xd4],
-            },
-        ]
+                color: [0x83, 0x83, 0x8a],
+            });
+        }
+        let value_text = if self.editing { self.edit_buffer.clone() } else { self.value.to_string() };
+        labels.push(TextLabel {
+            text: value_text,
+            x: self.x + 4.0,
+            y: self.y + 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 + 3.0,
+                font_size: 11.0,
+                color: [0x73, 0x73, 0x7a],
+            });
+        }
+        labels.push(TextLabel {
+            text: "-".to_string(),
+            x: self.x + self.w * 0.6625 - 4.0,
+            y: self.y + 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 + 2.0,
+            font_size: 12.0,
+            color: [0xcc, 0xcc, 0xd4],
+        });
+        labels
     }
 }
 
 #[derive(Debug, Clone)]
-pub struct ColorPicker {
+pub struct ColorSelector {
     x: f32, y: f32, w: f32, h: f32,
     pub color: [u8; 3],
     just_clicked: bool,
+    hovered: bool,
     editing: bool,
     edit_buffer: String,
     label: Option<String>,
 }
 
-impl ColorPicker {
+impl ColorSelector {
     pub fn new(color: [u8; 3]) -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, color, just_clicked: false, editing: false, edit_buffer: String::new(), label: None }
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, color, just_clicked: false, hovered: false, editing: false, edit_buffer: String::new(), label: None }
     }
 
     pub fn with_label(mut self, label: &str) -> Self {
@@ -557,14 +1466,24 @@ impl ColorPicker {
     }
 }
 
-impl Widget for ColorPicker {
+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_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    fn top_room(&self) -> f32 { if self.label.is_some() { 16.0 } else { 0.0 } }
 
     fn color(&self) -> [f32; 4] {
         [self.color[0] as f32 / 255.0, self.color[1] as f32 / 255.0, self.color[2] as f32 / 255.0, 1.0]
     }
 
+    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; }
@@ -633,13 +1552,25 @@ impl Widget for ColorPicker {
         }
     }
 
+    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)
+            };
+            quads.push((self.x, hy, self.w, 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 (r, g, b, _) = (self.color[0] as f32 / 255.0, self.color[1] as f32 / 255.0, self.color[2] as f32 / 255.0, 1.0);
-        vec![
-            (pick_x, self.y, pick_w, self.h, [r, g, b, 1.0]),
-        ]
+        quads.push((pick_x, self.y, pick_w, self.h, [r, g, b, 1.0]));
+        quads
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
@@ -665,6 +1596,108 @@ impl Widget for ColorPicker {
     }
 }
 
+const BREADCRUMB_PADDING: f32 = 8.0;
+const SEGMENT_GAP: f32 = 4.0;
+
+pub struct Breadcrumb {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+    path: Vec<String>,
+    hovered_seg: Option<usize>,
+    clicked_seg: Option<usize>,
+}
+
+impl Breadcrumb {
+    pub fn new() -> Self {
+        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false,
+               path: Vec::new(), hovered_seg: None, clicked_seg: None }
+    }
+
+    fn seg_at(&self, px: f32) -> Option<usize> {
+        let mut cx = self.x + BREADCRUMB_PADDING;
+        for (i, seg) in self.path.iter().enumerate() {
+            let w = seg.len() as f32 * 7.5;
+            if px >= cx && px < cx + w {
+                return Some(i);
+            }
+            cx += w + SEGMENT_GAP;
+        }
+        None
+    }
+}
+
+impl Widget for Breadcrumb {
+    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, 1.0] }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        let was = self.hovered;
+        self.hovered = self.hit_test(px, py);
+        let old = self.hovered_seg;
+        self.hovered_seg = if self.hovered { self.seg_at(px) } else { None };
+        was != self.hovered || old != self.hovered_seg
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32) -> bool {
+        if button != MouseButton::Left || state != ElementState::Pressed { return false; }
+        if let Some(i) = self.seg_at(px) {
+            if i < self.path.len() - 1 {
+                self.clicked_seg = Some(i);
+                return true;
+            }
+        }
+        false
+    }
+
+    fn set_path(&mut self, segments: &[String]) {
+        let mut s = Vec::with_capacity(segments.len().max(1));
+        if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
+            s.push("/".to_string());
+        } else {
+            s.push("/".to_string());
+            for name in segments {
+                s.push(format!(" \u{203A} {}", name));
+            }
+        }
+        self.path = s;
+    }
+
+    fn path_click(&mut self) -> Option<usize> { self.clicked_seg.take() }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if let Some(i) = self.hovered_seg {
+            let mut cx = self.x + BREADCRUMB_PADDING;
+            for j in 0..i {
+                let w = self.path[j].len() as f32 * 7.5;
+                cx += w + SEGMENT_GAP;
+            }
+            let w = self.path[i].len() as f32 * 7.5;
+            quads.push((cx, self.y, w, self.h, [1.0, 1.0, 1.0, 0.06]));
+        }
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        let mut cx = self.x + BREADCRUMB_PADDING;
+        for (i, seg) in self.path.iter().enumerate() {
+            labels.push(TextLabel {
+                text: seg.clone(),
+                x: cx,
+                y: self.y + 6.0,
+                font_size: 12.0,
+                color: if i == self.path.len() - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
+            });
+            cx += seg.len() as f32 * 7.5 + SEGMENT_GAP;
+        }
+        labels
+    }
+}
+
 fn parse_hex(s: &str) -> Option<[u8; 3]> {
     let s = s.trim_start_matches('#');
     if s.len() != 6 { return None; }
@@ -673,3 +1706,93 @@ fn parse_hex(s: &str) -> Option<[u8; 3]> {
     let b = u8::from_str_radix(&s[4..6], 16).ok()?;
     Some([r, g, b])
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use winit::event::{ElementState, MouseButton};
+
+    #[test]
+    fn test_node_toggle_geometry_visibility() {
+        let mut node = Node::new(100.0, 100.0, 200.0, 50.0, "Test Node");
+
+        // 1. Initial state
+        assert!(node.geom_visible());
+        assert!(!node.take_geom_toggle());
+        assert!(node.draggable());
+
+        // Get the toggle rect
+        let (tx, ty, tw, th) = node.toggle_rect();
+        
+        // 2. Hover toggle area
+        // Move cursor inside toggle area
+        let changed = node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
+        assert!(changed);
+        assert!(node.toggle_hovered);
+        assert!(!node.draggable(), "Node should not be draggable when hovering over the toggle widget");
+
+        // Move cursor outside toggle area but inside node
+        let changed2 = node.cursor_moved(tx - 10.0, ty + th / 2.0);
+        assert!(changed2);
+        assert!(!node.toggle_hovered);
+        assert!(node.draggable());
+
+        // 3. Click toggle area
+        // Move cursor back inside toggle area
+        node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
+        // Press Left button
+        let input_changed = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
+        assert!(input_changed);
+        assert!(!node.geom_visible(), "Geometry visibility should be toggled off");
+        assert!(node.take_geom_toggle(), "take_geom_toggle should return true after toggle click");
+        assert!(!node.take_geom_toggle(), "take_geom_toggle should clear state after being called once");
+
+        // Click again to toggle back on
+        let input_changed2 = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
+        assert!(input_changed2);
+        assert!(node.geom_visible(), "Geometry visibility should be toggled back on");
+        assert!(node.take_geom_toggle());
+    }
+
+    #[test]
+    fn test_menubar_vertical_horizontal_labels() {
+        // Create MenuBar with custom horizontal and vertical labels
+        let mut menubar = MenuBar::new(0.0, 0.0, 120.0, 30.0)
+            .with_title("App")
+            .with_item_vh("FileH", "FileV", &["Open", "Save"])
+            .with_item("Edit", &["Undo"]);
+
+        // 1. Horizontal mode (default)
+        assert!(!menubar.vertical);
+        let labels_h = menubar.text_labels();
+        // Title should be "App", item 0 should be "FileH", item 1 should be "Edit"
+        assert_eq!(labels_h[0].text, "App");
+        assert_eq!(labels_h[1].text, "FileH");
+        assert_eq!(labels_h[2].text, "Edit");
+
+        // Hover test in horizontal layout
+        let ix = menu_item_x("App", &["FileH".to_string(), "Edit".to_string()], 0);
+        let iw = menu_item_w(&["FileH".to_string(), "Edit".to_string()], 0);
+        
+        // Move cursor inside "FileH" bounds
+        menubar.cursor_moved(ix + iw / 2.0, 15.0);
+        assert_eq!(menubar.hovered_menu, Some(0));
+
+        // 2. Vertical mode
+        let mut menubar_v = menubar.with_vertical(true);
+        assert!(menubar_v.vertical);
+        let labels_v = menubar_v.text_labels();
+        // Title should be "App", item 0 should be "FileV", item 1 should be "Edit"
+        assert_eq!(labels_v[0].text, "App");
+        assert_eq!(labels_v[1].text, "FileV");
+        assert_eq!(labels_v[2].text, "Edit");
+
+        // Hover test in vertical layout
+        let iy = menubar_v.item_y_vertical(0);
+        let ih = menubar_v.item_h_vertical();
+
+        // Move cursor inside "FileV" bounds
+        menubar_v.cursor_moved(20.0, iy + ih / 2.0);
+        assert_eq!(menubar_v.hovered_menu, Some(0));
+    }
+}