From ecefae6f817d93b1e68636e6d54ea36ec59411b4 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 05:10:31 +0100 Subject: [PATCH] feat: resolve semantic material styles --- README.md | 3 ++ compositor/src/config.rs | 23 +++++++++++- compositor/src/effects.rs | 78 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0d5cb8..a527c66 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ window_radius = 14.0 offset_y = 4.0 blur = 18.0 opacity = 0.20 + +[theme.material] +style = "glass" # glass, translucent, or solid ``` Fullscreen windows are borderless and shadowless. Other managed windows use GPU diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 2cc4458..0651b89 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -114,6 +114,16 @@ pub struct ThemeSettings { pub shadow_offset_y: f64, pub shadow_blur: f64, pub shadow_opacity: f64, + pub material_style: MaterialStyle, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MaterialStyle { + #[default] + Glass, + Translucent, + Solid, } #[derive(Debug, Default, Deserialize)] @@ -124,6 +134,14 @@ struct ThemeConfig { geometry: ThemeGeometryConfig, #[serde(default)] shadow: ThemeShadowConfig, + #[serde(default)] + material: ThemeMaterialConfig, +} + +#[derive(Debug, Default, Deserialize)] +struct ThemeMaterialConfig { + #[serde(default)] + style: MaterialStyle, } #[derive(Debug, Deserialize)] @@ -788,6 +806,7 @@ impl Config { shadow_offset_y, shadow_blur, shadow_opacity, + material_style: self.theme.material.style, }) } @@ -1535,7 +1554,7 @@ mod tests { #[test] fn parses_and_validates_theme_window_tokens() { let configured = parse( - "[theme.colors]\nborder = \"#11223344\"\naccent = \"#AABBCC\"\nshadow = \"#01020380\"\n\n[theme.geometry]\nborder_width = 1.5\nfocus_ring_width = 3.0\nwindow_radius = 12.0\n\n[theme.shadow.soft]\noffset_y = -2.0\nblur = 24.0\nopacity = 0.4", + "[theme.colors]\nborder = \"#11223344\"\naccent = \"#AABBCC\"\nshadow = \"#01020380\"\n\n[theme.geometry]\nborder_width = 1.5\nfocus_ring_width = 3.0\nwindow_radius = 12.0\n\n[theme.shadow.soft]\noffset_y = -2.0\nblur = 24.0\nopacity = 0.4\n\n[theme.material]\nstyle = \"translucent\"", ); let invalid_color = parse("[theme.colors]\naccent = \"blue\""); let invalid_radius = parse("[theme.geometry]\nwindow_radius = -1.0"); @@ -1553,11 +1572,13 @@ mod tests { shadow_offset_y: -2.0, shadow_blur: 24.0, shadow_opacity: 0.4, + material_style: MaterialStyle::Translucent, } ); assert!(invalid_color.theme_settings().is_err()); assert!(invalid_radius.theme_settings().is_err()); assert!(invalid_opacity.theme_settings().is_err()); + assert!(toml::from_str::("[theme.material]\nstyle = \"mist\"").is_err()); } #[test] diff --git a/compositor/src/effects.rs b/compositor/src/effects.rs index 39a5dc6..6f1b0f4 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -15,7 +15,9 @@ use smithay::{ wayland::compositor::with_states, }; -use crate::{Ferese, private_client::ClientCapabilities, state::ClientState}; +use crate::{ + Ferese, config::MaterialStyle, private_client::ClientCapabilities, state::ClientState, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum SemanticRole { @@ -28,6 +30,56 @@ pub(crate) enum SemanticRole { Modal, } +#[derive(Clone, Copy, Debug, PartialEq)] +#[allow(dead_code)] // Used by the material renderer in the next M7 slice. +pub(crate) struct ResolvedMaterial { + pub style: MaterialStyle, + pub opacity: f32, + pub blur: f32, + pub saturation: f32, + pub brightness: f32, + pub noise: f32, +} + +#[allow(dead_code)] // Used by the material renderer in the next M7 slice. +pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle) -> ResolvedMaterial { + let (opacity, blur, saturation, brightness, noise) = match role { + SemanticRole::Panel => (0.78, 24.0, 1.08, 1.02, 0.012), + SemanticRole::PanelElevated + | SemanticRole::Popover + | SemanticRole::Menu + | SemanticRole::Notification => (0.84, 28.0, 1.10, 1.03, 0.014), + SemanticRole::Hud | SemanticRole::Modal => (0.88, 30.0, 1.06, 1.04, 0.010), + }; + + match style { + MaterialStyle::Glass => ResolvedMaterial { + style, + opacity, + blur, + saturation, + brightness, + noise, + }, + MaterialStyle::Translucent => ResolvedMaterial { + style, + opacity, + blur: 0.0, + saturation: 1.0, + brightness: 1.0, + noise: 0.0, + }, + MaterialStyle::Solid => ResolvedMaterial { + style, + opacity: 1.0, + blur: 0.0, + saturation: 1.0, + brightness: 1.0, + noise: 0.0, + }, + } +} + #[derive(Debug)] struct SurfaceEffectsState { attached: AtomicBool, @@ -221,4 +273,28 @@ mod tests { effects.set_role(Some(SemanticRole::Popover)); assert_eq!(effects.generation.load(Ordering::Acquire), 2); } + + #[test] + fn styles_preserve_semantics_but_disable_unsupported_effects() { + let glass = resolve_material(SemanticRole::Popover, MaterialStyle::Glass); + let translucent = resolve_material(SemanticRole::Popover, MaterialStyle::Translucent); + let solid = resolve_material(SemanticRole::Popover, MaterialStyle::Solid); + + assert!(glass.blur > 0.0); + assert!(glass.noise > 0.0); + assert_eq!(translucent.opacity, glass.opacity); + assert_eq!(translucent.blur, 0.0); + assert_eq!(translucent.noise, 0.0); + assert_eq!(solid.opacity, 1.0); + assert_eq!(solid.blur, 0.0); + } + + #[test] + fn elevated_roles_resolve_more_strongly_than_the_panel() { + let panel = resolve_material(SemanticRole::Panel, MaterialStyle::Glass); + let popover = resolve_material(SemanticRole::Popover, MaterialStyle::Glass); + + assert!(popover.opacity > panel.opacity); + assert!(popover.blur > panel.blur); + } }