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

commit06c6190ae75fce047e51c33076d8d3d966bd227e
parentaa91476439
authorLucas Galante <[email protected]>
date2026-08-26 15:16
feat: ButtonKind::MenuItem — a row that lets its menu own the relief

A menu is one carved thing, not a column of separately carved buttons:
the group shares a recess and the items butt together inside it. A
MenuItem therefore draws no inset plate and no border of its own, and is
transparent until hovered so the well reads as continuous.

Neither existing kind fits. Primary carves a groove per button (and,
once its fill goes transparent, paints a solid border block instead).
ListRow drops the plate and border but also switches to list typography
and IGNORES with_justify, which menus set explicitly.

A new variant rather than another bool: the compiler then finds every
match on the kind, which the existing  chains would not have.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/widget/input/button.rs | 30 ++++++++++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)

diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index c7cb64d..b2a3ee2 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -18,6 +18,12 @@ pub enum ButtonKind {
     Reset,
     ListRow,
     CopyIcon,
+    /// A row in a menu: no plate and no border of its own, transparent until
+    /// hovered, because a menu draws ONE recess around the whole run and the
+    /// items butt together inside it. Keeps button typography and honours
+    /// `with_justify`, which is what separates it from `ListRow` (list font,
+    /// list justification config).
+    MenuItem,
 }
 
 #[derive(Clone)]
@@ -102,6 +108,12 @@ impl Button {
         Button::adapted(ButtonKind::ListRow, x, y, w, h)
     }
 
+    /// A menu row — see [`ButtonKind::MenuItem`]. The host draws the shared
+    /// recess; this draws only its label and its hover.
+    pub fn new_menu_item(x: f32, y: f32, w: f32, h: f32) -> Adapted<Button> {
+        Button::adapted(ButtonKind::MenuItem, x, y, w, h)
+    }
+
     pub fn new_copy_icon(x: f32, y: f32, w: f32, h: f32) -> Adapted<Button> {
         let b = Button::adapted(ButtonKind::CopyIcon, x, y, w, h);
         // Copy icon face (cce-icons); label fallback if the icon set is
@@ -182,7 +194,10 @@ impl Button {
     /// `layout::render_widget` both read, so a flat host's groove can't drift
     /// from the drawn one.
     pub fn inset_face(&self, rect: Rect) -> Option<(Rect, f32, f32, [f32; 4])> {
-        if !self.raised || self.kind == ButtonKind::ListRow {
+        if !self.raised
+            || self.kind == ButtonKind::ListRow
+            || self.kind == ButtonKind::MenuItem
+        {
             return None;
         }
         let radius = crate::layout::button_corner_radius();
@@ -285,6 +300,17 @@ impl Paint for Button {
                     colors::button_background_color()
                 }
             }
+            ButtonKind::MenuItem => {
+                // Idle is fully transparent so the shared recess reads as one
+                // continuous well; only the hovered row lifts out of it.
+                if self.pressed {
+                    colors::button_press_color()
+                } else if self.hovered {
+                    colors::button_hover_color()
+                } else {
+                    [0.0, 0.0, 0.0, 0.0]
+                }
+            }
             ButtonKind::Reset => {
                 if self.pressed {
                     colors::RESET_BTN_PRESS
@@ -366,7 +392,7 @@ impl Paint for Button {
             // opt-out too: a focused row must show the ring, which is the whole point.
             let border_color = if self.focused {
                 Some(colors::tree_border_focus_color())
-            } else if self.kind == ButtonKind::ListRow {
+            } else if self.kind == ButtonKind::ListRow || self.kind == ButtonKind::MenuItem {
                 None
             } else {
                 colors::button_border_color()