GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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() {