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

commitba6eaebb7099724bb3533f8a6099ceb3afd81316
parente0fb168336
authorLucas Galante <[email protected]>
date2026-07-16 08:23
refactor: migrate onto cce-ui's Application/engine::run runner

Replace the bespoke smithay+calloop Wayland loop (~1.2k lines across
main.rs/window.rs) with cce-ui's engine: src/application.rs implements
the Application trait, using the new extended hooks — renderer_init
(persistent meshes), stage_renderer (pending-mesh flush, app-shaped
text, raster/RT staging), handle_resize, standard_csd/cursor_icon/
take_window_action (detached circular window's radial CSD), and on_exit
(autosave). HTTP-server startup moves into the trait impl.

Deliberate deltas: the zcce_inspector_v1 integration is dropped (the
:3000 HTTP API is the introspection surface); trackpad scrolls arrive
as PixelDelta; pinch maps to ctrl+wheel zoom; the engine titlebar-drag
band applies to the main window.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtNJJtCxLtNC7TL1NVhr5Z

 src/app.rs         | 314 +++++++++++----------
 src/application.rs | 338 ++++++++++++++++++++++
 src/main.rs        | 275 ++----------------
 src/project.rs     |   8 +-
 src/render.rs      |  11 +-
 src/window.rs      | 811 +++--------------------------------------------------
 6 files changed, 566 insertions(+), 1191 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index c145ec5..54b1f9d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -45,7 +45,7 @@ use crate::geometry::*;
 use crate::shortcut::{ShortcutManager, Action};
 use cce_ui::vk::{SceneDraw, TextSpan};
 use cce_ui::engine::Vertex;
-use crate::window::{AppState, WindowEvent};
+use crate::window::WindowEvent;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum TouchPhase {
@@ -317,7 +317,7 @@ pub struct Project {
     pub view_state: ProjectViewState,
 }
 
-#[derive(Debug, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize)]
 #[serde(tag = "action", rename_all = "snake_case")]
 pub enum HttpAction {
     Up,
@@ -341,10 +341,14 @@ pub enum HttpAction {
     MenuAction { label: String },
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum CustomEvent {
     GetState(std::sync::mpsc::Sender<String>),
     PostAction(HttpAction, std::sync::mpsc::Sender<Result<String, String>>),
+    /// App-requested exit (menu File > Exit, HTTP menu_action): the engine's
+    /// update hook is the only place with exit access, so input handlers that
+    /// see `exit_requested` route it here.
+    Exit,
 }
 
 #[derive(Clone)]
@@ -962,20 +966,48 @@ pub struct ViewportUniforms {
     pub _padding: f32,
 }
 
+/// The designer's persistent 3D meshes, created in `renderer_init` once the
+/// engine's renderer exists.
+#[derive(Clone, Copy)]
+pub struct SceneMeshes {
+    pub cube: cce_ui::vk::MeshId,
+    pub viewport_bg: cce_ui::vk::MeshId,
+    pub spheres: cce_ui::vk::MeshId,
+    pub grid: cce_ui::vk::MeshId,
+    pub origin: cce_ui::vk::MeshId,
+    pub pivot: cce_ui::vk::MeshId,
+}
+
+/// A left-press on the detached circular window's chrome that becomes an
+/// interactive move/resize once the pointer travels past a small threshold
+/// (so a plain click doesn't start a compositor grab).
+#[derive(Clone, Copy, Debug)]
+pub struct PendingWindowDrag {
+    pub start_x: f32,
+    pub start_y: f32,
+    pub action: cce_ui::engine::WindowAction,
+}
+
 pub struct State {
-    pub renderer: cce_ui::vk::VkRenderer,
     pub font_system: FontSystem,
     pub swash_cache: glyphon::SwashCache,
-    pub window: XdgWindow,
-    pub wl_surface: wl_surface::WlSurface,
+    /// Window title; the engine polls `Application::settings` and applies it.
+    pub title: String,
     pub vertex_data: Vec<Vertex>,
 
-    pub mesh_cube: cce_ui::vk::MeshId,
-    pub mesh_viewport_bg: cce_ui::vk::MeshId,
-    pub mesh_spheres: cce_ui::vk::MeshId,
-    pub mesh_grid: cce_ui::vk::MeshId,
-    pub mesh_origin: cce_ui::vk::MeshId,
-    pub mesh_pivot: cce_ui::vk::MeshId,
+    /// GPU meshes — `None` until `renderer_init`.
+    pub meshes: Option<SceneMeshes>,
+    /// CPU-staged mesh updates, flushed in `stage_renderer`.
+    pub pending_grid: Option<Vec<Vertex3D>>,
+    pub pending_origin: Option<Vec<Vertex3D>>,
+    pub pending_pivot: Option<Vec<Vertex3D>>,
+    pub pending_viewport_bg: Option<Vec<Vertex3D>>,
+    /// The spheres mesh needs re-upload from `rt_sphere_verts`.
+    pub spheres_dirty: bool,
+    /// Renderer corner radius applied last frame (physical px); re-set on change.
+    pub last_corner_radius: f32,
+    pub pending_window_drag: Option<PendingWindowDrag>,
+    pub window_action: Option<cce_ui::engine::WindowAction>,
     pub vertex_count_spheres: u32,
     pub node_color: [f32; 3],
     pub grid_color: [f32; 3],
@@ -1060,13 +1092,8 @@ pub struct State {
     pub detached_circular_network: bool,
     pub last_project_mod_time: Option<std::time::SystemTime>,
     pub last_project_check: std::time::Instant,
-    pub last_inspector_check: std::time::Instant,
-    pub last_inspector_update: std::time::Instant,
-    pub last_serialized: String,
     pub needs_autosave: bool,
     pub last_autosave_time: std::time::Instant,
-    pub window_x: i32,
-    pub window_y: i32,
     pub active_menu_cloud_pid: Option<u32>,
     pub active_menu_cloud_idx: Option<(usize, usize)>,
     pub uniform_background: bool,
@@ -1288,28 +1315,33 @@ impl State {
 
 
 
+    // The engine owns the renderer, so geometry changes stage CPU-side here
+    // and flush to the GPU meshes in `stage_renderer`.
+
     pub fn update_grid_geometry(&mut self) {
         let linear_grid_color = cce_ui::colors::to_linear_rgb(self.grid_color);
-        let grid_verts = grid_vertices(self.grid_thickness, linear_grid_color);
-        self.renderer.update_mesh(self.mesh_grid, bytemuck::cast_slice(&grid_verts));
+        self.pending_grid = Some(grid_vertices(self.grid_thickness, linear_grid_color));
         self.viewport_dirty = true;
     }
 
     pub fn update_origin_geometry(&mut self) {
-        let origin_verts = origin_vectors_vertices(self.origin_size);
-        self.renderer.update_mesh(self.mesh_origin, bytemuck::cast_slice(&origin_verts));
+        self.pending_origin = Some(origin_vectors_vertices(self.origin_size));
         self.viewport_dirty = true;
     }
 
     pub fn update_pivot_geometry(&mut self) {
-        let pivot_verts = camera_pivot_vertices(self.camera_pivot_size);
-        self.renderer.update_mesh(self.mesh_pivot, bytemuck::cast_slice(&pivot_verts));
+        self.pending_pivot = Some(camera_pivot_vertices(self.camera_pivot_size));
         self.viewport_dirty = true;
     }
 
     pub fn update_viewport_bg_geometry(&mut self) {
         let bg_color = cce_ui::colors::to_linear_rgb(self.viewport().bg_color);
-        let bg_verts = [
+        self.pending_viewport_bg = Some(Self::viewport_bg_vertices(bg_color));
+        self.viewport_dirty = true;
+    }
+
+    pub(crate) fn viewport_bg_vertices(bg_color: [f32; 3]) -> Vec<Vertex3D> {
+        vec![
             Vertex3D { position: [-1.0, -1.0, 9.99], color: bg_color }, // Bottom-left
             Vertex3D { position: [ 1.0, -1.0, 9.99], color: bg_color }, // Bottom-right
             Vertex3D { position: [-1.0,  1.0, 9.99], color: bg_color }, // Top-left
@@ -1317,9 +1349,7 @@ impl State {
             Vertex3D { position: [ 1.0, -1.0, 9.99], color: bg_color }, // Bottom-right
             Vertex3D { position: [ 1.0,  1.0, 9.99], color: bg_color }, // Top-right
             Vertex3D { position: [-1.0,  1.0, 9.99], color: bg_color }, // Top-left
-        ];
-        self.renderer.update_mesh(self.mesh_viewport_bg, bytemuck::cast_slice(&bg_verts));
-        self.viewport_dirty = true;
+        ]
     }
 
     pub fn body_h(&self) -> f32 { self.height - HEADER_H - STATUS_H }
@@ -2422,69 +2452,23 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
 
 
-    pub fn new(
-        conn: &Connection,
-        qh: &QueueHandle<AppState>,
-        compositor_state: &CompositorState,
-        xdg_shell_state: &XdgShell,
-        pw: u32,
-        ph: u32,
-        scale: f64,
-        is_detached_network: bool,
-    ) -> Self {
-        cce_ui::scale::set_scale_factor(scale as f32);
+    pub fn new(is_detached_network: bool) -> Self {
+        // The engine detected the output scale before constructing the app.
+        let scale = cce_ui::scale::scale_factor() as f64;
         let settings = DesignSettings::load();
-        let lw = pw as f32 / scale as f32;
-        let lh = ph as f32 / scale as f32;
-        let sw = lw;
-
-        let wl_surface = compositor_state.create_surface(qh);
-        wl_surface.set_buffer_scale(scale as i32);
-        let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, qh);
-        if is_detached_network {
-            window.set_title("Network Pane");
-            window.set_app_id("circular-network-pane");
-            window.set_min_size(Some((200, 200)));
+        let (lw, lh) = if is_detached_network {
+            (400.0f32, 400.0f32)
         } else {
-            window.set_title("Designer");
-            window.set_app_id("cce-designer");
-            window.set_min_size(Some((480, 320)));
-        }
-        window.commit();
-
-        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 corner_radius = cce_ui::color::backplate_corner_radius() * scale as f32;
-        let mut renderer = unsafe {
-            cce_ui::vk::VkRenderer::new(display_ptr, surface_ptr, pw, ph, corner_radius)
+            (1280.0f32, 800.0f32)
         };
+        let pw = (lw as f64 * scale) as u32;
+        let ph = (lh as f64 * scale) as u32;
+        let sw = lw;
+
         // Bundled fonts only (the designer's UI uses bundled families).
         let font_system = cce_ui::create_font_system();
         let swash_cache = glyphon::SwashCache::new();
 
-        // Static 3D meshes; the spheres mesh starts empty and is rebuilt from
-        // the node graph (rebuild_scene_geometry).
-        let cube_verts = cube_vertices();
-        let mesh_cube = renderer.create_mesh(bytemuck::cast_slice(&cube_verts));
-        let linear_grid_color = cce_ui::colors::to_linear_rgb(settings.viewport.grid_color);
-        let grid_verts = grid_vertices(settings.viewport.grid_thickness, linear_grid_color);
-        let mesh_grid = renderer.create_mesh(bytemuck::cast_slice(&grid_verts));
-        let origin_verts = origin_vectors_vertices(settings.viewport.origin_size);
-        let mesh_origin = renderer.create_mesh(bytemuck::cast_slice(&origin_verts));
-        let pivot_verts = camera_pivot_vertices(settings.viewport.camera_pivot_size);
-        let mesh_pivot = renderer.create_mesh(bytemuck::cast_slice(&pivot_verts));
-        let bg_color = cce_ui::colors::to_linear_rgb(settings.viewport.bg_color);
-        let bg_verts = [
-            Vertex3D { position: [-1.0, -1.0, 9.99], color: bg_color }, // Bottom-left
-            Vertex3D { position: [ 1.0, -1.0, 9.99], color: bg_color }, // Bottom-right
-            Vertex3D { position: [-1.0,  1.0, 9.99], color: bg_color }, // Top-left
-            Vertex3D { position: [ 1.0, -1.0, 9.99], color: bg_color }, // Bottom-right
-            Vertex3D { position: [ 1.0,  1.0, 9.99], color: bg_color }, // Top-right
-            Vertex3D { position: [-1.0,  1.0, 9.99], color: bg_color }, // Top-left
-        ];
-        let mesh_viewport_bg = renderer.create_mesh(bytemuck::cast_slice(&bg_verts));
-        let mesh_spheres = renderer.create_mesh(&[]);
-
         let splitter_layout = cce_ui::layout::SplitterLayout::new(sw, SPLITTER_W, MIN_COLUMN);
         let templates_root = load_fs_tree();
         let node_templates = flatten_node_templates(&templates_root);
@@ -2589,18 +2573,19 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         shortcut_manager.register("Ctrl+s", Action::Save).unwrap();
 
         let mut state = Self {
-            window,
-            wl_surface,
-            renderer,
             font_system,
             swash_cache,
+            title: String::new(),
             vertex_data: Vec::with_capacity(4096),
-            mesh_cube,
-            mesh_viewport_bg,
-            mesh_spheres,
-            mesh_grid,
-            mesh_origin,
-            mesh_pivot,
+            meshes: None,
+            pending_grid: None,
+            pending_origin: None,
+            pending_pivot: None,
+            pending_viewport_bg: None,
+            spheres_dirty: false,
+            last_corner_radius: -1.0,
+            pending_window_drag: None,
+            window_action: None,
             vertex_count_spheres: 0,
             node_color: {
                 let nc = cce_ui::color::graph_node_color();
@@ -2684,13 +2669,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 std::fs::metadata(&default_proj_path).and_then(|m| m.modified()).ok()
             },
             last_project_check: std::time::Instant::now(),
-            last_inspector_check: std::time::Instant::now(),
-            last_inspector_update: std::time::Instant::now() - std::time::Duration::from_secs(1),
-            last_serialized: String::new(),
             needs_autosave: false,
             last_autosave_time: std::time::Instant::now(),
-            window_x: 0,
-            window_y: 0,
             active_menu_cloud_pid: None,
             active_menu_cloud_idx: None,
             uniform_background: false,
@@ -3624,16 +3604,19 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
 
 
-    pub fn resize(&mut self, width: u32, height: u32) {
-        if width > 0 && height > 0 {
+    /// Logical size + scale, from the engine's `handle_resize` hook (the
+    /// engine has already resized the renderer; the corner radius re-applies
+    /// on the next `stage_renderer` flush).
+    pub fn resize(&mut self, width: f32, height: f32, scale: f64) {
+        if width > 0.0 && height > 0.0 {
             let old_width = self.width;
-            self.physical_width = width;
-            self.physical_height = height;
-            self.width = width as f32 / self.scale as f32;
-            self.height = height as f32 / self.scale as f32;
-            self.renderer
-                .set_corner_radius(cce_ui::color::backplate_corner_radius() * self.scale as f32);
-            self.renderer.resize(width, height);
+            self.scale = scale;
+            cce_ui::scale::set_scale_factor(scale as f32);
+            self.physical_width = (width as f64 * scale) as u32;
+            self.physical_height = (height as f64 * scale) as u32;
+            self.width = width;
+            self.height = height;
+            self.last_corner_radius = -1.0;
 
             if old_width > 0.0 {
                 let r = self.width / old_width;
@@ -3831,22 +3814,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     result
                 }
             }
-            WindowEvent::PinchGesture { delta, .. } => {
-                let dialog_open = self.node_palette_visible;
-                let in_network_pane = self.in_network_pane();
-
-                if !dialog_open && in_network_pane {
-                    if delta.is_finite() && *delta != 0.0 {
-                        let factor = (1.0 + *delta as f32).clamp(0.8, 1.25);
-                        self.zoom(factor, Some((self.cursor_x, self.cursor_y)));
-                        true
-                    } else {
-                        false
-                    }
-                } else {
-                    false
-                }
-            }
             WindowEvent::CursorMoved { position, .. } => {
                 self.cursor_x = position.x as f32;
                 self.cursor_y = position.y as f32;
@@ -4498,10 +4465,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 }
                 changed
             }
-            WindowEvent::ModifiersChanged(mods) => {
-                self.modifiers = mods.state();
-                false
-            }
             WindowEvent::KeyboardInput { event, .. } => {
                 if event.logical_key == Key::Named(NamedKey::Space) {
                     self.space_pressed = event.state == ElementState::Pressed;
@@ -4840,9 +4803,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         }
     }
 
-    pub fn render(&mut self) -> bool {
+    /// Per-tick simulation (engine `tick` hook): config polling, widget
+    /// ticks, inertia, drag edge-panning. Returns true when the frame needs
+    /// a rebuild. The render half lives in [`State::stage_frame`].
+    pub fn tick_frame(&mut self, dt: f32) -> bool {
         let now = Instant::now();
-        let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
         self.last_frame = now;
 
         if now.duration_since(self.last_config_read).as_secs_f32() > 2.0 {
@@ -5047,7 +5012,67 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             self.upload_vertices();
         }
 
-        self.prepare_text();
+        tick_changed || panned
+    }
+
+    /// Flush CPU-staged mesh updates to the renderer's persistent meshes.
+    fn flush_pending_meshes(&mut self, renderer: &mut cce_ui::vk::VkRenderer) {
+        let Some(meshes) = self.meshes else { return };
+        if let Some(verts) = self.pending_grid.take() {
+            renderer.update_mesh(meshes.grid, bytemuck::cast_slice(&verts));
+        }
+        if let Some(verts) = self.pending_origin.take() {
+            renderer.update_mesh(meshes.origin, bytemuck::cast_slice(&verts));
+        }
+        if let Some(verts) = self.pending_pivot.take() {
+            renderer.update_mesh(meshes.pivot, bytemuck::cast_slice(&verts));
+        }
+        if let Some(verts) = self.pending_viewport_bg.take() {
+            renderer.update_mesh(meshes.viewport_bg, bytemuck::cast_slice(&verts));
+        }
+        if self.spheres_dirty {
+            self.spheres_dirty = false;
+            renderer.update_mesh(meshes.spheres, bytemuck::cast_slice(&self.rt_sphere_verts));
+        }
+    }
+
+    /// One-time renderer setup (engine `renderer_init` hook): the persistent
+    /// 3D meshes. The spheres mesh starts empty and fills from the node graph
+    /// via the pending-mesh flush.
+    pub fn init_renderer(&mut self, renderer: &mut cce_ui::vk::VkRenderer) {
+        let cube_verts = cube_vertices();
+        let linear_grid_color = cce_ui::colors::to_linear_rgb(self.grid_color);
+        let grid_verts = grid_vertices(self.grid_thickness, linear_grid_color);
+        let origin_verts = origin_vectors_vertices(self.origin_size);
+        let pivot_verts = camera_pivot_vertices(self.camera_pivot_size);
+        let bg_verts =
+            Self::viewport_bg_vertices(cce_ui::colors::to_linear_rgb(self.viewport().bg_color));
+        self.meshes = Some(SceneMeshes {
+            cube: renderer.create_mesh(bytemuck::cast_slice(&cube_verts)),
+            viewport_bg: renderer.create_mesh(bytemuck::cast_slice(&bg_verts)),
+            spheres: renderer.create_mesh(&[]),
+            grid: renderer.create_mesh(bytemuck::cast_slice(&grid_verts)),
+            origin: renderer.create_mesh(bytemuck::cast_slice(&origin_verts)),
+            pivot: renderer.create_mesh(bytemuck::cast_slice(&pivot_verts)),
+        });
+        // Scene geometry built during `State::new` (before the renderer
+        // existed) uploads on the first frame's flush.
+        self.spheres_dirty = !self.rt_sphere_verts.is_empty();
+        self.viewport_dirty = true;
+    }
+
+    /// Frame staging (engine `stage_renderer` hook): corner radius, pending
+    /// meshes, text, and the 3D scene / RT pane. Returns true while the path
+    /// tracer is still refining, to keep frames coming.
+    pub fn stage_frame(&mut self, renderer: &mut cce_ui::vk::VkRenderer) -> bool {
+        let radius = cce_ui::color::backplate_corner_radius() * self.scale as f32;
+        if radius != self.last_corner_radius {
+            self.last_corner_radius = radius;
+            renderer.set_corner_radius(radius);
+        }
+        self.flush_pending_meshes(renderer);
+        self.prepare_text(renderer);
+        let meshes = self.meshes.expect("stage_frame before renderer_init");
 
         // 3D canvas: stage the scene into the renderer's backdrop when the
         // viewport is visible and its inputs changed; unstaged frames reuse the
@@ -5162,23 +5187,23 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
                     // Same draw order as the wgpu pass: bg quad, grid, origin,
                     // pivot, cube, spheres.
-                    let mut draws = vec![SceneDraw { mesh: self.mesh_viewport_bg, mvp }];
+                    let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp }];
                     if self.viewport().show_grid {
-                        draws.push(SceneDraw { mesh: self.mesh_grid, mvp });
+                        draws.push(SceneDraw { mesh: meshes.grid, mvp });
                     }
                     if self.viewport().show_origin {
-                        draws.push(SceneDraw { mesh: self.mesh_origin, mvp });
+                        draws.push(SceneDraw { mesh: meshes.origin, mvp });
                     }
                     if self.viewport().show_camera_pivot {
-                        draws.push(SceneDraw { mesh: self.mesh_pivot, mvp: mvp_pivot });
+                        draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot });
                     }
                     if self.viewport().show_cube {
-                        draws.push(SceneDraw { mesh: self.mesh_cube, mvp });
+                        draws.push(SceneDraw { mesh: meshes.cube, mvp });
                     }
                     if self.vertex_count_spheres > 0 {
-                        draws.push(SceneDraw { mesh: self.mesh_spheres, mvp });
+                        draws.push(SceneDraw { mesh: meshes.spheres, mvp });
                     }
-                    self.renderer.stage_scene((sx, sy, cw, ch), draws);
+                    renderer.stage_scene((sx, sy, cw, ch), draws);
                     }
 
                     // Update viewport cache
@@ -5211,17 +5236,17 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     let key = (self.viewport().show_cube, self.rt_geometry_version);
                     if self.last_rt_scene_key != Some(key) {
                         let (rt_tris, rt_mats) = self.collect_rt_scene();
-                        self.renderer.set_rt_scene(&rt_tris, &rt_mats);
+                        renderer.set_rt_scene(&rt_tris, &rt_mats);
                         self.last_rt_scene_key = Some(key);
                     }
                     let aspect = cw as f32 / ch as f32;
                     let (proj, view_mat, model) = self.viewport().get_matrices(aspect, Some(camera_pos), Some(Vec3::new(rx, ry, rz)), Some(pivot));
                     let inv_mvp = (proj * view_mat * model).inverse().to_cols_array_2d();
-                    self.renderer.stage_rt((sx, sy, cw, ch), cce_ui::vk::RtCamera { inv_mvp });
+                    renderer.stage_rt((sx, sy, cw, ch), cce_ui::vk::RtCamera { inv_mvp });
                 }
             } else if self.viewport_dirty {
                 // Zero-area pane: clear the backdrop once.
-                self.renderer
+                renderer
                     .stage_scene((0, 0, self.physical_width, self.physical_height), Vec::new());
                 self.last_viewport_show_viewport = false;
                 self.viewport_dirty = false;
@@ -5229,19 +5254,18 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         } else if !self.is_detached_network && self.viewport_dirty {
             // Viewport hidden: clear the backdrop (the old path's clear pass),
             // and force a re-stage when it comes back.
-            self.renderer
+            renderer
                 .stage_scene((0, 0, self.physical_width, self.physical_height), Vec::new());
             self.last_viewport_show_viewport = false;
             self.viewport_dirty = false;
         }
 
-        let _ = self.renderer.draw_frame(&self.vertex_data);
-        // Keep frames coming while the path tracer is still refining.
-        let rt_refining = !self.is_detached_network
+        // The engine draws the frame; keep frames coming while the path
+        // tracer is still refining.
+        !self.is_detached_network
             && self.show_viewport
             && self.viewport().rt_mode
-            && self.renderer.rt_accumulating();
-        tick_changed || panned || rt_refining
+            && renderer.rt_accumulating()
     }
 }
 
diff --git a/src/application.rs b/src/application.rs
new file mode 100644
index 0000000..b48b47d
--- /dev/null
+++ b/src/application.rs
@@ -0,0 +1,338 @@
+//! The designer on the cce-ui engine: `impl Application for State`.
+//!
+//! The engine owns the Wayland plumbing, event loop, and renderer; these
+//! hooks translate its callbacks into the designer's `WindowEvent`s, stage
+//! the 3D scene each frame, and run the detached circular window's
+//! non-rectangular CSD (radial border resize, top-arc move).
+
+use std::time::{Duration, Instant};
+
+use cce_ui::engine::{
+    xdg_toplevel::ResizeEdge, Application, CursorIcon, EngineState, LogicalPosition, LogicalSize,
+    Vertex, WindowAction, WindowSettings,
+};
+use cce_ui::vk::VkRenderer;
+use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
+use wayland_client::QueueHandle;
+
+use crate::api::start_http_server;
+use crate::app::{CustomEvent, PendingWindowDrag, State, TouchPhase, LEFT_MENUBAR_IDX};
+use crate::window::{LocalPosition, WindowEvent};
+
+/// Pointer travel (logical px) before a chrome press becomes an interactive
+/// move/resize, so a plain click on the border doesn't start a grab.
+const DRAG_THRESHOLD: f32 = 4.0;
+
+impl State {
+    /// What the detached circular window's chrome at (lx, ly) would do:
+    /// radial border band → resize, top menubar arc → move, an open menu
+    /// always wins. `None` outside the chrome.
+    fn circular_chrome_at(&self, lx: f32, ly: f32) -> Option<WindowAction> {
+        let dx = lx - self.circular_network_layout.x;
+        let dy = ly - self.circular_network_layout.y;
+        let dist = (dx * dx + dy * dy).sqrt();
+        if dist <= f32::EPSILON {
+            return None;
+        }
+        let r = self.circular_network_layout.r;
+        let on_border = dist >= r - 12.0 && dist <= r;
+        let in_menubar_bg = dy < 0.0 && dist >= r - 35.0 && dist <= r;
+        if !(on_border || in_menubar_bg)
+            || self.menu(LEFT_MENUBAR_IDX).get_menu_items_at(lx, ly).is_some()
+        {
+            return None;
+        }
+        if on_border {
+            let nx = dx / dist;
+            let ny = dy / dist;
+            let edge = if ny < -0.382 {
+                if nx < -0.382 {
+                    ResizeEdge::TopLeft
+                } else if nx > 0.382 {
+                    ResizeEdge::TopRight
+                } else {
+                    ResizeEdge::Top
+                }
+            } else if ny > 0.382 {
+                if nx < -0.382 {
+                    ResizeEdge::BottomLeft
+                } else if nx > 0.382 {
+                    ResizeEdge::BottomRight
+                } else {
+                    ResizeEdge::Bottom
+                }
+            } else if nx < -0.382 {
+                ResizeEdge::Left
+            } else {
+                ResizeEdge::Right
+            };
+            Some(WindowAction::Resize(edge))
+        } else {
+            Some(WindowAction::Move)
+        }
+    }
+
+    /// The engine syncs modifier state into the UiContext (on key and wheel
+    /// events); mirror it into the designer's own `ModifiersState`.
+    fn sync_modifiers_from_ctx(&mut self) {
+        self.modifiers.ctrl = self.ui_context.ctrl_pressed;
+        self.modifiers.shift = self.ui_context.shift_pressed;
+        self.modifiers.alt = self.ui_context.alt_pressed;
+        self.modifiers.logo = self.ui_context.logo_pressed;
+    }
+
+    fn default_project_path() -> std::path::PathBuf {
+        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json")
+    }
+
+    /// Detached-window sync (both directions): debounced autosave of the
+    /// shared `default_project.json`, and mtime-polled reload when the other
+    /// window wrote it. Returns true when a reload happened.
+    fn poll_shared_project(&mut self) -> bool {
+        let mut redraw = false;
+        let syncing = self.is_detached_network || self.detached_circular_network;
+        if !syncing {
+            return false;
+        }
+
+        if self.needs_autosave {
+            let now = Instant::now();
+            if now.duration_since(self.last_autosave_time) >= Duration::from_millis(200) {
+                self.needs_autosave = false;
+                self.last_autosave_time = now;
+                let path = Self::default_project_path();
+                if let Err(e) = self.save_to_file(&path) {
+                    eprintln!("Failed to auto-save default project: {:?}", e);
+                } else if let Ok(m) = std::fs::metadata(&path) {
+                    if let Ok(mod_time) = m.modified() {
+                        self.last_project_mod_time = Some(mod_time);
+                    }
+                }
+            }
+        }
+
+        let now = Instant::now();
+        if now.duration_since(self.last_project_check) >= Duration::from_millis(100) {
+            self.last_project_check = now;
+            let path = Self::default_project_path();
+            if let Ok(m) = std::fs::metadata(&path) {
+                if let Ok(mod_time) = m.modified() {
+                    if Some(mod_time) != self.last_project_mod_time {
+                        self.last_project_mod_time = Some(mod_time);
+                        if let Err(e) = self.load_from_file(&path) {
+                            eprintln!("Failed to auto-reload project: {:?}", e);
+                        } else {
+                            redraw = true;
+                        }
+                    }
+                }
+            }
+        }
+        redraw
+    }
+
+    pub(crate) fn autosave_on_exit(&mut self) {
+        if self.needs_autosave && (self.is_detached_network || self.detached_circular_network) {
+            let _ = self.save_to_file(&Self::default_project_path());
+        }
+    }
+}
+
+impl Application for State {
+    type Message = CustomEvent;
+
+    fn new(
+        _qh: &QueueHandle<EngineState<Self>>,
+        sender: calloop::channel::Sender<CustomEvent>,
+    ) -> Self {
+        let is_detached_network = std::env::args().any(|arg| arg == "--detached-network");
+        let state = State::new(is_detached_network);
+        if !is_detached_network {
+            start_http_server(sender);
+        }
+        state
+    }
+
+    fn settings(&self) -> WindowSettings {
+        let (app_id, min_size) = if self.is_detached_network {
+            ("circular-network-pane", (200, 200))
+        } else {
+            ("cce-designer", (480, 320))
+        };
+        WindowSettings {
+            title: self.title.clone(),
+            app_id: app_id.to_string(),
+            width: self.width as u32,
+            height: self.height as u32,
+            fullscreen: false,
+            min_size: Some(min_size),
+        }
+    }
+
+    fn update(&mut self, msg: CustomEvent, needs_rebuild: &mut bool, exit: &mut bool) {
+        if matches!(msg, CustomEvent::Exit) {
+            self.autosave_on_exit();
+            *exit = true;
+            return;
+        }
+        if self.apply_custom_event(msg) {
+            *needs_rebuild = true;
+        }
+        // HTTP can reach File > Exit through menu_action.
+        if self.exit_requested {
+            self.autosave_on_exit();
+            *exit = true;
+        }
+    }
+
+    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+        if self.tick_frame(dt) {
+            *needs_rebuild = true;
+        }
+        if self.poll_shared_project() {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
+        Some(&self.ui_context)
+    }
+
+    fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
+        Some(&mut self.ui_context)
+    }
+
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        self.sync_modifiers_from_ctx();
+        if let Some(pending) = self.pending_window_drag {
+            let dx = pos.x - pending.start_x;
+            let dy = pos.y - pending.start_y;
+            if (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
+                self.window_action = Some(pending.action);
+                self.pending_window_drag = None;
+            }
+        }
+        let ev = WindowEvent::CursorMoved {
+            position: LocalPosition { x: pos.x as f64, y: pos.y as f64 },
+        };
+        if self.process_window_event(ev) {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn handle_mouse_input(
+        &mut self,
+        button: MouseButton,
+        state: ElementState,
+        pos: LogicalPosition,
+        needs_rebuild: &mut bool,
+    ) -> Option<CustomEvent> {
+        self.sync_modifiers_from_ctx();
+        self.cursor_x = pos.x;
+        self.cursor_y = pos.y;
+        if self.is_detached_network && button == MouseButton::Left {
+            match state {
+                ElementState::Pressed => {
+                    if let Some(action) = self.circular_chrome_at(pos.x, pos.y) {
+                        self.pending_window_drag = Some(PendingWindowDrag {
+                            start_x: pos.x,
+                            start_y: pos.y,
+                            action,
+                        });
+                        return None; // consumed by the window chrome
+                    }
+                }
+                ElementState::Released => {
+                    self.pending_window_drag = None;
+                }
+            }
+        }
+        let ev = WindowEvent::MouseInput { state, button };
+        if self.process_window_event(ev) {
+            *needs_rebuild = true;
+        }
+        if self.exit_requested {
+            return Some(CustomEvent::Exit);
+        }
+        None
+    }
+
+    fn handle_mouse_wheel(
+        &mut self,
+        delta: &MouseScrollDelta,
+        pos: LogicalPosition,
+        needs_rebuild: &mut bool,
+    ) {
+        self.sync_modifiers_from_ctx();
+        self.cursor_x = pos.x;
+        self.cursor_y = pos.y;
+        let ev = WindowEvent::MouseWheel {
+            delta: delta.clone(),
+            phase: TouchPhase::Moved,
+        };
+        if self.process_window_event(ev) {
+            *needs_rebuild = true;
+        }
+    }
+
+    fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<CustomEvent> {
+        self.sync_modifiers_from_ctx();
+        let ev = WindowEvent::KeyboardInput { event: event.clone() };
+        if self.process_window_event(ev) {
+            *needs_rebuild = true;
+        }
+        if self.exit_requested {
+            return Some(CustomEvent::Exit);
+        }
+        None
+    }
+
+    fn custom_vertices(&mut self, verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {
+        verts.extend_from_slice(&self.vertex_data);
+    }
+
+    fn renderer_init(&mut self, renderer: &mut VkRenderer) {
+        self.init_renderer(renderer);
+    }
+
+    fn stage_renderer(&mut self, renderer: &mut VkRenderer, _size: LogicalSize, _scale: f64) -> bool {
+        self.stage_frame(renderer)
+    }
+
+    fn handle_resize(&mut self, width: f32, height: f32, scale: f64) {
+        self.resize(width, height, scale);
+    }
+
+    fn standard_csd(&self) -> bool {
+        // The detached window's chrome is the circle, not the rect.
+        !self.is_detached_network
+    }
+
+    fn cursor_icon(&self, x: f32, y: f32) -> Option<CursorIcon> {
+        if !self.is_detached_network {
+            return None;
+        }
+        Some(match self.circular_chrome_at(x, y) {
+            Some(WindowAction::Resize(edge)) => match edge {
+                ResizeEdge::TopLeft => CursorIcon::NwResize,
+                ResizeEdge::Top => CursorIcon::NResize,
+                ResizeEdge::TopRight => CursorIcon::NeResize,
+                ResizeEdge::Left => CursorIcon::WResize,
+                ResizeEdge::Right => CursorIcon::EResize,
+                ResizeEdge::BottomLeft => CursorIcon::SwResize,
+                ResizeEdge::Bottom => CursorIcon::SResize,
+                ResizeEdge::BottomRight => CursorIcon::SeResize,
+                _ => CursorIcon::Default,
+            },
+            _ => CursorIcon::Default,
+        })
+    }
+
+    fn take_window_action(&mut self) -> Option<WindowAction> {
+        self.window_action.take()
+    }
+
+    fn on_exit(&mut self) {
+        self.autosave_on_exit();
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 5645248..e01d2e8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,46 +1,10 @@
-#![allow(unused_imports)]
-use std::time::Instant;
-#[cfg(test)]
-use std::fs;
-#[cfg(test)]
-use std::path::Path;
-
-use serde::{Deserialize, Serialize};
-
-#[cfg(test)]
-use cce_ui::widget::{ElementState, MouseButton, MouseScrollDelta, KeyEvent};
-#[cfg(test)]
-use cce_ui::widget::{Key, NamedKey};
-
-use smithay_client_toolkit::{
-    compositor::{CompositorHandler, CompositorState},
-    delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
-    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
-    registry::{ProvidesRegistryState, RegistryState},
-    output::OutputState,
-    seat::{
-        keyboard::KeyboardHandler,
-        pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
-        Capability, SeatHandler, SeatState,
-    },
-    shell::{
-        xdg::{
-            window::{Window as XdgWindow, WindowConfigure, WindowDecorations},
-            XdgShell,
-        },
-    },
-    shm::{Shm, ShmHandler},
-};
-use wayland_client::{
-    globals::registry_queue_init,
-    Connection,
-};
-use calloop_wayland_source::WaylandSource;
-
-#[cfg(test)]
-use glam::{Mat4, Vec3};
 
 pub mod app;
+pub mod application;
+
+// Root-level aliases some modules import via `crate::` paths.
+#[allow(unused_imports)]
+use app::{CustomEvent, HttpAction, ModifiersState};
 pub mod viewport_3d;
 pub mod api;
 pub mod window;
@@ -50,9 +14,14 @@ pub mod render;
 pub mod shortcut;
 pub mod thumbnail;
 
-use app::{State, CustomEvent, HttpAction, ModifiersState};
-use window::{AppState, WindowEvent};
-use api::start_http_server;
+#[cfg(test)]
+mod test_prelude {
+    pub use std::fs;
+    pub use std::path::Path;
+    pub use glam::{Mat4, Vec3};
+    pub use cce_ui::widget::{Key, NamedKey};
+    pub use crate::app::{State, HttpAction, ModifiersState};
+}
 
 fn main() {
     let args: Vec<String> = std::env::args().collect();
@@ -84,224 +53,14 @@ fn main() {
         }
     }
 
-    let is_detached_network = args.iter().any(|arg| arg == "--detached-network");
-
-    let conn = Connection::connect_to_env().unwrap();
-    let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
-    let qh = event_queue.handle();
-
-    let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
-    let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
-    let shm_state = Shm::bind(&globals, &qh).unwrap();
-    let seat_state = SeatState::new(&globals, &qh);
-    let output_state = OutputState::new(&globals, &qh);
-    let inspector = globals.bind(&qh, 1..=1, ()).ok();
-    let (sender, channel) = calloop::channel::channel::<CustomEvent>();
-
-    let mut app = AppState {
-        registry_state: RegistryState::new(&globals),
-        compositor_state,
-        xdg_shell_state,
-        shm_state,
-        seat_state,
-        output_state,
-        seats: Vec::new(),
-        pointer: None,
-        keyboard: None,
-        window: None,
-        surface: None,
-        state: None,
-        exit: false,
-        redraw: true,
-        pressed_key: None,
-        inspector,
-        pending_resize: None,
-        _sender: sender.clone(),
-    };
-
-    // Perform a roundtrip to populate output_state with active output scales
-    event_queue.roundtrip(&mut app).unwrap();
-
-    let scale = cce_ui::wayland::detect_scale_factor(&app.output_state);
-
-    let (pw, ph) = if is_detached_network {
-        ((400.0 * scale) as u32, (400.0 * scale) as u32)
-    } else {
-        ((1280.0 * scale) as u32, (800.0 * scale) as u32)
-    };
-
-    let state = State::new(
-        &conn,
-        &qh,
-        &app.compositor_state,
-        &app.xdg_shell_state,
-        pw, ph,
-        scale,
-        is_detached_network,
-    );
-
-    app.window = Some(state.window.clone());
-    app.surface = Some(state.wl_surface.clone());
-    app.state = Some(state);
-
-    if let Some(ref inspector) = app.inspector {
-        if let Some(ref surface) = app.surface {
-            inspector.register_client(surface);
-        }
-    }
-
-    if !is_detached_network {
-        start_http_server(sender.clone());
-    }
-
-    let mut event_loop = calloop::EventLoop::try_new().unwrap();
-    let loop_handle = event_loop.handle();
-
-    WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
-
-    loop_handle.insert_source(channel, |event, _metadata, app_state: &mut AppState| {
-        if let calloop::channel::Event::Msg(msg) = event {
-            app_state.handle_user_event(msg);
-        }
-    }).unwrap();
-
-    const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
-    const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
-
-    loop {
-        let timeout = if app.redraw {
-            std::time::Duration::ZERO
-        } else {
-            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;
-        }
-
-        if let Some(state) = &mut app.state {
-            if let Some(ref inspector) = app.inspector {
-                let now = std::time::Instant::now();
-                if now.duration_since(state.last_inspector_check) >= std::time::Duration::from_millis(250) {
-                    state.last_inspector_check = now;
-                    inspector.get_inspected_surfaces();
-                }
-            }
-
-            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) {
-                    state.last_project_check = now;
-                    let default_proj_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
-                    if let Ok(m) = std::fs::metadata(&default_proj_path) {
-                        if let Ok(mod_time) = m.modified() {
-                            if Some(mod_time) != state.last_project_mod_time {
-                                state.last_project_mod_time = Some(mod_time);
-                                if let Err(e) = state.load_from_file(&default_proj_path) {
-                                    eprintln!("Failed to auto-reload project: {:?}", e);
-                                } else {
-                                    app.redraw = true;
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        if let Some(ref mut pk) = app.pressed_key {
-            let now = std::time::Instant::now();
-            if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
-                if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
-                    pk.last_repeated = now;
-                    if let Some(st) = &mut app.state {
-                        let custom_event = cce_ui::widget::KeyEvent {
-                            state: cce_ui::widget::ElementState::Pressed,
-                            logical_key: pk.logical_key.clone(),
-                            text: pk.text.clone(),
-                            repeat: true,
-                            ctrl: st.modifiers.ctrl,
-                            shift: st.modifiers.shift,
-                        };
-                        let ev = WindowEvent::KeyboardInput { event: custom_event };
-                        app.process_event(ev);
-                    }
-                }
-            }
-        }
-
-fn create_memfd_with_data(name: &str, data: &[u8]) -> std::io::Result<std::os::unix::io::RawFd> {
-    use std::io::{Seek, Write};
-    use std::os::unix::io::FromRawFd;
-    use std::os::unix::io::IntoRawFd;
-
-    let c_name = std::ffi::CString::new(name).unwrap();
-    let fd = unsafe { libc::memfd_create(c_name.as_ptr(), libc::MFD_CLOEXEC) };
-    if fd < 0 {
-        return Err(std::io::Error::last_os_error());
-    }
-    let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
-    file.write_all(data)?;
-    file.seek(std::io::SeekFrom::Start(0))?;
-    Ok(file.into_raw_fd())
-}
-
-        if app.redraw {
-            app.redraw = false;
-            if let Some(state) = &mut app.state {
-                if state.render() {
-                    app.redraw = true;
-                }
-                if let Some(ref inspector) = app.inspector {
-                    if let Some(ref surface) = app.surface {
-                        let json = cce_ui::widget::serialize_widgets(&state.slots.dyn_refs());
-                        if json != state.last_serialized {
-                            let now = std::time::Instant::now();
-                            if now.duration_since(state.last_inspector_update) >= std::time::Duration::from_millis(100) {
-                                state.last_serialized = json.clone();
-                                state.last_inspector_update = now;
-                                if let Ok(raw_fd) = create_memfd_with_data("cce_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);
-                                }
-                            } else {
-                                app.redraw = true;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
+    // Everything windowed runs on the cce-ui engine (application.rs holds the
+    // Application impl; --detached-network is read there).
+    cce_ui::engine::run::<app::State>();
 }
 
 #[cfg(test)]
 mod tests {
-    use super::*;
+    use crate::test_prelude::*;
     use crate::app::{get_next_visible_pane, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, DesignSettings, FsNode, Project, ProjectViewState};
     use crate::shortcut::{Shortcut, ShortcutManager, Action};
     use crate::geometry::{GAttribute, GVertex, Geometry, line_vertices};
diff --git a/src/project.rs b/src/project.rs
index b3bfbcd..3258aba 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -1,7 +1,6 @@
 use std::fs;
 use std::path::Path;
 
-use cce_ui::widget::Button;
 use crate::app::{State, Project, FsNode, ProjectViewState, CONTENT_IDX, ParamDef};
 
 fn color_to_hex(rgb: [f32; 3]) -> String {
@@ -19,7 +18,7 @@ fn hex_to_color(hex: &str) -> Option<[f32; 3]> {
 impl State {
 
 
-    pub(crate) fn update_window_title(&self) {
+    pub(crate) fn update_window_title(&mut self) {
         let base_title = if self.is_detached_network {
             "Network Pane"
         } else {
@@ -38,8 +37,9 @@ impl State {
         if self.has_unsaved_changes() {
             title.push_str("*");
         }
-        
-        self.window.set_title(&title);
+
+        // The engine polls `Application::settings` and applies title changes.
+        self.title = title;
     }
 
     pub(crate) fn load_recent_files() -> Vec<std::path::PathBuf> {
diff --git a/src/render.rs b/src/render.rs
index 0082042..073fce9 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -7,9 +7,9 @@ use crate::app::{
     State, FsNode, WIDGET_COUNT,
     make_text_buffer, make_text_buffer_with_font,
     CONTENT_IDX, VIEWPORT_IDX, PARAM_IDX, PARAM_PLATE_IDX,
-    BREADCRUMB_IDX, STATUS_IDX, HEADER_IDX, RIGHT_MENUBAR_IDX,
+    BREADCRUMB_IDX, HEADER_IDX, RIGHT_MENUBAR_IDX,
     SPREADSHEET_MENUBAR_IDX,
-    MENUBAR_H, STATUS_H,
+    MENUBAR_H,
     LEFT_MENUBAR_IDX, PARAM_MENUBAR_IDX, NETWORK_PANEL_IDX,
     push_circle_vertices, push_circle_border_vertices,
 };
@@ -400,10 +400,12 @@ impl State {
 
         let verts = geom.to_vertex3d_vec();
         self.vertex_count_spheres = verts.len() as u32;
-        self.renderer.update_mesh(self.mesh_spheres, bytemuck::cast_slice(&verts));
         // Cache for the path tracer, so RT mode never re-runs the node
         // graph / OpenCL kernels; the version bump invalidates its scene.
+        // The raster mesh uploads from this same cache on the next
+        // `stage_renderer` flush.
         self.rt_sphere_verts = verts;
+        self.spheres_dirty = true;
         self.rt_geometry_version += 1;
         self.viewport_dirty = true;
     }
@@ -430,7 +432,7 @@ impl State {
         }
     }
 
-    pub(crate) fn prepare_text(&mut self) {
+    pub(crate) fn prepare_text(&mut self, renderer: &mut cce_ui::vk::VkRenderer) {
         let mut current_popovers = Vec::new();
         {
             fn collect_popovers(
@@ -489,7 +491,6 @@ impl State {
         let _ = sh;
 
         let Self {
-            ref mut renderer,
             ref mut font_system,
             ref mut swash_cache,
             physical_width, physical_height, scale,
diff --git a/src/window.rs b/src/window.rs
index c873b8d..e7cef5e 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,33 +1,16 @@
-#![allow(unused_imports)]
-use std::path::Path;
+//! The designer's window-event layer on the cce-ui engine.
+//!
+//! The Wayland plumbing (seat/pointer/keyboard handlers, configure, CSD)
+//! lives in cce-ui's window runner; the `Application` impl (application.rs)
+//! translates the runner's hooks into [`WindowEvent`]s. What remains here is
+//! app policy: the post-event side-effect pass (`process_window_event`) and
+//! HTTP-action application (`apply_custom_event`).
 
-use smithay_client_toolkit::{
-    compositor::{CompositorHandler, CompositorState},
-    delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
-    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
-    registry::{ProvidesRegistryState, RegistryState},
-    output::{OutputHandler, OutputState},
-    seat::{
-        keyboard::KeyboardHandler,
-        pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
-        Capability, SeatHandler, SeatState,
-    },
-    shell::{
-        xdg::{
-            window::{Window as XdgWindow, WindowConfigure, WindowHandler},
-            XdgShell,
-        },
-    },
-    shm::{Shm, ShmHandler},
-};
-use wayland_client::{
-    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
-    Connection, QueueHandle,
-};
+use std::path::Path;
 
 use cce_ui::widget::WidgetHost;
 use crate::shortcut::Action;
-use crate::app::{State, CustomEvent, HttpAction, ModifiersState, TouchPhase, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, HEADER_IDX, CONTENT_IDX, BREADCRUMB_IDX, VIEWPORT_IDX, PARAM_IDX, SPREADSHEET_IDX, WIDGET_COUNT, get_next_visible_pane, Project, ProjectViewState, ParamDef, param_display};
+use crate::app::{State, CustomEvent, HttpAction, TouchPhase, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, HEADER_IDX, PARAM_IDX, WIDGET_COUNT, get_next_visible_pane, Project, ProjectViewState, ParamDef, param_display};
 
 #[derive(Debug, Clone, Copy)]
 pub struct LocalPosition {
@@ -37,745 +20,19 @@ pub struct LocalPosition {
 
 pub enum WindowEvent {
     MouseWheel { delta: cce_ui::widget::MouseScrollDelta, phase: TouchPhase },
-    PinchGesture { delta: f64 },
     CursorMoved { position: LocalPosition },
     MouseInput { state: cce_ui::widget::ElementState, button: cce_ui::widget::MouseButton },
-    ModifiersChanged(ModifiersState),
     KeyboardInput { event: cce_ui::widget::KeyEvent },
 }
 
-pub struct PressedKey {
-    pub logical_key: cce_ui::widget::Key,
-    pub text: Option<String>,
-    pub first_pressed: std::time::Instant,
-    pub last_repeated: std::time::Instant,
-}
-
-pub fn is_repeatable_key(key: &cce_ui::widget::Key) -> bool {
-    use cce_ui::widget::{Key, NamedKey};
-    match key {
-        Key::Named(NamedKey::Backspace) |
-        Key::Named(NamedKey::Delete) |
-        Key::Named(NamedKey::ArrowLeft) |
-        Key::Named(NamedKey::ArrowRight) |
-        Key::Named(NamedKey::ArrowUp) |
-        Key::Named(NamedKey::ArrowDown) |
-        Key::Named(NamedKey::Home) |
-        Key::Named(NamedKey::End) |
-        Key::Character(_) => true,
-        _ => false,
-    }
-}
-
-pub struct PendingResize {
-    pub serial: u32,
-    pub edge: smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge,
-    pub start_x: f32,
-    pub start_y: f32,
-    pub is_move: bool,
-}
-
-pub struct AppState {
-    pub registry_state: RegistryState,
-    pub compositor_state: CompositorState,
-    pub xdg_shell_state: XdgShell,
-    pub shm_state: Shm,
-    pub seat_state: SeatState,
-    pub output_state: OutputState,
-
-    pub seats: Vec<wl_seat::WlSeat>,
-    pub pointer: Option<ThemedPointer>,
-    pub keyboard: Option<wl_keyboard::WlKeyboard>,
-
-    pub window: Option<XdgWindow>,
-    pub surface: Option<wl_surface::WlSurface>,
-
-    pub state: Option<State>,
-    pub exit: bool,
-    pub redraw: bool,
-    pub pressed_key: Option<PressedKey>,
-    pub inspector: Option<cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1>,
-    pub pending_resize: Option<PendingResize>,
-    pub _sender: calloop::channel::Sender<CustomEvent>,
-}
-
-impl CompositorHandler for AppState {
-    fn scale_factor_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        scale_factor: i32,
-    ) {
-        _surface.set_buffer_scale(scale_factor);
-        if let Some(state) = &mut self.state {
-            state.scale = scale_factor as f64;
-            cce_ui::scale::set_scale_factor(scale_factor as f32);
-            let pw = (state.width as f64 * state.scale) as u32;
-            let ph = (state.height as f64 * state.scale) as u32;
-            state.resize(pw, ph);
-            self.redraw = true;
-        }
-    }
-
-    fn transform_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _new_transform: wl_output::Transform,
-    ) {}
-
-    fn frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _time: u32,
-    ) {}
-
-    fn surface_enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {}
-
-    fn surface_leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {}
-}
-
-impl OutputHandler for AppState {
-    fn output_state(&mut self) -> &mut OutputState {
-        &mut self.output_state
-    }
-
-    fn new_output(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {}
-
-    fn update_output(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {}
-
-    fn output_destroyed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {}
-}
-
-impl SeatHandler for AppState {
-    fn seat_state(&mut self) -> &mut SeatState {
-        &mut self.seat_state
-    }
-
-    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());
-    }
-
-    fn new_capability(
-        &mut self,
-        _conn: &Connection,
-        qh: &QueueHandle<Self>,
-        seat: wl_seat::WlSeat,
-        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(
-                qh,
-                &seat,
-                self.shm_state.wl_shm(),
-                surface,
-                ThemeSpec::System,
-            ).unwrap();
-            self.pointer = Some(themed_pointer);
-        }
-        if capability == Capability::Keyboard && self.keyboard.is_none() {
-            let keyboard = self
-                .seat_state
-                .get_keyboard(qh, &seat, None)
-                .unwrap();
-            self.keyboard = Some(keyboard);
-        }
-    }
-
-    fn remove_capability(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer {
-            self.pointer = None;
-        }
-        if capability == Capability::Keyboard {
-            self.keyboard = None;
-        }
-    }
-
-    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());
-    }
-}
-
-impl ShmHandler for AppState {
-    fn shm_state(&mut self) -> &mut Shm {
-        &mut self.shm_state
-    }
-}
-
-impl PointerHandler for AppState {
-    fn pointer_frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _pointer: &wl_pointer::WlPointer,
-        events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
-    ) {
-        use smithay_client_toolkit::seat::pointer::PointerEventKind;
-        for event in events {
-            if let Some(st) = &mut self.state {
-                st.cursor_x = event.position.0 as f32;
-                st.cursor_y = event.position.1 as f32;
-                match &event.kind {
-                    PointerEventKind::Motion { .. } => {
-                        if st.is_detached_network {
-                            let lx = event.position.0 as f32;
-                            let ly = event.position.1 as f32;
-                            let dx = lx - st.circular_network_layout.x;
-                            let dy = ly - st.circular_network_layout.y;
-                            let dist = (dx * dx + dy * dy).sqrt();
-                            let on_border = dist >= st.circular_network_layout.r - 12.0 && dist <= st.circular_network_layout.r;
-                            let hits_any_menu = st.menu(LEFT_MENUBAR_IDX).get_menu_items_at(lx, ly).is_some();
-
-                            if let Some(ref themed_pointer) = self.pointer {
-                                if on_border && !hits_any_menu {
-                                    let nx = dx / dist;
-                                    let ny = dy / dist;
-                                    let mut cursor = CursorIcon::Default;
-                                    if ny < -0.382 {
-                                        if nx < -0.382 {
-                                            cursor = CursorIcon::NwResize;
-                                        } else if nx > 0.382 {
-                                            cursor = CursorIcon::NeResize;
-                                        } else {
-                                            cursor = CursorIcon::NResize;
-                                        }
-                                    } else if ny > 0.382 {
-                                        if nx < -0.382 {
-                                            cursor = CursorIcon::SwResize;
-                                        } else if nx > 0.382 {
-                                            cursor = CursorIcon::SeResize;
-                                        } else {
-                                            cursor = CursorIcon::SResize;
-                                        }
-                                    } else {
-                                        if nx < -0.382 {
-                                            cursor = CursorIcon::WResize;
-                                        } else if nx > 0.382 {
-                                            cursor = CursorIcon::EResize;
-                                        }
-                                    }
-                                    let _ = themed_pointer.set_cursor(_conn, cursor);
-                                } else {
-                                    let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
-                                }
-                            }
-                        } else {
-                            let lx = event.position.0 as f32;
-                            let ly = event.position.1 as f32;
-                            let border = 8.0f32;
-                            let mut cursor = CursorIcon::Default;
-                            if ly < border {
-                                if lx < border {
-                                    cursor = CursorIcon::NwResize;
-                                } else if lx > st.width - border {
-                                    cursor = CursorIcon::NeResize;
-                                } else {
-                                    cursor = CursorIcon::NResize;
-                                }
-                            } else if ly > st.height - border {
-                                if lx < border {
-                                    cursor = CursorIcon::SwResize;
-                                } else if lx > st.width - border {
-                                    cursor = CursorIcon::SeResize;
-                                } else {
-                                    cursor = CursorIcon::SResize;
-                                }
-                            } else if lx < border {
-                                cursor = CursorIcon::WResize;
-                            } else if lx > st.width - border {
-                                cursor = CursorIcon::EResize;
-                            }
-
-                            if let Some(ref themed_pointer) = self.pointer {
-                                let _ = themed_pointer.set_cursor(_conn, cursor);
-                            }
-                        }
-
-                        if let Some(ref pending) = self.pending_resize {
-                            let lx = event.position.0 as f32;
-                            let ly = event.position.1 as f32;
-                            let rx = lx - pending.start_x;
-                            let ry = ly - pending.start_y;
-                            let rdist = (rx * rx + ry * ry).sqrt();
-                            if rdist > 4.0 {
-                                if let Some(ref window) = self.window {
-                                    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);
-                                            window.move_(seat, pending.serial);
-                                        } else {
-                                            // eprintln!("DEBUG RESIZE INITIATING window.resize with edge={:?}, serial={}", pending.edge, pending.serial);
-                                            window.resize(seat, pending.serial, pending.edge);
-                                        }
-                                    }
-                                }
-                                self.pending_resize = None;
-                            }
-                        }
-
-                        let ev = WindowEvent::CursorMoved {
-                            position: LocalPosition {
-                                x: event.position.0,
-                                y: event.position.1,
-                            },
-                        };
-                        self.process_event(ev);
-                    }
-                    PointerEventKind::Press { button, serial, .. } => {
-                        let btn = match *button {
-                            272 => cce_ui::widget::MouseButton::Left,
-                            273 => cce_ui::widget::MouseButton::Right,
-                            274 => cce_ui::widget::MouseButton::Middle,
-                            _ => continue,
-                        };
-                        // 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 == cce_ui::widget::MouseButton::Left {
-                                let lx = event.position.0 as f32;
-                                let ly = event.position.1 as f32;
-                                let dx = lx - st.circular_network_layout.x;
-                                let dy = ly - st.circular_network_layout.y;
-                                let dist = (dx * dx + dy * dy).sqrt();
-                                let on_border = dist >= st.circular_network_layout.r - 12.0 && dist <= st.circular_network_layout.r;
-                                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.menu(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());
-
-                                if (on_border || in_menubar_bg) && !hits_any_menu {
-                                    if let Some(ref _window) = self.window {
-                                        let seat = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
-                                        if let Some(ref _seat) = seat {
-                                            if on_border {
-                                                use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;
-                                                let nx = dx / dist;
-                                                let ny = dy / dist;
-                                                let mut edge = ResizeEdge::None;
-                                                if ny < -0.382 {
-                                                    if nx < -0.382 {
-                                                        edge = ResizeEdge::TopLeft;
-                                                    } else if nx > 0.382 {
-                                                        edge = ResizeEdge::TopRight;
-                                                    } else {
-                                                        edge = ResizeEdge::Top;
-                                                    }
-                                                } else if ny > 0.382 {
-                                                    if nx < -0.382 {
-                                                        edge = ResizeEdge::BottomLeft;
-                                                    } else if nx > 0.382 {
-                                                        edge = ResizeEdge::BottomRight;
-                                                    } else {
-                                                        edge = ResizeEdge::Bottom;
-                                                    }
-                                                } else {
-                                                    if nx < -0.382 {
-                                                        edge = ResizeEdge::Left;
-                                                    } else if nx > 0.382 {
-                                                        edge = ResizeEdge::Right;
-                                                    }
-                                                }
-                                                self.pending_resize = Some(PendingResize {
-                                                    serial: *serial,
-                                                    edge,
-                                                    start_x: lx,
-                                                    start_y: ly,
-                                                    is_move: false,
-                                                });
-                                                continue;
-                                            } else {
-                                                self.pending_resize = Some(PendingResize {
-                                                    serial: *serial,
-                                                    edge: smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge::None,
-                                                    start_x: lx,
-                                                    start_y: ly,
-                                                    is_move: true,
-                                                });
-                                                continue;
-                                            }
-                                        }
-                                    }
-                                }
-                            } else if !st.is_detached_network && btn == cce_ui::widget::MouseButton::Left {
-                                let lx = event.position.0 as f32;
-                                let ly = event.position.1 as f32;
-                                let border = 8.0f32;
-                                use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;
-                                let mut edge = ResizeEdge::None;
-                                if ly < border {
-                                    if lx < border {
-                                        edge = ResizeEdge::TopLeft;
-                                    } else if lx > st.width - border {
-                                        edge = ResizeEdge::TopRight;
-                                    } else {
-                                        edge = ResizeEdge::Top;
-                                    }
-                                } else if ly > st.height - border {
-                                    if lx < border {
-                                        edge = ResizeEdge::BottomLeft;
-                                    } else if lx > st.width - border {
-                                        edge = ResizeEdge::BottomRight;
-                                    } else {
-                                        edge = ResizeEdge::Bottom;
-                                    }
-                                } else if lx < border {
-                                    edge = ResizeEdge::Left;
-                                } else if lx > st.width - border {
-                                    edge = ResizeEdge::Right;
-                                }
-
-                                if edge != ResizeEdge::None {
-                                    self.pending_resize = Some(PendingResize {
-                                        serial: *serial,
-                                        edge,
-                                        start_x: lx,
-                                        start_y: ly,
-                                        is_move: false,
-                                    });
-                                    continue;
-                                }
-                            }
-                        }
-
-                        let ev = WindowEvent::MouseInput {
-                            state: cce_ui::widget::ElementState::Pressed,
-                            button: btn,
-                        };
-                        self.process_event(ev);
-                    }
-                    PointerEventKind::Release { button, .. } => {
-                        let btn = match *button {
-                            272 => cce_ui::widget::MouseButton::Left,
-                            273 => cce_ui::widget::MouseButton::Right,
-                            274 => cce_ui::widget::MouseButton::Middle,
-                            _ => continue,
-                        };
-                        // eprintln!("DEBUG MOUSE RELEASE: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
-                        if btn == cce_ui::widget::MouseButton::Left {
-                            self.pending_resize = None;
-                        }
-                        let ev = WindowEvent::MouseInput {
-                            state: cce_ui::widget::ElementState::Released,
-                            button: btn,
-                        };
-                        self.process_event(ev);
-                    }
-                    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);
-                        let ev = WindowEvent::MouseWheel {
-                            delta: cce_ui::widget::MouseScrollDelta::LineDelta(-h_val / 10.0, -v_val / 10.0),
-                            phase: TouchPhase::Moved,
-                        };
-                        self.process_event(ev);
-                    }
-                    PointerEventKind::Enter { .. } => {
-                        if let Some(ref themed_pointer) = self.pointer {
-                            let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
-                        }
-                    }
-                    _ => {}
-                }
-            }
-        }
-    }
-}
-
-impl KeyboardHandler for AppState {
-    fn enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-        _raw_modifiers: &[u32],
-        _keysyms: &[xkeysym::Keysym],
-    ) {}
-
-    fn leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-    ) {
-        self.pressed_key = None;
-        if let Some(st) = &mut self.state {
-            st.modifiers = ModifiersState::default();
-        }
-    }
-
-
-    fn press_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {
-        self.handle_key(event, cce_ui::widget::ElementState::Pressed);
-    }
-
-    fn release_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {
-        self.handle_key(event, cce_ui::widget::ElementState::Released);
-    }
-
-    fn update_modifiers(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
-        _layout: u32,
-    ) {
-        if let Some(st) = &mut self.state {
-            st.modifiers.ctrl = modifiers.ctrl;
-            st.modifiers.alt = modifiers.alt;
-            st.modifiers.shift = modifiers.shift;
-            st.modifiers.logo = modifiers.logo;
-        }
-    }
-}
-
-impl WindowHandler for AppState {
-    fn configure(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _window: &XdgWindow,
-        configure: WindowConfigure,
-        _serial: u32,
-    ) {
-        let (w, h) = configure.new_size;
-        // 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();
-            if let Some(state) = &mut self.state {
-                let pw = (width as f64 * state.scale) as u32;
-                let ph = (height as f64 * state.scale) as u32;
-                state.resize(pw, ph);
-            }
-        }
-        self.redraw = true;
-    }
-
-    fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
-        self.exit = true;
-    }
-}
-
-impl ProvidesRegistryState for AppState {
-    fn registry(&mut self) -> &mut RegistryState {
-        &mut self.registry_state
-    }
-    
-    fn runtime_add_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-        _version: u32,
-    ) {}
-    
-    fn runtime_remove_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-    ) {}
-}
-
-impl wayland_client::Dispatch<cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1, ()> for AppState {
-    fn event(
-        state: &mut Self,
-        _proxy: &cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1,
-        event: cce_ui::protocol::zcce_inspector_v1::Event,
-        _data: &(),
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-    ) {
-        match event {
-            cce_ui::protocol::zcce_inspector_v1::Event::InspectedSurface { app_id, x, y, .. } => {
-                if let Some(ref mut st) = state.state {
-                    let expected_id = if st.is_detached_network {
-                        "circular-network-pane"
-                    } else {
-                        "cce-designer"
-                    };
-                    if app_id == expected_id {
-                        st.window_x = x;
-                        st.window_y = y;
-                    }
-                }
-            }
-            _ => {}
-        }
-    }
-}
-
-delegate_compositor!(AppState);
-delegate_xdg_shell!(AppState);
-delegate_xdg_window!(AppState);
-delegate_shm!(AppState);
-delegate_seat!(AppState);
-delegate_pointer!(AppState);
-delegate_keyboard!(AppState);
-delegate_registry!(AppState);
-delegate_output!(AppState);
-
-impl AppState {
-    fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: cce_ui::widget::ElementState) {
-        use cce_ui::widget::{Key, KeyEvent, NamedKey};
-        let logical_key = match event.keysym {
-            xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
-            xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
-            xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
-            xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
-            xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
-            xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
-            xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
-            xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
-            xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
-            xkeysym::Keysym::space => Key::Named(NamedKey::Space),
-            xkeysym::Keysym::comma => Key::Character(",".into()),
-            xkeysym::Keysym::g | xkeysym::Keysym::G => Key::Character("g".into()),
-            xkeysym::Keysym::e | xkeysym::Keysym::E => Key::Character("e".into()),
-            xkeysym::Keysym::a | xkeysym::Keysym::A => Key::Character("a".into()),
-            xkeysym::Keysym::d | xkeysym::Keysym::D => Key::Character("d".into()),
-            xkeysym::Keysym::f | xkeysym::Keysym::F => Key::Character("f".into()),
-            xkeysym::Keysym::h | xkeysym::Keysym::H => Key::Character("h".into()),
-            xkeysym::Keysym::j | xkeysym::Keysym::J => Key::Character("j".into()),
-            xkeysym::Keysym::k | xkeysym::Keysym::K => Key::Character("k".into()),
-            xkeysym::Keysym::l | xkeysym::Keysym::L => Key::Character("l".into()),
-            xkeysym::Keysym::s | xkeysym::Keysym::S => Key::Character("s".into()),
-            xkeysym::Keysym::c | xkeysym::Keysym::C => Key::Character("c".into()),
-            xkeysym::Keysym::x | xkeysym::Keysym::X => Key::Character("x".into()),
-            xkeysym::Keysym::v | xkeysym::Keysym::V => Key::Character("v".into()),
-            xkeysym::Keysym::grave => Key::Character("`".into()),
-            _ => {
-                if let Some(ref text) = event.utf8 {
-                    Key::Character(text.clone())
-                } else if let Some(ch) = event.keysym.key_char() {
-                    Key::Character(ch.to_string())
-                } else {
-                    return;
-                }
-            }
-        };
-
-        // eprintln!("DEBUG KEY: keysym={:?}, state={:?}, logical_key={:?}", event.keysym, state, logical_key);
-
-        if let Some(st) = &mut self.state {
-            match event.keysym {
-                xkeysym::Keysym::Control_L | xkeysym::Keysym::Control_R => {
-                    st.modifiers.ctrl = state == cce_ui::widget::ElementState::Pressed;
-                }
-                xkeysym::Keysym::Alt_L | xkeysym::Keysym::Alt_R => {
-                    st.modifiers.alt = state == cce_ui::widget::ElementState::Pressed;
-                }
-                xkeysym::Keysym::Shift_L | xkeysym::Keysym::Shift_R => {
-                    st.modifiers.shift = state == cce_ui::widget::ElementState::Pressed;
-                }
-                xkeysym::Keysym::Super_L | xkeysym::Keysym::Super_R => {
-                    st.modifiers.logo = state == cce_ui::widget::ElementState::Pressed;
-                }
-                _ => {}
-            }
-
-            let custom_event = KeyEvent {
-                state,
-                logical_key,
-                text: event.utf8.clone(),
-                repeat: false,
-                ctrl: st.modifiers.ctrl,
-                shift: st.modifiers.shift,
-            };
-
-            if state == cce_ui::widget::ElementState::Pressed {
-                if is_repeatable_key(&custom_event.logical_key) {
-                    self.pressed_key = Some(PressedKey {
-                        logical_key: custom_event.logical_key.clone(),
-                        text: custom_event.text.clone(),
-                        first_pressed: std::time::Instant::now(),
-                        last_repeated: std::time::Instant::now(),
-                    });
-                } else {
-                    self.pressed_key = None;
-                }
-            } else if state == cce_ui::widget::ElementState::Released {
-                if let Some(ref pk) = self.pressed_key {
-                    if pk.logical_key == custom_event.logical_key {
-                        self.pressed_key = None;
-                    }
-                }
-            }
-
-            let ev = WindowEvent::KeyboardInput { event: custom_event };
-            self.process_event(ev);
-        }
-    }
-
-    pub fn process_event(&mut self, ev: WindowEvent) {
-        if let Some(state) = &mut self.state {
+impl State {
+    /// Route a window event through `handle_event`, then run the post-event
+    /// side-effect pass (menu clicks, pane toggles, pending actions).
+    /// Returns true when a redraw is needed.
+    pub(crate) fn process_window_event(&mut self, ev: WindowEvent) -> bool {
+        let mut result = false;
+        {
+            let state = &mut *self;
             let mut changed = state.handle_event(&ev);
 
             if let Some(seg) = state.path_mut().path_click() {
@@ -1309,14 +566,18 @@ impl AppState {
                 if state.is_detached_network || state.detached_circular_network {
                     state.needs_autosave = true;
                 }
-                self.redraw = true;
+                result = true;
             }
         }
+        result
     }
 
-    pub fn handle_user_event(&mut self, event: CustomEvent) {
+    /// Apply an HTTP/API event (the engine `update` hook). Returns true when
+    /// a redraw is needed.
+    pub(crate) fn apply_custom_event(&mut self, event: CustomEvent) -> bool {
         let mut needs_redraw = false;
-        if let Some(state) = &mut self.state {
+        {
+            let state = &mut *self;
             match event {
                 CustomEvent::GetState(tx) => {
                     let proj = Project {
@@ -1572,7 +833,7 @@ impl AppState {
                             match validated {
                                 Ok(label) => {
                                     state.menu_mut(widget_idx).trigger_menu_click(menu_idx, item_idx);
-                                    self.process_event(WindowEvent::CursorMoved { position: LocalPosition { x: -9999.0, y: -9999.0 } });
+                                    let _ = state.process_window_event(WindowEvent::CursorMoved { position: LocalPosition { x: -9999.0, y: -9999.0 } });
                                     needs_redraw = true;
                                     Ok(format!("Menu clicked: {label}"))
                                 }
@@ -1601,26 +862,18 @@ impl AppState {
                     };
                     let _ = tx.send(res);
                 }
-            }
-        } else {
-            match event {
-                CustomEvent::GetState(tx) => {
-                    let _ = tx.send("null".to_string());
-                }
-                CustomEvent::PostAction(_, tx) => {
-                    let _ = tx.send(Err("State not initialized".to_string()));
-                }
+                // Exit is handled by the Application::update wrapper
+                // (autosave + engine exit) before this is reached.
+                CustomEvent::Exit => {}
             }
         }
         if needs_redraw {
-            if let Some(state) = &mut self.state {
-                state.update_window_title();
-                if state.is_detached_network || state.detached_circular_network {
-                    state.needs_autosave = true;
-                }
+            self.update_window_title();
+            if self.is_detached_network || self.detached_circular_network {
+                self.needs_autosave = true;
             }
-            self.redraw = true;
         }
+        needs_redraw
     }
 }