GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: Prim::Trough — one lighting pass for the flush inset seam
`inset_plate` built the seam around every flush control from two prims: a
Recess on a rect outset by depth/2 and a Boss on the rect. Both walls
straddle their own boundary by ±depth/2 (shader2d `u = fd/t + 0.5`), so at
depth 4.8 they spanned [-4.8, +9.6] and [-4.8, +4.8] device px about the
edge — overlapping over half their width and shading twice there.
Measured on the cce-files view dropdown against a ridge of the same depth,
both over one flat field: the band ran 15px instead of 8 with THREE lobes
(bright +29 / dark -17 / bright +99) because the recess ring's own lit rim
landed ~depth outside the control rather than merging, and the highlight
peaked 22% hotter than a single evaluation. It read as two concentric
rings, which is what it was.
Prim::Trough is the sunken twin of Prim::Ridge and shares its shader
branch with the profile inverted, so neither can drift back into a
two-pass stack. Same measurement after: band 10px (target 9.6), one wall
down, one back up, peak +91.
The opaque face is now a zero-stroke Border rather than a Bevel — the
Bevel's lip WAS the second wall. Deliberately not a RoundedRect: the
legacy reverse bridges in widget/model.rs extract RoundedRect but neither
Bevel nor Border, so a RoundedRect would newly leak every raised control's
face into all_rounded_quads. Border also keeps all four radii.
Verified in the demo gallery (button, toggle, dropdown): fills, colors and
corners unchanged, only the seam; the diff bbox covers exactly those three
controls and cce-system-interface, which never calls inset_plate, is
byte-identical.
src/backend/window_runner.rs | 31 ++++++++++++++--
src/scene/paint.rs | 85 +++++++++++++++++++++++++++++++++-----------
src/vk/shader2d.wgsl | 22 ++++++++----
3 files changed, 109 insertions(+), 29 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 9ca4734..6b506f8 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1521,7 +1521,7 @@ fn prim_kind(p: &crate::scene::paint::Prim) -> &'static str {
P::Quad { .. } => "Quad", P::RoundedRect { .. } => "RoundedRect",
P::Border { .. } => "Border", P::Bevel { .. } => "Bevel",
P::Recess { .. } => "Recess", P::Boss { .. } => "Boss",
- P::Ridge { .. } => "Ridge", P::Plate { .. } => "Plate",
+ P::Ridge { .. } => "Ridge", P::Trough { .. } => "Trough", P::Plate { .. } => "Plate",
P::Arc { .. } => "Arc", P::ArcShaded { .. } => "ArcShaded",
P::Vector { .. } => "Vector", P::Circle { .. } => "Circle",
P::Sphere { .. } => "Sphere", P::ConcaveFillet { .. } => "ConcaveFillet",
@@ -1660,6 +1660,7 @@ pub fn tessellate_display_list(
Prim::Recess { rect, radii, depth, edges, .. }
| Prim::Boss { rect, radii, depth, edges, .. }
| Prim::Ridge { rect, radii, depth, edges }
+ | Prim::Trough { rect, radii, depth, edges }
if shader_plates =>
{
let tint = match &item.prim {
@@ -1669,11 +1670,13 @@ pub fn tessellate_display_list(
};
// Recess carves down into the surface; Boss raises a plateau out
// of it (same machinery, depth sign flipped); Ridge is a raised
- // rim straddling the boundary (its own overlay profile — never
- // grouped, the CSG features only model monotonic steps).
+ // rim straddling the boundary and Trough the sunken valley twin
+ // (their own overlay profiles — never grouped, the CSG features
+ // only model monotonic steps).
let mode = match &item.prim {
Prim::Boss { .. } => 3.0f32,
Prim::Ridge { .. } => 4.0,
+ Prim::Trough { .. } => 9.0,
_ => 2.0,
};
let raised = mode > 2.5;
@@ -1738,6 +1741,7 @@ pub fn tessellate_display_list(
let kind = match &item.prim {
Prim::Boss { .. } => "boss",
Prim::Ridge { .. } => "ridge",
+ Prim::Trough { .. } => "trough",
_ => "recess",
};
let infl = *depth * 0.5 + 2.0;
@@ -1942,6 +1946,27 @@ pub fn tessellate_display_list(
EdgeKind::Step, &mut verts,
);
}
+ Prim::Trough { rect, radii, depth, edges } => {
+ // Legacy approximation, the Ridge arm's two steps with the light
+ // signs swapped: down at the boundary, back up half a width in.
+ // The banded machinery has no valley profile, so this is the old
+ // stacked look — accepted here, as the legacy path exists only
+ // for A/B comparison against the SDF one.
+ let half = *depth * 0.5;
+ push_bevel_edge_vertices_banded(
+ rect.x, rect.y, rect.width, rect.height, *radii, half,
+ sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), *edges,
+ EdgeKind::Step, &mut verts,
+ );
+ let ir = (radii.0 - half).max(0.0);
+ push_bevel_edge_vertices_banded(
+ rect.x + half, rect.y + half,
+ rect.width - *depth, rect.height - *depth,
+ (ir, ir, ir, ir), half,
+ sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), *edges,
+ EdgeKind::Step, &mut verts,
+ );
+ }
Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
push_arc_background_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *color, segs(*radius), no, &mut verts);
}
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 7ee3631..3c285ca 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -108,6 +108,25 @@ pub enum Prim {
/// a Boss plus an inset Recess stacks two shading passes (double specular /
/// shoulder terms at the crest) and reads far hotter than a plate edge.
Ridge { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
+ /// The sunken twin of [`Prim::Ridge`]: a VALLEY riding the rect's boundary —
+ /// a bump profile straddling the outline (span ±depth/2), falling from the
+ /// surrounding surface to a trough on the boundary and rising back to the
+ /// same level inside, so both faces sit at the underlying surface's own
+ /// level. This is the seam a flush inset control leaves ([`PaintCtx::inset_plate`]).
+ ///
+ /// Same reason to exist as `Ridge`, measured: building this from a `Recess`
+ /// on an outset rect plus a `Boss` on the rect (what `inset_plate` used to
+ /// emit) stacks two independent shading passes. At depth 4.8 that read as a
+ /// band 15px wide instead of 8 with THREE lobes — bright, dark, brighter —
+ /// because the recess ring's own lit rim lands ~depth outside the control
+ /// instead of merging into one wall, and the highlight peaked 22% hotter
+ /// than a single evaluation of the same depth. It looked like two concentric
+ /// rings, which is what it was.
+ ///
+ /// `edges` and the host-box fade behave exactly as [`Prim::Recess`]'s.
+ /// SDF path only; the legacy banded tessellation approximates it with the
+ /// old two-step stack (like `Ridge`, which approximates itself there).
+ Trough { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
/// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
/// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
/// `depth` — a plate must fill the window exactly, or the compositor's rounded window
@@ -486,6 +505,7 @@ impl PaintCtx {
None => self.boss_edges(rect, radii, depth, edges),
},
Prim::Ridge { rect, radii, depth, edges } => self.ridge_edges(rect, radii, depth, edges),
+ Prim::Trough { rect, radii, depth, edges } => self.trough_edges(rect, radii, depth, edges),
Prim::Plate { rect, radii, color, depth } => self.plate(rect, radii, color, depth),
Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
self.arc(cx, cy, radius, thickness, start, end, color)
@@ -584,29 +604,54 @@ impl PaintCtx {
}
/// A flush inset control: `rect`'s plate sits SUNKEN into the surface with
- /// its face level with it — a groove ring carved around the control (the
- /// outward wall steps down) and the control's own beveled lip rising back
- /// up inside. Two opposite-facing bevels; the face never leaves the
- /// surface plane. An opaque `color` fills the face (Bevel); transparent
- /// degrades to edges-only (Boss), the surface below showing through as
- /// the face. `depth` is the roll width of both walls; the ring is
- /// expanded by depth/2, so the descending wall meets the rising lip in a
- /// tight V-groove with no flat floor between them.
+ /// its face level with it — a valley seam runs the boundary, the surface
+ /// falling into it on the way out and the control's own face rising back
+ /// out of it inside. The face never leaves the surface plane; the seam is
+ /// the only thing saying it is a separate part. `depth` is the full width
+ /// of that valley, which straddles the boundary by ±depth/2.
+ ///
+ /// One [`Prim::Trough`] — ONE lighting evaluation. This used to emit a
+ /// `Recess` on a rect outset by depth/2 plus a `Boss` on the rect, whose
+ /// walls overlapped over half their width and shaded twice; see
+ /// `Prim::Trough` for what that measured as. Do not re-expand this into its
+ /// parts.
+ ///
+ /// An opaque `color` fills the face; transparent leaves the surface below
+ /// showing through as the face.
pub fn inset_plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
- let g = depth * 0.5;
- let outer = Rect {
- x: rect.x - g,
- y: rect.y - g,
- width: rect.width + 2.0 * g,
- height: rect.height + 2.0 * g,
- };
- let (r1, r2, r3, r4) = radii;
- self.recess(outer, (r1 + g, r2 + g, r3 + g, r4 + g), depth);
if color[3] > 0.001 {
- self.bevel(rect, radii, color, depth);
- } else {
- self.boss(rect, radii, depth);
+ // Flat fill only — the relief is the trough's, so the face must not
+ // carry a lip of its own (that lip WAS the second wall).
+ //
+ // Deliberately a zero-stroke `Border` and NOT `rounded_rect`: this
+ // fill used to be a `Bevel`, and the legacy reverse bridges
+ // (`all_rounded_quads` and friends in `widget/model.rs`) extract
+ // `Prim::RoundedRect` but neither `Bevel` nor `Border`. Emitting a
+ // RoundedRect here would newly leak every raised control's face into
+ // those getters — a change to the legacy surface that has nothing to
+ // do with the relief. Border also keeps all four radii, which
+ // `Prim::RoundedRect`'s single radius cannot.
+ self.border(rect, radii, color, [0.0; 4], 0.0);
}
+ self.trough(rect, radii, depth);
+ }
+
+ /// Sink a valley along `rect`'s boundary — see [`Prim::Trough`]. `depth` is
+ /// the full width of the seam (it straddles the outline by ±depth/2).
+ pub fn trough(&mut self, rect: Rect, radii: Radii, depth: f32) {
+ self.trough_edges(rect, radii, depth, (true, true, true, true));
+ }
+
+ /// [`PaintCtx::trough`] with only some of the walls — see [`Prim::Trough`].
+ pub fn trough_edges(
+ &mut self,
+ rect: Rect,
+ radii: Radii,
+ depth: f32,
+ edges: (bool, bool, bool, bool),
+ ) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Trough { rect, radii, depth, edges });
}
/// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 05ecdac..59d380f 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -128,6 +128,7 @@ const MODE_SPHERE: i32 = 5; // hemisphere-lit disc
const MODE_FILLET_DOWN: i32 = 6; // concave inside-corner wall, recessed
const MODE_FILLET_UP: i32 = 7; // concave inside-corner wall, raised
const MODE_GROOVE: i32 = 8; // slab carve about an arbitrary line
+const MODE_TROUGH: i32 = 9; // sunken valley straddling the boundary
// Fillet modes rejoin the shared free-carve path as their flat equivalents.
const FILLET_TO_STEP: i32 = 4; // 6 -> RECESS, 7 -> BOSS
@@ -375,7 +376,9 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
// the same wall with the height sign flipped. MODE_RIDGE is a
// raised bump straddling the boundary, both sides at the base level — ONE
// profile evaluation, so its crest carries a single specular/shoulder term
- // instead of a boss+recess double-stack.
+ // instead of a boss+recess double-stack. MODE_TROUGH is that bump inverted
+ // (a valley), for the same reason: it replaced the recess-ring+boss stack
+ // `inset_plate` used to emit for every flush control in the DE.
// All profiles straddle the boundary (span [-t/2, t/2]). Darkening is exact
// multiplicative shading (black at alpha 1 - shade); brightening is a
// translucent white screen.
@@ -419,18 +422,25 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
let u = clamp(fd / t + 0.5, 0.0, 1.0);
var slope = 0.0;
var curv = 0.0;
- if (eff == MODE_RIDGE) {
+ if (eff == MODE_RIDGE || eff == MODE_TROUGH) {
// Ridge bump: the carve profile mirrored about the boundary (rising
// outer half, falling inner half), amplitude halved so the wall tilt
- // matches a step's despite the doubled profile rate.
+ // matches a step's despite the doubled profile rate. MODE_TROUGH is the
+ // same profile inverted — falling outer half, rising inner half — the
+ // valley a flush inset control leaves. Sharing this branch is the point:
+ // both get ONE evaluation, so neither can drift into the two-pass
+ // double-shading the stacked form had.
let w = clamp(select(2.0 * u, 2.0 - 2.0 * u, u > 0.5), 0.0, 1.0);
- let rising = select(-1.0, 1.0, u <= 0.5);
+ let up = select(-1.0, 1.0, eff == MODE_RIDGE);
+ let rising = select(-1.0, 1.0, u <= 0.5) * up;
slope = rising * 0.5 * RECESS_DEPTH * 2.0 * carve_slope(w);
// Each half-wall is a boss wall: concave fillet at its base, convex
// shoulder toward the crest — and ZERO at the plateaus and crest, so
// flat ground composites to exactly nothing (a constant term here
- // tints the whole cover quad).
- curv = -rrect_clip.p_mat.w * sin(w * TAU);
+ // tints the whole cover quad). A trough's curvature flips with it: the
+ // convex shoulders sit at the plateau lips, the concave fillet at the
+ // floor.
+ curv = -up * rrect_clip.p_mat.w * sin(w * TAU);
} else {
let dir = select(-1.0, 1.0, eff == MODE_BOSS);
// The profile slope is carve_slope's family: smoothstep-derived