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

commitbd6a7424b423297e6f6248e5c712e0e6616a656e
parent1c40e60459
authorLucas Galante <[email protected]>
date2026-09-21 10:28
Popovers compress their backdrop: style.surface.menu.compression

A menu, a context menu and a dropdown's open list all wear
Material::popover — the root plate colour at menu opacity, frosted at
the DE recipe. The recipe's compression is plate.backdrop_compression,
which is 0 in the shipped config, so a popover passed the backdrop's
mean luminance straight through at (1 - opacity): over the designer's
network pane the node context menu showed the wells and grid lines at
nearly their own brightness under its labels.

A popover now takes its own compression from style.surface.menu.
compression (color::menu_compression, default 0.6), swapped into the
recipe's frost in Material::popover. Menu-scoped rather than the
plate's key because a menu is read over whatever it opened above,
which a pane is not; the two dials are independent and a test pins
that. Verified in a scale-2 shadow on the designer's node context
menu: the face settles on the tint's key with the backdrop reduced to
a faint blur beneath the items.

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

 src/color.rs          | 21 +++++++++++++++++++++
 src/scene/material.rs | 28 ++++++++++++++++++++++++++--
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/src/color.rs b/src/color.rs
index 5919133..acc16b6 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -112,6 +112,13 @@ static ROOT_PLATE_MENUBAR_BLUR: RwLock<bool> = RwLock::new(false);
 /// Tint strength of frosted menus/popovers over the blurred backdrop:
 /// 1.0 is fully opaque (frost invisible), lower shows more content through.
 static MENU_OPACITY: RwLock<f32> = RwLock::new(0.8);
+/// Backdrop compression of frosted menus/popovers (`style.surface.menu.
+/// compression`, 0..1): how hard the blurred content beneath a menu is
+/// pulled toward the menu's own key, so the menu holds its legibility over
+/// whatever it opens above. Menu-scoped, overriding the DE recipe's
+/// `plate.backdrop_compression` for popovers only; a menu is read while
+/// something else is going on beneath it, which a pane is not.
+static MENU_COMPRESSION: RwLock<f32> = RwLock::new(0.6);
 
 static ROOT_PLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.10, 1.0]);
 static ROOT_PLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
@@ -341,6 +348,9 @@ fn parse_and_set_colors(content: &str) {
     if let Some(o) = val.pointer("/style/surface/menu/opacity").and_then(|v| v.as_f64()) {
         if let Ok(mut lock) = MENU_OPACITY.write() { *lock = (o as f32).clamp(0.0, 1.0); }
     }
+    if let Some(c) = val.pointer("/style/surface/menu/compression").and_then(|v| v.as_f64()) {
+        if let Ok(mut lock) = MENU_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
+    }
 
     let menubar_blur_ptr = val.pointer("/style/surface/plate/root/menubar/blur");
     if let Some(blur) = menubar_blur_ptr.and_then(|v| v.as_bool()) {
@@ -1212,6 +1222,17 @@ pub fn menu_opacity() -> f32 {
     style_read(&MENU_OPACITY)
 }
 
+/// Backdrop compression of frosted menus/popovers
+/// (`/style/surface/menu/compression`, default 0.6) — see `MENU_COMPRESSION`.
+pub fn menu_compression() -> f32 {
+    load_colors_once();
+    style_read(&MENU_COMPRESSION)
+}
+
+pub fn set_menu_compression(c: f32) {
+    style_write(&MENU_COMPRESSION, c.clamp(0.0, 1.0));
+}
+
 pub fn read_root_plate_opacity_if_configured() -> Option<f32> {
     load_colors_once();
     style_read(&ROOT_PLATE_OPACITY)
diff --git a/src/scene/material.rs b/src/scene/material.rs
index ffd0ce3..c2370ac 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -390,9 +390,17 @@ impl Material {
     /// (`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.
+    /// beneath it blurred and tinted rather than covering it, with the
+    /// recipe's compression replaced by the menu's own
+    /// (`style.surface.menu.compression`, `color::menu_compression`): a menu
+    /// is read over whatever it opened above, so it holds its key harder
+    /// than a pane does.
     pub fn popover(base: [f32; 4]) -> Self {
-        Self::opaque([base[0], base[1], base[2], crate::color::menu_opacity()]).with_frost(Frost::from_style())
+        let mut frost = Frost::from_style();
+        if let Frost::Frosted { compression, .. } = &mut frost {
+            *compression = crate::color::menu_compression();
+        }
+        Self::opaque([base[0], base[1], base[2], crate::color::menu_opacity()]).with_frost(frost)
     }
 
     // ---- derived materials -------------------------------------------------
@@ -721,6 +729,22 @@ mod tests {
         assert!(m.frost.is_frosted());
     }
 
+    /// A popover's frost compresses at the menu key, not the DE recipe's:
+    /// the two are independent dials.
+    #[test]
+    fn popover_compresses_at_the_menu_key() {
+        let _lock = crate::color::test_color_state_lock();
+        crate::color::set_plate_backdrop_compression(0.1);
+        crate::color::set_menu_compression(0.7);
+        let m = Material::popover([0.1, 0.2, 0.3, 1.0]);
+        let Frost::Frosted { compression, .. } = m.frost else { panic!("popover is frosted") };
+        assert!((compression - 0.7).abs() < 1e-6, "popover compression {compression}");
+        let Frost::Frosted { compression: pane, .. } = Frost::from_style() else { panic!("recipe is frosted") };
+        assert!((pane - 0.1).abs() < 1e-6, "recipe compression {pane}");
+        crate::color::set_plate_backdrop_compression(0.0);
+        crate::color::set_menu_compression(0.6);
+    }
+
     /// The control-face rule: opaque or nothing.
     #[test]
     fn control_face_is_opaque_or_none() {