graphic design tool
git clone https://git.lucas.co/cce-designer.git
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(¶ms);
- 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 {