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

commit58af8bcb5416740337e9d06d3e5d3a8fef26b8e8
parentd30d3eb369
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 10:49
fix: a field's run is padded the same at both ends

The seam ran the face's lip straight on down into the well, so the left
of the button read as one doubled line with half the gap of its right.
The left is the right mirrored now: the button's valley rises to a
ridge at the surface's level and the well's wall falls from there, a
whole wall from the button. A text row's box ends a wall short of its
picker to match.

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

 CLAUDE.md                             | 10 ++++++--
 src/vk/shader2d.wgsl                  | 43 ++++++++++++++++++++++++-----------
 src/widget/container/parameters_bg.rs | 17 ++++++++++----
 3 files changed, 51 insertions(+), 19 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index a7fe12d..eafea34 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -343,8 +343,14 @@ well's fall mirrored back up, and whose left corners are its right corners, so t
 button has the same padding and rounding at both ends (since 2026-10-02; until then
 the lip was the outline's own inner half and the face met the seam square). Where the
 face's rounded corner leaves room by the straight outline is the valley's flat floor.
-The seam is the well's right wall's inner half: from half the step at `split` down to
-the floor, meeting the face's lip there at the same height, faded out at the outline. Never grouped into a host
+The seam is the right side mirrored: the button's valley rises out of `split` to the
+surface (its outer half, as the outline's is on the right), and the well's own right
+wall, a whole wall further left, falls from there to the floor — a ridge at the
+surface's level between them, where the surface lies outside the button's right edge.
+Both fade out at the outline, which runs straight across. (For an hour the seam was the
+well's wall's inner half running straight into the lip, which read as one doubled line
+with half the gap of the button's right side.) A textpick row's TextBox ends a wall
+short of its picker accordingly, so its text stops at the well. Never grouped into a host
 plate (its profile is not a monotonic step); the overlay's host-box slot carries `split`.
 The legacy banded path and flat hosts (`layout.rs`'s bridge) draw the two-box form.
 
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index f095b89..eebe154 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -913,9 +913,16 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         //   both ends: the same padding, the same rounding. Where the face's
         //   rounded corner leaves room by the straight outline, that is the
         //   valley's floor, flat.
-        // * The SEAM, the well's own right wall's inner half: from half the
-        //   step at the seam down to the floor, meeting the face's lip at
-        //   the seam where both stand at half the step.
+        // * The SEAM, the right side mirrored: the button's valley rises out
+        //   of the seam to the surface (its outer half, as the outline's is
+        //   on the right), and the well's own right wall, a whole wall's
+        //   width further left, falls from there to the floor — a ridge at
+        //   the surface's level between the two, standing where the
+        //   surface lies outside the button's right edge. Until 2026-10-02
+        //   the face's lip ran straight on down into the well, so the left
+        //   of the button read as one doubled line with half the gap of
+        //   its right. Both walls fade out at the outline, which runs
+        //   straight across them.
         //
         // A field with no well (split left of it — a dropdown trigger, a
         // button) is all run: the face inset from the outline all round,
@@ -935,9 +942,11 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         let rtr = max(rr.y - hw, 0.0);
         let rbr = max(rr.z - hw, 0.0);
         let face_r = select(vec4f(max(rr.x - hw, 0.0), rtr, rbr, max(rr.w - hw, 0.0)), vec4f(rtr, rtr, rbr, rbr), has_well);
+        // The well's outline stands a whole wall left of the button's.
+        let well_r = split - t;
         // The outline: its outer half everywhere, its inner half over the
-        // well alone (`over_well`, antialiased across the seam's pixel).
-        let over_well = 1.0 - smoothstep(split - 0.5, split + 0.5, frag.x);
+        // well alone (`over_well`, antialiased across the well's end).
+        let over_well = 1.0 - smoothstep(well_r - 0.5, well_r + 0.5, frag.x);
         let inner = select(1.0, over_well, u > 0.5);
         slope = -cd * carve_slope(u) * inner;
         curv = rrect_clip.p_mat.w * sin(u * TAU) * inner;
@@ -949,15 +958,23 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
             sv_seam = gf.xy * (cd * carve_slope(1.0 - uf));
             curv_seam = rrect_clip.p_mat.w * sin((1.0 - uf) * TAU);
         }
-        // The seam: the well's right wall, its inner half, faded out at the
-        // outline (outside the field there is no well to fall into).
+        // The seam, faded out at the outline (outside the field there is
+        // no well to fall into, and across the outline's band the valley
+        // runs straight through).
         if (has_well) {
-            let us = (split - frag.x) / t + 0.5;
-            if (us >= 0.5) {
-                let usc = min(us, 1.0);
-                let fade = clamp(d / hw, 0.0, 1.0);
-                sv_seam = sv_seam + vec2f(1.0, 0.0) * (-cd * carve_slope(usc)) * fade;
-                curv_seam = curv_seam + rrect_clip.p_mat.w * sin(usc * TAU) * fade;
+            let fade = clamp(d / hw, 0.0, 1.0);
+            // The button's valley, its outer half: from the ridge down to
+            // half the step at the seam, where the face's lip takes over.
+            let ub = (frag.x - split) / t + 0.5;
+            if (ub >= 0.0 && ub <= 0.5) {
+                sv_seam = sv_seam + vec2f(-1.0, 0.0) * (-cd * carve_slope(ub)) * fade;
+                curv_seam = curv_seam + rrect_clip.p_mat.w * sin(ub * TAU) * fade;
+            }
+            // The well's right wall, whole: from the ridge to the floor.
+            let us = (well_r - frag.x) / t + 0.5;
+            if (us >= 0.0 && us <= 1.0) {
+                sv_seam = sv_seam + vec2f(1.0, 0.0) * (-cd * carve_slope(us)) * fade;
+                curv_seam = curv_seam + rrect_clip.p_mat.w * sin(us * TAU) * fade;
             }
         }
     } else if (eff == MODE_RIDGE || eff == MODE_TROUGH) {
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 4f5dac2..ca00376 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1109,8 +1109,14 @@ impl ParametersBg {
                 // A textpick row's box ends where its picker begins.
                 let joined = self.display_params[i].2.starts_with("textpick") && self.choices[i].is_some();
                 tb.inner_mut().joined_right = joined;
-                let w = if joined { (r.2 - PICK_W).max(0.0) } else { r.2 };
-                tb.set_rect(r.0, r.1, w, r.3);
+                tb.set_rect(r.0, r.1, r.2, r.3);
+                if joined {
+                    // The box's well ends a whole wall short of the picker
+                    // (the field's seam: the well's wall, a ridge, the
+                    // picker's valley), so its text stops there too.
+                    let depth = crate::layout::bevel_width().min((r.3 - tb.label_strip()) * 0.2);
+                    tb.set_rect(r.0, r.1, (r.2 - PICK_W - depth).max(0.0), r.3);
+                }
             }
         }
         for (i, cb_opt) in self.toggles.iter_mut().enumerate() {
@@ -3869,8 +3875,11 @@ mod tests {
         let (tx, ty, tw, th) = p.texts[0].as_ref().unwrap().rect();
         let (dx, dy, dw, dh) = p.choices[0].as_ref().unwrap().rect();
         assert_eq!(dw, PICK_W);
-        assert_eq!(dx, tx + tw, "the picker begins where the box ends");
+        // The box stops a wall short of the picker: the field's seam is the
+        // well's wall, a ridge and the picker's valley.
         let label = p.texts[0].as_ref().unwrap().label_strip();
+        let depth = crate::layout::bevel_width().min((th - label) * 0.2);
+        assert!((dx - (tx + tw + depth)).abs() < 1e-3, "the picker begins a wall past the box's end: {dx} vs {}", tx + tw + depth);
         assert_eq!((dy, dy + dh), (ty + label, ty + th), "the picker spans the field's band, edge to edge");
         assert!(p.texts[0].as_ref().unwrap().inner().joined_right, "the box is square at the seam");
         assert!(!p.texts[1].as_ref().unwrap().inner().joined_right, "a plain text row is not");
@@ -3893,7 +3902,7 @@ mod tests {
         // the box width and hangs below it, not off the button sliver.
         let anchor = p.choices[0].as_ref().unwrap().popover_anchor.expect("anchor set");
         assert_eq!(anchor.x, tx);
-        assert_eq!(anchor.width, tw + dw);
+        assert!((anchor.width - (dx + dw - tx)).abs() < 1e-3, "the whole field");
         let d = p.choices[0].as_ref().unwrap();
         let (px_, py_, pw, _ph) = d.popover_geom(crate::scene::layout::Rect {
             x: dx, y: dy, width: dw, height: dh,