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

commit69ab3c24bdd139fdae37f38a111e9a18d3aeb227
parentc1fc81df4e
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 21:50
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]);
     }
 }