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

commitc86d6f666bb0e81148606808e3a4351f43c8171b
parentcc36c1f310
authorLucas Galante <[email protected]>
date2026-09-20 14:30
No widget writes the frost sentinel by hand (RFC material, step 2d)

The context menu, the Dropdown popover and the menubar panel shared one
recipe spelled three times — the page or dropdown colour at menu_opacity
with the alpha negated — and the Graph negated its blur value into an
alpha. Material::popover is that recipe, and the Graph's plate is an
opaque or frosted Material; each site encodes through fill(Nested)
where the colour-typed flat path needs a colour. Material::fill_tint is
now the only place in the toolkit that writes a negative alpha.

Prim-identical against the pre-2a golden; 379 lib tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/scene/material.rs        | 21 +++++++++++++++++++++
 src/widget/container/menu.rs | 10 +++++-----
 src/widget/core.rs           |  9 ++++-----
 src/widget/display/graph.rs  | 17 ++++++++++++-----
 src/widget/input/dropdown.rs | 17 ++++++-----------
 5 files changed, 48 insertions(+), 26 deletions(-)

diff --git a/src/scene/material.rs b/src/scene/material.rs
index 74aef9b..49ebd46 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -213,6 +213,16 @@ impl Material {
         }
     }
 
+    /// The popover material: a menu, a context menu, a dropdown's open
+    /// surface. `base`'s colour at `style.surface.menu.opacity`
+    /// (`color::menu_opacity`) — not the colour's own alpha, since a page
+    /// colour is typically opaque and would resolve the frost to a solid
+    /// tint — and frosted at the DE recipe, so a menu shows the content
+    /// beneath it blurred and tinted rather than covering it.
+    pub fn popover(base: [f32; 4]) -> Self {
+        Self::opaque([base[0], base[1], base[2], crate::color::menu_opacity()]).with_frost(Frost::from_style())
+    }
+
     // ---- derived materials -------------------------------------------------
 
     /// The well floor cut into this plate: the same material with the tint
@@ -379,6 +389,17 @@ mod tests {
         assert_eq!(Material::face([0.3, 0.3, 0.3, 0.7]).map(|m| m.tint), Some([0.3, 0.3, 0.3, 0.7]));
     }
 
+    /// A popover is the base colour at menu opacity, frosted — the bytes the
+    /// three menu sites used to write by negating an alpha.
+    #[test]
+    fn popover_is_the_menu_recipe() {
+        let m = Material::popover([0.1, 0.2, 0.3, 1.0]);
+        let mut old = [0.1, 0.2, 0.3, 1.0];
+        old[3] = -crate::color::menu_opacity();
+        assert_eq!(m.fill(PlateRole::Nested), old);
+        assert!(m.frost.is_frosted());
+    }
+
     /// The control-face rule: opaque or nothing.
     #[test]
     fn control_face_is_opaque_or_none() {
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 1b925e4..2934b7f 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -713,11 +713,11 @@ impl Paint for MenuBar {
             pc.rect([0.02, 0.02, 0.05, 0.08], dx + 3.0, dy + 3.0, dw, dh);
             pc.rect([0.02, 0.02, 0.05, 0.04], dx + 5.0, dy + 5.0, dw, dh);
             pc.rect(theme.surface_border, dx, dy, dw, dh);
-            // Frosted, like the Dropdown popover and the context menu (the
-            // negative-alpha blur-behind sentinel): menus show what is
-            // beneath them blurred and tinted, not covered.
-            let mut bg = theme.surface_bg;
-            bg[3] = -crate::color::menu_opacity();
+            // Frosted, like the Dropdown popover and the context menu: the
+            // popover material (`Material::popover`), encoded for the
+            // colour-typed flat path — menus show what is beneath them
+            // blurred and tinted, not covered.
+            let bg = crate::scene::Material::popover(theme.surface_bg).fill(crate::scene::PlateRole::Nested);
             pc.rect(bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
             if let Some(di) = hovered {
                 let iy = dy + di as f32 * DROPDOWN_ITEM_H;
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 297d855..d455ce5 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -518,11 +518,10 @@ pub mod context_menu {
             let depth = crate::layout::bevel_width().min(self.h * 0.2);
             let face = crate::color::page_low_color();
             if face[3] > 0.001 {
-                let mut frosted = face;
-                // menu_opacity, not the color's own alpha: an opaque page
-                // color resolved the frost to a solid tint (invisible).
-                frosted[3] = -crate::color::menu_opacity();
-                ctx.plate(rect, (r, r, r, r), &crate::scene::material::Material::from_fill(frosted), depth);
+                // The popover material: the page colour at menu_opacity,
+                // frosted (an opaque page colour would resolve the frost
+                // to a solid tint, invisible).
+                ctx.plate(rect, (r, r, r, r), &crate::scene::material::Material::popover(face), depth);
             } else {
                 let (plateau, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
                 ctx.boss(plateau, radii, depth);
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 3bbfbe7..06d53ca 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -340,17 +340,24 @@ impl Graph {
         }
     }
 
+    /// The graph's plate, as the colour-typed host paints it: the cell
+    /// colour (or a near-transparent black) at the network opacity, and
+    /// under `graph_blur` a frosted material whose tint alpha IS the blur
+    /// value — the knob doubles as the frost's opacity.
     fn bg_color(&self) -> [f32; 4] {
-        let mut c = if self.uniform_background {
+        use crate::scene::{Frost, Material, PlateRole};
+        let c = if self.uniform_background {
             [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity]
         } else {
             [0.0, 0.0, 0.0, 0.01 * self.network_opacity]
         };
         let blur_val = crate::layout::graph_blur();
-        if blur_val > 0.0 {
-            c[3] = -blur_val.abs() * self.network_opacity;
-        }
-        c
+        let m = if blur_val > 0.0 {
+            Material::opaque([c[0], c[1], c[2], blur_val.abs() * self.network_opacity]).with_frost(Frost::from_style())
+        } else {
+            Material::opaque(c)
+        };
+        m.fill(PlateRole::Nested)
     }
 
     /// The grid cells' rounded-corner radius at the current zoom: the desktop
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 95f518f..ab1bd36 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -941,20 +941,15 @@ impl Paint for Dropdown {
         // PaintCtx-backed target; collector hosts degrade to a rounded fill).
         // The trigger's configured fill is usually transparent — the window
         // plate IS its face — so the expansion substitutes the plate color,
-        // FROSTED: the negative-alpha blur-behind sentinel, the same
-        // material the context menu wears (`ContextMenuState::paint`), so
-        // the menu shows the content beneath it blurred and tinted rather
-        // than covering it. The magnitude is the color's own alpha — an
-        // app styled fully opaque stays opaque, matching its windows.
+        // FROSTED: the popover material (`Material::popover`), the same the
+        // context menu wears (`ContextMenuState::paint`), so the menu shows
+        // the content beneath it blurred and tinted rather than covering it.
+        // Encoded here because the flat-path `RenderTarget` speaks colours.
         let radius = crate::layout::dropdown_corner_radius();
         let raw_bg = colors::dropdown_background_color();
         let face = {
-            let mut c = if raw_bg[3] > 0.001 { raw_bg } else { crate::color::page_low_color() };
-            // Magnitude from menu_opacity, not the color's own alpha: page
-            // colors are typically fully opaque, which would resolve the
-            // frost to a solid tint and hide it entirely.
-            c[3] = -crate::color::menu_opacity();
-            c
+            let base = if raw_bg[3] > 0.001 { raw_bg } else { crate::color::page_low_color() };
+            crate::scene::Material::popover(base).fill(crate::scene::PlateRole::Nested)
         };
         if self.raised() {
             let depth = crate::layout::bevel_width().min(rect.height * 0.2);