text editor
git clone https://git.lucas.co/cce-text-editor.git
feat: whole frame through display_list — walk text rendered, TextItem shaping deleted (Phase 6h)
Flip Application::display_list_text: widget text (menu dropdown, editor
content) now comes from the paint walk's text_with prims — the editor's
monospace family via the new Paint::text_font hook — and the toolbar/
status chrome text is emitted directly as prims in the system monospace
family. view()'s load-bearing side effects (widget registration, initial
focus, size-change relayout, popover registration) move into
display_list; rebuild_text_items, the TextItem cache, and the hand-rolled
Buffer shaping are deleted. The app FontSystem stays for
editor.prepare_text (glyph advances — cursor↔pixel mapping).
First app rendering scene-walk text prims. Verified live: frame matches
baseline (uniform ~2px baseline shift from engine line-height shaping),
File-menu popup renders with underlying editor text occluded, click
cursor placement identical to baseline (Line 9 Col 1 on the same click).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe
src/main.rs | 178 +++++++++++++++---------------------------------------------
1 file changed, 43 insertions(+), 135 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 402b4f8..4339ec0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,9 @@
use wayland_client::QueueHandle;
-use glyphon::{FontSystem, Buffer, Metrics, Attrs};
+use glyphon::FontSystem;
use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
use cce_ui::widget::{
- MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element,
- TextBox, TextLabel, Key, Dropdown
+ MouseButton, ElementState, MouseScrollDelta, KeyEvent, Element,
+ TextBox, Key, Dropdown
};
#[derive(Debug, Clone)]
@@ -29,7 +29,8 @@ struct TextEditorApp {
width: u32,
height: u32,
scale_factor: f64,
- text_items: Vec<TextItem>,
+ // Shapes the editor's glyph advances (prepare_text) — load-bearing for cursor↔pixel
+ // mapping; all rendered text is display-list prims shaped by the engine.
font_system: FontSystem,
needs_rebuild: bool,
ui_context: cce_ui::context::UiContext,
@@ -106,16 +107,13 @@ impl TextEditorApp {
}
}
- fn rebuild_text_items(&mut self) {
- self.editor.prepare_text(&mut self.font_system);
- self.text_items.clear();
- let scale = cce_ui::scale::scale_factor();
- let mut labels = Vec::new();
+ /// The toolbar/status-bar chrome text — everything not owned by a widget (widget text
+ /// comes from the paint walk). Emitted as display-list prims in the system monospace
+ /// family, matching the app's legacy hand-shaped look.
+ fn push_chrome_text(&self, pc: &mut cce_ui::scene::paint::PaintCtx) {
+ let mono = || Some(cce_ui::layout::get_system_monospace_font().to_string());
- // 1. Menu dropdown labels
- labels.extend(self.menu_dropdown.text_labels());
-
- // 2. File path info in the toolbar
+ // 1. File path info in the toolbar
let is_dirty = if self.editor.editing {
self.editor.text != self.editor.edit_buffer
} else {
@@ -130,39 +128,9 @@ impl TextEditorApp {
} else {
file_name_str
};
- labels.push(TextLabel {
- text: format!("File: {}", display_name),
- x: 420.0,
- y: 15.0,
- font_size: 12.0,
- color: [0xdd, 0xdd, 0xe2],
- });
-
- // 3. Editor text labels
- let font_family = self.editor.font_family.clone();
- for (label, bounds) in self.editor.text_labels_with_bounds(&self.ui_context) {
- let physical_size = label.font_size * scale;
- let metrics = Metrics::new(physical_size, physical_size * 1.4);
- let mut buf = Buffer::new(&mut self.font_system, metrics);
- let family_val = match font_family.as_str() {
- "monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
- "sans-serif" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
- "serif" => glyphon::Family::Serif,
- _ => glyphon::Family::Name(&font_family),
- };
- let attrs = Attrs::new().family(family_val);
- buf.set_text(&mut self.font_system, &label.text, attrs, glyphon::Shaping::Advanced);
- buf.shape_until_scroll(&mut self.font_system, true);
- self.text_items.push(TextItem {
- buffer: buf,
- x: label.x,
- y: label.y,
- color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
- bounds,
- });
- }
+ pc.text_with(format!("File: {}", display_name), 420.0, 15.0, 12.0, [0xdd, 0xdd, 0xe2], mono(), None);
- // 4. Status Bar indicators
+ // 2. Status Bar indicators
let text_src = if self.editor.editing { &self.editor.edit_buffer } else { &self.editor.text };
let mut logical_line = 1;
let mut logical_col = 1;
@@ -177,42 +145,27 @@ impl TextEditorApp {
logical_col += 1;
}
}
-
- labels.push(TextLabel {
- text: format!("Line: {}, Col: {} | Length: {} chars", logical_line, logical_col, text_src.chars().count()),
- x: 15.0,
- y: self.height as f32 - 20.0,
- font_size: 11.0,
- color: [0x83, 0x83, 0x8a],
- });
+ pc.text_with(
+ format!("Line: {}, Col: {} | Length: {} chars", logical_line, logical_col, text_src.chars().count()),
+ 15.0,
+ self.height as f32 - 20.0,
+ 11.0,
+ [0x83, 0x83, 0x8a],
+ mono(),
+ None,
+ );
if let Some((msg, is_error)) = &self.status_message {
let color = if *is_error { [0xfa, 0x52, 0x52] } else { [0x40, 0xc0, 0x57] };
- labels.push(TextLabel {
- text: msg.clone(),
- x: (self.width as f32 - 400.0).max(300.0),
- y: self.height as f32 - 20.0,
- font_size: 11.0,
+ pc.text_with(
+ msg.clone(),
+ (self.width as f32 - 400.0).max(300.0),
+ self.height as f32 - 20.0,
+ 11.0,
color,
- });
- }
-
- // 5. Build static text items
- for label in labels {
- let physical_size = label.font_size * scale;
- let metrics = Metrics::new(physical_size, physical_size * 1.4);
- let mut buf = Buffer::new(&mut self.font_system, metrics);
- let family_val = glyphon::Family::Name(cce_ui::layout::get_system_monospace_font());
- let attrs = Attrs::new().family(family_val);
- buf.set_text(&mut self.font_system, &label.text, attrs, glyphon::Shaping::Advanced);
- buf.shape_until_scroll(&mut self.font_system, true);
- self.text_items.push(TextItem {
- buffer: buf,
- x: label.x,
- y: label.y,
- color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
- bounds: None,
- });
+ mono(),
+ None,
+ );
}
}
}
@@ -269,7 +222,6 @@ impl Application for TextEditorApp {
width: 800,
height: 600,
scale_factor: 1.0,
- text_items: Vec::new(),
font_system: cce_ui::create_font_system_with_system_fonts(),
needs_rebuild: true,
ui_context: cce_ui::context::UiContext::new(),
@@ -363,7 +315,10 @@ impl Application for TextEditorApp {
fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
- fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
+ fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
+ // Phase 6 single paint path: the whole frame — chrome geometry, chrome text, and the
+ // two top-level widgets (menu_dropdown, editor) walked into the list — is built here.
+ // Widget text comes from the paint walk (Adapted::paint_self serves per-widget fonts).
if !self.widgets_registered {
self.widgets_registered = true;
let self_ptr = self as *mut Self;
@@ -393,70 +348,21 @@ impl Application for TextEditorApp {
let editor_h = (self.height as f32 - 92.0).max(100.0);
self.editor.set_rect(10.0, 52.0, editor_w, editor_h);
- self.rebuild_text_items();
+ // Glyph-advance shaping — load-bearing for cursor↔pixel mapping.
+ self.editor.prepare_text(&mut self.font_system);
self.needs_rebuild = false;
self.ui_context.rebuild_spatial_grid();
}
- let radius = cce_ui::colors::backplate_corner_radius();
- if radius <= 0.1 {
- // 1. Editor Window Background (slate-dark design)
- quads.push((0.0, 0.0, self.width as f32, self.height as f32, [0.05, 0.05, 0.07, 1.0]));
-
- // 2. Toolbar Header quads
- quads.push((0.0, 0.0, self.width as f32, 42.0, [0.08, 0.08, 0.12, 1.0]));
-
- // 3. Status Bar quads
- let status_y = self.height as f32 - 30.0;
- quads.push((0.0, status_y, self.width as f32, 30.0, [0.08, 0.08, 0.10, 1.0]));
- }
-
- // Horizontal split lines (borders)
- quads.push((0.0, 42.0, self.width as f32, 1.0, [0.18, 0.18, 0.22, 1.0]));
- let status_y = self.height as f32 - 30.0;
- quads.push((0.0, status_y, self.width as f32, 1.0, [0.18, 0.18, 0.22, 1.0]));
-
- // 4. Menu dropdown graphics
- quads.extend(self.menu_dropdown.all_quads(&self.ui_context));
-
- // 5. TextBox Editor graphics
- quads.extend(self.editor.all_quads(&self.ui_context));
-
- // 6. Popovers registration (since this app bypasses the layout engine)
+ // Popovers registration (since this app bypasses the layout engine)
self.ui_context.clear_popovers();
cce_ui::widget::popovers::clear();
if self.menu_dropdown.popover_rect().is_some() {
self.ui_context.register_popover(&self.menu_dropdown);
cce_ui::widget::popovers::register(&self.menu_dropdown);
}
- }
-
- fn view_rounded_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))>, _size: LogicalSize, _scale: f64) {
- let radius = cce_ui::colors::backplate_corner_radius();
- if radius > 0.1 {
- // 1. Editor Window Background (rounded)
- quads.push((0.0, 0.0, self.width as f32, self.height as f32, radius, [0.05, 0.05, 0.07, 1.0], (true, true, true, true)));
- // 2. Toolbar Header (rounded at top)
- quads.push((0.0, 0.0, self.width as f32, 42.0, radius, [0.08, 0.08, 0.12, 1.0], (true, true, false, false)));
-
- // 3. Status Bar (rounded at bottom)
- let status_y = self.height as f32 - 30.0;
- quads.push((0.0, status_y, self.width as f32, 30.0, radius, [0.08, 0.08, 0.10, 1.0], (false, false, true, true)));
- }
-
- quads.extend(self.menu_dropdown.all_rounded_quads(&self.ui_context));
- quads.extend(self.editor.all_rounded_quads(&self.ui_context));
- }
-
- fn display_list(&mut self, _size: cce_ui::engine::LogicalSize, _scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
- // Phase 3 single paint path. This app composes its own chrome plus two top-level widgets
- // (menu_dropdown, editor) rather than a root_window tree, so build the display list here:
- // chrome quads first, then walk each widget into it. CCE_LEGACY_PAINT falls back.
- if std::env::var("CCE_LEGACY_PAINT").is_ok() {
- return None;
- }
use cce_ui::scene::layout::Rect;
let mut pc = cce_ui::scene::paint::PaintCtx::new();
let w = self.width as f32;
@@ -475,6 +381,8 @@ impl Application for TextEditorApp {
pc.quad(Rect { x: 0.0, y: 42.0, width: w, height: 1.0 }, [0.18, 0.18, 0.22, 1.0]);
pc.quad(Rect { x: 0.0, y: status_y, width: w, height: 1.0 }, [0.18, 0.18, 0.22, 1.0]);
+ self.push_chrome_text(&mut pc);
+
let menu: *mut (dyn cce_ui::widget::Element + 'static) = self.menu_dropdown.as_ptr_mut();
let editor: *mut (dyn cce_ui::widget::Element + 'static) = self.editor.as_ptr_mut();
cce_ui::scene::painter::paint_root_into(&self.ui_context, menu, &mut pc);
@@ -482,12 +390,12 @@ impl Application for TextEditorApp {
Some(pc.finish())
}
- fn render_popovers(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
- cce_ui::layout::render_popovers(pc, &self.ui_context);
+ fn display_list_text(&self) -> bool {
+ true
}
- fn text_items(&self) -> &[TextItem] {
- &self.text_items
+ fn render_popovers(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
+ cce_ui::layout::render_popovers(pc, &self.ui_context);
}
fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {