GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
control-plate: a flat stance, for a control made of its pane's material
Raised and Flush both carve INSIDE the footprint, which costs a control
two things a pane has. Its visible edge sits half the carve depth in, so
a control laid out on the same numbers as a pane does not line up with
one — cce-mail's menubar items sat a pixel inside its panes and no single
outset could fix it, since widening them to meet the outer edges closed
the gap between two of them by twice as much. And their face goes down
through a Border/Trough stroke, which the blur-behind sentinel does not
reach, so a control could not be frosted even when its pane was.
PlateStance::Flat fills the footprint with a quad and nothing else:
silhouette equal to the rect, frost carried, and the focus tint drawn as
a ring because there is no rim to light. The cost is one radius rather
than four — a flat fill has no per-corner form, so the concentric
adjustment a nested relief control computes has no equivalent, and that
is documented rather than papered over.
Button::with_flat and Dropdown::with_flat select it; both default off,
so every existing control is untouched.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 21 +++++++++++++++++----
src/scene/paint.rs | 39 +++++++++++++++++++++++++++++++++++++++
src/widget/input/button.rs | 25 ++++++++++++++++++++-----
src/widget/input/dropdown.rs | 24 ++++++++++++++++++++++++
4 files changed, 100 insertions(+), 9 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 1e05d7c..f0a3935 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -253,13 +253,26 @@ What this buys, and where the code is heading:
- **One plate spec per rung, not five copies.** The root and pane rungs are
`scene::paint::PlateSpec` (RFC 7b, painted by `PaintCtx::plate`). The
control rung is `scene::paint::ControlPlate` (re-exported from `widget`):
- footprint, per-corner silhouette, `PlateStance` (raised or flush), face and
- depth, painted by `PaintCtx::control_plate` — the ONE place a control face's
- relief is composed (raised with a face = bevel; raised faceless = carve
- inside + boss; flush = carve inside + inset plate). Button, Dropdown,
+ footprint, per-corner silhouette, `PlateStance` (raised, flush or flat), face
+ and depth, painted by `PaintCtx::control_plate` — the ONE place a control
+ face's relief is composed (raised with a face = bevel; raised faceless =
+ carve inside + boss; flush = carve inside + inset plate). Button, Dropdown,
FontSelector, Breadcrumb and the ButtonStrip's selected plateau draw through
it; the migration was prim-identical against a dump of every face. A new
control face goes through `ControlPlate`, never a hand-rolled carve.
+- **`Flat` is the stance for a control made of its pane's material.** The two
+ relief stances both carve INSIDE the footprint, which costs a control two
+ things a pane has: its visible edge sits half the carve depth in, so a
+ control laid out on the same numbers as a pane does not line up with one;
+ and its face is laid through a stroke, which the blur-behind sentinel (a
+ negative alpha) does not reach, so it cannot be frosted. `Flat` fills the
+ footprint with a quad and nothing else — silhouette equal to the rect,
+ frost carried, focus `tint` drawn as a ring since there is no rim to light.
+ It carries ONE radius, not four, so the concentric corner adjustment a
+ nested relief control computes has no equivalent. Reach for it when a bar
+ or a toolbar should read as plates at a smaller scale rather than as
+ controls of a different kind (`Button::with_flat`, `Dropdown::with_flat`);
+ leave the relief stances alone for things that should feel pressable.
- **Radii are configured per rung, overridden per widget.** Root:
`style.surface.plate.root.corner_radius` (`color::root_plate_corner_radius`).
Pane: `plate_corner_radius`, falling back to the root's. Control:
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index e724202..cb26f83 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -150,6 +150,26 @@ pub enum PlateStance {
/// inside its footprint, with the face as a flat fill when it has one
/// ([`PaintCtx::inset_plate`]).
Flush,
+ /// No relief at all — the face alone, filling the footprint as a flat
+ /// rounded rect. This is the PANE rung's material brought down to the
+ /// control rung, and it exists because the other two stances cannot give
+ /// a control two things a pane has:
+ ///
+ /// - **Its silhouette IS its rect.** `Raised` and `Flush` both carve
+ /// inside the footprint, so their visible edge sits half the carve depth
+ /// in and a control laid out on the same numbers as a pane does not line
+ /// up with it. Nothing is inset here, so it does.
+ /// - **It can be frosted.** The blur-behind sentinel (a negative alpha)
+ /// only reaches quads, and the relief stances lay their face through
+ /// `Border`/`Trough` strokes. This one fills with a quad, so a control
+ /// can be made of the same frosted material as the pane behind it.
+ ///
+ /// The cost is that a flat fill carries ONE radius, not four: the
+ /// per-corner silhouette a nested relief control computes (Dropdown's
+ /// concentric corner adjustment) has no equivalent here, and `radii.0` is
+ /// used for all four corners. A focus `tint` is drawn as a ring, since
+ /// there is no rim to light.
+ Flat,
}
/// A control plate: the thing you press, at the control rung of the plate
@@ -1150,6 +1170,25 @@ impl PaintCtx {
None => self.inset_plate(trough, radii, plate.face, plate.depth),
}
}
+ PlateStance::Flat => {
+ // abs(): a negative alpha is the frost sentinel, a real face.
+ if plate.face[3].abs() > 0.001 {
+ // A QUAD deliberately, not the `Border` the relief stances
+ // fill through: carrying the blur-behind sentinel is half
+ // the point of this stance, and only quads reach it.
+ self.rounded_rect(
+ plate.rect,
+ plate.radii.0,
+ (true, true, true, true),
+ plate.face,
+ );
+ }
+ if let Some(t) = plate.tint {
+ // No relief, so no rim to light: the focus ring is drawn as
+ // one, over the face and keeping the per-corner silhouette.
+ self.border(plate.rect, plate.radii, [0.0; 4], [t[0], t[1], t[2], 1.0], 1.0);
+ }
+ }
}
}
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 9b98ce4..d37a923 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -79,6 +79,9 @@ pub struct Button {
/// Raised style: the background is an SDF-lit `Bevel` plate — fill plus a
/// rolled, lit edge — instead of a flat fill + border stroke.
raised: Option<bool>,
+ /// Flat stance ([`crate::widget::PlateStance::Flat`]): the face alone,
+ /// no relief, silhouette equal to the rect. Overrides `raised`.
+ flat: bool,
}
impl std::fmt::Debug for Button {
@@ -121,6 +124,7 @@ impl Button {
hovered: false,
focused: false,
raised: None,
+ flat: false,
}
}
@@ -244,17 +248,21 @@ impl Button {
/// (flat styling, or a ListRow / MenuItem, transparent-until-hover
/// surfaces that would wear a permanent carved ring on every idle row).
pub fn plate(&self, rect: Rect) -> Option<crate::widget::ControlPlate> {
- if !self.raised()
- || self.kind == ButtonKind::ListRow
- || self.kind == ButtonKind::MenuItem
- {
+ if self.kind == ButtonKind::ListRow || self.kind == ButtonKind::MenuItem {
return None;
}
+ let stance = if self.flat {
+ crate::widget::PlateStance::Flat
+ } else if self.raised() {
+ crate::widget::PlateStance::Flush
+ } else {
+ return None;
+ };
let radius = crate::layout::button_corner_radius();
// Keyboard focus lights the plate's own rim — the ring IS the silhouette.
let tint = self.focused.then(crate::widget::ControlPlate::focus_tint);
Some(
- crate::widget::ControlPlate::control(rect, radius, crate::widget::PlateStance::Flush, self.color())
+ crate::widget::ControlPlate::control(rect, radius, stance, self.color())
.with_tint(tint),
)
}
@@ -307,6 +315,13 @@ impl Adapted<Button> {
self
}
+ /// Draw the face with no relief at all — see
+ /// [`crate::widget::PlateStance::Flat`]. Overrides `with_raised`.
+ pub fn with_flat(mut self, flat: bool) -> Self {
+ self.flat = flat;
+ self
+ }
+
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index ceab67f..b1987d1 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -107,6 +107,11 @@ pub struct Dropdown {
/// Raised style: the closed control's background is an SDF-lit `Bevel`
/// plate (fill + rolled lit edge) instead of a flat fill + border stroke.
raised: Option<bool>,
+ /// Flat stance ([`crate::widget::PlateStance::Flat`]): the trigger's face
+ /// alone, no groove, silhouette equal to its rect. Overrides `raised`.
+ /// The concentric corner adjustment below does not apply — a flat fill
+ /// carries one radius, and the adjustment exists to nest relief outlines.
+ flat: bool,
/// Per-widget override for the trigger plate's FACE, bypassing
/// [`ControlPlate::face_from_fill`] on the configured fill. The default
/// forces the face opaque; an app that wants its controls made of the
@@ -168,6 +173,7 @@ impl Dropdown {
hovered: false,
corner_frame: None,
raised: None,
+ flat: false,
face: None,
focused: false,
menu_replaces_trigger: false,
@@ -439,6 +445,17 @@ impl Dropdown {
// flat outlines, which a rolled edge replaces). A transparent
// configured fill degrades to a Boss: edges only, plate as the face —
// judged on the RAW alpha, before the opacity force above.
+ if self.flat {
+ let plate = crate::widget::ControlPlate::control(
+ Rect { x, y, width: w, height: visual_h },
+ radius,
+ crate::widget::PlateStance::Flat,
+ self.face.unwrap_or_else(|| crate::widget::ControlPlate::face_from_fill(raw_bg)),
+ )
+ .with_tint(self.focused.then(crate::widget::ControlPlate::focus_tint));
+ ctx.control_plate(&plate);
+ return;
+ }
if self.raised() {
let depth = crate::layout::bevel_width().min(visual_h * 0.2);
// Concentric corner_frame adjustment applies to the relief too: a
@@ -767,6 +784,13 @@ impl Adapted<Dropdown> {
self
}
+ /// Draw the trigger with no relief at all — see
+ /// [`crate::widget::PlateStance::Flat`]. Overrides `with_raised`.
+ pub fn with_flat(mut self, flat: bool) -> Self {
+ self.flat = flat;
+ self
+ }
+
pub fn with_custom_display_text(mut self, text: &str) -> Self {
self.custom_display_text = Some(text.to_string());
self