git.lucas.co / cce-text-editor
text editor
git clone https://git.lucas.co/cce-text-editor.git

commit8d2480d7a1b24d003807d29d7e59648a794f5733
parent1488864c5c
authorLucas Galante <[email protected]>
date2026-07-09 12:12
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) {