GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Revert "fix: a text box is a field that is all well"
This reverts commit 6634ba3, except its unrelated cleanup in
dropdown.rs (an unused binding), which stays.
6634ba3 drew a plain text box as an all-well field so it would not be a
recess grouped into its plate, because grouped recesses drew a doubled
outline. 51d5f25 fixed that in the plate shader, and 3cdc9f6 holds a
grouped carve to its overlay pixel for pixel, so the workaround has
nothing left to work around. A text box is a Prim::Recess again,
grouped when its plate's grouping window is open. In the designer, with
the user's config, its text wells group (3 of 3) and differ from the
well_field rendering by at most 1/255, the 8-bit rounding between
in-shader and hardware blending. NVIDIA and Intel agree to 1/255.
PaintCtx::well_field and field_well_only go with it; nothing outside
cce-ui used them. CLAUDE.md keeps a note of the episode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 17 +++++--------
src/backend/window_runner.rs | 48 ++++++++++++++---------------------
src/layout.rs | 29 +++++----------------
src/scene/paint.rs | 16 ------------
src/widget/container/parameters_bg.rs | 41 ++++++++----------------------
src/widget/input/text_box.rs | 29 +++------------------
6 files changed, 47 insertions(+), 133 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d636499..af2e0b5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -382,15 +382,12 @@ explicit valley and `cce-relief`'s preview. `a_dropdown_trigger_wears_the_runs_e
`focus_lights_the_plate_rim`, `a_breadcrumb_and_a_font_selector_wear_the_runs_edge` and
`an_inset_plate_is_a_field_that_is_all_run` are the tests.
-**A text box is a field that is all well** (since 2026-10-02): `PaintCtx::well_field`, a
-`Prim::Field` whose seam is `FIELD_RUN_ONLY` px past its right end (`field_well_only`),
-in `TextBox`'s own paint and in `ParametersBg::fields` for every text row without a
-picker. It was a `Prim::Recess`, which the runner GROUPS into a live host plate as a CSG
-feature, and a grouped recess drew a doubled outline where the field beside it — never
-grouped, its own overlay — drew the single edge. That was first put down to the NVIDIA
-device; it was not (see "A grouped carve shades as its overlay does" below, which fixed
-the cause the same day, so the two now agree whichever is used). Both fallbacks draw an
-all-well field as one plain recess. `a_text_box_is_a_field_that_is_all_well` is the test.
+**A plain text box is a `Prim::Recess`**, grouped into the plate under it when the
+plate's grouping window is open. For a few hours on 2026-10-02 it was a field that was
+all well (`PaintCtx::well_field`, 6634ba3), to dodge a doubled outline that grouped
+recesses drew; the cause was fixed in the plate shader the same day (see "A grouped carve
+shades as its overlay does" below) and the workaround reverted, since a grouped recess
+and an overlaid one now draw the same pixels.
The pieces that feed it: `ParametersBg::fields` (the pane's list, drawn after its troughs,
hover-tinted like them; textpick rows and spinboxes with a run are in neither `reliefs`
@@ -433,7 +430,7 @@ overlay drew one soft edge. Two causes, both in how the plate path applied the c
Measured with `examples/grouped_recess_probe.rs` (a plate whose recesses group beside the
same recesses forced to overlay by a transparent quad): grouped and overlay columns
5,462 px apart before, 0 after; in the designer with grouped text wells, 0 px from the
-`well_field` rendering.
+`well_field` rendering the text boxes then had (since reverted, above).
**And it is tested by rendering, not by reading** (`vk::plate_probe`, `cfg(test)`): an
offscreen 2D render — a `DisplayList` through the runner's own `tessellate_display_list`
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 41d1a3b..9fd99c5 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2549,40 +2549,30 @@ pub fn tessellate_display_list(
// has no blended outline, and the legacy path exists only for
// A/B comparison.
let all = (true, true, true, true);
- // All well (`PaintCtx::well_field`): the plain step, its own
- // corners, and no run.
- if *split >= rect.x + rect.width {
+ if *split > rect.x {
push_bevel_edge_vertices_banded(
- rect.x, rect.y, rect.width, rect.height, *radii, *depth,
+ rect.x, rect.y, *split - rect.x, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
EdgeKind::Step, &mut verts,
);
- } else {
- if *split > rect.x {
- push_bevel_edge_vertices_banded(
- rect.x, rect.y, *split - rect.x, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
- sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
- EdgeKind::Step, &mut verts,
- );
- }
- let rx = split.max(rect.x);
- let rw = (rect.x + rect.width - rx).max(0.0);
- // All run (`PaintCtx::inset_plate`'s edge): its own left corners.
- let (l0, l3) = if *split > rect.x { (0.0, 0.0) } else { (radii.0, radii.3) };
- let half = *depth * 0.5;
- push_bevel_edge_vertices_banded(
- rx, rect.y, rw, rect.height, (l0, radii.1, radii.2, l3), half,
- sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), all,
- EdgeKind::Step, &mut verts,
- );
- let ir = (radii.1 - half).max(0.0);
- let il = if *split > rect.x { 0.0 } else { (radii.0 - half).max(0.0) };
- push_bevel_edge_vertices_banded(
- rx + half, rect.y + half, rw - *depth, rect.height - *depth, (il, ir, ir, il), half,
- sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), all,
- EdgeKind::Step, &mut verts,
- );
}
+ let rx = split.max(rect.x);
+ let rw = (rect.x + rect.width - rx).max(0.0);
+ // All run (`PaintCtx::inset_plate`'s edge): its own left corners.
+ let (l0, l3) = if *split > rect.x { (0.0, 0.0) } else { (radii.0, radii.3) };
+ let half = *depth * 0.5;
+ push_bevel_edge_vertices_banded(
+ rx, rect.y, rw, rect.height, (l0, radii.1, radii.2, l3), half,
+ sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), all,
+ EdgeKind::Step, &mut verts,
+ );
+ let ir = (radii.1 - half).max(0.0);
+ let il = if *split > rect.x { 0.0 } else { (radii.0 - half).max(0.0) };
+ push_bevel_edge_vertices_banded(
+ rx + half, rect.y + half, rw - *depth, rect.height - *depth, (il, ir, ir, il), half,
+ sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), all,
+ 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/layout.rs b/src/layout.rs
index 3d97d52..72c3bae 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4351,20 +4351,7 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
// All run and no well (`PaintCtx::inset_plate`'s edge): the
// plate alone, with the face that came before it.
let face = if split <= rect.x { pending_face.take().map(|(_, _, fill)| fill) } else { None }.unwrap_or([0.0; 4]);
- // All well and no run (`PaintCtx::well_field`, a text box):
- // the plain recess, its own corners.
- if split >= rect.x + rect.width {
- pc.relief_carve(&ReliefCarve {
- kind: CarveKind::Recess { tint },
- x: rect.x,
- y: rect.y,
- w: rect.width,
- h: rect.height,
- radii,
- depth,
- edges: (true, true, true, true),
- });
- } else if split > rect.x {
+ if split > rect.x {
pc.relief_carve(&ReliefCarve {
kind: CarveKind::Recess { tint },
x: rect.x,
@@ -4376,14 +4363,12 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
edges: (true, true, true, true),
});
}
- if split < rect.x + rect.width {
- let r = radii.1.max(radii.2);
- let rx = split.max(rect.x);
- let rw = (rect.x + rect.width - rx).max(0.0);
- match tint {
- Some(t) => pc.inset_plate_tinted(face, rx, rect.y, rw, rect.height, r, depth, t),
- None => pc.inset_plate(face, rx, rect.y, rw, rect.height, r, depth),
- }
+ let r = radii.1.max(radii.2);
+ let rx = split.max(rect.x);
+ let rw = (rect.x + rect.width - rx).max(0.0);
+ match tint {
+ Some(t) => pc.inset_plate_tinted(face, rx, rect.y, rw, rect.height, r, depth, t),
+ None => pc.inset_plate(face, rx, rect.y, rw, rect.height, r, depth),
}
}
Prim::Trough { rect, radii, depth, tint, .. } => {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 848a08f..c709792 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -222,12 +222,6 @@ pub enum PlateStance {
/// seam and the seam's own wall land nowhere near the field.
pub const FIELD_RUN_ONLY: f32 = 1.0e4;
-/// The seam of a field that is all well and no run ([`PaintCtx::well_field`]):
-/// this far past the field's right end.
-pub fn field_well_only(rect: Rect) -> f32 {
- rect.x + rect.width + FIELD_RUN_ONLY
-}
-
/// A control plate: the thing you press, at the control rung of the plate
/// ladder. One description for every control face — Button, Dropdown,
/// FontSelector, Breadcrumb, a ButtonStrip's selected plateau — so their
@@ -1572,16 +1566,6 @@ impl PaintCtx {
self.push(Prim::Trough { rect, radii, depth, edges, tint: Some(tint) });
}
- /// A sunken well and nothing else: a [`Prim::Field`] that is all well,
- /// its seam [`FIELD_RUN_ONLY`] px past its right end — the well a text
- /// box is. It shades as a field's well does, an overlay of its own
- /// rather than a feature grouped into the plate under it, which is the
- /// look a textpick row's well had beside a plain text box's (since
- /// 2026-10-02; a plain box was a `Prim::Recess`, grouped).
- pub fn well_field(&mut self, rect: Rect, radii: Radii, depth: f32, tint: Option<[f32; 3]>) {
- self.field(rect, radii, depth, field_well_only(rect), tint);
- }
-
/// A sunken well ending in a flush run at `split` — see [`Prim::Field`].
/// `tint` lights its rim (the focus and hover treatment).
pub fn field(&mut self, rect: Rect, radii: Radii, depth: f32, split: f32, tint: Option<[f32; 3]>) {
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 635a64d..36cb9ef 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1732,9 +1732,9 @@ impl ParametersBg {
}
// (control, its configured corner radius, raised vs recessed)
let ctl: Option<(&dyn WidgetHost, f32, bool)> = if is_text_row(&p.2) {
- // A text row is a field ([`Self::fields`]): with its picker a
- // well ending in a flush run, without one all well.
- None
+ // The textpick picker is NOT in this list: with it the row is
+ // a field ([`Self::fields`]) — a well ending in a flush run.
+ self.texts[i].as_ref().map(|w| (w as &dyn WidgetHost, crate::layout::textbox_corner_radius(), false))
} else if p.2.starts_with("choice") {
// The dropdown trigger is a FLUSH control, a field that is all
// run ([`Self::fields`]). A boss here read as a raised island
@@ -1817,6 +1817,11 @@ impl ParametersBg {
// The top-label band stays outside the relief like every other host.
let ty = w.label_strip();
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
+ // A text box joined to its picker is half of a field
+ // ([`Self::fields`]), drawn there.
+ if is_text_row(&p.2) && self.texts[i].as_ref().is_some_and(|t| t.inner().joined_right) {
+ continue;
+ }
out.push((x, y + ty, ww, h - ty, r4(radius), depth, raised, all));
}
}
@@ -1843,24 +1848,7 @@ impl ParametersBg {
if hidden[i] {
continue;
}
- let joined = self.texts[i].as_ref().is_some_and(|t| t.inner().joined_right);
- if is_text_row(&p.2) && !joined {
- // A plain text row (a textpick with no candidates included):
- // a field that is all well, so it shades as the well of a
- // textpick row beside it does — an overlay of its own, where
- // as a recess in [`Self::reliefs`] it was grouped into the
- // pane's plate and read flatter (until 2026-10-02).
- if let Some(tb) = &self.texts[i] {
- let (x, y, w, h) = tb.rect();
- let ty = tb.label_strip();
- if w > 0.0 && h - ty > 0.0 {
- let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
- let r = crate::layout::textbox_corner_radius();
- let band = Rect { x, y: y + ty, width: w, height: h - ty };
- out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, crate::scene::paint::field_well_only(band)));
- }
- }
- } else if p.2.starts_with("textpick") {
+ if p.2.starts_with("textpick") {
if let (Some(tb), Some(d)) = (&self.texts[i], &self.choices[i]) {
if !tb.inner().joined_right {
continue;
@@ -3922,17 +3910,8 @@ mod tests {
// there.
let rr = crate::layout::textbox_corner_radius();
let fields = p.inner().fields();
- assert_eq!(fields.len(), 3, "{fields:?}");
+ assert_eq!(fields.len(), 1, "{fields:?}");
let (fx, fy, fw, fh, fradii, _, split) = fields[0];
- // The plain row and the textpick with nothing to pick are fields
- // too, all well: their seam past their right end.
- for (i, f) in fields[1..].iter().enumerate() {
- let (x, _, w, _, radii, _, s) = *f;
- let (bx, _, bw, _) = p.texts[i + 1].as_ref().unwrap().rect();
- assert_eq!((x, w), (bx, bw), "row {}'s field is its box", i + 1);
- assert_eq!(radii, (rr, rr, rr, rr));
- assert!(s >= x + w + 100.0, "all well: {s}");
- }
assert_eq!((fx, fy, fx + fw, fy + fh), (tx, dy, dx + dw, dy + dh), "the field spans box and picker");
assert_eq!(fradii, (rr, rr, rr, rr), "its own radius at all four corners");
assert_eq!(split, dx, "the seam is where the picker begins");
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 50704db..1513e57 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1568,9 +1568,10 @@ impl Paint for TextBox {
if let Some((well, radii, depth, tint)) = self.well() {
// Focus lights the well's rim in the highlight accent (with
// the shader's complementary shadow) — the TreeList treatment.
- // A field that is all well (`PaintCtx::well_field`), the
- // well a textpick row's box is half of.
- ctx.well_field(well, radii, depth, tint);
+ match tint {
+ Some(t) => ctx.recess_tinted(well, radii, depth, t),
+ None => ctx.recess(well, radii, depth),
+ }
}
}
@@ -1825,28 +1826,6 @@ unsafe impl Sync for TextBox {}
mod tests {
use super::*;
- /// A text box's well is a field that is all well — the well a textpick
- /// row's box is half of — and not a recess, which grouped into the plate
- /// under it and read flatter beside the textpick's.
- #[test]
- fn a_text_box_is_a_field_that_is_all_well() {
- use crate::scene::paint::{PaintCtx, Prim};
- let mut tb = TextBox::new("x".to_string()).with_recessed(true);
- tb.set_rect(10.0, 10.0, 200.0, 26.0);
- let Some((well, radii, depth, _)) = tb.well() else {
- return; // a square-cornered config draws no well
- };
- let mut pc = PaintCtx::new();
- Paint::paint(tb.inner(), Rect { x: 10.0, y: 10.0, width: 200.0, height: 26.0 }, &mut pc);
- let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
- assert!(
- prims.iter().any(|p| matches!(p, Prim::Field { rect, radii: r, depth: d, split, .. }
- if *rect == well && *r == radii && *d == depth && *split >= rect.x + rect.width + 100.0)),
- "{prims:?}"
- );
- assert!(!prims.iter().any(|p| matches!(p, Prim::Recess { .. })), "no recess of its own");
- }
-
/// The multiline caret/click math reads shaped per-line offsets; a caret
/// must land exactly where it is drawn, on every column of every line.
#[test]