GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Graph: flat grid lines over the plate, in place of the relief lattice
The lattice read too heavy at the DE relief depth. paint_grid draws the
gaps as flat quads in the gap colour at the network opacity — right gap
at the cell's height, bottom gap at the full step, so no strip overlaps
another and a crossing carries one alpha — plus the 2px origin axes. The
cells stay the plate. Gated on visibility only, as before; the
GraphController hook is now required (ContentBg forwards its own grid),
so a host cannot silently inherit a no-op.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/widget/container/content_bg.rs | 6 +++
src/widget/display/graph.rs | 102 ++++++++++++++++++++-----------------
src/widget/mod.rs | 6 +--
tests/lattice_probe.rs | 11 ++--
4 files changed, 69 insertions(+), 56 deletions(-)
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 726fa20..65d0ece 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -266,6 +266,12 @@ impl GraphController for ContentBg {
fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
fn grid_origin(&self) -> (f32, f32) { (self.grid_origin_x, self.grid_origin_y) }
fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+ /// The same gradient grid its own `paint` draws, for a host walking the quads itself.
+ fn paint_grid(&self, rect: Rect, pc: &mut PaintCtx) {
+ for (qx, qy, qw, qh, qc) in self.grid_quads(rect) {
+ pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
+ }
fn take_pending_connection(&mut self) -> Option<(String, String)> { None }
fn cancel_connecting(&mut self) {}
fn is_node_rect(&self, _qx: f32, _qy: f32, _qw: f32, _qh: f32) -> bool { false }
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 3202163..7ef79bd 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -385,56 +385,63 @@ impl Graph {
crate::layout::graph_node_corner_radius().min(cell / 2.0)
}
- /// The grid as relief in whatever the graph is painted over — the pane
- /// plate: every cell a well carved as ONE lattice (the desktop grid's
- /// primitive), the rails between them the plate's face, and the origin
- /// axes two grooves engraved down the rail centrelines beside row and
- /// column 0. Pure shading: no colour is mixed in, so the pane stays the
- /// same material as every other plate however wide the rails are. (The
- /// grid was tinted quads until 2026-09-20 — gap-coloured rails at the
- /// network opacity — which at the designer's rail widths covered 40% of
- /// the pane in a second colour; no gap colour could match the plate.)
- ///
- /// The wall runs from each cell edge outward over half the narrower gap,
- /// so a rail carries one wall from each side and keeps a flat centre
- /// (a single carve per cell would stack at the crossings; the lattice
- /// mitres them). Relief cannot fade, so the network opacity does not
- /// apply here — it fades wires and text, which are colour.
- pub fn paint_grid_relief(&self, rect: Rect, pc: &mut PaintCtx) {
+ /// The grid lines, flat, over whatever the graph is painted on — the
+ /// pane plate: the cells are the plate showing through (no fill of their
+ /// own), and only the gaps are drawn, in the gap colour at the network
+ /// opacity, plus the origin axes as 2px lines down the rail centrelines
+ /// beside row and column 0. Painted per cell — right gap at the cell's
+ /// height, bottom gap at the full step width — so no two strips overlap
+ /// and a crossing carries one alpha, not two. Gated on the grid's
+ /// visibility only: `uniform_background` describes the widget's own
+ /// background fill and the designer hard-codes it true, which is how
+ /// its grid went undrawn until 2026-09-20. (A relief lattice was tried
+ /// in between and read too heavy at the DE's relief depth.)
+ pub fn paint_grid(&self, rect: Rect, pc: &mut PaintCtx) {
if self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
return;
}
+ let (min_x, min_y) = (rect.x, rect.y);
+ let (max_x, max_y) = (rect.x + rect.width, rect.y + rect.height);
+ let clipped = |qx: f32, qy: f32, qw: f32, qh: f32, c: [f32; 4], pc: &mut PaintCtx| {
+ let x1 = qx.max(min_x);
+ let y1 = qy.max(min_y);
+ let x2 = (qx + qw).min(max_x);
+ let y2 = (qy + qh).min(max_y);
+ if x2 > x1 && y2 > y1 {
+ pc.quad(Rect { x: x1, y: y1, width: x2 - x1, height: y2 - y1 }, c);
+ }
+ };
+
let step_x = self.grid_size_x + self.skipped_col_w;
let step_y = self.grid_size_y + self.skipped_row_h;
- // Half the narrower rail, the same clamp the desktop grid applies.
- let run = (self.skipped_col_w.min(self.skipped_row_h) * 0.5).max(1.0);
-
- // Gated on the grid's visibility ONLY — not on `uniform_background`,
- // which describes the widget's own background fill (cell colour over
- // the whole rect vs the near-clear one) and has nothing to say about
- // relief. The old cell/gap quads were gated on both, and the designer
- // hard-codes uniform_background = true, so its network grid was never
- // drawn at all — found 2026-09-20 by counting lattice prims in the
- // tessellated frame under CCE_PLATE_DEBUG.
if self.show_network_grid && step_x >= 4.0 && step_y >= 4.0 {
- pc.lattice(
- rect,
- (step_x, step_y),
- (self.grid_origin_x + self.grid_size_x * 0.5, self.grid_origin_y + self.grid_size_y * 0.5),
- (self.grid_size_x, self.grid_size_y),
- self.cell_corner_radius(),
- run,
- );
+ let gap_rgba = [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity];
+ let ry_start = (((rect.y - self.grid_origin_y) / step_y).floor() as i32 - 1).max(-100_000);
+ let ry_end = (((rect.y + rect.height - self.grid_origin_y) / step_y).ceil() as i32 + 1).min(100_000);
+ let cx_start = (((rect.x - self.grid_origin_x) / step_x).floor() as i32 - 1).max(-100_000);
+ let cx_end = (((rect.x + rect.width - self.grid_origin_x) / step_x).ceil() as i32 + 1).min(100_000);
+ // Rounded pixel boundaries, so adjacent strips meet without seams.
+ for r in ry_start..=ry_end {
+ let y_cell_start = (self.grid_origin_y + (r as f32) * step_y).round();
+ let y_cell_end = (self.grid_origin_y + (r as f32) * step_y + self.grid_size_y).round();
+ let y2 = (self.grid_origin_y + ((r + 1) as f32) * step_y).round();
+ for c in cx_start..=cx_end {
+ let x_cell_start = (self.grid_origin_x + (c as f32) * step_x).round();
+ let x_cell_end = (self.grid_origin_x + (c as f32) * step_x + self.grid_size_x).round();
+ let x2 = (self.grid_origin_x + ((c + 1) as f32) * step_x).round();
+ clipped(x_cell_end, y_cell_start, x2 - x_cell_end, y_cell_end - y_cell_start, gap_rgba, pc);
+ clipped(x_cell_start, y_cell_end, x2 - x_cell_start, y2 - y_cell_end, gap_rgba, pc);
+ }
+ }
}
- // Origin axes, down the rail centrelines beside row/column 0 — the
- // lines the 2px black quads used to draw, now engraved.
- let axis_w = 2.0;
- let axis_run = run.min(axis_w);
+ // Origin axes, in the gaps beside row/column 0.
+ let axis = [0.0, 0.0, 0.0, self.network_opacity];
+ let thickness = 2.0;
let y_center = self.grid_origin_y + self.grid_size_y + self.skipped_row_h / 2.0;
- pc.groove((rect.x, y_center), (rect.x + rect.width, y_center), axis_w, axis_run, rect);
+ clipped(rect.x, y_center - thickness / 2.0, rect.width, thickness, axis, pc);
let x_center = self.grid_origin_x - self.skipped_col_w / 2.0;
- pc.groove((x_center, rect.y), (x_center, rect.y + rect.height), axis_w, axis_run, rect);
+ clipped(x_center - thickness / 2.0, rect.y, thickness, rect.height, axis, pc);
}
/// The wires / connection preview / grid cells / axes / node bodies / toggles as plain
@@ -505,9 +512,8 @@ impl Graph {
}
}
- // The grid and the origin axes are RELIEF, not quads — see
- // `paint_grid_relief`. Hosts that draw these quads themselves call it
- // at the same point in their own walk.
+ // The grid lines and the origin axes are `paint_grid`'s — hosts that
+ // draw these quads themselves call it at the same point in their walk.
// Node bodies (culled, not clipped — legacy) + geometry toggles (clipped)
for i in 0..self.nodes.len() {
@@ -715,12 +721,12 @@ impl Paint for Graph {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- // The background is the first entry; the grid relief is carved into
- // it before the wires and nodes go on top.
+ // The background is the first entry; the grid lines go over it
+ // before the wires and nodes.
for (i, (qx, qy, qw, qh, r, c, corners)) in self.rounded_geometry(rect).into_iter().enumerate() {
ctx.rounded_rect(Rect { x: qx, y: qy, width: qw, height: qh }, r, corners, c);
if i == 0 {
- self.paint_grid_relief(rect, ctx);
+ self.paint_grid(rect, ctx);
}
}
for (cx, cy, r, c) in self.port_circles(rect) {
@@ -1185,8 +1191,8 @@ impl Graph {
}
impl GraphController for Graph {
- fn paint_grid_relief(&self, rect: Rect, pc: &mut PaintCtx) {
- Graph::paint_grid_relief(self, rect, pc)
+ fn paint_grid(&self, rect: Rect, pc: &mut PaintCtx) {
+ Graph::paint_grid(self, rect, pc)
}
fn set_nodes(&mut self, nodes: &[GraphNode]) {
// Hover carries a node INDEX, so remap it by id across the rebuild
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 14687ed..514a9c7 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -646,10 +646,10 @@ pub trait GraphController {
/// rounded corners — for hosts that draw the graph's quads themselves
/// (the designer) and want cells as superellipse tiles.
fn geometry_quads_tagged(&self, rect: crate::scene::layout::Rect) -> Vec<TaggedQuad>;
- /// The grid and origin axes as relief carved into what is beneath — for
+ /// The grid lines and origin axes, flat, over what is beneath — for
/// hosts that draw the graph's quads themselves, called at the point in
- /// their walk where the grid used to be. Default: no grid.
- fn paint_grid_relief(&self, _rect: crate::scene::layout::Rect, _pc: &mut crate::scene::paint::PaintCtx) {}
+ /// their walk where the grid goes (under the wires and nodes).
+ fn paint_grid(&self, rect: crate::scene::layout::Rect, pc: &mut crate::scene::paint::PaintCtx);
/// The pixel rect of the cell an in-flight node drag will deposit on
/// (`commit_drag`'s resolution), for hosts' drop-target highlight.
/// None outside a node drag.
diff --git a/tests/lattice_probe.rs b/tests/lattice_probe.rs
index 026b66a..a0321f7 100644
--- a/tests/lattice_probe.rs
+++ b/tests/lattice_probe.rs
@@ -20,7 +20,7 @@ fn lattice_survives_tessellation_inside_a_clip() {
}
#[test]
-fn graph_emits_grid_relief() {
+fn graph_emits_flat_grid_lines() {
use cce_ui::widget::GraphController;
let mut g = cce_ui::widget::Graph::new();
g.set_grid_sizes(71.0, 31.0);
@@ -31,9 +31,10 @@ fn graph_emits_grid_relief() {
g.set_uniform_background(true);
let mut pc = PaintCtx::new();
let rect = Rect { x: 40.0, y: 40.0, width: 600.0, height: 300.0 };
- g.paint_grid_relief(rect, &mut pc);
+ g.paint_grid(rect, &mut pc);
let dl = pc.finish();
- let n_lattice = dl.items.iter().filter(|i| matches!(i.prim, Prim::Lattice { .. })).count();
- let n_groove = dl.items.iter().filter(|i| matches!(i.prim, Prim::Groove { .. })).count();
- assert_eq!((n_lattice, n_groove), (1, 2));
+ let n_quad = dl.items.iter().filter(|i| matches!(i.prim, Prim::Quad { .. })).count();
+ // ~8 columns x ~10 rows of gap strips (two per cell) plus the two axes.
+ assert!(n_quad > 100, "only {n_quad} quads");
+ assert!(dl.items.iter().all(|i| matches!(i.prim, Prim::Quad { .. })), "flat lines only");
}