GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
relief: the flat-path bridge carves a TextBox's well too
`render_widget` offered only a Dropdown's flush inset trough through
`RenderTarget::inset_plate`, so on a flat host every text field rendered as
bare text: a recessed TextBox with the DE's transparent face draws NO border
and NO background (`paint`'s `bare` branch) — the carve is its entire
decoration, and the carve is exactly what `all_quads` cannot carry. That is
why the settings app's Browser page had a naked Homepage line sitting under
two decorated dropdowns.
A TextBox's chrome is a WELL, not a trough — a trough is a seam about the
boundary with the face left level, a well drops the whole interior — so this
is a second hook, `RenderTarget::recess`, not the existing one at another
rect. Its default is a no-op rather than a fill: the face is transparent by
design (the host surface IS the well floor), so a host that cannot carve has
nothing truthful to draw, and a solid box would paint every field a slab it
never had. Only a host that overrides it changes (today: the settings app).
The geometry comes from the widget: `TextBox::well` is now the single source
both `paint` and the bridge read, so the flat host's well cannot drift from
the real one through label layout, the side-label inset, or the focus tint.
Verified in a headless shadow session against the pixels, not the prim
stream: the Homepage row's top edge goes flat (9,25,13) → lit lip (69,73,70)
→ shadowed inner wall (5,17,8), the recess profile, while the dropdowns keep
their trough profile (dark 6,19,9 → bright 130,131,130) — the two carves stay
distinct. Focused, the same edge lights cyan (39,86,84) against the warm
counter-shadow (93,12,5): `recess_tinted` through the new hook.
cce-files is untouched by this — its host is its own `PageContent`, which
takes the no-op default, so its app-side `relief_recessed` compensations
stay the only carve there (measured: byte-identical well profile before and
after, 105/77/65/71).
src/layout.rs | 24 ++++++++++++++++++++++++
src/scene/paint.rs | 8 ++++++++
src/widget/input/text_box.rs | 44 +++++++++++++++++++++++++++++++++++---------
3 files changed, 67 insertions(+), 9 deletions(-)
diff --git a/src/layout.rs b/src/layout.rs
index 3d003f0..f54795c 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3476,6 +3476,17 @@ pub trait RenderTarget {
fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, _depth: f32) {
self.rect_with_radius(color, x, y, w, h, radius);
}
+ /// A sunken well ([`crate::scene::paint::PaintCtx::recess`]) — the carve a
+ /// TextBox leaves, offered here for the same reason as `inset_plate`: the
+ /// legacy `all_quads` stream carries no relief prims, so a flat-path host
+ /// never sees it. `tint` is the focus accent (`recess_tinted`).
+ ///
+ /// The default is deliberately a NO-OP, not a fill: a recessed control's
+ /// face is transparent by design (the host surface IS the well floor), so
+ /// the carve is the entire decoration — a host that can't carve has
+ /// nothing truthful to draw, and a solid box here would paint every text
+ /// field a flat slab it never had.
+ fn recess(&mut self, _x: f32, _y: f32, _w: f32, _h: f32, _radius: f32, _depth: f32, _tint: Option<[f32; 3]>) {}
/// Whether this host renders sections as sunken wells (the designer idiom).
/// `SectionContext` then lays the title out left-aligned over its tab box
/// instead of centered on the top border.
@@ -3571,6 +3582,19 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
);
}
+ // The same gap for the TextBox, whose chrome is a WELL rather than a
+ // trough (`TextBox::well` — the geometry it carves in `paint`, asked for
+ // here so the two cannot drift). A recessed box with the DE's transparent
+ // face draws no border or background at all — the carve is the only thing
+ // marking the field — so on a flat host the row rendered as bare text.
+ if control_relief() {
+ if let Some(tb) = w.as_any().downcast_ref::<crate::widget::TextBox>() {
+ if let Some((well, radius, depth, tint)) = tb.well() {
+ pc.recess(well.x, well.y, well.width, well.height, radius, depth, tint);
+ }
+ }
+ }
+
for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
let extra_corners = (
corners.0 && qx <= wx + 1.5 && qy <= wy + 1.5,
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 3c285ca..19bd99e 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -881,6 +881,14 @@ impl crate::layout::RenderTarget for PaintCtx {
fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), color, depth);
}
+ fn recess(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: Option<[f32; 3]>) {
+ let rect = Rect { x, y, width: w, height: h };
+ let radii = (radius, radius, radius, radius);
+ match tint {
+ Some(t) => PaintCtx::recess_tinted(self, rect, radii, depth, t),
+ None => PaintCtx::recess(self, rect, radii, depth),
+ }
+ }
}
#[cfg(test)]
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 1d648e9..9cac127 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1012,6 +1012,36 @@ impl TextBox {
}
labels
}
+
+ /// The well this TextBox carves, as `(rect, corner radius, depth, focus
+ /// tint)` — `None` when it draws no relief at all (square-cornered legacy
+ /// geometry, `control_relief` off, or a box that draws no background).
+ ///
+ /// The SINGLE source for that geometry: `paint` carves it here, and the
+ /// flat-path bridge in `layout::render_widget` re-offers the same rect
+ /// through [`crate::layout::RenderTarget::recess`] for hosts that consume
+ /// `all_quads` and so never see the carve. A second copy of this math in
+ /// the bridge is exactly how the two would drift apart.
+ pub fn well(&self) -> Option<(Rect, f32, f32, Option<[f32; 3]>)> {
+ let radius = crate::layout::textbox_corner_radius();
+ if radius <= 0.0 || !self.recessed || !self.draw_bg_border {
+ return None;
+ }
+ let top = self.label_top();
+ let label_x = side_offset(&self.label);
+ let well = Rect {
+ x: self.rect.x + label_x,
+ y: self.rect.y + top,
+ width: self.rect.width - label_x,
+ height: self.rect.height - top,
+ };
+ let depth = crate::layout::bevel_width().min(well.height * 0.2);
+ let tint = self.editing.then(|| {
+ let hc = crate::color::highlight_primary_color();
+ [hc[0], hc[1], hc[2]]
+ });
+ Some((well, radius, depth, tint))
+ }
}
impl Adapted<TextBox> {
@@ -1326,17 +1356,13 @@ impl Paint for TextBox {
bg_color,
);
}
- if self.recessed {
+ if let Some((well, r, depth, tint)) = self.well() {
// Focus lights the well's rim in the highlight accent (with
// the shader's complementary shadow) — the TreeList treatment.
- let depth = crate::layout::bevel_width().min(visual_h * 0.2);
- let well = Rect { x, y: self.rect.y + top, width: w, height: visual_h };
- let radii = (radius, radius, radius, radius);
- if self.editing {
- let hc = crate::color::highlight_primary_color();
- ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
- } else {
- ctx.recess(well, radii, depth);
+ let radii = (r, r, r, r);
+ match tint {
+ Some(t) => ctx.recess_tinted(well, radii, depth, t),
+ None => ctx.recess(well, radii, depth),
}
}
}