GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: frosted menus — dropdowns and menubar popovers blur what they cover
The Dropdown popover forced its face opaque ("the menu must cover the
content beneath it") and menu.rs's panels painted solid surface_bg — both
predating the blur-behind frost the context menu already wears. Menus now
share one material: the negative-alpha sentinel, so an open menu shows the
app's content beneath it blurred and tinted instead of covered. The
renderer already does the work — at the first frosted plate the UI pass
snapshots the frame-so-far and blur plates sample it.
The tint magnitude is a new style knob rather than the plate color's own
alpha: page colors are typically fully opaque, which resolved the frost
mix to a solid tint — the context menu has been "frosted" invisibly for
exactly that reason. `/style/surface/menu/opacity` (default 0.8) now sets
the strength for all three popover flavors — Dropdown, menu.rs panels,
and ContextMenuState::paint — and 1.0 restores full opacity.
inset_plate's fill gate reads |alpha| now: a frosted face is a real face,
and the positive-only test silently dropped it.
Measured in cce-files with the view dropdown open over the selected row:
the popover face shifts [~0, +6, +11] where the blue row passes beneath —
about 20% of the raw content delta, consistent with 0.8 tint over the
blurred snapshot.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/color.rs | 14 ++++++++++++++
src/scene/paint.rs | 4 +++-
src/widget/container/menu.rs | 7 ++++++-
src/widget/core.rs | 4 +++-
src/widget/input/dropdown.rs | 21 ++++++++++++---------
5 files changed, 38 insertions(+), 12 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index e15d8eb..e6b8e16 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -63,6 +63,9 @@ static TEXTBOX_BACKGROUND_EDIT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.10
static BACKPLATE_MENUBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
static BACKPLATE_MENUBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]);
static BACKPLATE_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);
static BACKPLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.10, 1.0]);
static BACKPLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
@@ -312,6 +315,10 @@ fn parse_and_set_colors(content: &str) {
{
if let Ok(mut lock) = BACKPLATE_MENUBAR_TEXT_COLOR.write() { *lock = c; }
}
+ 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); }
+ }
+
let menubar_blur_ptr = val.pointer("/style/surface/plate/root/menubar/blur")
.or_else(|| val.pointer("/style/surface/backplate/menubar/blur"));
if let Some(blur) = menubar_blur_ptr.and_then(|v| v.as_bool()) {
@@ -1161,6 +1168,13 @@ pub fn read_opacity_if_configured() -> Option<f32> {
*OPACITY.read().unwrap()
}
+/// Tint strength for frosted menus/popovers (`/style/surface/menu/opacity`,
+/// default 0.8): the |alpha| of the blur-behind sentinel their plates carry.
+pub fn menu_opacity() -> f32 {
+ load_colors_once();
+ *MENU_OPACITY.read().unwrap()
+}
+
pub fn read_backplate_opacity_if_configured() -> Option<f32> {
load_colors_once();
*BACKPLATE_OPACITY.read().unwrap()
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 866dfc7..97c0ebf 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -956,7 +956,9 @@ impl PaintCtx {
/// An opaque `color` fills the face; transparent leaves the surface below
/// showing through as the face.
pub fn inset_plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
- if color[3] > 0.001 {
+ // abs(): negative alpha is the blur-behind frost sentinel, a real
+ // face — only a genuinely transparent color skips the fill.
+ if color[3].abs() > 0.001 {
// Flat fill only — the relief is the trough's, so the face must not
// carry a lip of its own (that lip WAS the second wall).
//
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 0818949..59435bf 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -694,7 +694,12 @@ 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);
- pc.rect(theme.surface_bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
+ // 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();
+ 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;
pc.rect(theme.primary_accent, dx + 2.0, iy + 2.0, dw - 4.0, DROPDOWN_ITEM_H - 4.0);
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 305af91..4fa37a5 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -495,7 +495,9 @@ pub mod context_menu {
let face = crate::color::page_low_color();
if face[3] > 0.001 {
let mut frosted = face;
- frosted[3] = -frosted[3];
+ // 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), frosted, depth);
} else {
ctx.boss(rect, (r, r, r, r), depth);
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 1369df4..5fda2f0 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -979,17 +979,20 @@ impl Paint for Dropdown {
// inset plate grown over the unified box (real relief prims on a
// 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 opaque plate
- // color: the menu must cover the content beneath it.
+ // 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.
let radius = crate::layout::dropdown_corner_radius();
let raw_bg = colors::dropdown_background_color();
- let face = if raw_bg[3] > 0.001 {
- let mut c = raw_bg;
- c[3] = 1.0;
- c
- } else {
- let mut c = crate::color::page_low_color();
- c[3] = 1.0;
+ 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
};
if self.raised {