GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: a button's flush plate wears the field run's edge
As the dropdown trigger does: one edge for every flush control in a
parameter pane. The pane's button rows are fields too, where they had
been a raised boss the button itself never drew.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 10 ++++++----
src/widget/container/parameters_bg.rs | 18 +++++++++++++++++-
src/widget/input/button.rs | 35 ++++++++++++++++++++++++-----------
3 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6aea560..5199f25 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -347,10 +347,12 @@ its seam `FIELD_RUN_ONLY` px to its left (`PaintCtx::flush_run`), in place of
`Prim::Trough` — `ControlPlate::with_run_edge`, which the Dropdown's raised trigger sets,
the expanded menu's plate (`RenderTarget::flush_run`, which a host without the prim
answers with its inset plate), and the pane's dropdown rows, now in `fields` (the pane's
-`troughs()` list went: nothing in the pane draws a trough any more). So every flush
-control in a parameter pane has one edge. Other flush controls — buttons, breadcrumbs,
-the font selector — still wear the trough. `a_dropdown_trigger_wears_the_runs_edge` is
-the test.
+`troughs()` list went: nothing in the pane draws a trough any more). **So does every
+button's flush plate** (`Button::plate` sets the run edge; the pane's button rows are
+fields too, where they had been a raised boss the button itself never drew). So every
+flush control in a parameter pane has one edge. Breadcrumbs, the font selector and the
+other `inset_plate` callers still wear the trough. `a_dropdown_trigger_wears_the_runs_edge`
+and `focus_lights_the_plate_rim` are the tests.
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`
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index cad86ee..4f5dac2 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1731,7 +1731,10 @@ impl ParametersBg {
// the widget itself never draws.
None
} else if p.2 == "button" {
- self.buttons[i].as_ref().map(|w| (w as &dyn WidgetHost, crate::layout::button_corner_radius(), true))
+ // A flush control with a field run's edge, as its own paint
+ // draws it ([`Self::fields`]) — it was a boss here, a raised
+ // island the button itself never drew.
+ None
} else if p.2 == "toggle" || p.2 == "checkbox" {
// A toggle paints no fill at all, so these carves — the
// widget's own (`Toggle::flat_carves`, exactly what its paint
@@ -1849,6 +1852,19 @@ impl ParametersBg {
out.push((x, y + ty, dx + dw - x, h - ty, (r, r, r, r), depth, dx));
}
}
+ } else if p.2 == "button" {
+ // A button: a field that is all run, as its own flush plate
+ // (`Button::plate`, `ControlPlate::with_run_edge`) on its
+ // band, at the button radius.
+ if let Some(b) = &self.buttons[i] {
+ let (x, y, w, h) = b.rect();
+ let ty = b.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::button_corner_radius();
+ out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - crate::scene::paint::FIELD_RUN_ONLY));
+ }
+ }
} else if p.2.starts_with("choice") {
// The dropdown trigger: a field that is all run — the
// widget's own raised paint (`ControlPlate::with_run_edge`)
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 363b942..261e73b 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -293,9 +293,14 @@ impl Button {
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);
+ // A flush plate wears a field run's edge (`ControlPlate::with_run_edge`),
+ // as the dropdown trigger does: one edge for every flush control
+ // beside the wells and fields of a pane, where a trough's outer half
+ // read as a compressed copy of a well's.
Some(
crate::widget::ControlPlate::control(rect, radius, stance, crate::scene::Material::face(self.color()))
- .with_tint(tint),
+ .with_tint(tint)
+ .with_run_edge(stance == crate::widget::PlateStance::Flush),
)
}
@@ -495,9 +500,8 @@ impl Paint for Button {
let radius = crate::layout::button_corner_radius();
let color = self.color();
- // Relief style: a flush inset plate — the button sits sunken in a
- // carved groove ring with its beveled lip rising back to the surface
- // plane, face level with the surface. Transparent fills degrade to
+ // Relief style: a flush plate — its face level with the surface, its
+ // edge a field run's (a well's fall, mirrored back up to the face). Transparent fills degrade to
// edges-only inside the groove (an opaque hover_color fills the face).
// List rows are exempt: they are transparent-until-hover/selected
// surfaces, and the edges-only groove would stack a permanent carved
@@ -872,24 +876,33 @@ mod focus_ring_tests {
use crate::scene::paint::{PaintCtx, Prim};
use crate::widget::{Event, WidgetHost};
- /// The focus ring is the plate's own rim lit: focused, the trough carries
- /// the highlight tint; unfocused, the same trough untinted — no extra geometry.
+ /// The focus ring is the plate's own rim lit: focused, its edge (a field
+ /// that is all run) carries the highlight tint; unfocused, the same edge
+ /// untinted — no extra geometry. Never a trough.
#[test]
fn focus_lights_the_plate_rim() {
let mut ctx = crate::widget::UiContext::new();
let mut b = Button::new(0.0, 0.0, 120.0, 26.0).with_label("Plate").with_raised(true);
WidgetHost::set_rect(&mut b, 10.0, 20.0, 120.0, 26.0);
let rect = Rect { x: 10.0, y: 20.0, width: 120.0, height: 26.0 };
- let troughs = |b: &Adapted<Button>| -> Vec<Option<[f32; 3]>> {
+ let edges = |b: &Adapted<Button>| -> Vec<Option<[f32; 3]>> {
let mut pc = PaintCtx::new();
Paint::paint(b.inner(), rect, &mut pc);
- pc.finish().items.into_iter().filter_map(|i| match i.prim { Prim::Trough { tint, .. } => Some(tint), _ => None }).collect()
+ let items = pc.finish().items;
+ assert!(!items.iter().any(|i| matches!(i.prim, Prim::Trough { .. })), "no trough");
+ items
+ .into_iter()
+ .filter_map(|i| match i.prim {
+ Prim::Field { rect, split, tint, .. } if split < rect.x - 100.0 => Some(tint),
+ _ => None,
+ })
+ .collect()
};
- assert_eq!(troughs(&b), vec![None], "unfocused: one untinted trough");
+ assert_eq!(edges(&b), vec![None], "unfocused: one untinted edge, a field that is all run");
b.handle_event(&Event::FocusIn, &mut ctx);
- assert_eq!(troughs(&b), vec![Some(crate::widget::ControlPlate::focus_tint())], "focused: the rim lit");
+ assert_eq!(edges(&b), vec![Some(crate::widget::ControlPlate::focus_tint())], "focused: the rim lit");
b.handle_event(&Event::FocusOut, &mut ctx);
- assert_eq!(troughs(&b), vec![None]);
+ assert_eq!(edges(&b), vec![None]);
}
}