GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: style.surface.menu.color, Material::menu and a shared menu-plate paint
A menu's face was always the root plate colour (page_low_color), so an
app could not darken its menus without darkening its window. The new
`style.surface.menu.color` key names the face on its own; absent, the
root plate colour as before, so no config changes look. Its alpha is
ignored — `menu.opacity` is the tint strength, as it was.
Material::menu is popover(menu_color): THE menu material, for a context
menu and for any surface that should read as one. context_menu::
paint_menu_plate paints that plate over a rect — the material, the menu
corner radius and the relief-width roll, with the popup-hosted opacity
fold when asked — and ContextMenuState::paint now draws its plate
through it, so a host that paints another surface (cce-designer's
command palette) with the same call cannot be configured apart from
the menus.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/color.rs | 24 +++++++++++++++++++
src/scene/material.rs | 9 +++++++
src/widget/core.rs | 66 +++++++++++++++++++++++++++++++--------------------
3 files changed, 73 insertions(+), 26 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index acc16b6..fc253e4 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -119,6 +119,13 @@ static MENU_OPACITY: RwLock<f32> = RwLock::new(0.8);
/// `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);
+/// The colour a menu's face is tinted with (`style.surface.menu.color`),
+/// when the config names one; `None` follows the root plate colour
+/// (`page_low_color`), which is what every menu wore before the key
+/// existed. The alpha is ignored — `menu.opacity` is the tint strength —
+/// so a menu can be dark on a light window without the window's own
+/// plate going dark with it.
+static MENU_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
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]);
@@ -351,6 +358,9 @@ fn parse_and_set_colors(content: &str) {
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); }
}
+ if let Some(c) = get_color("/style/surface/menu/color") {
+ if let Ok(mut lock) = MENU_COLOR.write() { *lock = Some(c); }
+ }
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()) {
@@ -1233,6 +1243,20 @@ pub fn set_menu_compression(c: f32) {
style_write(&MENU_COMPRESSION, c.clamp(0.0, 1.0));
}
+/// The colour a menu's face is tinted with: `style.surface.menu.color` when
+/// configured, else the root plate colour (`page_low_color`) — the face
+/// every menu wore before the key existed, so an unconfigured menu looks as
+/// it always did. Its alpha is not the tint strength; `menu_opacity` is
+/// (`Material::popover` reads both).
+pub fn menu_color() -> [f32; 4] {
+ load_colors_once();
+ style_read(&MENU_COLOR).unwrap_or_else(page_low_color)
+}
+
+pub fn set_menu_color(c: Option<[f32; 4]>) {
+ style_write(&MENU_COLOR, c);
+}
+
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 c2370ac..6fe26fb 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -403,6 +403,15 @@ impl Material {
Self::opaque([base[0], base[1], base[2], crate::color::menu_opacity()]).with_frost(frost)
}
+ /// THE menu material: [`Material::popover`] of `color::menu_color` —
+ /// `style.surface.menu.color`, else the root plate colour. What a
+ /// context menu's plate is made of, and what any other surface that
+ /// should read as one (a command palette, a modal list) asks for, so
+ /// the `style.surface.menu` block is the one place both are configured.
+ pub fn menu() -> Self {
+ Self::popover(crate::color::menu_color())
+ }
+
// ---- derived materials -------------------------------------------------
/// The well floor cut into this plate: the same material with the tint
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 584a250..86368a4 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -841,32 +841,7 @@ pub mod context_menu {
};
let r = crate::layout::menu_corner_radius();
let depth = crate::layout::bevel_width().min(self.h * 0.2);
- let face = crate::color::page_low_color();
- if face[3] > 0.001 {
- // The popover material: the page colour at menu_opacity,
- // frosted (an opaque page colour would resolve the frost
- // to a solid tint, invisible).
- let material = crate::scene::material::Material::popover(face);
- let material = if self.in_popup {
- // The compositor's blur frosts but cannot COMPRESS: the
- // in-app pass pulls the backdrop's luminance a fraction
- // `k` toward the plate's key, which is what keeps the
- // labels legible over a bright scene. Over glass that
- // only blurs, hold the same swing with opacity instead —
- // the backdrop reaches the eye at (1 - a)(1 - k) either
- // way — or a menu opened over something white washes out.
- let k = crate::color::menu_compression().clamp(0.0, 1.0);
- let mut m = material.for_role(crate::scene::material::PlateRole::Root);
- m.tint[3] = 1.0 - (1.0 - m.tint[3]) * (1.0 - k);
- m
- } else {
- material
- };
- ctx.plate(rect, (r, r, r, r), &material, depth);
- } else {
- let (plateau, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
- ctx.boss(plateau, radii, depth);
- }
+ paint_menu_plate(ctx, rect, self.in_popup);
// What the rows draw — hover, separators, slider bands — is cut at
// the plate, so a row scrolled half out of a shortened menu stops
@@ -1063,6 +1038,45 @@ pub mod context_menu {
}
}
+ /// The menu PLATE alone, over `rect`: [`Material::menu`] on a rounded
+ /// face at `style.surface.menu.corner_radius` with the rolled perimeter
+ /// at the relief width (capped at a fifth of the height) — or, for a
+ /// transparent configured face, the edges-only boss. What
+ /// [`ContextMenuState::paint`] draws under its rows, and what any other
+ /// surface that should look like a menu draws under its own (the
+ /// designer's command palette): one function, so the two cannot be
+ /// configured apart.
+ ///
+ /// `in_popup` says the plate is being painted into the runner's popup
+ /// surface, where the compositor's blur frosts but cannot COMPRESS: the
+ /// in-app pass pulls the backdrop's luminance a fraction `k` toward the
+ /// plate's key, which is what keeps the labels legible over a bright
+ /// scene. Over glass that only blurs, the same swing is held with
+ /// opacity instead — the backdrop reaches the eye at (1 - a)(1 - k)
+ /// either way — or a menu opened over something white washes out.
+ ///
+ /// [`Material::menu`]: crate::scene::material::Material::menu
+ pub fn paint_menu_plate(ctx: &mut crate::scene::paint::PaintCtx, rect: crate::scene::layout::Rect, in_popup: bool) {
+ let r = crate::layout::menu_corner_radius();
+ let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+ let face = crate::color::menu_color();
+ if face[3] > 0.001 {
+ let material = crate::scene::material::Material::menu();
+ let material = if in_popup {
+ let k = crate::color::menu_compression().clamp(0.0, 1.0);
+ let mut m = material.for_role(crate::scene::material::PlateRole::Root);
+ m.tint[3] = 1.0 - (1.0 - m.tint[3]) * (1.0 - k);
+ m
+ } else {
+ material
+ };
+ ctx.plate(rect, (r, r, r, r), &material, depth);
+ } else {
+ let (plateau, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
+ ctx.boss(plateau, radii, depth);
+ }
+ }
+
thread_local! {
pub static CONTEXT_MENU: RefCell<ContextMenuState> = RefCell::new(ContextMenuState::new());
}