git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitc9b073aed16f3cebd3544d97a1fd5811b16a6f5a
parent78b194c4a5
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 06:09
feat(layout): a flat host gets a widget's strokes, arcs and discs

render_widget replayed a widget onto a host's RenderTarget and dropped
every Prim::Vector, Prim::Arc and Prim::Circle, so a graph in a flat host
(cce-files' Graph page) drew its nodes and never a wire or a port.

RenderTarget grows line, arc and circle, no-ops by default, and the
replay hands them each stroke, arc and disc under the item's clip.
PaintCtx implements them.

Test: a_graphs_wires_reach_a_flat_host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md          |  8 ++++++
 src/layout.rs      | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/scene/paint.rs |  9 ++++++
 3 files changed, 100 insertions(+)

diff --git a/CLAUDE.md b/CLAUDE.md
index 64f8f7e..6ce306d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1822,6 +1822,14 @@ falls back to, and never built from primitives. Apps hold to the same rule.
   markdown's unrenderable embed (`Draw::Icon`, `link`), the tree list's
   missing-icon fallback (now an empty slot) and the copy button's fallback
   ("📋" → "Copy").
+- **A flat host gets glyphs, strokes, arcs and discs.** `render_widget`
+  replays a widget onto a host's `RenderTarget` and dropped every image,
+  vector, arc and circle: after the symbols became glyphs a flat host lost
+  them all, and a graph there (cce-files' Graph page) never showed a wire. A
+  bundled glyph now goes to `RenderTarget::icon`, a stroke to `line`, an arc
+  to `arc`, a disc to `circle` — each a no-op by default, so a host draws what
+  it implements (`a_widget_glyph_reaches_a_flat_host`,
+  `a_graphs_wires_reach_a_flat_host`).
 - **Kept as shapes**, being indicators and not symbols: `StatusDot`'s LED
   disc and the plate dock's corner dot.
 
diff --git a/src/layout.rs b/src/layout.rs
index 9ca2045..abf0096 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4115,6 +4115,14 @@ pub trait RenderTarget {
     /// A bundled cce-icons glyph — see `PaintCtx::icon`. A target that
     /// cannot draw images (the legacy `PopoverCollector`) draws nothing.
     fn icon(&mut self, _name: &str, _rect: crate::scene::layout::Rect, _color: [f32; 4]) {}
+    /// A straight stroke — see `PaintCtx::vector`. What a graph's wires are
+    /// made of; a target that draws none (the default) shows no wires.
+    fn line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _thickness: f32, _color: [f32; 4], _cap: crate::scene::paint::Cap) {}
+    /// A stroked arc, `radius` its OUTER edge — see `PaintCtx::arc`. A
+    /// rounded wire's bends.
+    fn arc(&mut self, _cx: f32, _cy: f32, _radius: f32, _thickness: f32, _start: f32, _end: f32, _color: [f32; 4]) {}
+    /// A filled disc — see `PaintCtx::circle`. A graph's ports.
+    fn circle(&mut self, _cx: f32, _cy: f32, _radius: f32, _color: [f32; 4]) {}
     /// A flush inset control plate ([`PaintCtx::inset_plate`]) — the raised
     /// control surface (groove ring down, beveled lip back up). Lets a popover
     /// draw the ACTUAL widget surface expanded (the Dropdown's grown trigger).
@@ -4452,6 +4460,36 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
                     None => pc.text_with_bounds(&text, x, y, font_size, color_f32, merged),
                 }
             }
+            // Strokes, arcs and discs — a graph's wires and ports, chiefly.
+            // Until 2026-10-06 these were dropped too, so a graph in a flat
+            // host (cce-files' Graph page) drew its nodes and no wires.
+            Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
+                if let Some(c) = item.clip {
+                    pc.push_clip_rect(c.x, c.y, c.width, c.height);
+                }
+                pc.line(x1, y1, x2, y2, thickness, color, cap);
+                if item.clip.is_some() {
+                    pc.pop_clip_rect();
+                }
+            }
+            Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
+                if let Some(c) = item.clip {
+                    pc.push_clip_rect(c.x, c.y, c.width, c.height);
+                }
+                pc.arc(cx, cy, radius, thickness, start, end, color);
+                if item.clip.is_some() {
+                    pc.pop_clip_rect();
+                }
+            }
+            Prim::Circle { cx, cy, radius, color } => {
+                if let Some(c) = item.clip {
+                    pc.push_clip_rect(c.x, c.y, c.width, c.height);
+                }
+                pc.circle(cx, cy, radius, color);
+                if item.clip.is_some() {
+                    pc.pop_clip_rect();
+                }
+            }
             // A widget's GLYPH (a dropdown's arrow, a spinbox's −/+, a menu
             // mark): drawn by name through the host's `RenderTarget::icon`,
             // since an image id means nothing to a flat host. Until this arm
@@ -6628,6 +6666,51 @@ mod tests {
         }
     }
 
+    /// A graph's wires and ports reach a flat host: `render_widget` hands
+    /// its strokes to `RenderTarget::line` (and arcs and discs to `arc` and
+    /// `circle`), where it used to drop them, so cce-files' Graph page drew
+    /// nodes and no wires.
+    #[test]
+    fn a_graphs_wires_reach_a_flat_host() {
+        #[derive(Default)]
+        struct Strokes {
+            lines: usize,
+            circles: usize,
+        }
+        impl RenderTarget for Strokes {
+            fn rect(&mut self, _: [f32; 4], _: f32, _: f32, _: f32, _: f32) {}
+            fn text(&mut self, _: &str, _: f32, _: f32, _: f32, _: [f32; 4]) {}
+            fn line(&mut self, _: f32, _: f32, _: f32, _: f32, _: f32, _: [f32; 4], _: crate::scene::paint::Cap) {
+                self.lines += 1;
+            }
+            fn arc(&mut self, _: f32, _: f32, _: f32, _: f32, _: f32, _: f32, _: [f32; 4]) {
+                self.lines += 1;
+            }
+            fn circle(&mut self, _: f32, _: f32, _: f32, _: [f32; 4]) {
+                self.circles += 1;
+            }
+        }
+        use crate::widget::GraphController;
+        let node = |name: &str, pos: (f32, f32), input: Option<&str>| crate::widget::GraphNode {
+            id: name.to_string(),
+            name: name.to_string(),
+            position: pos,
+            parameters: input.map(|i| vec![("input".to_string(), i.to_string(), "string".to_string())]).unwrap_or_default(),
+            geom_visible: true,
+            node_type: String::new(),
+            inputs: 1,
+            outputs: 1,
+        };
+        let mut graph = crate::widget::Graph::new();
+        graph.set_grid_origin(100.0, 100.0);
+        graph.set_nodes(&[node("a", (0.0, 0.0), None), node("b", (0.0, 1.0), Some("a"))]);
+        let mut pc = Strokes::default();
+        let mut ctx = crate::context::UiContext::new();
+        render_widget(&mut pc, &mut graph, 0.0, 0.0, 600.0, 600.0, &mut ctx);
+        assert!(pc.lines > 0, "the wire a -> b reached the host");
+        assert!(pc.circles > 0, "the ports reached the host");
+    }
+
     /// A widget's glyph reaches a flat host: `render_widget` hands the
     /// dropdown's arrow to the host's `RenderTarget::icon` by name. It used
     /// to drop every image, so after the toolkit's symbols became glyphs a
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 3615abd..c034ece 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -2018,6 +2018,15 @@ impl crate::layout::RenderTarget for PaintCtx {
     fn icon(&mut self, name: &str, rect: Rect, color: [f32; 4]) {
         PaintCtx::icon(self, name, rect, color);
     }
+    fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap) {
+        PaintCtx::vector(self, x1, y1, x2, y2, thickness, color, cap);
+    }
+    fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
+        PaintCtx::arc(self, cx, cy, radius, thickness, start, end, color);
+    }
+    fn circle(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
+        PaintCtx::circle(self, cx, cy, radius, color);
+    }
 
     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
         self.quad(Rect { x, y, width: w, height: h }, color);