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

commit7b3230f2b2aa93e7b2c012ebe2843f3fea07ee2a
parenta2cb8d9267
authorLucas Galante <[email protected]>
date2026-07-16 13:46
refactor: 2D frame onto the engine's display-list paint path

collect_vertices + app-side prepare_text become one collect_display_list:
the frame's geometry AND text as a scene::paint::DisplayList, in the same
hand-maintained slot draw order (widget plates via append_widget_plate,
graph extras scissored to the pane, grid cursor, params rounded view,
popovers, context border). display_list_text opts the text into the
engine's shaping/glyph pass — the app-side FontSystem, SwashCache,
text_buffer_cache, vertex_data, upload_vertices and make_text_buffer* are
gone (glyphon remains only for the vk-smoke bin), closing the two-stack
divergences (line-height centering, fontdb ID matching) for good.

The circular pane rides PaintItem::clip_circle; network fade rides
Prim::Text alpha; the curved rim-label path was dead since the engine
migration (menubars never draw) and is deleted rather than ported.
Needs cce-ui's scene-layer prereqs (clip circles, text alpha, adaptive
fans, append_widget_plate, per-line code labels).

Co-Authored-By: Claude Fable 5 <[email protected]>

 CLAUDE.md          |  22 +-
 Cargo.toml         |   4 +-
 src/app.rs         | 154 ---------
 src/application.rs |  13 +-
 src/project.rs     |   5 -
 src/render.rs      | 891 ++++++++++++++---------------------------------------
 src/window.rs      |   9 -
 7 files changed, 262 insertions(+), 836 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 445801f..4d5bdbb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,12 +54,13 @@ The designer runs on cce-ui's standard `Application` trait / `engine::run` patte
 loop, and the `VkRenderer`). Because it draws a 3D scene and shapes its own text, it
 uses the engine's extended hooks — it is the reference consumer for them:
 `renderer_init` (create persistent meshes), `stage_renderer` (flush pending mesh
-updates, prepare app-shaped text spans, stage the raster scene / RT pane; returns
-true while the path tracer refines), `custom_vertices` (the cached 2D vertex list),
-`handle_resize`, and `standard_csd` / `cursor_icon` / `take_window_action` (the
-detached circular window's radial border resize + top-arc move). `glyphon` is a
-dependency only for cosmic-text/swash (shaping + rasterization); with
-`display_list_text` off, the engine never touches the renderer's text state.
+updates, stage the raster scene / RT pane; returns true while the path tracer
+refines), `handle_resize`, and `standard_csd` / `cursor_icon` / `take_window_action`
+(the detached circular window's radial border resize + top-arc move). The 2D frame —
+geometry AND text — is the engine's single paint path: `display_list` returns
+`State::collect_display_list()` and `display_list_text` opts the text into the
+engine's shaping/glyph pass (the app has no `FontSystem` or buffer cache of its own;
+`glyphon` remains a dependency only for the standalone `vk-smoke` bin).
 
 - `src/app.rs` (~5k lines) — the heart: `State` (the entire app model), `HttpAction` /
   `CustomEvent`, node-template loading, pane layout. Top-level widgets live in fixed
@@ -74,10 +75,11 @@ dependency only for cosmic-text/swash (shaping + rasterization); with
 - `src/window.rs` — `WindowEvent` plus the post-event side-effect pass
   (`process_window_event`: menu clicks, pane toggles) and HTTP-action application
   (`apply_custom_event`).
-- `src/render.rs` — `State::collect_vertices`: builds the frame's vertex batches,
-  hand-maintained draw order over the widget slots, circular-pane clipping;
-  `prepare_text` shapes spans against the app's own `FontSystem` (created with
-  `create_font_system()`, matching the engine's — fontdb IDs must line up).
+- `src/render.rs` — `State::collect_display_list`: the frame's 2D content as one
+  `cce_ui::scene::paint::DisplayList` (prims + `Prim::Text`), hand-maintained draw
+  order over the widget slots, circular-pane clipping via `PaintItem::clip_circle`,
+  network fade via text alpha. Rebuilt every drawn frame; the engine tessellates,
+  shapes, and draws it.
 - `src/geometry.rs` — node-graph evaluation. Each OpenCL node's kernel code is
   preprocessed: `chf("name", default)` / `chi` / `chv` calls are parsed into dynamic
   UI parameters (`parse_dynamic_params`) and rewritten to `param_values[i]` reads
diff --git a/Cargo.toml b/Cargo.toml
index 5a7fc97..f764d3e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,8 +12,8 @@ wayland-client = { version = "0.31", features = ["system"] }
 xkeysym = "0.2"
 raw-window-handle = "0.6"
 bytemuck = { version = "1", features = ["derive"] }
-# glyphon supplies cosmic-text/swash (shaping + rasterization) for the ash text
-# stage; its wgpu renderer half is unused since the VkRenderer cutover.
+# Only the vk-smoke bin shapes text directly now — the app's text goes through
+# the engine's display-list pass (display_list_text) since the paint migration.
 glyphon = "0.8"
 tokio = { version = "1", features = ["full"] }
 glam = "0.29"
diff --git a/src/app.rs b/src/app.rs
index 4f62be4..b0ab1ec 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -38,7 +38,6 @@ use cce_ui::widget::{Adapted, Breadcrumb, MenuBar, MenuController, ParametersBg,
 use cce_ui::widget::UiContext;
 use crate::viewport_3d::Viewport3D;
 use cce_ui::colors;
-use glyphon::{Attrs, Buffer, FontSystem, Metrics};
 use glam::{Mat4, Vec3};
 
 use crate::geometry::*;
@@ -803,42 +802,6 @@ impl cce_ui::widget::Input for NodePalette {
 }
 
 
-/// Line box for a shaped buffer. Single-line labels use `size * 1.0`, matching the
-/// engine's shaping and the `cce_ui::layout::center_text_y` family (`line_height`
-/// multiplier 1.0) that widget paint code positions labels with — a taller box makes
-/// every label render below its intended center. Multi-line text (the code editor)
-/// keeps the historical `1.4` spacing its hand-drawn cursor math is tuned against.
-fn buffer_line_height(text: &str, size: f32) -> f32 {
-    if text.contains('\n') { size * 1.4 } else { size }
-}
-
-pub fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
-    let metrics = Metrics::new(size, buffer_line_height(text, size));
-    let mut buffer = Buffer::new(font_system, metrics);
-    buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
-    buffer.shape_until_scroll(font_system, true);
-    buffer
-}
-
-pub fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
-    let metrics = Metrics::new(size, buffer_line_height(text, size));
-    let mut buffer = Buffer::new(font_system, metrics);
-    let mut attrs = Attrs::new();
-    let family_name = font.map(|f| cce_ui::layout::parse_font_string(f).0);
-    if let Some(ref name) = family_name {
-        let family = match name.as_str() {
-            "monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
-            "sans-serif" => glyphon::Family::SansSerif,
-            "serif" => glyphon::Family::Serif,
-            _ => glyphon::Family::Name(name),
-        };
-        attrs = attrs.family(family);
-    }
-    buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
-    buffer.shape_until_scroll(font_system, true);
-    buffer
-}
-
 pub fn get_next_visible_pane(
     current_pane: usize,
     show_network: bool,
@@ -873,77 +836,6 @@ pub fn get_next_visible_pane(
     visible_panes[next_pos]
 }
 
-pub fn push_circle_vertices(
-    cx: f32, cy: f32, r: f32,
-    sw: f32, sh: f32,
-    color: [f32; 4],
-    segments: usize,
-    clip_circle: [f32; 3],
-    out: &mut Vec<Vertex>,
-) {
-    for i in 0..segments {
-        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-        let x0 = cx;
-        let y0 = cy;
-        let x1 = cx + r * theta1.cos();
-        let y1 = cy + r * theta1.sin();
-        let x2 = cx + r * theta2.cos();
-        let y2 = cy + r * theta2.sin();
-        
-        let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
-        let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
-        let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
-        let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
-        let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
-        let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-        
-        out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
-        out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
-        out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-    }
-}
-
-pub fn push_circle_border_vertices(
-    cx: f32, cy: f32, r: f32,
-    thickness: f32,
-    sw: f32, sh: f32,
-    color: [f32; 4],
-    segments: usize,
-    clip_circle: [f32; 3],
-    out: &mut Vec<Vertex>,
-) {
-    for i in 0..segments {
-        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-        
-        let x0 = cx + (r - thickness) * theta1.cos();
-        let y0 = cy + (r - thickness) * theta1.sin();
-        let x1 = cx + r * theta1.cos();
-        let y1 = cy + r * theta1.sin();
-        
-        let x2 = cx + r * theta2.cos();
-        let y2 = cy + r * theta2.sin();
-        let x3 = cx + (r - thickness) * theta2.cos();
-        let y3 = cy + (r - thickness) * theta2.sin();
-        
-        let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
-        let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
-        let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-        let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
-        
-        out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
-        out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
-        out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-        
-        out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
-        out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-        out.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
-    }
-}
-
-
-
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct ResizeDirection {
     pub left: bool,
@@ -998,11 +890,8 @@ pub struct PendingWindowDrag {
 }
 
 pub struct State {
-    pub font_system: FontSystem,
-    pub swash_cache: glyphon::SwashCache,
     /// Window title; the engine polls `Application::settings` and applies it.
     pub title: String,
-    pub vertex_data: Vec<Vertex>,
 
     /// GPU meshes — `None` until `renderer_init`.
     pub meshes: Option<SceneMeshes>,
@@ -1117,10 +1006,7 @@ pub struct State {
     pub loaded_project_path: Option<std::path::PathBuf>,
     pub last_saved_root_json: String,
     pub recent_files: Vec<std::path::PathBuf>,
-    pub text_buffer_cache: std::collections::HashMap<(String, u32, Option<String>), Buffer>,
     pub viewport_dirty: bool,
-    pub text_dirty: bool,
-    pub last_popover_rects: Vec<(f32, f32, f32, f32)>,
     pub last_status_text: String,
     pub last_viewport_camera_pos: Vec3,
     pub last_viewport_camera_rx: f32,
@@ -2016,13 +1902,11 @@ impl State {
         self.node_palette_query = String::new();
         self.node_palette_selected = 0;
         self.refresh_node_palette();
-        self.upload_vertices();
     }
 
     pub fn close_node_palette(&mut self) {
         self.node_palette_visible = false;
         self.refresh_node_palette();
-        self.upload_vertices();
     }
 
 
@@ -2047,7 +1931,6 @@ impl State {
         self.apply_layout();
         self.update_panel_bounds();
         self.rebuild_scene_geometry();
-        self.upload_vertices();
         true
     }
 
@@ -2064,7 +1947,6 @@ impl State {
                 if !self.node_palette_filtered.is_empty() {
                     self.node_palette_selected = (self.node_palette_selected + 1).min(self.node_palette_filtered.len() - 1);
                     self.refresh_node_palette();
-                    self.upload_vertices();
                 }
                 true
             }
@@ -2072,14 +1954,12 @@ impl State {
                 if self.node_palette_selected > 0 {
                     self.node_palette_selected -= 1;
                     self.refresh_node_palette();
-                    self.upload_vertices();
                 }
                 true
             }
             Key::Named(NamedKey::Backspace) => {
                 self.node_palette_query.pop();
                 self.refresh_node_palette();
-                self.upload_vertices();
                 true
             }
             Key::Named(NamedKey::Tab) => {
@@ -2092,7 +1972,6 @@ impl State {
                         self.node_palette_query.push(ch);
                     }
                     self.refresh_node_palette();
-                    self.upload_vertices();
                     true
                 } else {
                     false
@@ -2297,7 +2176,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         self.rebuild_positions();
         self.apply_layout();
         self.update_panel_bounds();
-        self.upload_vertices();
         self.sync_cursor_and_selection();
     }
 
@@ -2318,7 +2196,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.grid_cursor_col = pos_x as i32;
                     self.grid_cursor_row = pos_y as i32;
                     self.sync_cursor_and_selection();
-                    self.upload_vertices();
                 }
             }
             true
@@ -2364,7 +2241,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             self.apply_layout();
             self.update_panel_bounds();
             self.rebuild_scene_geometry();
-            self.upload_vertices();
             self.viewport_dirty = true;
             true
         } else {
@@ -2475,8 +2351,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         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();
 
         let splitter_layout = cce_ui::layout::SplitterLayout::new(sw, SPLITTER_W, MIN_COLUMN);
         let templates_root = load_fs_tree();
@@ -2596,10 +2470,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         }
 
         let mut state = Self {
-            font_system,
-            swash_cache,
             title: String::new(),
-            vertex_data: Vec::with_capacity(4096),
             meshes: None,
             pending_grid: None,
             pending_origin: None,
@@ -2725,10 +2596,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             loaded_project_path: None,
             last_saved_root_json: serde_json::to_string(&fs_root).unwrap_or_default(),
             recent_files,
-            text_buffer_cache: std::collections::HashMap::new(),
             viewport_dirty: true,
-            text_dirty: true,
-            last_popover_rects: Vec::new(),
             last_status_text: String::new(),
             last_viewport_camera_pos: Vec3::ZERO,
             last_viewport_camera_rx: 0.0,
@@ -2795,7 +2663,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         state.apply_layout();
         state.update_panel_bounds();
         state.sync_pane_focus();
-        state.upload_vertices();
         state.sync_cursor_and_selection();
         state.sync_parameters_pane();
         for i in 0..WIDGET_COUNT {
@@ -3481,7 +3348,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 self.sync_pane_focus();
                 self.rebuild_positions();
                 self.apply_layout();
-                self.upload_vertices();
                 return;
             }
             Action::ToggleSpreadsheet => {
@@ -3663,7 +3529,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             self.sync_layout();
             self.read_panel_offsets();
             self.keep_cursor_in_view();
-            self.upload_vertices();
             self.viewport_dirty = true;
         }
     }
@@ -3783,7 +3648,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
                     self.sync_layout();
                     self.read_panel_offsets();
-                    self.upload_vertices();
                     true
                 } else if !dialog_open && in_network_pane {
                     if self.modifiers.control_key() {
@@ -3842,7 +3706,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 if focus_changed {
                     self.sync_layout();
                     self.read_panel_offsets();
-                    self.upload_vertices();
                     true
                 } else {
                     result
@@ -3987,7 +3850,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             self.close_node_palette();
                         }
                     }
-                    self.upload_vertices();
                     return true;
                 }
                 let dialog_open = self.node_palette_visible;
@@ -4019,7 +3881,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                 self.is_panning = false;
                                 self.sync_layout();
                                 self.read_panel_offsets();
-                                self.upload_vertices();
                                 return true;
                             }
                         }
@@ -4030,7 +3891,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.is_panning = false;
                     self.sync_layout();
                     self.read_panel_offsets();
-                    self.upload_vertices();
                     return true;
                 }
 
@@ -4400,7 +4260,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             if matches!(drag, AppDrag::NetworkResize { .. }) {
                                 self.read_panel_offsets();
                             }
-                            self.upload_vertices();
                             changed = true;
                         }
                         if self.drag_widget.is_some() {
@@ -4411,14 +4270,12 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                                 self.sync_layout();
-                                self.upload_vertices();
                             } else if idx == SPREADSHEET_IDX {
                                 {
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                                 self.sync_layout();
-                                self.upload_vertices();
                             } else if idx == CONTENT_IDX {
                                 {
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
@@ -4440,7 +4297,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                 self.rebuild_positions();
                                 self.apply_layout();
                                 self.update_panel_bounds();
-                                self.upload_vertices();
                             } else {
                                 {
                                     let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
@@ -4485,7 +4341,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             p.default = output_node_name;
                             self.sync_nodes();
                             self.rebuild_scene_geometry();
-                            self.upload_vertices();
                             self.sync_parameters_pane();
                             changed = true;
                         }
@@ -4526,7 +4381,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
                 if event.state == ElementState::Pressed && event.logical_key == Key::Named(NamedKey::Escape) {
                     self.graph_mut().cancel_connecting();
-                    self.upload_vertices();
                     return true;
                 }
                 let mut changed = false;
@@ -4587,7 +4441,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                     self.current_dir_mut().children[idx].position = (new_x, new_y);
                                     self.sync_nodes();
                                     self.sync_layout();
-                                    self.upload_vertices();
                                 }
                             }
                             self.grid_cursor_col += dc;
@@ -4745,7 +4598,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                                 self.rebuild_positions();
                                                 self.apply_layout();
                                                 self.update_panel_bounds();
-                                                self.upload_vertices();
                                                 changed = true;
                                             }
                                         }
@@ -4794,7 +4646,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                                                 self.apply_layout();
                                                 self.update_panel_bounds();
                                                 self.rebuild_scene_geometry();
-                                                self.upload_vertices();
                                                 self.viewport_dirty = true;
                                                 changed = true;
                                             }
@@ -4862,7 +4713,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 self.update_graph_settings_from_config();
                 self.rebuild_positions();
                 self.apply_layout();
-                self.upload_vertices();
             } else {
                 self.update_inertial_settings();
             }
@@ -4904,7 +4754,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         self.update_pivot_geometry();
                         self.update_viewport_bg_geometry();
                         self.sync_grid_settings();
-                        self.upload_vertices();
                     }
                 }
             }
@@ -4990,7 +4839,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
 
         if tick_changed {
-            self.upload_vertices();
         }
 
         let mut panned = false;
@@ -5039,7 +4887,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             }
             self.sync_layout();
             self.read_panel_offsets();
-            self.upload_vertices();
         }
 
         tick_changed || panned
@@ -5101,7 +4948,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             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
diff --git a/src/application.rs b/src/application.rs
index b48b47d..7376486 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -9,7 +9,7 @@ use std::time::{Duration, Instant};
 
 use cce_ui::engine::{
     xdg_toplevel::ResizeEdge, Application, CursorIcon, EngineState, LogicalPosition, LogicalSize,
-    Vertex, WindowAction, WindowSettings,
+    WindowAction, WindowSettings,
 };
 use cce_ui::vk::VkRenderer;
 use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
@@ -287,8 +287,15 @@ impl Application for State {
         None
     }
 
-    fn custom_vertices(&mut self, verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {
-        verts.extend_from_slice(&self.vertex_data);
+    fn display_list(&mut self, _size: LogicalSize, _scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
+        // The single paint path: the whole 2D frame — geometry and text — rebuilt
+        // every drawn frame (the engine only draws on demand). The 3D scene / RT
+        // panes stay in stage_renderer.
+        Some(self.collect_display_list())
+    }
+
+    fn display_list_text(&self) -> bool {
+        true
     }
 
     fn renderer_init(&mut self, renderer: &mut VkRenderer) {
diff --git a/src/project.rs b/src/project.rs
index 3258aba..09b1b94 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -110,7 +110,6 @@ impl State {
     }
 
     pub(crate) fn load_from_file(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
-        self.text_buffer_cache.clear();
         if path.file_name().map_or(false, |n| n == "default_project.json") {
             let content = fs::read_to_string(path)?;
             let proj: Project = serde_json::from_str(&content)?;
@@ -150,7 +149,6 @@ impl State {
             self.rebuild_positions();
             self.apply_layout();
             self.update_panel_bounds();
-            self.upload_vertices();
             self.loaded_project_path = None;
             self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
             self.update_window_title();
@@ -206,7 +204,6 @@ impl State {
         self.rebuild_positions();
         self.apply_layout();
         self.update_panel_bounds();
-        self.upload_vertices();
         self.loaded_project_path = Some(project_dir);
         self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
         self.update_window_title();
@@ -214,7 +211,6 @@ impl State {
     }
 
     pub(crate) fn new_project(&mut self) {
-        self.text_buffer_cache.clear();
         self.fs_root = FsNode {
             id: "root".to_string(),
             name: "root".to_string(),
@@ -255,7 +251,6 @@ impl State {
         self.rebuild_positions();
         self.apply_layout();
         self.update_panel_bounds();
-        self.upload_vertices();
         self.loaded_project_path = None;
         self.last_saved_root_json = serde_json::to_string(&self.fs_root).unwrap_or_default();
         self.update_window_title();
diff --git a/src/render.rs b/src/render.rs
index bc31d4e..00e1d84 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1,54 +1,63 @@
 
-use cce_ui::widget::TextLabel;
 use cce_ui::colors;
 use cce_ui::widget::WidgetHost;
 
 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, HEADER_IDX, RIGHT_MENUBAR_IDX,
     SPREADSHEET_MENUBAR_IDX, SPREADSHEET_IDX,
-    MENUBAR_H,
     LEFT_MENUBAR_IDX, PARAM_MENUBAR_IDX, NETWORK_PANEL_IDX,
-    push_circle_vertices, push_circle_border_vertices,
-};
-use cce_ui::engine::Vertex;
-use crate::geometry::{
-    network_sphere_vertices_with_errors,
-};
-use cce_ui::vk::TextSpan;
-use cce_ui::engine::{
-    push_widget_vertices, push_extra_quad_vertices,
-    push_extra_quad_vertices_clipped, push_arc_background_vertices,
-    push_plate_solid_border_vertices, push_rounded_rect_vertices_corners,
 };
+use crate::geometry::network_sphere_vertices_with_errors;
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::{DisplayList, PaintCtx, Prim};
+use cce_ui::scene::painter::{append_widget_plate, append_widget_text};
 
-impl State {
-    pub(crate) fn collect_vertices(&mut self, verts: &mut Vec<Vertex>) {
-        verts.clear();
-        let sw = self.width;
-        let sh = self.height;
+const TAU: f32 = 2.0 * std::f32::consts::PI;
+
+fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
+    Rect { x, y, width: w, height: h }
+}
 
-        let node_area_y = self.positions[CONTENT_IDX].1;
+/// Intersect two logical `[l, t, r, b]` text bounds.
+fn merge_bounds(a: Option<[f32; 4]>, b: Option<[f32; 4]>) -> Option<[f32; 4]> {
+    match (a, b) {
+        (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
+        (Some(a), None) => Some(a),
+        (None, b) => b,
+    }
+}
+
+impl State {
+    /// The frame's entire 2D content — geometry AND text — as one display list (the
+    /// engine's single paint path; `Application::display_list_text` opts the designer's
+    /// text into the engine's shaping/glyph pass, so the app-side FontSystem and buffer
+    /// cache are gone). Draw order is the hand-maintained slot order the vertex path
+    /// used; the circular network pane rides `PaintItem::clip_circle`.
+    pub(crate) fn collect_display_list(&mut self) -> DisplayList {
         let show_cursor = self.drag_widget.is_none() && self.app_drag.is_none();
 
+        // The graph content clip (node quads, cursor) and the circular pane clip.
         let clip = if self.circular_network_pane {
-            (
+            rect(
                 self.circular_network_layout.x - self.circular_network_layout.r,
                 self.circular_network_layout.y - self.circular_network_layout.r,
-                self.circular_network_layout.x + self.circular_network_layout.r,
-                self.circular_network_layout.y + self.circular_network_layout.r,
+                2.0 * self.circular_network_layout.r,
+                2.0 * self.circular_network_layout.r,
             )
         } else {
             let (cx, cy, cw, ch) = self.positions[CONTENT_IDX];
-            (cx, cy, cx + cw, cy + ch)
+            rect(cx, cy, cw, ch)
         };
-
-        let clip_circle_val = if self.circular_network_pane {
-            [self.circular_network_layout.x * self.scale as f32, self.circular_network_layout.y * self.scale as f32, self.circular_network_layout.r * self.scale as f32]
+        let clip_circle = if self.circular_network_pane {
+            Some([
+                self.circular_network_layout.x,
+                self.circular_network_layout.y,
+                self.circular_network_layout.r,
+            ])
         } else {
-            [0.0, 0.0, 0.0]
+            None
         };
 
         let mut draw_order: Vec<usize> = (0..WIDGET_COUNT).collect();
@@ -65,14 +74,13 @@ impl State {
             {
                 -3
             } else {
-                self.slots.get_dyn_mut(i).z_index()
+                self.slots.get_dyn(i).z_index()
             };
             (self.has_any_open_menu(i), base_key)
         });
 
-        // Root Backplate DISSOLVED (Phase 6as): it was a transparent draw-order shim —
-        // register the visible widgets (registry consumers: coverage/parent walks) and
-        // draw each top-level widget directly in the sorted order.
+        // Root Backplate DISSOLVED (Phase 6as): register the widgets (registry consumers:
+        // coverage/parent walks) and paint each top-level widget directly in sorted order.
         self.ui_context.clear_hierarchy();
         let widget_ptrs: Vec<*mut (dyn WidgetHost + 'static)> = (0..WIDGET_COUNT)
             .map(|i| self.slots.get_dyn(i) as *const (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static))
@@ -86,125 +94,33 @@ impl State {
             self.ui_context.register_widget(w.base().id(), w as *const (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static));
         }
 
+        let mut pc = PaintCtx::new();
         let mut visited = vec![false; WIDGET_COUNT];
         for &i in &draw_order {
-            if !self.slots.get_dyn_mut(i).visible() {
+            if !self.slots.get_dyn(i).visible() {
                 continue;
             }
             unsafe {
-                self.draw_element_recursive(
-                    &*widget_ptrs[i],
-                    verts,
-                    sw,
-                    sh,
-                    [0.0, 0.0, 0.0],
-                    show_cursor,
-                    node_area_y,
-                    &mut visited,
-                    clip,
-                    clip_circle_val,
-                );
+                self.paint_element(&*widget_ptrs[i], &mut pc, show_cursor, &mut visited, clip, clip_circle);
             }
         }
 
-        self.push_context_border(verts, sw, sh, clip_circle_val);
-    }
-
-    /// Highlight border around the focused context's pane. The per-pane
-    /// menubars are hidden in the floating layout, so this border is the
-    /// only visual indicator of `focused_pane`.
-    fn push_context_border(
-        &self,
-        verts: &mut Vec<Vertex>,
-        sw: f32,
-        sh: f32,
-        clip_circle_val: [f32; 3],
-    ) {
-        if self.is_detached_network {
-            return;
-        }
-
-        let thickness = 2.0;
-        let mut color = colors::highlight_primary_color();
-        color[3] = 0.9;
+        self.append_context_border(&mut pc);
+        self.append_frame_text(&mut pc);
 
-        let (x, y, w, h) = match self.focused_pane {
-            LEFT_MENUBAR_IDX => {
-                if !self.show_network || self.detached_circular_network {
-                    return;
-                }
-                if self.circular_network_pane {
-                    color[3] *= self.network_opacity;
-                    push_circle_border_vertices(
-                        self.circular_network_layout.x,
-                        self.circular_network_layout.y,
-                        self.circular_network_layout.r,
-                        3.0,
-                        sw,
-                        sh,
-                        color,
-                        64,
-                        clip_circle_val,
-                        verts,
-                    );
-                    return;
-                }
-                color[3] *= self.network_opacity;
-                self.positions[NETWORK_PANEL_IDX]
-            }
-            RIGHT_MENUBAR_IDX => {
-                if !self.show_viewport {
-                    return;
-                }
-                self.positions[VIEWPORT_IDX]
-            }
-            PARAM_MENUBAR_IDX => {
-                if !self.show_parameters {
-                    return;
-                }
-                self.positions[PARAM_IDX]
-            }
-            SPREADSHEET_MENUBAR_IDX => {
-                if !self.show_spreadsheet {
-                    return;
-                }
-                self.positions[SPREADSHEET_IDX]
-            }
-            _ => return,
-        };
-
-        if w <= 0.0 || h <= 0.0 {
-            return;
-        }
-        let r = cce_ui::layout::plate_corner_radius();
-        let radii = cce_ui::widget::CornerRadii::new(r, r, r, r);
-        push_plate_solid_border_vertices(
-            x, y, w, h,
-            radii,
-            thickness,
-            sw, sh,
-            color,
-            [0.0, 0.0, 0.0],
-            verts,
-        );
+        pc.finish()
     }
 
-    pub(crate) fn draw_widget_recursive(
+    fn paint_widget(
         &self,
         idx: usize,
-        verts: &mut Vec<Vertex>,
-        sw: f32,
-        sh: f32,
-        clip: (f32, f32, f32, f32),
-        clip_circle_val: [f32; 3],
+        pc: &mut PaintCtx,
         show_cursor: bool,
-        node_area_y: f32,
         visited: &mut [bool],
+        clip: Rect,
+        clip_circle: Option<[f32; 3]>,
     ) {
-        if idx >= visited.len() {
-            return;
-        }
-        if visited[idx] {
+        if idx >= visited.len() || visited[idx] {
             return;
         }
         visited[idx] = true;
@@ -215,48 +131,31 @@ impl State {
         }
 
         let is_network_part = idx == CONTENT_IDX || idx == LEFT_MENUBAR_IDX || idx == BREADCRUMB_IDX || idx == NETWORK_PANEL_IDX;
-        let active_clip_circle = if is_network_part { clip_circle_val } else { [0.0, 0.0, 0.0] };
-
-        if idx == NETWORK_PANEL_IDX {
-            if self.circular_network_pane {
-                push_circle_vertices(
-                    self.circular_network_layout.x,
-                    self.circular_network_layout.y,
-                    self.circular_network_layout.r,
-                    sw,
-                    sh,
-                    w.color(),
-                    64,
-                    active_clip_circle,
-                    verts,
-                );
-                push_circle_border_vertices(
-                    self.circular_network_layout.x,
-                    self.circular_network_layout.y,
-                    self.circular_network_layout.r,
-                    3.0,
-                    sw,
-                    sh,
-                    [0.35, 0.65, 0.95, 0.80 * self.network_opacity],
-                    64,
-                    active_clip_circle,
-                    verts,
-                );
-            } else {
-                push_widget_vertices(w, sw, sh, active_clip_circle, verts);
-            }
+        let active_circle = if is_network_part { clip_circle } else { None };
+        if let Some(c) = active_circle {
+            pc.push_clip_circle(c);
+        }
+
+        if idx == NETWORK_PANEL_IDX && self.circular_network_pane {
+            let cx = self.circular_network_layout.x;
+            let cy = self.circular_network_layout.y;
+            let r = self.circular_network_layout.r;
+            pc.circle(cx, cy, r, w.color());
+            pc.arc(cx, cy, r, 3.0, 0.0, TAU, [0.35, 0.65, 0.95, 0.80 * self.network_opacity]);
         } else if idx == CONTENT_IDX {
             if !self.circular_network_pane {
-                push_widget_vertices(w, sw, sh, active_clip_circle, verts);
+                append_widget_plate(w, pc);
             }
 
-            for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                push_extra_quad_vertices_clipped(w, qx, qy, qw, qh, sw, sh, qc, clip, active_clip_circle, verts);
-            }
+            pc.clip(clip, |pc| {
+                for (qx, qy, qw, qh, qc) in w.extra_quads() {
+                    pc.quad(rect(qx, qy, qw, qh), qc);
+                }
+            });
 
             for (cx, cy, cr, cc) in w.extra_circles() {
-                if cx >= clip.0 && cx <= clip.2 && cy >= clip.1 && cy <= clip.3 {
-                    push_circle_vertices(cx, cy, cr, sw, sh, cc, 16, active_clip_circle, verts);
+                if cx >= clip.x && cx <= clip.x + clip.width && cy >= clip.y && cy <= clip.y + clip.height {
+                    pc.circle(cx, cy, cr, cc);
                 }
             }
 
@@ -272,74 +171,18 @@ impl State {
                 color[3] = 0.9;
                 if self.graph().is_node_rect(cx, cy, cw, ch) {
                     let r = cce_ui::layout::graph_node_corner_radius();
-                    let radii = cce_ui::widget::CornerRadii::new(r, r, r, r);
-                    push_plate_solid_border_vertices(
-                        cx, cy, cw, ch,
-                        radii,
-                        thickness,
-                        sw, sh,
-                        color,
-                        active_clip_circle,
-                        verts,
-                    );
+                    pc.border(rect(cx, cy, cw, ch), (r, r, r, r), [0.0; 4], color, thickness);
                 } else {
-                    push_extra_quad_vertices_clipped(w, cx, cy, cw, thickness, sw, sh, color, clip, active_clip_circle, verts);
-                    push_extra_quad_vertices_clipped(w, cx, cy + ch - thickness, cw, thickness, sw, sh, color, clip, active_clip_circle, verts);
-                    push_extra_quad_vertices_clipped(w, cx, cy + thickness, thickness, ch - thickness * 2.0, sw, sh, color, clip, active_clip_circle, verts);
-                    push_extra_quad_vertices_clipped(w, cx + cw - thickness, cy + thickness, thickness, ch - thickness * 2.0, sw, sh, color, clip, active_clip_circle, verts);
+                    pc.clip(clip, |pc| {
+                        pc.quad(rect(cx, cy, cw, thickness), color);
+                        pc.quad(rect(cx, cy + ch - thickness, cw, thickness), color);
+                        pc.quad(rect(cx, cy + thickness, thickness, ch - thickness * 2.0), color);
+                        pc.quad(rect(cx + cw - thickness, cy + thickness, thickness, ch - thickness * 2.0), color);
+                    });
                 }
             }
-        } else if idx == LEFT_MENUBAR_IDX && self.circular_network_pane {
-            let cx = self.circular_network_layout.x;
-            let cy = self.circular_network_layout.y;
-            let r = self.circular_network_layout.r;
-
-            let bg_color = w.color();
-            push_arc_background_vertices(
-                cx, cy, r,
-                MENUBAR_H,
-                std::f32::consts::PI,
-                2.0 * std::f32::consts::PI,
-                sw, sh,
-                bg_color,
-                64,
-                active_clip_circle,
-                verts,
-            );
-
-            let border_color = [0.22, 0.22, 0.28, 0.90 * self.network_opacity];
-            push_arc_background_vertices(
-                cx, cy, r - MENUBAR_H,
-                1.5,
-                std::f32::consts::PI,
-                2.0 * std::f32::consts::PI,
-                sw, sh,
-                border_color,
-                64,
-                active_clip_circle,
-                verts,
-            );
-
-            for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                push_extra_quad_vertices(w, qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
-            }
-            for (cx, cy, cr, cc) in w.extra_circles() {
-                push_circle_vertices(cx, cy, cr, sw, sh, cc, 16, active_clip_circle, verts);
-            }
-            for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
-                push_arc_background_vertices(
-                    acx, acy, ar,
-                    ath,
-                    a_start, a_end,
-                    sw, sh,
-                    acolor,
-                    64,
-                    active_clip_circle,
-                    verts,
-                );
-            }
         } else {
-            push_widget_vertices(w, sw, sh, active_clip_circle, verts);
+            append_widget_plate(w, pc);
 
             // The params pane serves its chrome through the legacy plain-quad view,
             // which carries flat quads only — the controls' rounded-rect backgrounds
@@ -347,144 +190,220 @@ impl State {
             // drawn under the flat chrome and clipped to the pane's scroll viewport.
             if idx == PARAM_IDX {
                 let (px, py, pw, ph) = self.positions[PARAM_IDX];
-                let view = (px, py + 4.0, px + pw, py + ph - 4.0);
+                let view = rect(px, py + 4.0, pw, (ph - 8.0).max(0.0));
                 let param_bg = self
                     .slots
                     .param
                     .as_any()
                     .downcast_ref::<cce_ui::widget::ParametersBg>()
                     .expect("PARAM_IDX must be a ParametersBg");
-                for (qx, qy, qw, qh, qr, qc, corners) in param_bg.rounded_quads(&self.ui_context) {
-                    let radii = cce_ui::widget::CornerRadii::new(
-                        if corners.0 { qr } else { 0.0 },
-                        if corners.1 { qr } else { 0.0 },
-                        if corners.2 { qr } else { 0.0 },
-                        if corners.3 { qr } else { 0.0 },
-                    );
-                    push_rounded_rect_vertices_corners(
-                        qx, qy, qw, qh,
-                        radii,
-                        sw, sh,
-                        qc,
-                        active_clip_circle,
-                        Some(view),
-                        verts,
-                    );
-                }
+                pc.clip(view, |pc| {
+                    for (qx, qy, qw, qh, qr, qc, corners) in param_bg.rounded_quads(&self.ui_context) {
+                        pc.rounded_rect(rect(qx, qy, qw, qh), qr, corners, qc);
+                    }
+                });
             }
 
             for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                push_extra_quad_vertices(w, qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
+                pc.quad(rect(qx, qy, qw, qh), qc);
             }
             for (cx, cy, cr, cc) in w.extra_circles() {
-                push_circle_vertices(cx, cy, cr, sw, sh, cc, 16, active_clip_circle, verts);
+                pc.circle(cx, cy, cr, cc);
             }
         }
 
-        // Draw child elements recursively
+        // Child elements, then the widget's popover on top of them.
         for child_ptr in self.ui_context.tree.children_ptrs(w.base().id()) {
             if let Some(child_idx) = self.find_widget_index(child_ptr as *const ()) {
-                self.draw_widget_recursive(child_idx, verts, sw, sh, clip, clip_circle_val, show_cursor, node_area_y, visited);
+                self.paint_widget(child_idx, pc, show_cursor, visited, clip, clip_circle);
             } else {
                 unsafe {
-                    self.draw_element_recursive(
-                        &*child_ptr,
-                        verts,
-                        sw,
-                        sh,
-                        active_clip_circle,
-                        show_cursor,
-                        node_area_y,
-                        visited,
-                        clip,
-                        clip_circle_val,
-                    );
+                    self.paint_element(&*child_ptr, pc, show_cursor, visited, clip, clip_circle);
                 }
             }
         }
 
-        // Dropdown popover
         if w.visible() && (self.focused_widget == Some(idx) || idx == PARAM_IDX) {
             let mut popover_pc = cce_ui::layout::PopoverCollector::new();
             w.render_popover(&mut popover_pc);
             for (color, px, py, pw, ph) in popover_pc.rects {
-                let ndc_x = (px / sw) * 2.0 - 1.0;
-                let ndc_y = 1.0 - (py / sh) * 2.0;
-                let ndc_w = (pw / sw) * 2.0;
-                let ndc_h = (ph / sh) * 2.0;
-
-                let v_tl = Vertex { position: [ndc_x, ndc_y], color, clip_circle: [0.0, 0.0, 0.0] };
-                let v_tr = Vertex { position: [ndc_x + ndc_w, ndc_y], color, clip_circle: [0.0, 0.0, 0.0] };
-                let v_bl = Vertex { position: [ndc_x, ndc_y - ndc_h], color, clip_circle: [0.0, 0.0, 0.0] };
-                let v_br = Vertex { position: [ndc_x + ndc_w, ndc_y - ndc_h], color, clip_circle: [0.0, 0.0, 0.0] };
-
-                verts.push(v_tl);
-                verts.push(v_tr);
-                verts.push(v_bl);
-
-                verts.push(v_tr);
-                verts.push(v_br);
-                verts.push(v_bl);
+                pc.quad(rect(px, py, pw, ph), color);
             }
         }
+
+        if active_circle.is_some() {
+            pc.pop_clip_circle();
+        }
     }
 
-    fn draw_element_recursive(
+    fn paint_element(
         &self,
         element: &dyn WidgetHost,
-        verts: &mut Vec<Vertex>,
-        sw: f32,
-        sh: f32,
-        active_clip_circle: [f32; 3],
+        pc: &mut PaintCtx,
         show_cursor: bool,
-        node_area_y: f32,
         visited: &mut [bool],
-        clip: (f32, f32, f32, f32),
-        clip_circle_val: [f32; 3],
+        clip: Rect,
+        clip_circle: Option<[f32; 3]>,
     ) {
         if !element.visible() {
             return;
         }
 
         if let Some(idx) = self.find_widget_index(element as *const dyn WidgetHost as *const ()) {
-            self.draw_widget_recursive(idx, verts, sw, sh, clip, clip_circle_val, show_cursor, node_area_y, visited);
+            self.paint_widget(idx, pc, show_cursor, visited, clip, clip_circle);
             return;
         }
 
-        push_widget_vertices(element, sw, sh, active_clip_circle, verts);
-
+        append_widget_plate(element, pc);
         for (qx, qy, qw, qh, qc) in element.extra_quads() {
-            push_extra_quad_vertices(element, qx, qy, qw, qh, sw, sh, qc, active_clip_circle, verts);
+            pc.quad(rect(qx, qy, qw, qh), qc);
         }
         for (cx, cy, cr, cc) in element.extra_circles() {
-            push_circle_vertices(cx, cy, cr, sw, sh, cc, 16, active_clip_circle, verts);
+            pc.circle(cx, cy, cr, cc);
         }
 
         for child_ptr in self.ui_context.tree.children_ptrs(element.base().id()) {
             unsafe {
-                self.draw_element_recursive(
-                    &*child_ptr,
-                    verts,
-                    sw,
-                    sh,
-                    active_clip_circle,
-                    show_cursor,
-                    node_area_y,
-                    visited,
-                    clip,
-                    clip_circle_val,
-                );
+                self.paint_element(&*child_ptr, pc, show_cursor, visited, clip, clip_circle);
+            }
+        }
+    }
+
+    /// Highlight border around the focused context's pane. The per-pane
+    /// menubars are hidden in the floating layout, so this border is the
+    /// only visual indicator of `focused_pane`.
+    fn append_context_border(&self, pc: &mut PaintCtx) {
+        if self.is_detached_network {
+            return;
+        }
+
+        let thickness = 2.0;
+        let mut color = colors::highlight_primary_color();
+        color[3] = 0.9;
+
+        let (x, y, w, h) = match self.focused_pane {
+            LEFT_MENUBAR_IDX => {
+                if !self.show_network || self.detached_circular_network {
+                    return;
+                }
+                if self.circular_network_pane {
+                    color[3] *= self.network_opacity;
+                    pc.arc(
+                        self.circular_network_layout.x,
+                        self.circular_network_layout.y,
+                        self.circular_network_layout.r,
+                        3.0,
+                        0.0,
+                        TAU,
+                        color,
+                    );
+                    return;
+                }
+                color[3] *= self.network_opacity;
+                self.positions[NETWORK_PANEL_IDX]
+            }
+            RIGHT_MENUBAR_IDX => {
+                if !self.show_viewport {
+                    return;
+                }
+                self.positions[VIEWPORT_IDX]
+            }
+            PARAM_MENUBAR_IDX => {
+                if !self.show_parameters {
+                    return;
+                }
+                self.positions[PARAM_IDX]
+            }
+            SPREADSHEET_MENUBAR_IDX => {
+                if !self.show_spreadsheet {
+                    return;
+                }
+                self.positions[SPREADSHEET_IDX]
             }
+            _ => return,
+        };
+
+        if w <= 0.0 || h <= 0.0 {
+            return;
         }
+        let r = cce_ui::layout::plate_corner_radius();
+        pc.border(rect(x, y, w, h), (r, r, r, r), [0.0; 4], color, thickness);
     }
 
-    /// Rebuild the 2D vertex stream. The actual GPU upload happens in
-    /// `VkRenderer::draw_frame`, which consumes `vertex_data` every frame.
-    pub(crate) fn upload_vertices(&mut self) {
-        self.text_dirty = true;
-        let mut verts = std::mem::take(&mut self.vertex_data);
-        self.collect_vertices(&mut verts);
-        self.vertex_data = verts;
+    /// The frame's text, as `Prim::Text` items shaped and drawn by the engine
+    /// (`display_list_text`): each non-menubar widget's walk-derived labels — the
+    /// graph's clamped to the network pane (and distance-filtered against the circular
+    /// pane), network text fading with `network_opacity` — then the open popovers'.
+    fn append_frame_text(&self, pc: &mut PaintCtx) {
+        let circular = self.circular_network_pane;
+        let ncx = self.circular_network_layout.x;
+        let ncy = self.circular_network_layout.y;
+        let ncr = self.circular_network_layout.r;
+
+        for i in 0..WIDGET_COUNT {
+            let w = self.slots.get_dyn(i);
+            if !w.visible() {
+                continue;
+            }
+            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
+            if is_menubar {
+                continue;
+            }
+            let is_node = i == CONTENT_IDX;
+            let is_network_part = i == CONTENT_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
+
+            // Widget-level clip bounds (logical px), matching the old TextBounds.
+            let widget_bounds = if is_node {
+                if circular {
+                    Some([ncx - ncr, ncy - ncr, ncx + ncr, ncy + ncr])
+                } else {
+                    let (gx, gy, gw, gh) = self.positions[CONTENT_IDX];
+                    Some([gx, gy, gx + gw, gy + gh])
+                }
+            } else {
+                None
+            };
+
+            let mut scratch = PaintCtx::new();
+            append_widget_text(&self.ui_context, w, &mut scratch);
+            for item in scratch.finish().items {
+                if let Prim::Text { text, x, y, font_size, color, font, bounds: label_bounds, .. } = item.prim {
+                    if circular && is_network_part {
+                        let dx = x - ncx;
+                        let dy = y - ncy;
+                        if dx * dx + dy * dy > ncr * ncr {
+                            continue;
+                        }
+                    }
+                    let alpha = if is_network_part { self.network_opacity.clamp(0.0, 1.0) } else { 1.0 };
+                    pc.text_faded(text, x, y, font_size, color, alpha, font, merge_bounds(widget_bounds, label_bounds));
+                }
+            }
+        }
+
+        // Popover text, on top of (i.e. after) all widget labels.
+        for i in 0..WIDGET_COUNT {
+            let w = self.slots.get_dyn(i);
+            if !w.visible() {
+                continue;
+            }
+            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
+            if is_menubar {
+                continue;
+            }
+            if self.focused_widget == Some(i) || i == PARAM_IDX {
+                let mut popover_pc = cce_ui::layout::PopoverCollector::new();
+                w.render_popover(&mut popover_pc);
+                for (t, size, x, y, tc, font_opt, label_bounds) in popover_pc.texts {
+                    let color = [
+                        (tc[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+                        (tc[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+                        (tc[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+                    ];
+                    pc.text_with(t, x, y, size, color, font_opt, label_bounds);
+                }
+            }
+        }
     }
 
     pub(crate) fn rebuild_scene_geometry(&mut self) {
@@ -541,340 +460,6 @@ impl State {
         if self.last_status_text != text {
             self.last_status_text = text.to_string();
             self.slots.status.set_text(text);
-            self.text_dirty = true;
         }
     }
-
-    pub(crate) fn prepare_text(&mut self, renderer: &mut cce_ui::vk::VkRenderer) {
-        let mut current_popovers = Vec::new();
-        {
-            fn collect_popovers(
-                w: &dyn WidgetHost,
-                popovers: &mut Vec<(f32, f32, f32, f32)>,
-                ctx: &cce_ui::context::UiContext,
-            ) {
-                if let Some(rect) = w.popover_rect() {
-                    popovers.push(rect);
-                }
-                for child_ptr in ctx.tree.children_ptrs(w.base().id()) {
-                    unsafe {
-                        if let Some(child) = child_ptr.as_ref() {
-                            collect_popovers(child, popovers, ctx);
-                        }
-                    }
-                }
-            }
-
-            for i in 0..WIDGET_COUNT {
-                let w = self.slots.get_dyn(i);
-                if w.visible() {
-                    collect_popovers(w, &mut current_popovers, &self.ui_context);
-                }
-            }
-        }
-
-        if current_popovers != self.last_popover_rects {
-            self.last_popover_rects = current_popovers;
-            self.text_dirty = true;
-        }
-
-        if !self.text_dirty {
-            return;
-        }
-        self.text_dirty = false;
-
-        // 1. Prepare text on all widgets using self.font_system
-        for i in 0..WIDGET_COUNT {
-            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
-            if !is_menubar {
-                self.slots.get_dyn_mut(i).prepare_text(&mut self.font_system);
-            }
-        }
-
-        // 2. Destructure self
-        let sw = self.width;
-        let sh = self.height;
-        let circular_network_pane = self.circular_network_pane;
-        let network_circle_x = self.circular_network_layout.x;
-        let network_circle_y = self.circular_network_layout.y;
-        let network_circle_radius = self.circular_network_layout.r;
-        let network_opacity = self.network_opacity;
-        let focused_widget = self.focused_widget;
-        let _ = sw;
-        let _ = sh;
-
-        let Self {
-            ref mut font_system,
-            ref mut swash_cache,
-            physical_width, physical_height, scale,
-            ref slots,
-            ref mut text_buffer_cache,
-            ref ui_context,
-            ref positions,
-            ..
-        } = *self;
-
-        // Clear cache if too large to prevent unbounded memory growth
-        if text_buffer_cache.len() > 500 {
-            text_buffer_cache.clear();
-        }
-
-        // Walk-derived widget text (RFC Phase 6ap): each non-menubar widget's subtree text
-        // as prims — (text, x, y, size, color, font, clip bounds), the walk's per-widget
-        // content font and container clips composed in — replacing the legacy
-        // get_text_items / text_labels_with_font_and_bounds getters. Shaping stays
-        // app-side in text_buffer_cache (engine-matched metrics — see buffer_line_height).
-        let mut widget_text: Vec<Vec<(String, f32, f32, f32, [u8; 3], Option<String>, Option<[f32; 4]>)>> =
-            Vec::with_capacity(WIDGET_COUNT);
-        for i in 0..WIDGET_COUNT {
-            let w = slots.get_dyn(i);
-            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
-            if !w.visible() || is_menubar {
-                widget_text.push(Vec::new());
-                continue;
-            }
-            let mut scratch = cce_ui::scene::paint::PaintCtx::new();
-            cce_ui::scene::painter::append_widget_text(ui_context, w, &mut scratch);
-            widget_text.push(
-                scratch
-                    .finish()
-                    .items
-                    .into_iter()
-                    .filter_map(|item| match item.prim {
-                        cce_ui::scene::paint::Prim::Text { text, x, y, font_size, color, font, bounds, .. } =>
-                            Some((text, x, y, font_size, color, font, bounds)),
-                        _ => None,
-                    })
-                    .collect(),
-            );
-        }
-
-        // Pass 1: Populate text_buffer_cache with shaped buffers
-        for (i, texts) in widget_text.iter().enumerate() {
-            for (text, x, y, font_size, _color, font, _bounds) in texts {
-                let mut is_curved = false;
-                if circular_network_pane && i == LEFT_MENUBAR_IDX && text.chars().count() == 1 {
-                    let dx = x - network_circle_x;
-                    let dy = y - network_circle_y;
-                    let dist = (dx * dx + dy * dy).sqrt();
-                    if dist >= network_circle_radius - 35.0 && dist <= network_circle_radius + 5.0 {
-                        is_curved = true;
-                    }
-                }
-
-                if is_curved {
-                    let key = (text.clone(), (12.0 * 100.0) as u32, None);
-                    if !text_buffer_cache.contains_key(&key) {
-                        let buf = make_text_buffer(font_system, text, 12.0);
-                        text_buffer_cache.insert(key, buf);
-                    }
-                } else {
-                    let key = (text.clone(), (font_size * 100.0) as u32, font.clone());
-                    if !text_buffer_cache.contains_key(&key) {
-                        let buf = make_text_buffer_with_font(font_system, text, *font_size, font.as_deref());
-                        text_buffer_cache.insert(key, buf);
-                    }
-                }
-            }
-        }
-
-        // Pass 1b: Populate text_buffer_cache with popover texts
-        for i in 0..WIDGET_COUNT {
-            let w = slots.get_dyn(i);
-            if !w.visible() {
-                continue;
-            }
-            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
-            if is_menubar {
-                continue;
-            }
-            if focused_widget == Some(i) || i == PARAM_IDX {
-                let mut popover_pc = cce_ui::layout::PopoverCollector::new();
-                w.render_popover(&mut popover_pc);
-                for (t, size, _x, _y, _tc, font_opt, _bounds) in popover_pc.texts {
-                    let key = (t.clone(), (size * 100.0) as u32, font_opt.clone());
-                    if !text_buffer_cache.contains_key(&key) {
-                        let buf = make_text_buffer_with_font(font_system, &t, size, font_opt.as_deref());
-                        text_buffer_cache.insert(key, buf);
-                    }
-                }
-            }
-        }
-
-        let s = scale as f32;
-
-        // Build the frame's spans against the shaped cache. From here the cache
-        // is only read (`get`), so the borrows stay immutable for the spans.
-        let text_buffer_cache = &*text_buffer_cache;
-        let mut spans: Vec<TextSpan> = Vec::new();
-
-        for i in 0..WIDGET_COUNT {
-            let w = slots.get_dyn(i);
-            if !w.visible() {
-                continue;
-            }
-            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
-            if is_menubar {
-                continue;
-            }
-            let is_node = i == CONTENT_IDX;
-            let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX || i == NETWORK_PANEL_IDX;
-
-            // Widget-level clip bounds (physical px), matching the old TextBounds.
-            let bounds = if is_node {
-                if circular_network_pane {
-                    [
-                        ((network_circle_x - network_circle_radius) * s) as i32,
-                        ((network_circle_y - network_circle_radius) * s) as i32,
-                        ((network_circle_x + network_circle_radius) * s) as i32,
-                        (((network_circle_y + network_circle_radius) * s) as i32).max(0),
-                    ]
-                } else {
-                    let (gx, gy, gw, gh) = positions[CONTENT_IDX];
-                    [
-                        (gx * s) as i32,
-                        (gy * s) as i32,
-                        ((gx + gw) * s) as i32,
-                        (((gy + gh) * s) as i32).max(0),
-                    ]
-                }
-            } else {
-                [0, 0, physical_width as i32, physical_height as i32]
-            };
-
-            for (text, x, y, font_size, color, font, label_bounds) in &widget_text[i] {
-                let mut is_curved = false;
-                if circular_network_pane && i == LEFT_MENUBAR_IDX && text.chars().count() == 1 {
-                    let dx = x - network_circle_x;
-                    let dy = y - network_circle_y;
-                    let dist = (dx * dx + dy * dy).sqrt();
-                    if dist >= network_circle_radius - 35.0 && dist <= network_circle_radius + 5.0 {
-                        is_curved = true;
-                    }
-                }
-
-                if is_curved {
-                    // Curved rim label: rotate the glyph quad about its center to
-                    // face outward, clipped to the circular pane — the ash port of
-                    // the old render-to-texture + TexturedVertex path.
-                    let font_size = 12.0;
-                    let tw = TextLabel::estimate_width(text, font_size);
-                    let th = font_size;
-                    let cx = x + tw / 2.0;
-                    let cy = y + th / 2.0;
-                    let theta = (cy - network_circle_y).atan2(cx - network_circle_x);
-                    let angle = theta + std::f32::consts::FRAC_PI_2;
-                    let key = (text.clone(), (font_size * 100.0) as u32, None);
-                    if let Some(buf) = text_buffer_cache.get(&key) {
-                        spans.push(TextSpan {
-                            buffer: buf,
-                            left: (x * s).round(),
-                            top: (y * s).round(),
-                            scale: s,
-                            bounds: None,
-                            default_color: [
-                                color[0] as f32 / 255.0,
-                                color[1] as f32 / 255.0,
-                                color[2] as f32 / 255.0,
-                                1.0,
-                            ],
-                            rotation: Some((angle, cx * s, cy * s)),
-                            clip_circle: [
-                                network_circle_x * s,
-                                network_circle_y * s,
-                                network_circle_radius * s,
-                            ],
-                        });
-                    }
-                } else {
-                    if circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
-                        let dx = x - network_circle_x;
-                        let dy = y - network_circle_y;
-                        let dist_sq = dx * dx + dy * dy;
-                        if dist_sq > network_circle_radius * network_circle_radius {
-                            continue;
-                        }
-                    }
-                    let mut item_bounds = bounds;
-                    if let Some([l, t, r, b]) = label_bounds {
-                        let pl = (l * s).round() as i32;
-                        let pt = (t * s).round() as i32;
-                        let pr = (r * s).round() as i32;
-                        let pb = (b * s).round() as i32;
-                        item_bounds = [
-                            item_bounds[0].max(pl),
-                            item_bounds[1].max(pt),
-                            item_bounds[2].min(pr),
-                            item_bounds[3].min(pb),
-                        ];
-                    }
-                    let key = (text.clone(), (font_size * 100.0) as u32, font.clone());
-                    let Some(buf) = text_buffer_cache.get(&key) else { continue };
-                    let alpha = if is_network_part { network_opacity.clamp(0.0, 1.0) } else { 1.0 };
-                    spans.push(TextSpan {
-                        buffer: buf,
-                        left: (x * s).round(),
-                        top: (y * s).round(),
-                        scale: s,
-                        bounds: Some(item_bounds),
-                        default_color: [
-                            color[0] as f32 / 255.0,
-                            color[1] as f32 / 255.0,
-                            color[2] as f32 / 255.0,
-                            alpha,
-                        ],
-                        rotation: None,
-                        clip_circle: [0.0; 3],
-                    });
-                }
-            }
-        }
-
-        // Popover text spans
-        for i in 0..WIDGET_COUNT {
-            let w = slots.get_dyn(i);
-            if !w.visible() {
-                continue;
-            }
-            let is_menubar = i == HEADER_IDX || i == LEFT_MENUBAR_IDX || i == RIGHT_MENUBAR_IDX || i == PARAM_MENUBAR_IDX || i == SPREADSHEET_MENUBAR_IDX;
-            if is_menubar {
-                continue;
-            }
-            if focused_widget == Some(i) || i == PARAM_IDX {
-                let mut popover_pc = cce_ui::layout::PopoverCollector::new();
-                w.render_popover(&mut popover_pc);
-                for (t, size, x, y, tc, font_opt, label_bounds) in popover_pc.texts {
-                    let key = (t.clone(), (size * 100.0) as u32, font_opt.clone());
-                    if let Some(buf) = text_buffer_cache.get(&key) {
-                        let mut item_bounds = [0, 0, physical_width as i32, physical_height as i32];
-                        if let Some([l, t_bound, r, b]) = label_bounds {
-                            let pl = (l * s).round() as i32;
-                            let pt = (t_bound * s).round() as i32;
-                            let pr = (r * s).round() as i32;
-                            let pb = (b * s).round() as i32;
-                            item_bounds = [
-                                item_bounds[0].max(pl),
-                                item_bounds[1].max(pt),
-                                item_bounds[2].min(pr),
-                                item_bounds[3].min(pb),
-                            ];
-                        }
-                        spans.push(TextSpan {
-                            buffer: buf,
-                            left: (x * s).round(),
-                            top: (y * s).round(),
-                            scale: s,
-                            bounds: Some(item_bounds),
-                            default_color: [tc[0], tc[1], tc[2], 1.0],
-                            rotation: None,
-                            clip_circle: [0.0; 3],
-                        });
-                    }
-                }
-            }
-        }
-
-        renderer.prepare_text(font_system, swash_cache, &spans);
-    }
-
 }
diff --git a/src/window.rs b/src/window.rs
index e7cef5e..fd422f2 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -53,7 +53,6 @@ impl State {
                             state.grid_cursor_col = pos_x as i32;
                             state.grid_cursor_row = pos_y as i32;
                             state.sync_cursor_and_selection();
-                            state.upload_vertices();
                         }
                     }
                     changed = true;
@@ -454,7 +453,6 @@ impl State {
                             }
                             state.sync_nodes();
                             state.rebuild_scene_geometry();
-                            state.upload_vertices();
                             changed = true;
                         }
                     }
@@ -473,7 +471,6 @@ impl State {
                                 }
                                 state.sync_nodes();
                                 state.rebuild_scene_geometry();
-                                state.upload_vertices();
                                 changed = true;
                             }
                         }
@@ -553,7 +550,6 @@ impl State {
                 };
                 state.param_mut().set_display_params(&params);
 
-                state.upload_vertices();
             }
 
             state.update_status_text(&format!(
@@ -626,7 +622,6 @@ impl State {
                                     state.sync_grid_settings();
                                     state.sync_nodes();
                                     state.rebuild_scene_geometry();
-                                    state.upload_vertices();
                                     needs_redraw = true;
                                     Ok("Parameter updated".to_string())
                                 } else {
@@ -714,7 +709,6 @@ impl State {
                                     state.apply_layout();
                                     state.update_panel_bounds();
                                     state.rebuild_scene_geometry();
-                                    state.upload_vertices();
                                     needs_redraw = true;
                                     Ok("Node added".to_string())
                                 }
@@ -750,7 +744,6 @@ impl State {
                                 state.rebuild_positions();
                                 state.apply_layout();
                                 state.update_panel_bounds();
-                                state.upload_vertices();
                                 needs_redraw = true;
                                 Ok("Node moved".to_string())
                             } else {
@@ -801,7 +794,6 @@ impl State {
                             state.rebuild_positions();
                             state.apply_layout();
                             state.sync_grid_settings();
-                            state.upload_vertices();
                             needs_redraw = true;
                             Ok(format!("Circular pane: {}", state.circular_network_pane))
                         }
@@ -844,7 +836,6 @@ impl State {
                             if state.execute_menu_action(&label) {
                                 // The arms relayout themselves but render() draws the last
                                 // uploaded buffer (same ritual as ToggleCircularPane).
-                                state.upload_vertices();
                                 needs_redraw = true;
                                 Ok(format!("Menu action executed: {}", label.replace(['"', '\\'], "'")))
                             } else {