git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitb662b8656415ddb3a3464c2d8e5c5b6b7594a4bb
parent28d330ea15
authorLucas Galante <[email protected]>
date2026-06-09 10:09
Refactor and adapt widgets to use UiContext and scale factor

 Cargo.lock      |  48 ++---
 Cargo.toml      |   2 +-
 Makefile        |   2 +-
 src/geometry.rs |  69 ++++--
 src/main.rs     | 657 +++++++++++++++++++++++++++++++++-----------------------
 src/shortcut.rs | 108 ++++++++++
 6 files changed, 566 insertions(+), 320 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index be18632..c4dc04c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -155,30 +155,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
-
-[[package]]
-name = "cl3"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b823f24e72fa0c68aa14a250ae1c0848e68d4ae188b71c3972343e45b46f8644"
-dependencies = [
- "libc",
- "opencl-sys",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "clear-design-interface"
+name = "cce-design-interface"
 version = "0.1.0"
 dependencies = [
  "bytemuck",
@@ -201,6 +178,29 @@ dependencies = [
  "xkeysym",
 ]
 
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "cl3"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823f24e72fa0c68aa14a250ae1c0848e68d4ae188b71c3972343e45b46f8644"
+dependencies = [
+ "libc",
+ "opencl-sys",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "clear-ui"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 18e547a..bf2a8be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "clear-design-interface"
+name = "cce-design-interface"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/Makefile b/Makefile
index 803f9e3..6c15473 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ build:
 
 install: build
 	mkdir -p ~/.local/bin
-	install -m 755 target/release/clear-design-interface ~/.local/bin/clear-design-interface
+	install -m 755 target/release/cce-design-interface ~/.local/bin/cce-design-interface
 
 run:
 	cargo run
diff --git a/src/geometry.rs b/src/geometry.rs
index 2205fb3..557aca9 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -315,18 +315,20 @@ pub fn resolve_transform_geometry(root: &FsNode, target: &FsNode, visited: &mut
 }
 
 
-pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String> {
-    let is_generator = code.contains("out_count");
-    if geom.vertices.is_empty() && !is_generator {
-        return Ok(());
-    }
+struct OpenClCache {
+    device: opencl3::device::Device,
+    context: opencl3::context::Context,
+    queue: opencl3::command_queue::CommandQueue,
+    kernels: std::collections::HashMap<String, opencl3::kernel::Kernel>,
+}
+
+static OPENCL_CACHE: std::sync::OnceLock<std::sync::Mutex<Option<OpenClCache>>> = std::sync::OnceLock::new();
 
-    let platforms = get_platforms().map_err(|e| format!("Failed to get platforms: {:?}", e))?;
+fn init_opencl() -> Option<OpenClCache> {
+    let platforms = get_platforms().ok()?;
     if platforms.is_empty() {
-        return Err("No OpenCL platforms found".to_string());
+        return None;
     }
-
-    // Try to find a GPU device first, then fallback to CPU
     let mut device_id = None;
     for platform in &platforms {
         if let Ok(devices) = platform.get_devices(CL_DEVICE_TYPE_GPU) {
@@ -346,20 +348,45 @@ pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String>
             }
         }
     }
-    let device_id = device_id.ok_or_else(|| "No OpenCL devices found".to_string())?;
+    let device_id = device_id?;
     let device = Device::new(device_id);
+    let context = Context::from_device(&device).ok()?;
+    let queue = unsafe { CommandQueue::create(&context, device_id, 0) }.ok()?;
+    Some(OpenClCache {
+        device,
+        context,
+        queue,
+        kernels: std::collections::HashMap::new(),
+    })
+}
 
-    let context = Context::from_device(&device).map_err(|e| format!("Failed to create Context: {:?}", e))?;
-    let queue = unsafe { CommandQueue::create(&context, device_id, 0) }
-        .map_err(|e| format!("Failed to create CommandQueue: {:?}", e))?;
-
-    let mut program = Program::create_from_source(&context, code).map_err(|e| format!("Failed to create Program: {:?}", e))?;
-    if let Err(e) = program.build(&[device_id], "") {
-        let log = program.get_build_log(device_id).unwrap_or_else(|_| "Failed to retrieve build log".to_string());
-        return Err(format!("OpenCL JIT compilation error: {}\nLog:\n{}", e, log));
+pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String> {
+    let is_generator = code.contains("out_count");
+    if geom.vertices.is_empty() && !is_generator {
+        return Ok(());
     }
 
-    let kernel = Kernel::create(&program, "process").map_err(|e| format!("Failed to create kernel 'process': {:?}", e))?;
+    let mut cache_guard = OPENCL_CACHE
+        .get_or_init(|| std::sync::Mutex::new(init_opencl()))
+        .lock()
+        .map_err(|e| format!("Failed to lock OpenCL cache: {:?}", e))?;
+
+    let cache = cache_guard.as_mut().ok_or_else(|| "No OpenCL platforms/devices found".to_string())?;
+
+    if !cache.kernels.contains_key(code) {
+        let mut program = Program::create_from_source(&cache.context, code)
+            .map_err(|e| format!("Failed to create Program: {:?}", e))?;
+        if let Err(e) = program.build(&[cache.device.id()], "") {
+            let log = program.get_build_log(cache.device.id()).unwrap_or_else(|_| "Failed to retrieve build log".to_string());
+            return Err(format!("OpenCL JIT compilation error: {}\nLog:\n{}", e, log));
+        }
+        let kernel = Kernel::create(&program, "process")
+            .map_err(|e| format!("Failed to create kernel 'process': {:?}", e))?;
+        cache.kernels.insert(code.to_string(), kernel);
+    }
+    let kernel = cache.kernels.get(code).unwrap();
+    let context = &cache.context;
+    let queue = &cache.queue;
 
     if is_generator {
         let in_count = geom.vertices.len();
@@ -417,7 +444,7 @@ pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String>
         // Execute kernel
         let global_work_size = if in_count == 0 { 1 } else { in_count };
         let kernel_event = unsafe {
-            ExecuteKernel::new(&kernel)
+            ExecuteKernel::new(kernel)
                 .set_arg(&in_pos_buf)
                 .set_arg(&in_col_buf)
                 .set_arg(&(in_count as cl_int))
@@ -499,7 +526,7 @@ pub fn run_opencl_kernel(code: &str, geom: &mut Geometry) -> Result<(), String>
 
         // Execute kernel
         let kernel_event = unsafe {
-            ExecuteKernel::new(&kernel)
+            ExecuteKernel::new(kernel)
                 .set_arg(&pos_buf)
                 .set_arg(&col_buf)
                 .set_arg(&(count as cl_int))
diff --git a/src/main.rs b/src/main.rs
index 46ec9b0..4109e17 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -203,6 +203,7 @@ enum HttpAction {
     ToggleCircularPane,
     MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
     MenuClosed { widget_idx: usize, menu_idx: usize },
+    SelectPage { page: usize },
 }
 
 #[derive(Debug)]
@@ -372,7 +373,7 @@ impl DesignSettings {
         let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
         let mut path = std::path::PathBuf::from(home);
         path.push(".config");
-        path.push("ccec");
+        path.push("cce");
         path.push("design.json");
         path
     }
@@ -422,7 +423,10 @@ impl Element for NodePalette {
     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 hit_test(&self, px: f32, py: f32) -> bool {
+    fn hit_test(&self, px: f32, py: f32, ctx: &clear_ui::context::UiContext) -> bool {
+        if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+            return false;
+        }
         self.visible && px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
     }
     fn set_visible(&mut self, visible: bool) { self.visible = visible; }
@@ -487,50 +491,8 @@ impl Element for NodePalette {
     }
 }
 
-#[derive(Clone, Copy, PartialEq)]
-enum Action {
-    ToggleGrid,
-    ToggleCube,
-    ToggleSquareViewport,
-    ToggleConfigure,
-    ToggleSpreadsheet,
-    ToggleOrigin,
-    ToggleCameraPivot,
-    ToggleCircularPane,
-    DetachCircularWindow,
-    Save,
-}
-
-struct KeyBind {
-    ctrl: bool,
-    shift: bool,
-    alt: bool,
-    super_: bool,
-    key: Key,
-    action: Action,
-}
-
-impl KeyBind {
-    fn matches(&self, mods: &ModifiersState, key: &Key) -> bool {
-        mods.control_key() == self.ctrl
-            && mods.shift_key() == self.shift
-            && mods.alt_key() == self.alt
-            && mods.super_key() == self.super_
-            && key == &self.key
-    }
-}
-
-fn default_keybinds() -> Vec<KeyBind> {
-    vec![
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("g".into()), action: Action::ToggleGrid },
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("e".into()), action: Action::ToggleCube },
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("a".into()), action: Action::ToggleSquareViewport },
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character(",".into()), action: Action::ToggleConfigure },
-        KeyBind { ctrl: false, shift: false, alt: false, super_: false, key: Key::Character("`".into()), action: Action::ToggleSpreadsheet },
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("d".into()), action: Action::ToggleCircularPane },
-        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("s".into()), action: Action::Save },
-    ]
-}
+mod shortcut;
+use shortcut::{Shortcut, ShortcutManager, Action};
 
 use clear_ui::engine::{
     Vertex, widget_vertices,
@@ -634,10 +596,7 @@ struct ResizeDirection {
 }
 
 struct State {
-    surface: wgpu::Surface<'static>,
-    device: wgpu::Device,
-    queue: wgpu::Queue,
-    config: wgpu::SurfaceConfiguration,
+    wgpu_adapter: clear_ui::backend::WgpuAdapter,
     render_pipeline: wgpu::RenderPipeline,
     vertex_buffer: wgpu::Buffer,
     window: XdgWindow,
@@ -696,7 +655,7 @@ struct State {
     last_click: Option<(Instant, usize)>,
     last_frame: Instant,
 
-    keybinds: Vec<KeyBind>,
+    shortcut_manager: ShortcutManager,
     pending_action: Option<Action>,
     exit_requested: bool,
 
@@ -708,12 +667,6 @@ struct State {
     node_palette_filtered: Vec<usize>,
     node_palette_selected: usize,
 
-    font_system: FontSystem,
-    swash_cache: SwashCache,
-    text_atlas: TextAtlas,
-    text_renderer: TextRenderer,
-    text_viewport: Viewport,
-
     curved_text_texture: wgpu::Texture,
     curved_text_texture_view: wgpu::TextureView,
     curved_text_sampler: wgpu::Sampler,
@@ -789,6 +742,9 @@ struct State {
     last_project_mod_time: Option<std::time::SystemTime>,
     last_project_check: std::time::Instant,
     last_inspector_check: std::time::Instant,
+    last_inspector_update: std::time::Instant,
+    needs_autosave: bool,
+    last_autosave_time: std::time::Instant,
     pub window_x: i32,
     pub window_y: i32,
     pub active_menu_cloud_pid: Option<u32>,
@@ -813,6 +769,7 @@ struct State {
     recent_files: Vec<std::path::PathBuf>,
     recent_files_list: ScrollingList,
     recent_files_buttons: Vec<Button>,
+    ui_context: clear_ui::context::UiContext,
 }
 
 impl State {
@@ -851,7 +808,7 @@ impl State {
         std::env::var("HOME").ok().map(|h| {
             let mut path = std::path::PathBuf::from(h);
             path.push(".config");
-            path.push("clear-design-interface");
+            path.push("cce-design-interface");
             path.push("recent_files.json");
             path
         })
@@ -1212,17 +1169,17 @@ impl State {
 
     fn update_grid_geometry(&mut self) {
         let grid_verts = grid_vertices(self.grid_thickness, self.grid_color);
-        self.queue.write_buffer(&self.vertex_buffer_grid, 0, bytemuck::cast_slice(&grid_verts));
+        self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_grid, 0, bytemuck::cast_slice(&grid_verts));
     }
 
     fn update_origin_geometry(&mut self) {
         let origin_verts = origin_vectors_vertices(self.origin_size);
-        self.queue.write_buffer(&self.vertex_buffer_origin, 0, bytemuck::cast_slice(&origin_verts));
+        self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_origin, 0, bytemuck::cast_slice(&origin_verts));
     }
 
     fn update_pivot_geometry(&mut self) {
         let pivot_verts = camera_pivot_vertices(self.camera_pivot_size);
-        self.queue.write_buffer(&self.vertex_buffer_pivot, 0, bytemuck::cast_slice(&pivot_verts));
+        self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_pivot, 0, bytemuck::cast_slice(&pivot_verts));
     }
 
     fn body_h(&self) -> f32 { self.height - HEADER_H - STATUS_H }
@@ -1948,6 +1905,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn on_path_changed(&mut self) {
+        self.widgets[CONTENT_IDX].set_selected_node(None);
         self.drag_widget = None;
         self.focused_widget = None;
         self.pan_x = 0.0;
@@ -2305,7 +2263,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn create_depth_texture(&self) -> (wgpu::Texture, wgpu::TextureView) {
-        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
+        let tex = self.wgpu_adapter.device.create_texture(&wgpu::TextureDescriptor {
             label: Some("Depth Texture"),
             size: wgpu::Extent3d { width: self.physical_width.max(1), height: self.physical_height.max(1), depth_or_array_layers: 1 },
             mip_level_count: 1,
@@ -2320,13 +2278,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn create_backdrop_texture(&self) -> (wgpu::Texture, wgpu::TextureView) {
-        let tex = self.device.create_texture(&wgpu::TextureDescriptor {
+        let tex = self.wgpu_adapter.device.create_texture(&wgpu::TextureDescriptor {
             label: Some("Backdrop Texture"),
             size: wgpu::Extent3d { width: self.physical_width.max(1), height: self.physical_height.max(1), depth_or_array_layers: 1 },
             mip_level_count: 1,
             sample_count: 1,
             dimension: wgpu::TextureDimension::D2,
-            format: self.config.format,
+            format: self.wgpu_adapter.config.format,
             usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
             view_formats: &[],
         });
@@ -2359,61 +2317,19 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             window.set_min_size(Some((200, 200)));
         } else {
             window.set_title("Clear Design Interface");
-            window.set_app_id("clear-design-interface");
+            window.set_app_id("cce-design-interface");
             window.set_min_size(Some((480, 320)));
         }
         window.commit();
 
-        let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
-            display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
-            surface_ptr: wl_surface.id().as_ptr() as *mut std::ffi::c_void,
-        }));
+        let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
+        let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
+        let wgpu_adapter = clear_ui::backend::WgpuAdapter::new(display_ptr, surface_ptr, pw, ph).await;
 
-        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::VULKAN,
-            ..Default::default()
-        });
-
-        let surface = instance
-            .create_surface(wayland_handle)
-            .expect("Failed to create surface");
-
-        let adapter = instance
-            .request_adapter(&wgpu::RequestAdapterOptions {
-                power_preference: wgpu::PowerPreference::LowPower,
-                compatible_surface: Some(&surface),
-                force_fallback_adapter: false,
-            })
-            .await
-            .expect("Failed to find adapter");
-
-        let (device, queue) = adapter
-            .request_device(
-                &wgpu::DeviceDescriptor {
-                    label: Some("GPU Device"),
-                    required_features: wgpu::Features::empty(),
-                    required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
-                    memory_hints: wgpu::MemoryHints::MemoryUsage,
-                },
-                None,
-            )
-            .await
-            .expect("Failed to create device");
-
-        let mut config = surface
-            .get_default_config(&adapter, pw, ph)
-            .expect("Failed to get surface config");
-        let capabilities = surface.get_capabilities(&adapter);
-        let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
-            wgpu::CompositeAlphaMode::PreMultiplied
-        } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
-            wgpu::CompositeAlphaMode::PostMultiplied
-        } else {
-            capabilities.alpha_modes[0]
-        };
-        config.alpha_mode = alpha_mode;
-        config.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST;
-        surface.configure(&device, &config);
+        let device = &wgpu_adapter.device;
+        let queue = &wgpu_adapter.queue;
+        let surface = &wgpu_adapter.surface;
+        let config = &wgpu_adapter.config;
 
         let backdrop_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
             label: Some("Backdrop Bind Group Layout"),
@@ -2615,7 +2531,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let vertex_buffer_spheres = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some("Sphere Node Vertex Buffer"),
             size: 1,
-            usage: wgpu::BufferUsages::VERTEX,
+            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
 
@@ -2683,14 +2599,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             cache: None,
         });
 
-        let mut font_system = FontSystem::new();
-        let swash_cache = SwashCache::new();
-        let cache = Cache::new(&device);
-        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
-        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
-
-        let mut text_viewport = Viewport::new(&device, &cache);
-        text_viewport.update(&queue, Resolution { width: pw, height: ph });
+        let cache = Cache::new(device);
 
         let shader_textured = device.create_shader_module(wgpu::ShaderModuleDescriptor {
             label: Some("Textured Shader"),
@@ -2874,7 +2783,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             vec![]
         };
         let mut widgets: Vec<Box<dyn Element>> = vec![
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_item("File", &["New Project", "Open", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane"]).with_item("Help", &["About"]).with_z_index(110)),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_label("Main Menu Bar").with_item("File", &["New Project", "Open", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane"]).with_item("Help", &["About"]).with_z_index(110)),
             Box::new(Graph::new()),
             Box::new(Splitter::new(SPLITTER_W)),
             Box::new(ViewportBg::new()),
@@ -2882,23 +2791,23 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             Box::new(Plate::new(0.0, 0.0, 0.0, 0.0).with_color(colors::PARAM_BG).with_blur(true)),
             Box::new(ParametersBg::new()),
             Box::new(Canvas::new()),
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_item("File", &["New", "Open", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_item("Settings", &[])),
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_item("Settings", &[])),
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"])),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Open", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_item("Settings", &[])),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_item("Settings", &[])),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"])),
             Box::new(StatusBar::new().with_text("Ready")),
             Box::new(Breadcrumb::new()),
             Box::new(NodePalette::new()),
             Box::new(Spreadsheet::new()),
         ];
         
-        let mut spreadsheet_menubar = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet").with_item("View", &["Close Pane"]);
+        let mut spreadsheet_menubar = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet").with_label("Spreadsheet Menu Bar").with_item("View", &["Close Pane"]);
         spreadsheet_menubar.visible = false;
         widgets.push(Box::new(spreadsheet_menubar));
 
         let network_panel = Plate::new(0.0, 0.0, 0.0, 0.0).with_color([0.10, 0.10, 0.13, 0.95]);
         widgets.push(Box::new(network_panel));
 
-        let paginator = Paginator::new(56.0, vec![]).with_sidebar_mode(true).with_column_layout(true);
+        let paginator = Paginator::new(56.0, vec![]).with_sidebar_mode(true).with_column_layout(true).with_tabs_rotated(false);
         widgets.push(Box::new(paginator));
 
         let mut positions = Vec::with_capacity(PAGINATOR_IDX + 1);
@@ -2922,13 +2831,19 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             mapped_at_creation: false,
         });
 
+        let mut shortcut_manager = ShortcutManager::new();
+        shortcut_manager.register("Ctrl+g", Action::ToggleGrid).unwrap();
+        shortcut_manager.register("Ctrl+e", Action::ToggleCube).unwrap();
+        shortcut_manager.register("Ctrl+a", Action::ToggleSquareViewport).unwrap();
+        shortcut_manager.register("Ctrl+,", Action::ToggleConfigure).unwrap();
+        shortcut_manager.register("`", Action::ToggleSpreadsheet).unwrap();
+        shortcut_manager.register("Ctrl+d", Action::ToggleCircularPane).unwrap();
+        shortcut_manager.register("Ctrl+s", Action::Save).unwrap();
+
         let mut state = Self {
             window,
             wl_surface,
-            surface,
-            device,
-            queue,
-            config,
+            wgpu_adapter,
             render_pipeline,
             vertex_buffer,
             vertex_count: 0,
@@ -2982,7 +2897,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             current_path,
             last_click: None,
             last_frame: Instant::now(),
-            keybinds: default_keybinds(),
+            shortcut_manager,
             pending_action: None,
             exit_requested: false,
             widgets,
@@ -2992,11 +2907,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             node_palette_query: String::new(),
             node_palette_filtered: Vec::new(),
             node_palette_selected: 0,
-            font_system,
-            swash_cache,
-            text_atlas,
-            text_renderer,
-            text_viewport,
             curved_text_texture,
             curved_text_texture_view,
             curved_text_sampler,
@@ -3072,6 +2982,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             },
             last_project_check: std::time::Instant::now(),
             last_inspector_check: std::time::Instant::now(),
+            last_inspector_update: std::time::Instant::now(),
+            needs_autosave: false,
+            last_autosave_time: std::time::Instant::now(),
             window_x: 0,
             window_y: 0,
             active_menu_cloud_pid: None,
@@ -3099,6 +3012,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             recent_files,
             recent_files_list,
             recent_files_buttons,
+            ui_context: clear_ui::context::UiContext::new(),
         };
 
         state.update_inertial_settings();
@@ -3141,18 +3055,26 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         self.widgets[CONTENT_IDX].set_skipped_sizes(self.skipped_row_h, self.skipped_col_w);
         self.widgets[CONTENT_IDX].set_grid_origin(active_node_area_x + self.pan_x, active_node_area_y + self.pan_y);
         self.widgets[CONTENT_IDX].set_grid_snap_enabled(self.grid_snap_enabled);
-        self.widgets[CONTENT_IDX].set_uniform_background(self.uniform_background);
-        self.widgets[CONTENT_IDX].set_network_opacity(self.network_opacity);
-        self.widgets[CONTENT_IDX].set_cell_color(self.cell_color);
-        self.widgets[CONTENT_IDX].set_gap_color(self.gap_color);
-        self.widgets[LEFT_MENUBAR_IDX].set_network_opacity(self.network_opacity);
-        self.widgets[BREADCRUMB_IDX].set_network_opacity(self.network_opacity);
-        self.widgets[NETWORK_PANEL_IDX].set_network_opacity(self.network_opacity);
+        if let Some(graph) = self.widgets[CONTENT_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Graph>() {
+            graph.set_uniform_background(self.uniform_background);
+            graph.set_network_opacity(self.network_opacity);
+            graph.set_cell_color(self.cell_color);
+            graph.set_gap_color(self.gap_color);
+        }
+        if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+            menubar.set_network_opacity(self.network_opacity);
+        }
+        if let Some(breadcrumb) = self.widgets[BREADCRUMB_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Breadcrumb>() {
+            breadcrumb.set_network_opacity(self.network_opacity);
+        }
+        if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+            plate.set_network_opacity(self.network_opacity);
+        }
     }
 
     fn update_inertial_settings(&mut self) {
         self.last_config_read = Instant::now();
-        let config_path = "/home/lsgalante/.config/ccec/config.toml";
+        let config_path = "/home/lsgalante/.config/cce/config.toml";
         
         let mut enabled = true;
         let mut friction = 0.90;
@@ -3276,12 +3198,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
             self.positions[0] = (0.0, 0.0, 0.0, 0.0);
             self.positions[LEFT_MENUBAR_IDX] = (cx - r, cy - r, 2.0 * r, 35.0);
-            self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(Some((cx, cy, r)));
+            if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+                menubar.set_curved_circle(Some((cx, cy, r)));
+            }
             self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H, 2.0 * r, BREADCRUMB_H);
             self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + MENUBAR_H + BREADCRUMB_H));
             self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
             self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
-            self.widgets[NETWORK_PANEL_IDX].set_curved_circle(Some((cx, cy, r)));
+            if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+                plate.set_curved_circle(Some((cx, cy, r)));
+            }
             self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[PARAM_IDX] = (0.0, 0.0, 0.0, 0.0);
@@ -3467,12 +3393,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 let cy = self.circular_network_layout.y;
 
                 self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-                self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(None);
+                if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+                    menubar.set_curved_circle(None);
+                }
                 self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, BREADCRUMB_H);
                 self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + BREADCRUMB_H));
                 self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
                 self.widgets[NETWORK_PANEL_IDX].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
-                self.widgets[NETWORK_PANEL_IDX].set_curved_circle(Some((cx, cy, r)));
+                if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+                    plate.set_curved_circle(Some((cx, cy, r)));
+                }
                 self.widgets[NETWORK_PANEL_IDX].set_drag_bounds(0.0, 0.0, self.width, self.height);
                 self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
                 self.positions[SPLITTER2_IDX] = (s2_x, 0.0, s2_w, body_h);
@@ -3551,7 +3481,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     (0.0, 0.0, 0.0, 0.0)
                 };
 
-                self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(None);
+                if let Some(menubar) = self.widgets[LEFT_MENUBAR_IDX].as_any_mut().downcast_mut::<clear_ui::widget::MenuBar>() {
+                    menubar.set_curved_circle(None);
+                }
                 
                 let mb_h = 0.0;
                 let bc_h = if self.show_network { BREADCRUMB_H } else { 0.0 };
@@ -3560,7 +3492,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 self.positions[CONTENT_IDX] = (px, py + mb_h + bc_h, pw, content_h);
                 self.positions[NETWORK_PANEL_IDX] = (px, py, pw, ph);
                 self.widgets[NETWORK_PANEL_IDX].set_rect(px, py, pw, ph);
-                self.widgets[NETWORK_PANEL_IDX].set_curved_circle(None);
+                if let Some(plate) = self.widgets[NETWORK_PANEL_IDX].as_any_mut().downcast_mut::<clear_ui::widget::Plate>() {
+                    plate.set_curved_circle(None);
+                }
                 self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
                 self.positions[SPLITTER2_IDX] = (0.0, 0.0, 0.0, 0.0);
                 let p_rect = if self.show_parameters { (param_x, param_y, param_w, param_h) } else { (0.0, 0.0, 0.0, 0.0) };
@@ -3662,11 +3596,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     fn update_paginator(&mut self) {
         let active_menubar = self.focused_pane;
         let title = self.widgets[active_menubar].label().unwrap_or_default();
-        let label_name = if let Some(colon_idx) = title.find(':') {
+        let mut label_name = if let Some(colon_idx) = title.find(':') {
             title.split_at(colon_idx + 1).1.trim().to_uppercase()
         } else {
             title.to_uppercase()
         };
+        if label_name.ends_with(" MENU BAR") {
+            label_name = label_name.replace(" MENU BAR", "");
+        }
         self.widgets[PAGINATOR_IDX].set_sidebar_label(Some(label_name));
 
         let menu_names = self.widgets[active_menubar].menu_names();
@@ -3850,17 +3787,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         }
 
         let sel_page = self.widgets[PAGINATOR_IDX].selected_page();
-        self.widgets[PARAM_IDX].clear_children();
+        self.widgets[PARAM_IDX].clear_children(&mut self.ui_context);
         if !self.widgets[PAGINATOR_IDX].is_page_hidden() {
             if sel_page < self.paginator_page_widgets.len() {
                 for widget in &self.paginator_page_widgets[sel_page] {
                     let ptr = &**widget as *const (dyn Element + 'static) as *mut (dyn Element + 'static);
-                    self.widgets[PARAM_IDX].add_child(ptr);
+                    self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
                 }
                 if menu_names.get(sel_page).map(|s| s.as_str()) == Some("File") {
                     for btn in &mut self.recent_files_buttons {
                         let ptr = btn as *mut Button as *mut (dyn Element + 'static);
-                        self.widgets[PARAM_IDX].add_child(ptr);
+                        self.widgets[PARAM_IDX].add_child(ptr, &mut self.ui_context);
                     }
                 }
             }
@@ -4033,10 +3970,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn sync_cursor_and_selection(&mut self) {
-        if let Some(old) = self.focused_widget {
-            if old != CONTENT_IDX && old != SPREADSHEET_IDX {
-                return;
-            }
+        if self.focused_widget != Some(CONTENT_IDX) {
+            return;
         }
         let dir = self.current_dir();
         let mut node_at_cursor_idx = None;
@@ -4099,6 +4034,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 self.widgets[i].z_index()
             }
         });
+        println!("DEBUG_DRAW_ORDER: {:?}", draw_order.iter().map(|&i| (i, self.widgets[i].visible(), self.positions[i])).collect::<Vec<_>>());
 
         for &i in &draw_order {
             let w = &self.widgets[i];
@@ -4187,8 +4123,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 }
             } else {
                 verts.extend(widget_vertices(w.as_ref(), sw, sh, active_clip_circle));
-                for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                    verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle));
+                if i == PAGINATOR_IDX {
+                    let eq = w.extra_quads();
+                    println!("DEBUG_COLLECT: Paginator extra_quads len = {}", eq.len());
+                    for (qx, qy, qw, qh, qc) in eq {
+                        verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle));
+                    }
+                } else {
+                    for (qx, qy, qw, qh, qc) in w.extra_quads() {
+                        verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc, active_clip_circle));
+                    }
                 }
                 for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
                     verts.extend(arc_background_vertices(
@@ -4250,14 +4194,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let data = bytemuck::cast_slice(&verts);
         let needed = data.len() as wgpu::BufferAddress;
         if needed > self.vertex_buffer.size() {
-            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
+            self.vertex_buffer = self.wgpu_adapter.device.create_buffer(&wgpu::BufferDescriptor {
                 label: Some("Vertex Buffer"),
                 size: needed,
                 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
                 mapped_at_creation: false,
             });
         }
-        self.queue.write_buffer(&self.vertex_buffer, 0, data);
+        self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer, 0, data);
     }
 
     fn rebuild_scene_geometry(&mut self) {
@@ -4296,19 +4240,18 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
         let verts = geom.to_vertex3d_vec();
         self.vertex_count_spheres = verts.len() as u32;
-        if verts.is_empty() {
-            self.vertex_buffer_spheres = self.device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("Sphere Node Vertex Buffer"),
-                size: 1,
-                usage: wgpu::BufferUsages::VERTEX,
-                mapped_at_creation: false,
-            });
-        } else {
-            self.vertex_buffer_spheres = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some("Sphere Node Vertex Buffer"),
-                contents: bytemuck::cast_slice(&verts),
-                usage: wgpu::BufferUsages::VERTEX,
-            });
+        if !verts.is_empty() {
+            let data = bytemuck::cast_slice(&verts);
+            let needed = data.len() as wgpu::BufferAddress;
+            if needed > self.vertex_buffer_spheres.size() {
+            self.vertex_buffer_spheres = self.wgpu_adapter.device.create_buffer(&wgpu::BufferDescriptor {
+                    label: Some("Sphere Node Vertex Buffer"),
+                    size: needed,
+                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                    mapped_at_creation: false,
+                });
+            }
+            self.wgpu_adapter.queue.write_buffer(&self.vertex_buffer_spheres, 0, data);
         }
     }
 
@@ -4317,9 +4260,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn prepare_text(&mut self) {
-        // 1. Prepare text on all widgets using self.font_system
+        // 1. Prepare text on all widgets using self.wgpu_adapter.font_system
         for w in &mut self.widgets {
-            w.prepare_text(&mut self.font_system);
+            w.prepare_text(&mut self.wgpu_adapter.font_system);
         }
 
         // 2. Destructure self
@@ -4333,9 +4276,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let network_circle_radius = self.circular_network_layout.r;
 
         let Self {
-            ref mut text_renderer, ref device, ref queue,
-            ref mut font_system, ref mut text_atlas, ref mut text_viewport,
-            ref mut swash_cache,
+            ref mut wgpu_adapter,
             physical_width, physical_height, scale,
             ref widgets,
             ref curved_text_texture,
@@ -4347,6 +4288,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             ..
         } = self;
 
+        let clear_ui::backend::WgpuAdapter {
+            ref device,
+            ref queue,
+            ref mut font_system,
+            ref mut text_atlas,
+            ref mut text_viewport,
+            ref mut text_renderer,
+            ref mut swash_cache,
+            ..
+        } = wgpu_adapter;
+
         let viewport = Resolution { width: *physical_width, height: *physical_height };
         text_viewport.update(queue, viewport);
         let s = *scale as f32;
@@ -4394,10 +4346,11 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             };
 
             let cached_items = w.get_text_items();
+            let has_cached_items = !cached_items.is_empty();
             // if i == LEFT_MENUBAR_IDX {
             //     eprintln!("DEBUG_PREPARE: i={} cached_items.len={} circular_network_pane={}", i, cached_items.len(), circular_network_pane);
             // }
-            if !cached_items.is_empty() && !(circular_network_pane && i == LEFT_MENUBAR_IDX) {
+            if has_cached_items && !(circular_network_pane && i == LEFT_MENUBAR_IDX) {
                 for (buf, x, y, color) in cached_items {
                     if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
                         let dx = x - network_circle_x;
@@ -4409,16 +4362,17 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     }
                     areas.push(TextArea {
                         buffer: buf,
-                        left: x * s,
-                        top: y * s,
+                        left: (x * s).round(),
+                        top: (y * s).round(),
                         scale: s,
                         bounds,
                         default_color: color,
                         custom_glyphs: &[],
                     });
                 }
-            } else {
-                let labels = w.text_labels_with_font_and_bounds();
+            }
+            if !has_cached_items || (circular_network_pane && i == LEFT_MENUBAR_IDX) || w.is_menu_bar() {
+                let labels = w.text_labels_with_font_and_bounds(&self.ui_context);
                 // if i == LEFT_MENUBAR_IDX {
                 //     eprintln!("DEBUG_PREPARE_ELSE: i={} labels.len={}", i, labels.len());
                 // }
@@ -4458,9 +4412,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 bottom: item_bounds.bottom.min(pb),
                             };
                         }
-                        if label.text.contains("Grid") || label.text.contains("Background") || label.text.contains("Snap") || label.text.contains("Opacity") {
-                            eprintln!("DEBUG_PREPARE_LABEL: text='{}' x={} y={} font_size={} s={} left={} top={} bounds={:?}", label.text, label.x, label.y, label.font_size, s, label.x * s, label.y * s, item_bounds);
-                        }
+                        // if label.text.contains("Grid") || label.text.contains("Background") || label.text.contains("Snap") || label.text.contains("Opacity") {
+                        //     eprintln!("DEBUG_PREPARE_LABEL: text='{}' x={} y={} font_size={} s={} left={} top={} bounds={:?}", label.text, label.x, label.y, label.font_size, s, label.x * s, label.y * s, item_bounds);
+                        // }
                         legacy_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font_opt.as_deref()));
                         legacy_labels.push(label);
                         legacy_bounds.push(item_bounds);
@@ -4473,8 +4427,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         for ((buf, label), bounds) in legacy_buffers.iter().zip(legacy_labels.iter()).zip(legacy_bounds.iter()) {
             areas.push(TextArea {
                 buffer: buf,
-                left: label.x * s,
-                top: label.y * s,
+                left: (label.x * s).round(),
+                top: (label.y * s).round(),
                 scale: s,
                 bounds: *bounds,
                 default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
@@ -4532,8 +4486,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             for draw in &curved_draws {
                 curved_areas.push(TextArea {
                     buffer: &draw.buffer,
-                    left: draw.tx,
-                    top: draw.ty,
+                    left: draw.tx.round(),
+                    top: draw.ty.round(),
                     scale: s,
                     bounds: TextBounds {
                         left: 0,
@@ -4672,10 +4626,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             self.physical_height = height;
             self.width = width as f32 / self.scale as f32;
             self.height = height as f32 / self.scale as f32;
-            self.config.width = width;
-            self.config.height = height;
-            self.config.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST;
-            self.surface.configure(&self.device, &self.config);
+            self.wgpu_adapter.resize(width, height);
 
             let (tex, view) = self.create_depth_texture();
             self.depth_texture = tex;
@@ -4685,7 +4636,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             self.backdrop_texture = b_tex;
             self.backdrop_texture_view = b_view;
 
-            self.backdrop_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            self.backdrop_bind_group = self.wgpu_adapter.device.create_bind_group(&wgpu::BindGroupDescriptor {
                 label: Some("Backdrop Bind Group"),
                 layout: &self.backdrop_bind_group_layout,
                 entries: &[
@@ -4720,7 +4671,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             WindowEvent::MouseWheel { delta, phase, .. } => {
                 let dialog_open = self.node_palette_visible;
                 let in_network_pane = self.in_network_pane();
-                eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
+                // eprintln!("DEBUG MOUSEWHEEL: delta={:?}, phase={:?}, cursor=({}, {}), in_network_pane={}", delta, phase, self.cursor_x, self.cursor_y, in_network_pane);
                 let node_area_y = self.positions[CONTENT_IDX].1;
 
                 let in_viewport = self.cursor_x >= self.content_right_x()
@@ -4754,8 +4705,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 let mut handled = false;
                 let mut needs_sync_grid = false;
                 if !dialog_open && !self.modifiers.control_key() {
+                    let ctx = &mut self.ui_context;
                     for (i, w) in self.widgets.iter_mut().enumerate() {
-                        if w.mouse_wheel(delta, self.cursor_x, self.cursor_y) {
+                        if w.mouse_wheel(delta, self.cursor_x, self.cursor_y, ctx) {
                             handled = true;
                             if i == CONTENT_IDX {
                                 let active_node_area_y = self.positions[CONTENT_IDX].1;
@@ -5005,8 +4957,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             WindowEvent::CursorMoved { position, .. } => {
                 self.cursor_x = position.x as f32;
                 self.cursor_y = position.y as f32;
-                println!("DEBUG: CursorMoved position=({:?}) scale={} => ({}, {})", position, self.scale, self.cursor_x, self.cursor_y);
-                let _ = std::io::Write::flush(&mut std::io::stdout());
                 let mut changed = false;
 
                 if self.is_panning {
@@ -5092,15 +5042,15 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 } else if i == BREADCRUMB_IDX {
                                     self.circular_network_layout.hit_test_breadcrumb(cx, cy, MENUBAR_H, BREADCRUMB_H)
                                 } else if i == NETWORK_PANEL_IDX {
-                                    self.widgets[NETWORK_PANEL_IDX].hit_test(cx, cy)
+                                    self.widgets[NETWORK_PANEL_IDX].hit_test(cx, cy, &self.ui_context)
                                 } else {
                                     false
                                 }
                             } else {
-                                self.widgets[i].hit_test(cx, cy)
+                                self.widgets[i].hit_test(cx, cy, &self.ui_context)
                             };
                             let (tx, ty) = if inside { (cx, cy) } else { (-9999.0, -9999.0) };
-                            if self.widgets[i].cursor_moved(tx, ty) {
+                            if self.widgets[i].cursor_moved(tx, ty, &mut self.ui_context) {
                                 changed = true;
                             }
                         }
@@ -5109,7 +5059,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 changed
             }
             WindowEvent::MouseInput { state: btn_state, button, .. } => {
-                println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
+                // println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
                 if *btn_state == ElementState::Pressed {
                     self.pan_velocity_x = 0.0;
                     self.pan_velocity_y = 0.0;
@@ -5189,7 +5139,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             false
                         }
                     } else {
-                        state.widgets[i].hit_test(x, y)
+                        state.widgets[i].hit_test(x, y, &state.ui_context)
                     }
                 };
 
@@ -5201,9 +5151,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
                 match btn_state {
                     ElementState::Pressed => {
-                        println!("DEBUG: Mouse Pressed button={:?} position=({}, {})", button, self.cursor_x, self.cursor_y);
-                        let _ = std::io::Write::flush(&mut std::io::stdout());
-
                         let hits_any_menu = (0..self.widgets.len()).any(|i| {
                             hits_widget(self, i, self.cursor_x, self.cursor_y)
                                 && self.widgets[i].get_menu_items_at(self.cursor_x, self.cursor_y).is_some()
@@ -5229,6 +5176,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             let hit_menubar = self.circular_network_layout.hit_test_menubar(self.cursor_x, self.cursor_y, MENUBAR_H);
                             if hit_menubar {
                                 if let Some((menu_idx, title, items, rx, ry, rw, rh)) = self.widgets[LEFT_MENUBAR_IDX].get_menu_items_at(self.cursor_x, self.cursor_y) {
+                                    self.focused_pane = LEFT_MENUBAR_IDX;
+                                    self.widgets[PAGINATOR_IDX].set_page_hidden(false);
+                                    self.widgets[PAGINATOR_IDX].set_selected_page(menu_idx);
+                                    self.sync_pane_focus();
                                     self.spawn_menu_cloud(LEFT_MENUBAR_IDX, menu_idx, title, items, rx, ry, rw, rh);
                                     return true;
                                 }
@@ -5278,7 +5229,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 self.sync_parameters_to_project();
                                 return true;
                             } else if cx >= fx && cx < fx + fw && cy >= fy && cy < fy + (if self.show_network { BREADCRUMB_H } else { 0.0 }) {
-                                if self.widgets[BREADCRUMB_IDX].mouse_input(*button, *btn_state, cx, cy) {
+                                if self.widgets[BREADCRUMB_IDX].mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context) {
                                     return true;
                                 }
                                 self.is_resizing_network = None;
@@ -5464,11 +5415,15 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                         }
                         if let Some(i) = click_target {
                             if let Some((menu_idx, title, items, rx, ry, rw, rh)) = self.widgets[i].get_menu_items_at(self.cursor_x, self.cursor_y) {
+                                self.focused_pane = i;
+                                self.widgets[PAGINATOR_IDX].set_page_hidden(false);
+                                self.widgets[PAGINATOR_IDX].set_selected_page(menu_idx);
+                                self.sync_pane_focus();
                                 self.spawn_menu_cloud(i, menu_idx, title, items, rx, ry, rw, rh);
                                 return true;
                             }
                             self.widgets[i].set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
-                            if self.widgets[i].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y) {
+                            if self.widgets[i].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y, &mut self.ui_context) {
                                 changed = true;
                                 if i == PARAM_IDX {
                                     self.sync_parameters_to_project();
@@ -5493,7 +5448,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             if i != PARAM_IDX {
                                 self.widgets[i].focus();
                                 self.focused_widget = Some(i);
-                                if self.widgets[i].is_menu_bar() && !self.widgets[i].focused() {
+                                if self.widgets[i].is_menu_bar() && !self.widgets[i].focused(&self.ui_context) {
                                     self.widgets[i].unfocus();
                                     self.focused_widget = None;
                                 }
@@ -5570,9 +5525,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             self.drag_widget = None;
                             changed = true;
                         }
+                        let ctx = &mut self.ui_context;
                         for w in &mut self.widgets {
                             w.set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
-                            if w.mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y) {
+                            if w.mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y, ctx) {
                                 changed = true;
                             }
                         }
@@ -5623,7 +5579,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     }
                 }
 
-                if self.widgets[PARAM_IDX].keyboard_input(event) {
+                if self.widgets[PARAM_IDX].keyboard_input(event, &mut self.ui_context) {
                     self.sync_parameters_to_project();
                     return true;
                 }
@@ -5866,18 +5822,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             self.open_node_palette();
                             return true;
                         }
-                        for bind in &self.keybinds {
-                            if bind.matches(&self.modifiers, &event.logical_key) {
-                                self.pending_action = Some(bind.action);
-                                return true;
-                            }
+                        if let Some(action) = self.shortcut_manager.match_action(&self.modifiers, &event.logical_key) {
+                            self.pending_action = Some(action);
+                            return true;
                         }
                     }
                 }
                 if changed {
                     true
                 } else if let Some(idx) = self.focused_widget {
-                    self.widgets[idx].keyboard_input(event)
+                    self.widgets[idx].keyboard_input(event, &mut self.ui_context)
                 } else { false }
             }
         }
@@ -5983,11 +5937,15 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         }
 
         let mut tick_changed = false;
+        let ctx = &mut self.ui_context;
         for w in &mut self.widgets {
-            if w.tick(dt) {
+            if w.tick(dt, ctx) {
                 tick_changed = true;
             }
         }
+        if clear_ui::widget::hover_animation::tick(dt) {
+            tick_changed = true;
+        }
         self.update_recent_files_layout();
 
         // Panning kinetic slide
@@ -6108,10 +6066,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
         self.prepare_text();
 
-        let output = match self.surface.get_current_texture() {
+        let output = match self.wgpu_adapter.surface.get_current_texture() {
             Ok(t) => t,
             Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
-                self.surface.configure(&self.device, &self.config);
+                self.wgpu_adapter.surface.configure(&self.wgpu_adapter.device, &self.wgpu_adapter.config);
                 return false;
             }
             Err(wgpu::SurfaceError::Timeout) => return false,
@@ -6119,7 +6077,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         };
 
         let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
-        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+        let mut encoder = self.wgpu_adapter.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
             label: Some("Encoder"),
         });
 
@@ -6211,10 +6169,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
                 let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
                 let mvp = proj * view_mat * model;
-                self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
+                self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
 
                 let mvp_grid = proj * view_mat * model;
-                self.queue.write_buffer(&self.uniform_buffer_grid, 0, bytemuck::cast_slice(&[mvp_grid.to_cols_array_2d()]));
+                self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_grid, 0, bytemuck::cast_slice(&[mvp_grid.to_cols_array_2d()]));
 
                 let cam_angle_y = camera_pos.x.atan2(camera_pos.z);
                 let rot_angle = if self.active_camera != "Default Camera" {
@@ -6224,7 +6182,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 };
                 let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
                 let mvp_pivot = proj * view_mat * model_pivot;
-                self.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[mvp_pivot.to_cols_array_2d()]));
+                self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[mvp_pivot.to_cols_array_2d()]));
 
                 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("3D Render Pass"),
@@ -6382,11 +6340,11 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 pass.draw(0..self.textured_vertex_count, 0..1);
             }
 
-            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
+            self.wgpu_adapter.text_renderer.render(&self.wgpu_adapter.text_atlas, &self.wgpu_adapter.text_viewport, &mut pass).unwrap();
 
         }
 
-        self.queue.submit(std::iter::once(encoder.finish()));
+        self.wgpu_adapter.queue.submit(std::iter::once(encoder.finish()));
         output.present();
         tick_changed || panned
     }
@@ -6533,7 +6491,7 @@ impl SeatHandler for AppState {
 
     fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
         self.seats.push(seat);
-        eprintln!("DEBUG SEAT: new_seat called, total seats now: {}", self.seats.len());
+        // eprintln!("DEBUG SEAT: new_seat called, total seats now: {}", self.seats.len());
     }
 
     fn new_capability(
@@ -6543,7 +6501,7 @@ impl SeatHandler for AppState {
         seat: wl_seat::WlSeat,
         capability: Capability,
     ) {
-        eprintln!("DEBUG SEAT: new_capability: {:?}", capability);
+        // eprintln!("DEBUG SEAT: new_capability: {:?}", capability);
         if capability == Capability::Pointer && self.pointer.is_none() {
             let surface = self.compositor_state.create_surface(qh);
             let themed_pointer = self.seat_state.get_pointer_with_theme(
@@ -6581,7 +6539,7 @@ impl SeatHandler for AppState {
 
     fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
         self.seats.retain(|s| s != &seat);
-        eprintln!("DEBUG SEAT: remove_seat called, total seats now: {}", self.seats.len());
+        // eprintln!("DEBUG SEAT: remove_seat called, total seats now: {}", self.seats.len());
     }
 }
 
@@ -6662,10 +6620,10 @@ impl PointerHandler for AppState {
                                     let seat = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
                                     if let Some(ref seat) = seat {
                                         if pending.is_move {
-                                            eprintln!("DEBUG DRAG INITIATING window.move_ with serial={}", pending.serial);
+                                            // eprintln!("DEBUG DRAG INITIATING window.move_ with serial={}", pending.serial);
                                             window.move_(seat, pending.serial);
                                         } else {
-                                            eprintln!("DEBUG RESIZE INITIATING window.resize with edge={:?}, serial={}", pending.edge, pending.serial);
+                                            // eprintln!("DEBUG RESIZE INITIATING window.resize with edge={:?}, serial={}", pending.edge, pending.serial);
                                             window.resize(seat, pending.serial, pending.edge);
                                         }
                                     }
@@ -6689,7 +6647,7 @@ impl PointerHandler for AppState {
                             274 => clear_ui::widget::MouseButton::Middle,
                             _ => continue,
                         };
-                        eprintln!("DEBUG MOUSE PRESS: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
+                        // eprintln!("DEBUG MOUSE PRESS: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
 
                         if let Some(ref st) = self.state {
                             if st.is_detached_network && btn == clear_ui::widget::MouseButton::Left {
@@ -6702,9 +6660,9 @@ impl PointerHandler for AppState {
                                 let in_menubar_bg = dy < 0.0 && dist >= st.circular_network_layout.r - 35.0 && dist <= st.circular_network_layout.r;
                                 let hits_any_menu = st.widgets[LEFT_MENUBAR_IDX].get_menu_items_at(lx, ly).is_some();
 
-                                eprintln!("DEBUG DRAG: lx={}, ly={}, cx={}, cy={}, r={}, dx={}, dy={}, dist={}, on_border={}, in_menubar_bg={}, hits_any_menu={}, seats_len={}, has_window={}",
-                                    lx, ly, st.circular_network_layout.x, st.circular_network_layout.y, st.circular_network_layout.r,
-                                    dx, dy, dist, on_border, in_menubar_bg, hits_any_menu, self.seats.len(), self.window.is_some());
+                                // eprintln!("DEBUG DRAG: lx={}, ly={}, cx={}, cy={}, r={}, dx={}, dy={}, dist={}, on_border={}, in_menubar_bg={}, hits_any_menu={}, seats_len={}, has_window={}",
+                                //     lx, ly, st.circular_network_layout.x, st.circular_network_layout.y, st.circular_network_layout.r,
+                                //     dx, dy, dist, on_border, in_menubar_bg, hits_any_menu, self.seats.len(), self.window.is_some());
 
                                 if (on_border || in_menubar_bg) && !hits_any_menu {
                                     if let Some(ref _window) = self.window {
@@ -6775,7 +6733,7 @@ impl PointerHandler for AppState {
                             274 => clear_ui::widget::MouseButton::Middle,
                             _ => continue,
                         };
-                        eprintln!("DEBUG MOUSE RELEASE: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
+                        // eprintln!("DEBUG MOUSE RELEASE: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
                         if btn == clear_ui::widget::MouseButton::Left {
                             self.pending_resize = None;
                         }
@@ -6788,7 +6746,7 @@ impl PointerHandler for AppState {
                     PointerEventKind::Axis { horizontal, vertical, .. } => {
                         let h_val = horizontal.absolute as f32;
                         let v_val = vertical.absolute as f32;
-                        eprintln!("DEBUG AXIS EVENT: horizontal={:?}, vertical={:?}, scale={}", horizontal, vertical, st.scale);
+                        // eprintln!("DEBUG AXIS EVENT: horizontal={:?}, vertical={:?}, scale={}", horizontal, vertical, st.scale);
                         let ev = WindowEvent::MouseWheel {
                             delta: clear_ui::widget::MouseScrollDelta::LineDelta(-h_val / 10.0, -v_val / 10.0),
                             phase: TouchPhase::Moved,
@@ -6884,7 +6842,7 @@ impl WindowHandler for AppState {
         _serial: u32,
     ) {
         let (w, h) = configure.new_size;
-        eprintln!("DEBUG CONFIGURE: new_size={:?}, configure={:?}", configure.new_size, configure);
+        // eprintln!("DEBUG CONFIGURE: new_size={:?}, configure={:?}", configure.new_size, configure);
         if let (Some(w), Some(h)) = (w, h) {
             let width = w.get();
             let height = h.get();
@@ -6940,7 +6898,7 @@ impl wayland_client::Dispatch<clear_ui::protocol::zclear_inspector_v1::ZclearIns
                     let expected_id = if st.is_detached_network {
                         "circular-network-pane"
                     } else {
-                        "clear-design-interface"
+                        "cce-design-interface"
                     };
                     if app_id == expected_id {
                         st.window_x = x;
@@ -7000,7 +6958,7 @@ impl AppState {
             }
         };
 
-        eprintln!("DEBUG KEY: keysym={:?}, state={:?}, logical_key={:?}", event.keysym, state, logical_key);
+        // eprintln!("DEBUG KEY: keysym={:?}, state={:?}, logical_key={:?}", event.keysym, state, logical_key);
 
         if let Some(st) = &mut self.state {
             match event.keysym {
@@ -7148,6 +7106,22 @@ impl AppState {
                     }
                 } else if menu_idx == 2 { // View
                     match item_idx {
+                        0 => { // Zoom In
+                            state.zoom(1.15, None);
+                            changed = true;
+                        }
+                        1 => { // Zoom Out
+                            state.zoom(1.0 / 1.15, None);
+                            changed = true;
+                        }
+                        2 => { // Reset Zoom
+                            state.grid_size_x = 150.0;
+                            state.grid_size_y = 75.0;
+                            state.skipped_col_w = 37.5;
+                            state.skipped_row_h = 37.5;
+                            state.sync_grid_settings();
+                            changed = true;
+                        }
                         3 => { // Detach Circular Window
                             state.execute_action(Action::DetachCircularWindow);
                             changed = true;
@@ -7276,6 +7250,14 @@ impl AppState {
                     }
                 } else if menu_idx == 2 { // View
                     match item_idx {
+                        0 => { // Zoom In
+                            state.zoom(1.15, None);
+                            changed = true;
+                        }
+                        1 => { // Zoom Out
+                            state.zoom(1.0 / 1.15, None);
+                            changed = true;
+                        }
                         2 => {
                             state.circular_network_pane = !state.circular_network_pane;
                             state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
@@ -7372,7 +7354,69 @@ impl AppState {
             }
 
             if let Some((menu_idx, item_idx)) = state.widgets[PARAM_MENUBAR_IDX].menu_click() {
-                if menu_idx == 2 { // View
+                if menu_idx == 0 { // Preset
+                    if let Some(slot_idx) = state.widgets[CONTENT_IDX].selected_node() {
+                        let node_type = state.current_dir().children[slot_idx].node_type.clone();
+                        let template_params = state.node_templates.iter()
+                            .find(|t| t.node.node_type == node_type)
+                            .map(|t| t.node.params.clone());
+                        if let Some(params_to_reset) = template_params {
+                            if item_idx == 0 { // Default
+                                for template_param in &params_to_reset {
+                                    if let Some(p) = state.current_dir_mut().children[slot_idx].params.iter_mut().find(|p| p.name == template_param.name) {
+                                        p.default = template_param.default.clone();
+                                    }
+                                }
+                            } else if item_idx == 1 { // Custom
+                                for template_param in &params_to_reset {
+                                    if let Some(p) = state.current_dir_mut().children[slot_idx].params.iter_mut().find(|p| p.name == template_param.name) {
+                                        if let Ok(v) = template_param.default.parse::<f32>() {
+                                            p.default = format!("{:.2}", v * 1.5);
+                                        } else if let Ok(v) = template_param.default.parse::<i32>() {
+                                            p.default = format!("{}", v * 2);
+                                        } else if template_param.default.contains(':') {
+                                            let parts: Vec<&str> = template_param.default.split(':').collect();
+                                            let custom_parts: Vec<String> = parts.iter().map(|p_str| {
+                                                if let Ok(v) = p_str.parse::<f32>() {
+                                                    format!("{:.2}", v * 1.5)
+                                                } else {
+                                                    p_str.to_string()
+                                                }
+                                            }).collect();
+                                            p.default = custom_parts.join(":");
+                                        } else {
+                                            p.default = template_param.default.clone();
+                                        }
+                                    }
+                                }
+                            }
+                            state.sync_nodes();
+                            state.rebuild_scene_geometry();
+                            state.upload_vertices();
+                            changed = true;
+                        }
+                    }
+                } else if menu_idx == 1 { // Reset
+                    if item_idx == 0 { // All
+                        if let Some(slot_idx) = state.widgets[CONTENT_IDX].selected_node() {
+                            let node_type = state.current_dir().children[slot_idx].node_type.clone();
+                            let template_params = state.node_templates.iter()
+                                .find(|t| t.node.node_type == node_type)
+                                .map(|t| t.node.params.clone());
+                            if let Some(params_to_reset) = template_params {
+                                for template_param in &params_to_reset {
+                                    if let Some(p) = state.current_dir_mut().children[slot_idx].params.iter_mut().find(|p| p.name == template_param.name) {
+                                        p.default = template_param.default.clone();
+                                    }
+                                }
+                                state.sync_nodes();
+                                state.rebuild_scene_geometry();
+                                state.upload_vertices();
+                                changed = true;
+                            }
+                        }
+                    }
+                } else if menu_idx == 2 { // View
                     if item_idx == 0 { // Close Pane
                         state.show_parameters = false;
                         state.widgets[PARAM_IDX].set_visible(false);
@@ -7446,6 +7490,7 @@ impl AppState {
                     vec![]
                 };
                 state.widgets[PARAM_IDX].set_display_params(&params);
+                state.update_paginator();
 
                 state.upload_vertices();
             }
@@ -7458,14 +7503,7 @@ impl AppState {
             if changed {
                 state.update_window_title();
                 if state.is_detached_network || state.detached_circular_network {
-                    let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
-                    if let Err(e) = state.save_to_file(&default_proj_path) {
-                        eprintln!("Failed to auto-save default project in process_event: {:?}", e);
-                    } else if let Ok(m) = std::fs::metadata(&default_proj_path) {
-                        if let Ok(mod_time) = m.modified() {
-                            state.last_project_mod_time = Some(mod_time);
-                        }
-                    }
+                    state.needs_autosave = true;
                 }
                 self.redraw = true;
             }
@@ -7716,6 +7754,16 @@ impl AppState {
                             }
                             Ok("Menu closed".to_string())
                         }
+                        HttpAction::SelectPage { page } => {
+                            state.widgets[PAGINATOR_IDX].set_page_hidden(false);
+                            state.widgets[PAGINATOR_IDX].set_selected_page(page);
+                            state.update_paginator();
+                            state.rebuild_positions();
+                            state.apply_layout();
+                            state.upload_vertices();
+                            needs_redraw = true;
+                            Ok("Page selected".to_string())
+                        }
                     };
                     let _ = tx.send(res);
                 }
@@ -7734,14 +7782,7 @@ impl AppState {
             if let Some(state) = &mut self.state {
                 state.update_window_title();
                 if state.is_detached_network || state.detached_circular_network {
-                    let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
-                    if let Err(e) = state.save_to_file(&default_proj_path) {
-                        eprintln!("Failed to auto-save default project in handle_user_event: {:?}", e);
-                    } else if let Ok(m) = std::fs::metadata(&default_proj_path) {
-                        if let Ok(mod_time) = m.modified() {
-                            state.last_project_mod_time = Some(mod_time);
-                        }
-                    }
+                    state.needs_autosave = true;
                 }
             }
             self.redraw = true;
@@ -7958,14 +7999,16 @@ fn main() {
     const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
 
     loop {
-        let timeout = if app.redraw {
-            std::time::Duration::from_millis(0)
-        } else {
-            std::time::Duration::from_millis(16)
-        };
+        let timeout = std::time::Duration::from_millis(16);
         event_loop.dispatch(timeout, &mut app).unwrap();
 
         if app.exit || app.state.as_ref().map(|s| s.exit_requested).unwrap_or(false) {
+            if let Some(state) = &mut app.state {
+                if state.needs_autosave && (state.is_detached_network || state.detached_circular_network) {
+                    let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+                    let _ = state.save_to_file(&default_proj_path);
+                }
+            }
             break;
         }
 
@@ -7978,6 +8021,22 @@ fn main() {
                 }
             }
 
+            if state.needs_autosave && (state.is_detached_network || state.detached_circular_network) {
+                let now = std::time::Instant::now();
+                if now.duration_since(state.last_autosave_time) >= std::time::Duration::from_millis(200) {
+                    state.needs_autosave = false;
+                    state.last_autosave_time = now;
+                    let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
+                    if let Err(e) = state.save_to_file(&default_proj_path) {
+                        eprintln!("Failed to auto-save default project in main loop: {:?}", e);
+                    } else if let Ok(m) = std::fs::metadata(&default_proj_path) {
+                        if let Ok(mod_time) = m.modified() {
+                            state.last_project_mod_time = Some(mod_time);
+                        }
+                    }
+                }
+            }
+
             if state.is_detached_network || state.detached_circular_network {
                 let now = std::time::Instant::now();
                 if now.duration_since(state.last_project_check) >= std::time::Duration::from_millis(100) {
@@ -8044,11 +8103,15 @@ fn create_memfd_with_data(name: &str, data: &[u8]) -> std::io::Result<std::os::u
                 }
                 if let Some(ref inspector) = app.inspector {
                     if let Some(ref surface) = app.surface {
-                        let json = clear_ui::widget::serialize_widgets(&state.widgets);
-                        if let Ok(raw_fd) = create_memfd_with_data("clear_ui_state", json.as_bytes()) {
-                            use std::os::unix::io::{FromRawFd, AsFd};
-                            let file = unsafe { std::fs::File::from_raw_fd(raw_fd) };
-                            inspector.update_state(surface, file.as_fd(), json.len() as u32);
+                        let now = std::time::Instant::now();
+                        if now.duration_since(state.last_inspector_update) >= std::time::Duration::from_millis(100) {
+                            state.last_inspector_update = now;
+                            let json = clear_ui::widget::serialize_widgets(&state.widgets);
+                            if let Ok(raw_fd) = create_memfd_with_data("clear_ui_state", json.as_bytes()) {
+                                use std::os::unix::io::{FromRawFd, AsFd};
+                                let file = unsafe { std::fs::File::from_raw_fd(raw_fd) };
+                                inspector.update_state(surface, file.as_fd(), json.len() as u32);
+                            }
                         }
                     }
                 }
@@ -8485,5 +8548,53 @@ mod tests {
         assert_eq!(vx_e, 236.0);
         assert_eq!(vw_e, 764.0);
     }
+
+    #[test]
+    fn test_keyboard_shortcut_system() {
+        // Test parsing simple shortcut
+        let ctrl_g = Shortcut::parse("Ctrl+g").unwrap();
+        assert_eq!(ctrl_g.ctrl, true);
+        assert_eq!(ctrl_g.shift, false);
+        assert_eq!(ctrl_g.alt, false);
+        assert_eq!(ctrl_g.logo, false);
+        assert_eq!(ctrl_g.key, Key::Character("g".to_string()));
+
+        // Test parsing complex shortcut
+        let complex = Shortcut::parse("Ctrl+Shift+Alt+Logo+s").unwrap();
+        assert_eq!(complex.ctrl, true);
+        assert_eq!(complex.shift, true);
+        assert_eq!(complex.alt, true);
+        assert_eq!(complex.logo, true);
+        assert_eq!(complex.key, Key::Character("s".to_string()));
+
+        // Test parsing named keys
+        let tab_sc = Shortcut::parse("Tab").unwrap();
+        assert_eq!(tab_sc.key, Key::Named(NamedKey::Tab));
+
+        // Test parsing case insensitivity
+        let case_sc = Shortcut::parse("cTrL+sHiFt+ArrowDown").unwrap();
+        assert_eq!(case_sc.ctrl, true);
+        assert_eq!(case_sc.shift, true);
+        assert_eq!(case_sc.key, Key::Named(NamedKey::ArrowDown));
+
+        // Test register and match
+        let mut mgr = ShortcutManager::new();
+        mgr.register("Ctrl+g", Action::ToggleGrid).unwrap();
+        mgr.register("`", Action::ToggleSpreadsheet).unwrap();
+
+        // Matches with ctrl and g
+        let mods_ctrl = ModifiersState { ctrl: true, alt: false, shift: false, logo: false };
+        let key_g = Key::Character("g".to_string());
+        assert_eq!(mgr.match_action(&mods_ctrl, &key_g), Some(Action::ToggleGrid));
+
+        // No match with ctrl and a
+        let key_a = Key::Character("a".to_string());
+        assert_eq!(mgr.match_action(&mods_ctrl, &key_a), None);
+
+        // Matches backtick with no modifiers
+        let mods_none = ModifiersState::default();
+        let key_tick = Key::Character("`".to_string());
+        assert_eq!(mgr.match_action(&mods_none, &key_tick), Some(Action::ToggleSpreadsheet));
+    }
 }
 
diff --git a/src/shortcut.rs b/src/shortcut.rs
new file mode 100644
index 0000000..c8ef0be
--- /dev/null
+++ b/src/shortcut.rs
@@ -0,0 +1,108 @@
+use clear_ui::widget::{Key, NamedKey};
+use crate::ModifiersState;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Action {
+    ToggleGrid,
+    ToggleCube,
+    ToggleSquareViewport,
+    ToggleConfigure,
+    ToggleSpreadsheet,
+    ToggleOrigin,
+    ToggleCameraPivot,
+    ToggleCircularPane,
+    DetachCircularWindow,
+    Save,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Shortcut {
+    pub ctrl: bool,
+    pub shift: bool,
+    pub alt: bool,
+    pub logo: bool,
+    pub key: Key,
+}
+
+impl Shortcut {
+    pub fn parse(s: &str) -> Result<Self, String> {
+        let parts: Vec<&str> = s.split('+').map(|p| p.trim()).collect();
+        let mut ctrl = false;
+        let mut shift = false;
+        let mut alt = false;
+        let mut logo = false;
+        let mut key_opt = None;
+
+        if parts.is_empty() {
+            return Err("Empty shortcut".to_string());
+        }
+
+        for (i, &part) in parts.iter().enumerate() {
+            let lower = part.to_lowercase();
+            if i < parts.len() - 1 {
+                match lower.as_str() {
+                    "ctrl" | "control" => ctrl = true,
+                    "shift" => shift = true,
+                    "alt" => alt = true,
+                    "super" | "win" | "logo" | "cmd" | "command" => logo = true,
+                    _ => return Err(format!("Unknown modifier: {}", part)),
+                }
+            } else {
+                let key = match lower.as_str() {
+                    "tab" => Key::Named(NamedKey::Tab),
+                    "enter" | "return" => Key::Named(NamedKey::Enter),
+                    "escape" | "esc" => Key::Named(NamedKey::Escape),
+                    "space" => Key::Named(NamedKey::Space),
+                    "backspace" => Key::Named(NamedKey::Backspace),
+                    "down" | "arrowdown" => Key::Named(NamedKey::ArrowDown),
+                    "up" | "arrowup" => Key::Named(NamedKey::ArrowUp),
+                    "left" | "arrowleft" => Key::Named(NamedKey::ArrowLeft),
+                    "right" | "arrowright" => Key::Named(NamedKey::ArrowRight),
+                    "end" => Key::Named(NamedKey::End),
+                    "home" => Key::Named(NamedKey::Home),
+                    "pagedown" | "pgdown" => Key::Named(NamedKey::PageDown),
+                    "pageup" | "pgup" => Key::Named(NamedKey::PageUp),
+                    "delete" | "del" => Key::Named(NamedKey::Delete),
+                    _ => Key::Character(part.to_string()),
+                };
+                key_opt = Some(key);
+            }
+        }
+
+        let key = key_opt.ok_or_else(|| "Missing key in shortcut".to_string())?;
+        Ok(Shortcut { ctrl, shift, alt, logo, key })
+    }
+
+    pub fn matches(&self, mods: &ModifiersState, key: &Key) -> bool {
+        mods.control_key() == self.ctrl
+            && mods.shift_key() == self.shift
+            && mods.alt_key() == self.alt
+            && mods.super_key() == self.logo
+            && key == &self.key
+    }
+}
+
+pub struct ShortcutManager {
+    bindings: Vec<(Shortcut, Action)>,
+}
+
+impl ShortcutManager {
+    pub fn new() -> Self {
+        ShortcutManager { bindings: Vec::new() }
+    }
+
+    pub fn register(&mut self, shortcut_str: &str, action: Action) -> Result<(), String> {
+        let shortcut = Shortcut::parse(shortcut_str)?;
+        self.bindings.push((shortcut, action));
+        Ok(())
+    }
+
+    pub fn match_action(&self, mods: &ModifiersState, key: &Key) -> Option<Action> {
+        for (shortcut, action) in &self.bindings {
+            if shortcut.matches(mods, key) {
+                return Some(*action);
+            }
+        }
+        None
+    }
+}