GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Prim::Grout: flat colour outside a field of rounded cells; the graph grid uses it
Flat strips gave the graph square cells: no quad can paint the notch a
rounded cell leaves at each crossing. Grout is the lattice's nearest-cell
fold painted flat (shader mode 15) — the vertex colour wherever the cell
SDF is outside, 1px anti-aliased — so the whole grid is one draw with the
cells' superellipse corners exact, at the node corner radius the DE's
cells and cursors already share. Graph::paint_grid emits one grout plus
the two axis lines; the probe test pins the emission and the mode-15
batch in its colour.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/backend/window_runner.rs | 26 ++++++++++++++++++++++----
src/scene/paint.rs | 19 +++++++++++++++++++
src/vk/shader2d.wgsl | 22 ++++++++++++++++++++++
src/widget/display/graph.rs | 43 +++++++++++++++++--------------------------
tests/lattice_probe.rs | 20 ++++++++++++++++----
5 files changed, 96 insertions(+), 34 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index c385faa..9ba27d2 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1712,7 +1712,7 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
P::Sphere { .. } => "Sphere", P::Droplet { .. } => "Droplet",
P::DropletScrim { .. } => "DropletScrim",
P::ConcaveFillet { .. } => "ConcaveFillet",
- P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice",
+ P::Groove { .. } => "Groove", P::Lattice { .. } => "Lattice", P::Grout { .. } => "Grout",
P::CarveUnion { .. } => "CarveUnion", P::Glow { .. } => "Glow",
P::Text { .. } => "Text", P::Image { .. } => "Image",
}
@@ -2636,9 +2636,27 @@ pub fn tessellate_display_list(
shape: crate::layout::corner_shape(),
});
}
- // Legacy banded path: no periodic wall — the lattice draws nothing
- // there, like the fillet (A/B comparison path only).
- Prim::Lattice { .. } => {}
+ Prim::Grout { rect, period, origin, cell, radius, color } if shader_plates => {
+ // The lattice's fold, painted flat (shader mode 15): one cover
+ // quad in the grout colour; the shader keeps it outside the
+ // cells. Same push layout as the lattice; light/material are
+ // carried but unread.
+ let (pw, ph) = (period.0.max(1e-3), period.1.max(1e-3));
+ verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+ plate = Some(crate::vk::PlatePush {
+ rect: [origin.0 * scale, origin.1 * scale, cell.0 * 0.5 * scale, cell.1 * 0.5 * scale],
+ radii: [*radius * scale; 4],
+ light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
+ material: plate_mat,
+ host: [pw * scale, ph * scale, 1e6, 1e6],
+ specular_tint: [1.0, 1.0, 1.0, 0.0],
+ mode: 15.0,
+ shape: crate::layout::corner_shape(),
+ });
+ }
+ // Legacy banded path: no periodic wall — the lattice and the grout
+ // draw nothing there, like the fillet (A/B comparison path only).
+ Prim::Lattice { .. } | Prim::Grout { .. } => {}
Prim::CarveUnion { boxes, depth, raised } if shader_plates => {
// The union of several boxes as ONE wall (shader mode 14): the
// boxes go into the frame's feature buffer as a contiguous run
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 8fb8b3e..de7f7ab 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -639,6 +639,14 @@ pub enum Prim {
/// surface holds (a free carve per cell also runs into the per-frame
/// feature budget long before a zoomed-out grid does). SDF path only.
Lattice { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, depth: f32 },
+ /// `color`, flat, everywhere inside `rect` that is OUTSIDE a periodic
+ /// field of rounded cells — the same field [`Prim::Lattice`] carves
+ /// (`period`, one cell centred at `origin`, each `cell` big with `radius`
+ /// corners), painted as grout rather than shaded. One draw for the whole
+ /// grid, with the cells' superellipse corners exact: what a graph's grid
+ /// lines are when the cells are the surface beneath showing through.
+ /// SDF path only — the legacy banded tessellation draws nothing.
+ Grout { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4] },
/// Several rounded boxes carved (`raised` false) or raised (`raised`
/// true) as ONE shape: the union of the boxes is the well, and its wall
/// follows the union's outline — straddling it by ±`depth`/2 like every
@@ -1050,6 +1058,9 @@ impl PaintCtx {
Prim::Lattice { rect, period, origin, cell, radius, depth } => {
self.lattice(rect, period, origin, cell, radius, depth)
}
+ Prim::Grout { rect, period, origin, cell, radius, color } => {
+ self.grout(rect, period, origin, cell, radius, color)
+ }
Prim::CarveUnion { boxes, depth, raised } => self.carve_union(boxes, depth, raised),
Prim::Image { image, rect, alpha } => self.image(image, rect, alpha),
}
@@ -1086,6 +1097,14 @@ impl PaintCtx {
self.push(Prim::Lattice { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, depth });
}
+ /// Grout between a periodic field of rounded cells — see [`Prim::Grout`].
+ /// `origin` is any one cell's centre; `rect` bounds the paint.
+ pub fn grout(&mut self, rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4]) {
+ let (ox, oy) = self.offset;
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Grout { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, color });
+ }
+
/// Carve (or raise, with `raised`) the union of `boxes` as one shape with
/// one wall — see [`Prim::CarveUnion`]. `depth` is the wall's run in px,
/// as for [`PaintCtx::recess`].
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 55c3d77..89ab916 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -156,6 +156,7 @@ const MODE_ROLL: i32 = 11; // fill-less rolled perimeter, composited as a
const MODE_DROPLET_SCRIM: i32 = 12; // flat feathered fill of the droplet silhouette
const MODE_LATTICE: i32 = 13; // periodic well field: nearest-cell carve, one evaluation
const MODE_UNION: i32 = 14; // union of feature boxes carved/raised as one wall
+const MODE_GROUT: i32 = 15; // flat colour outside a periodic field of rounded cells
// Fillet modes rejoin the shared free-carve path as their flat equivalents.
const FILLET_TO_STEP: i32 = 4; // 6 -> RECESS, 7 -> BOSS
@@ -385,6 +386,27 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
// the surface tilt meets the half-vector, ~a third of the way out toward
// the light) — and, like a plate face, the shade is expressed relative to
// the flat face so the color at the lit center is exactly the app's.
+ // MODE_GROUT: the vertex colour, flat, everywhere OUTSIDE a periodic
+ // field of identical rounded cells — the grid lines of a graph whose
+ // cells are whatever lies beneath showing through, corners included.
+ // The same fold as MODE_LATTICE (p_rect = one cell's centre and
+ // half-extents, p_host.xy = the period, p_radii = the corner radius),
+ // but no lighting: coverage is the cell SDF's outside, 1px anti-aliased,
+ // so the cells' superellipse corners are exact and the whole grid is one
+ // draw. Flat strips could never paint the notch a rounded cell leaves at
+ // each crossing.
+ if (mode == MODE_GROUT) {
+ let per = max(rrect_clip.p_host.xy, vec2f(1e-3));
+ var gc = frag - rrect_clip.p_rect.xy;
+ gc = gc - per * round(gc / per);
+ let lg = rr_sdf_grad(gc, vec4f(0.0, 0.0, rrect_clip.p_rect.zw), rrect_clip.p_radii);
+ let cov = clamp(lg.z + 0.5, 0.0, 1.0);
+ if (cov <= 0.0) {
+ discard;
+ }
+ return vec4f(vcol.rgb, vcol.a * cov);
+ }
+
if (mode == MODE_SPHERE) {
let c = frag - rrect_clip.p_rect.xy;
let r = max(rrect_clip.p_rect.z, 0.001);
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 7ef79bd..f0b6431 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -387,15 +387,15 @@ impl Graph {
/// 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.)
+ /// own), and everything outside them is grout in the gap colour at the
+ /// network opacity — ONE [`Prim::Grout`] draw, so the cells' rounded
+ /// corners (the node corner radius, the DE's shared corner language) are
+ /// exact, notch at every crossing included. Plus the origin axes as 2px
+ /// lines down the rail centrelines beside row and column 0. 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. (Flat per-cell strips came
+ /// between: seamless, but square-cornered — no quad can paint the notch.)
pub fn paint_grid(&self, rect: Rect, pc: &mut PaintCtx) {
if self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
return;
@@ -416,23 +416,14 @@ impl Graph {
let step_y = self.grid_size_y + self.skipped_row_h;
if self.show_network_grid && step_x >= 4.0 && step_y >= 4.0 {
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);
- }
- }
+ pc.grout(
+ 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(),
+ gap_rgba,
+ );
}
// Origin axes, in the gaps beside row/column 0.
diff --git a/tests/lattice_probe.rs b/tests/lattice_probe.rs
index a0321f7..00244e3 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_flat_grid_lines() {
+fn graph_emits_grout_and_axes() {
use cce_ui::widget::GraphController;
let mut g = cce_ui::widget::Graph::new();
g.set_grid_sizes(71.0, 31.0);
@@ -33,8 +33,20 @@ fn graph_emits_flat_grid_lines() {
let rect = Rect { x: 40.0, y: 40.0, width: 600.0, height: 300.0 };
g.paint_grid(rect, &mut pc);
let dl = pc.finish();
+ let n_grout = dl.items.iter().filter(|i| matches!(i.prim, Prim::Grout { .. })).count();
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");
+ assert_eq!((n_grout, n_quad), (1, 2), "one grout draw + two axis lines");
+}
+
+#[test]
+fn grout_tessellates_to_a_mode_15_batch_in_its_colour() {
+ let mut pc = PaintCtx::new();
+ let clip = Rect { x: 40.0, y: 40.0, width: 600.0, height: 300.0 };
+ pc.clip(clip, |pc| pc.grout(clip, (85.0, 43.0), (60.0, 60.0), (71.0, 31.0), 15.5, [0.5, 0.5, 0.5, 0.1]));
+ let dl = pc.finish();
+ let (verts, batches, _, _) = cce_ui::backend::window_runner::tessellate_display_list(&dl, 1280.0, 720.0, 2.0);
+ let modes: Vec<f32> = batches.iter().filter_map(|b| b.plate.as_ref().map(|p| p.mode)).collect();
+ assert_eq!(modes, vec![15.0]);
+ assert_eq!(verts.len(), 6);
+ assert!(verts.iter().all(|v| (v.color[3] - 0.1).abs() < 1e-6), "cover quad carries the grout colour");
}