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

commit5ac63626721488065e5dc5c0eb9c1f340915033c
parenta693b97c4e
authorLucas Galante <[email protected]>
date2026-08-25 10:09
button: an icon face has a rect and an opacity a flat host can read

`Button::paint` computed its icon rect inline and drew it at a hardcoded
alpha 1.0. Neither was reachable from a flat-path host, which consumes
`all_quads` and a text list — an image is neither a quad nor a label, so a
host that wants to draw the icon itself had to re-derive where it goes.

`icon_rect(rect)` is that geometry, now public and read by `paint` as well,
so the glyph lands in the same place on both paths. `with_icon_alpha` gives
the icon the one state lever it can have: `PaintCtx::image` carries no color
and images ignore vertex color, so a disabled icon button dims rather than
graying its glyph the way a label would.

 src/widget/input/button.rs | 57 ++++++++++++++++++++++++++++++++++------------
 1 file changed, 43 insertions(+), 14 deletions(-)

diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 0b0bd71..c7cb64d 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -35,6 +35,10 @@ pub struct Button {
     /// Icon face: an uploaded texture `(image id, pixel w, pixel h)` drawn
     /// centered in place of the label (see [`crate::upload_icon`]).
     icon: Option<(u32, f32, f32)>,
+    /// Opacity of the icon face — the ONLY state lever an icon has, since
+    /// `PaintCtx::image` carries no color and images ignore vertex color. A
+    /// disabled icon button dims instead of graying its glyph.
+    icon_alpha: f32,
     hovered: bool,
     /// Keyboard focus, tracked from `FocusIn`/`FocusOut` the way Checkbox does —
     /// `Paint` never sees the `Widget` base, so the flag has to live here to be
@@ -73,6 +77,7 @@ impl Button {
             justify: Justification::Center,
             label: None,
             icon: None,
+            icon_alpha: 1.0,
             hovered: false,
             focused: false,
             raised: crate::layout::control_relief(),
@@ -112,6 +117,34 @@ impl Button {
         self.icon.is_some()
     }
 
+    /// Where the icon face draws inside `rect`: centered, inset one 4px margin
+    /// per side from the shorter extent, native aspect kept. `None` when this
+    /// button has no icon.
+    ///
+    /// Public because a flat-path host draws the icon itself — it consumes
+    /// `all_quads` and a text list, so `paint` never runs for it and an image
+    /// is neither a quad nor a label. Keeping the geometry here means the icon
+    /// lands in the same place on both paths.
+    pub fn icon_rect(&self, rect: Rect) -> Option<(u32, Rect, f32)> {
+        let (image, iw, ih) = self.icon?;
+        let s = (rect.width.min(rect.height) - 8.0).max(4.0);
+        let (dw, dh) = if iw >= ih {
+            (s, s * ih / iw.max(1.0))
+        } else {
+            (s * iw / ih.max(1.0), s)
+        };
+        Some((
+            image,
+            Rect {
+                x: rect.x + (rect.width - dw) / 2.0,
+                y: rect.y + (rect.height - dh) / 2.0,
+                width: dw,
+                height: dh,
+            },
+            self.icon_alpha,
+        ))
+    }
+
     /// Hover state, also settable by immediate-mode hosts that hit-test themselves.
     pub fn hovered(&self) -> bool {
         self.hovered
@@ -170,6 +203,12 @@ impl Adapted<Button> {
         self
     }
 
+    /// Dim the icon face — see the `icon_alpha` field. 1.0 is fully opaque.
+    pub fn with_icon_alpha(mut self, alpha: f32) -> Self {
+        self.icon_alpha = alpha;
+        self
+    }
+
     /// Raised style: see the `raised` field.
     pub fn with_raised(mut self, raised: bool) -> Self {
         self.raised = raised;
@@ -354,20 +393,10 @@ impl Paint for Button {
             }
         }
 
-        // Icon face: centered, inset one 4px margin per side from the shorter
-        // extent, native aspect kept. Replaces the label.
-        if let Some((image, iw, ih)) = self.icon {
-            let s = (w.min(h) - 8.0).max(4.0);
-            let (dw, dh) = if iw >= ih {
-                (s, s * ih / iw.max(1.0))
-            } else {
-                (s * iw / ih.max(1.0), s)
-            };
-            ctx.image(
-                image,
-                Rect { x: x + (w - dw) / 2.0, y: y + (h - dh) / 2.0, width: dw, height: dh },
-                1.0,
-            );
+        // Icon face: replaces the label. Geometry from `icon_rect` — see there
+        // for why it is not inlined here.
+        if let Some((image, rect, alpha)) = self.icon_rect(rect) {
+            ctx.image(image, rect, alpha);
             return;
         }