feat(blur): better align with designs and remove transparency from theme when not on wayland

This commit is contained in:
Ashley Wulber 2026-04-14 23:44:01 -04:00
parent d04aa41d6a
commit 9d51e8fda4
8 changed files with 223 additions and 42 deletions

View file

@ -10,7 +10,7 @@ use palette::{
IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::num::NonZeroUsize; use std::{default, num::NonZeroUsize};
/// ID for the current dark `ThemeBuilder` config /// ID for the current dark `ThemeBuilder` config
pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder"; pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder";
@ -90,8 +90,15 @@ pub struct Theme {
/// cosmic-comp custom window hint color /// cosmic-comp custom window hint color
pub window_hint: Option<Srgb>, pub window_hint: Option<Srgb>,
/// enables blurred transparency /// enables blurred transparency
/// If None, frosted effect is disabled. pub frosted: BlurStrength,
pub frosted: Option<BlurStrength>, /// frosted windows
pub frosted_windows: bool,
/// frosted system interface
pub frosted_system_interface: bool,
/// frosted panel
pub frosted_panel: bool,
/// frosted applet popups
pub frosted_applets: bool,
/// shade color for dialogs /// shade color for dialogs
#[serde(with = "color_serde")] #[serde(with = "color_serde")]
#[cosmic_config_entry(with = ColorRepr)] #[cosmic_config_entry(with = ColorRepr)]
@ -815,7 +822,7 @@ pub struct ThemeBuilder {
#[cosmic_config_entry(with = ColorReprOption)] #[cosmic_config_entry(with = ColorReprOption)]
pub destructive: Option<Srgb>, pub destructive: Option<Srgb>,
/// enabled blurred transparency /// enabled blurred transparency
pub frosted: Option<BlurStrength>, pub frosted: BlurStrength,
/// cosmic-comp window gaps size (outer, inner) /// cosmic-comp window gaps size (outer, inner)
pub gaps: (u32, u32), pub gaps: (u32, u32),
/// cosmic-comp active hint window outline width /// cosmic-comp active hint window outline width
@ -824,6 +831,14 @@ pub struct ThemeBuilder {
#[serde(with = "color_serde_option")] #[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)] #[cosmic_config_entry(with = ColorReprOption)]
pub window_hint: Option<Srgb>, pub window_hint: Option<Srgb>,
/// frosted windows
pub frosted_windows: bool,
/// frosted system interface
pub frosted_system_interface: bool,
/// frosted panel
pub frosted_panel: bool,
/// frosted applet popups
pub frosted_applets: bool,
} }
impl Default for ThemeBuilder { impl Default for ThemeBuilder {
@ -841,11 +856,15 @@ impl Default for ThemeBuilder {
success: Default::default(), success: Default::default(),
warning: Default::default(), warning: Default::default(),
destructive: Default::default(), destructive: Default::default(),
frosted: None, frosted: BlurStrength::default(),
// cosmic-comp theme settings // cosmic-comp theme settings
gaps: (0, 8), gaps: (0, 8),
active_hint: 3, active_hint: 3,
window_hint: None, window_hint: None,
frosted_windows: false,
frosted_system_interface: false,
frosted_panel: false,
frosted_applets: false,
} }
} }
} }
@ -988,9 +1007,13 @@ impl ThemeBuilder {
active_hint, active_hint,
window_hint, window_hint,
frosted, frosted,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
} = self; } = self;
let container_alpha = frosted.map_or(1.0, |f| f.alpha()); let container_alpha = frosted.alpha();
let is_dark = palette.is_dark(); let is_dark = palette.is_dark();
let is_high_contrast = palette.is_high_contrast(); let is_high_contrast = palette.is_high_contrast();
@ -1080,7 +1103,7 @@ impl ThemeBuilder {
} else { } else {
get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
}; };
container_bg.alpha = container_alpha; container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0);
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index: usize = color_index(container_bg, step_array.len()); let base_index: usize = color_index(container_bg, step_array.len());
@ -1203,7 +1226,7 @@ impl ThemeBuilder {
} else { } else {
get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
}; };
container_bg.alpha = container_alpha; container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0);
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index = color_index(container_bg, step_array.len()); let base_index = color_index(container_bg, step_array.len());
@ -1359,6 +1382,10 @@ impl ThemeBuilder {
accent_text, accent_text,
control_tint: neutral_tint, control_tint: neutral_tint,
text_tint, text_tint,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
}; };
theme.spacing = spacing; theme.spacing = spacing;
theme.corner_radii = corner_radii; theme.corner_radii = corner_radii;
@ -1378,15 +1405,18 @@ impl ThemeBuilder {
} }
} }
/// Actual blur radius is decided by cosmic-comp,
/// but this represents the strength of the blur effect.
#[repr(u8)] #[repr(u8)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum BlurStrength { pub enum BlurStrength {
ExtremelyLow = 1, ExtremelyLow,
ExtremelyLow2, ExtremelyLow2,
VeryLow, VeryLow,
VeryLow2, VeryLow2,
Low, Low,
Low2, Low2,
#[default]
Medium, Medium,
Medium2, Medium2,
High, High,
@ -1402,7 +1432,7 @@ impl BlurStrength {
/// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows: /// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows:
pub fn alpha(&self) -> f32 { pub fn alpha(&self) -> f32 {
match self { match self {
Self::ExtremelyLow => 0.95, Self::ExtremelyLow => 0.90,
Self::ExtremelyLow2 => 0.85, Self::ExtremelyLow2 => 0.85,
Self::VeryLow => 0.8, Self::VeryLow => 0.8,
Self::VeryLow2 => 0.75, Self::VeryLow2 => 0.75,
@ -1414,8 +1444,8 @@ impl BlurStrength {
Self::High2 => 0.45, Self::High2 => 0.45,
Self::VeryHigh => 0.4, Self::VeryHigh => 0.4,
Self::VeryHigh2 => 0.35, Self::VeryHigh2 => 0.35,
Self::ExtremelyHigh => 0.2, Self::ExtremelyHigh => 0.25,
Self::ExtremelyHigh2 => 0.05, Self::ExtremelyHigh2 => 0.2,
} }
} }
} }
@ -1425,20 +1455,20 @@ impl TryFrom<u8> for BlurStrength {
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
match value { match value {
1 => Ok(BlurStrength::ExtremelyLow), 0 => Ok(BlurStrength::ExtremelyLow),
2 => Ok(BlurStrength::ExtremelyLow2), 1 => Ok(BlurStrength::ExtremelyLow2),
3 => Ok(BlurStrength::VeryLow), 2 => Ok(BlurStrength::VeryLow),
4 => Ok(BlurStrength::VeryLow2), 3 => Ok(BlurStrength::VeryLow2),
5 => Ok(BlurStrength::Low), 4 => Ok(BlurStrength::Low),
6 => Ok(BlurStrength::Low2), 5 => Ok(BlurStrength::Low2),
7 => Ok(BlurStrength::Medium), 6 => Ok(BlurStrength::Medium),
8 => Ok(BlurStrength::Medium2), 7 => Ok(BlurStrength::Medium2),
9 => Ok(BlurStrength::High), 8 => Ok(BlurStrength::High),
10 => Ok(BlurStrength::High2), 9 => Ok(BlurStrength::High2),
11 => Ok(BlurStrength::VeryHigh), 10 => Ok(BlurStrength::VeryHigh),
12 => Ok(BlurStrength::VeryHigh2), 11 => Ok(BlurStrength::VeryHigh2),
13 => Ok(BlurStrength::ExtremelyHigh), 12 => Ok(BlurStrength::ExtremelyHigh),
14 => Ok(BlurStrength::ExtremelyHigh2), 13 => Ok(BlurStrength::ExtremelyHigh2),
_ => Err(()), _ => Err(()),
} }
} }

2
iced

@ -1 +1 @@
Subproject commit 1bf1e33317b8da299ffd9a620e7e0e099c0c3478 Subproject commit 13b8d3eab67df7f40d3d9e932a9412f85ff8413c

View file

@ -115,8 +115,8 @@ where
( (
Self::new(model), Self::new(model),
Task::batch([ Task::batch([
command,
iced_runtime::window::run_with_handle(id, init_windowing_system), iced_runtime::window::run_with_handle(id, init_windowing_system),
command,
]), ]),
) )
} }
@ -776,7 +776,17 @@ impl<T: Application> Cosmic<T> {
} }
} }
let new_blur = theme.cosmic().frosted.is_some(); let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && {
let t = theme.cosmic();
match self.app.core().app_type() {
crate::core::AppType::Window => t.frosted_windows,
crate::core::AppType::System => t.frosted_system_interface,
crate::core::AppType::Applet => t.frosted_applets,
}
};
if !new_blur {
theme = theme.into_opaque();
}
THEME.lock().unwrap().set_theme(theme.theme_type); THEME.lock().unwrap().set_theme(theme.theme_type);
let core = self.app.core(); let core = self.app.core();
@ -979,9 +989,19 @@ impl<T: Application> Cosmic<T> {
// Only apply update if the theme is set to load a system theme // Only apply update if the theme is set to load a system theme
if let ThemeType::System { .. } = cosmic_theme.theme_type { if let ThemeType::System { .. } = cosmic_theme.theme_type {
// TODO adjust theme container alphas to remove transparency? let new_blur =
// if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && {
let new_blur = new_theme.cosmic().frosted.is_some(); let t = new_theme.cosmic();
match self.app.core().app_type() {
crate::core::AppType::Window => t.frosted_windows,
crate::core::AppType::System => t.frosted_system_interface,
crate::core::AppType::Applet => t.frosted_applets,
}
};
if !new_blur {
new_theme = new_theme.into_opaque();
}
cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.set_theme(new_theme.theme_type);
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
if self.app.core().sync_window_border_radii_to_theme() { if self.app.core().sync_window_border_radii_to_theme() {
@ -1181,7 +1201,7 @@ impl<T: Application> Cosmic<T> {
if changed { if changed {
core.theme_sub_counter += 1; core.theme_sub_counter += 1;
let new_theme = if is_dark { let mut new_theme = if is_dark {
crate::theme::system_dark() crate::theme::system_dark()
} else { } else {
crate::theme::system_light() crate::theme::system_light()
@ -1195,7 +1215,21 @@ impl<T: Application> Cosmic<T> {
let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len());
if core.auto_blur { if core.auto_blur {
let blur = if new_theme.cosmic().frosted.is_some() { let new_blur =
WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && {
let t = new_theme.cosmic();
match self.app.core().app_type() {
crate::core::AppType::Window => t.frosted_windows,
crate::core::AppType::System => {
t.frosted_system_interface
}
crate::core::AppType::Applet => t.frosted_applets,
}
};
if !new_blur {
new_theme = new_theme.into_opaque();
}
let blur = if new_blur {
iced::window::enable_blur iced::window::enable_blur
} else { } else {
iced::window::disable_blur iced::window::disable_blur
@ -1316,7 +1350,7 @@ impl<T: Application> Cosmic<T> {
use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::CornerRadius;
use iced_winit::platform_specific::commands::corner_radius::corner_radius; use iced_winit::platform_specific::commands::corner_radius::corner_radius;
let theme = THEME.lock().unwrap(); let mut theme = THEME.lock().unwrap();
let t = theme.cosmic(); let t = theme.cosmic();
let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
let cur_rad = CornerRadius { let cur_rad = CornerRadius {
@ -1328,8 +1362,19 @@ impl<T: Application> Cosmic<T> {
// TODO do we need per window sharp corners? // TODO do we need per window sharp corners?
let rounded = !self.app.core().window.sharp_corners; let rounded = !self.app.core().window.sharp_corners;
let core = self.app.core(); let core = self.app.core();
let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && {
let t = theme.cosmic();
match self.app.core().app_type() {
crate::core::AppType::Window => t.frosted_windows,
crate::core::AppType::System => t.frosted_system_interface,
crate::core::AppType::Applet => t.frosted_applets,
}
};
if !new_blur {
*theme = theme.into_opaque();
}
let blur_cmd = if core.auto_blur { let blur_cmd = if core.auto_blur {
let blur = if t.frosted.is_some() { let blur = if new_blur {
iced::window::enable_blur iced::window::enable_blur
} else { } else {
iced::window::disable_blur iced::window::disable_blur
@ -1343,6 +1388,7 @@ impl<T: Application> Cosmic<T> {
} else { } else {
Task::none() Task::none()
}; };
let t = theme.cosmic();
return Task::batch([ return Task::batch([
blur_cmd, blur_cmd,
corner_radius( corner_radius(
@ -1365,7 +1411,45 @@ impl<T: Application> Cosmic<T> {
} }
return iced_runtime::window::run_with_handle(id, init_windowing_system); return iced_runtime::window::run_with_handle(id, init_windowing_system);
} }
_ => {} Action::WindowingSystemInitialized => {
let core = self.app.core();
let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && {
let t = core.system_theme.cosmic();
match self.app.core().app_type() {
crate::core::AppType::Window => t.frosted_windows,
crate::core::AppType::System => t.frosted_system_interface,
crate::core::AppType::Applet => t.frosted_applets,
}
};
let mut t = THEME.lock().unwrap();
if let ThemeType::System { prefer_dark, theme } = &t.theme_type
&& new_blur
{
let mut reloaded = if theme.is_dark {
crate::theme::system_dark()
} else {
crate::theme::system_light()
};
reloaded.theme_type.prefer_dark(*prefer_dark);
*t = reloaded;
}
if core.auto_blur {
let blur = if new_blur {
iced::window::enable_blur
} else {
iced::window::disable_blur
};
let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len());
if let Some(main_id) = core.main_window_id() {
cmds.push(blur(main_id));
}
for id in &self.tracked_windows {
cmds.push(blur(*id));
}
return Task::batch(cmds);
}
}
} }
iced::Task::none() iced::Task::none()

View file

@ -378,9 +378,11 @@ impl Context {
Container::<Message, _, Renderer>::new(content).style(|theme| { Container::<Message, _, Renderer>::new(content).style(|theme| {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
let corners = cosmic.corner_radii; let corners = cosmic.corner_radii;
let mut bg = cosmic.background.base;
bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.);
iced_widget::container::Style { iced_widget::container::Style {
text_color: Some(cosmic.background.on.into()), text_color: Some(cosmic.background.on.into()),
background: Some(Color::from(cosmic.background.base).into()), background: Some(Color::from(bg).into()),
border: iced::Border { border: iced::Border {
radius: corners.radius_m.into(), radius: corners.radius_m.into(),
width: 1.0, width: 1.0,
@ -565,6 +567,7 @@ pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
core.window.show_maximize = false; core.window.show_maximize = false;
core.window.show_minimize = false; core.window.show_minimize = false;
core.window.use_template = false; core.window.use_template = false;
core.app_type = crate::core::AppType::Applet;
window_settings.decorations = false; window_settings.decorations = false;
window_settings.exit_on_close_request = true; window_settings.exit_on_close_request = true;

View file

@ -103,6 +103,18 @@ pub struct Core {
pub(crate) sync_window_border_radii_to_theme: bool, pub(crate) sync_window_border_radii_to_theme: bool,
pub(crate) auto_blur: bool, pub(crate) auto_blur: bool,
pub(crate) app_type: AppType,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AppType {
/// A regular application
Window,
/// A system application
System,
/// An applet
Applet,
} }
impl Default for Core { impl Default for Core {
@ -164,6 +176,7 @@ impl Default for Core {
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
sync_window_border_radii_to_theme: true, sync_window_border_radii_to_theme: true,
auto_blur: true, auto_blur: true,
app_type: AppType::Window,
} }
} }
} }
@ -509,4 +522,16 @@ impl Core {
pub fn set_auto_blur(&mut self, auto_blur: bool) { pub fn set_auto_blur(&mut self, auto_blur: bool) {
self.auto_blur = auto_blur; self.auto_blur = auto_blur;
} }
pub fn auto_blur(&self) -> bool {
self.auto_blur
}
pub fn set_app_type(&mut self, app_type: AppType) {
self.app_type = app_type;
}
pub fn app_type(&self) -> AppType {
self.app_type
}
} }

View file

@ -294,6 +294,29 @@ impl Theme {
pub fn set_theme(&mut self, theme: ThemeType) { pub fn set_theme(&mut self, theme: ThemeType) {
self.theme_type = theme; self.theme_type = theme;
} }
pub fn into_opaque(&self) -> Self {
let mut new_theme = Theme {
theme_type: match &self.theme_type {
ThemeType::System { theme, prefer_dark } => {
let mut new_t = (**theme).clone();
new_t.background.base.alpha = 1.0;
new_t.primary.base.alpha = 1.0;
new_t.secondary.base.alpha = 1.0;
ThemeType::System {
theme: Arc::new(new_t),
prefer_dark: *prefer_dark,
}
}
other => other.clone(),
},
layer: self.layer,
};
let cosmic = new_theme.cosmic();
// copy theme but make all container colors opaque
new_theme
}
} }
impl LayeredTheme for Theme { impl LayeredTheme for Theme {

View file

@ -478,7 +478,21 @@ impl iced_container::Catalog for Theme {
let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
match class { match class {
Container::Transparent => iced_container::Style::default(), Container::Transparent => {
let component = &self.current_container().component;
iced_container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: None,
border: Border {
radius: 0.into(),
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
}
Container::Custom(f) => f(self), Container::Custom(f) => f(self),
@ -566,7 +580,7 @@ impl iced_container::Catalog for Theme {
Container::ContextDrawer => { Container::ContextDrawer => {
let mut a = Container::primary(cosmic); let mut a = Container::primary(cosmic);
if let Some(Background::Color(ref mut color)) = a.background { if let Some(Background::Color(ref mut color)) = a.background {
color.a = 1.; color.a = (color.a + if cosmic.is_dark { 0.60 } else { 0.5 }).min(1.);
} }
if cosmic.is_high_contrast { if cosmic.is_high_contrast {

View file

@ -65,10 +65,12 @@ impl StyleSheet for Theme {
fn appearance(&self, style: &Self::Style) -> Appearance { fn appearance(&self, style: &Self::Style) -> Appearance {
let cosmic = self.cosmic(); let cosmic = self.cosmic();
let component = &cosmic.background.component; let component = &cosmic.background.component;
let mut bg = component.base;
bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.);
match style { match style {
MenuBarStyle::Default => Appearance { MenuBarStyle::Default => Appearance {
background: component.base.into(), background: bg.into(),
border_width: 1.0, border_width: 1.0,
bar_border_radius: cosmic.corner_radii.radius_xl, bar_border_radius: cosmic.corner_radii.radius_xl,
menu_border_radius: cosmic.corner_radii.radius_s.map(|x| x + 2.0), menu_border_radius: cosmic.corner_radii.radius_s.map(|x| x + 2.0),