From bec679efc931c78afd4a4c8019bb51f6cfb3a25e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 15:51:34 -0400 Subject: [PATCH 01/26] feat(config): support for fallback to previous config version --- cosmic-config/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index c8eda064..b315f194 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -162,6 +162,7 @@ pub trait ConfigSet { pub struct Config { system_path: Option, user_path: Option, + previous: Option>, } /// Check that the name is relative and doesn't contain . or .. @@ -180,9 +181,13 @@ fn sanitize_name(name: &str) -> Result<&Path, Error> { impl Config { /// Get a system config for the given name and config version pub fn system(name: &str, version: u64) -> Result { + Self::system_inner(name, version, true) + } + + fn system_inner(name: &str, version: u64, look_for_previous: bool) -> Result { let path = sanitize_name(name)?.join(format!("v{version}")); #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); + let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); #[cfg(windows)] let system_path = @@ -192,6 +197,13 @@ impl Config { Ok(Self { system_path, user_path: None, + previous: if version > 1 && look_for_previous { + Self::system_inner(name, version - 1, false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -199,6 +211,10 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new(name: &str, version: u64) -> Result { + Self::new_inner(name, version, true) + } + + fn new_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -223,15 +239,29 @@ impl Config { Ok(Self { system_path, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::new_inner(name, version - 1, false).ok().map(Box::new) + } else { + None + }, }) } /// Get config for the given application name and config version and custom path. pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result { + Self::with_custom_path_inner(name, version, custom_path, true) + } + + fn with_custom_path_inner( + name: &str, + version: u64, + custom_path: PathBuf, + look_for_previous: bool, + ) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{version}")); - let mut user_path = custom_path; + let mut user_path = custom_path.clone(); user_path.push("cosmic"); user_path.push(path); // Create new configuration directory if not found. @@ -241,6 +271,13 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::with_custom_path_inner(name, version - 1, custom_path.clone(), false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -250,6 +287,10 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new_state(name: &str, version: u64) -> Result { + Self::new_state_inner(name, version, true) + } + + fn new_state_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -263,6 +304,13 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::new_state_inner(name, version - 1, false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -373,7 +421,13 @@ impl ConfigGet for Config { Ok(ron::from_str(&data)?) } - _ => Err(Error::NotFound), + _ => { + if let Some(previous) = self.previous.as_ref() { + previous.get_local(key) + } else { + Err(Error::NotFound) + } + } } } From 6653157def557f368ab5dd6e921aaa8ccc8632ea Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 15:51:41 -0400 Subject: [PATCH 02/26] feat(config): support for intermediate serialization/deserialization type via field attribute --- cosmic-config-derive/src/lib.rs | 116 ++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index cc19a91e..861398e4 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -1,8 +1,8 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{self}; +use syn; -#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] +#[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -12,6 +12,25 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { impl_cosmic_config_entry_macro(&ast) } +fn get_cosmic_config_attrs(field: &syn::Field) -> Result, syn::Error> { + let mut with = None; + + for attr in &field.attrs { + if !attr.path().is_ident("cosmic_config_entry") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("with") { + let value = meta.value()?; + with = Some(value.parse()?); + } + Ok(()) + })?; + } + + Ok(with) +} + fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let attributes = &ast.attrs; let version = attributes @@ -48,19 +67,54 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; - quote! { - cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + { + let conv = self.#field_name.clone().into(); + cosmic_config::ConfigSet::set::<#with>(&tx, stringify!(#field_name), conv)?; + } + } + } else { + quote! { + cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; + } } }); let get_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - quote! { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(#field_name) => default.#field_name = #field_name, - Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), - Err(e) => errors.push(e), + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { + Ok(value) => { + default.#field_name = value.into(); + } + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), + } + } + } else { + quote! { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(#field_name) => default.#field_name = #field_name, + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), + } } } }); @@ -68,17 +122,39 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let update_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - quote! { - stringify!(#field_name) => { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(value) => { - if self.#field_name != value { - keys.push(stringify!(#field_name)); - } - self.#field_name = value; - }, - Err(e) => { - errors.push(e); + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { + Ok(value) => { + let value = value.into(); + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => errors.push(e), + } + } + } + } else { + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(value) => { + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => errors.push(e), } } } From 141bbd23ec24518930f884cb653678eedc2ab8bf Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 16:17:36 -0400 Subject: [PATCH 03/26] feat: hex_color serialization for the theme can also can deserialize the previous version of the theme, so existing themes should not be affected --- cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/model/color.rs | 173 +++++++++++++++++++++++ cosmic-theme/src/model/cosmic_palette.rs | 33 +++++ cosmic-theme/src/model/derivation.rs | 17 +++ cosmic-theme/src/model/mod.rs | 1 + cosmic-theme/src/model/theme.rs | 63 +++++---- src/command.rs | 1 - 7 files changed, 264 insertions(+), 25 deletions(-) create mode 100644 cosmic-theme/src/model/color.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7e408d8d..faec2fd5 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -15,6 +15,7 @@ export = ["serde_json"] no-default = [] [dependencies] +hex_color = { version = "3", features = ["serde"] } palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" serde = { version = "1.0.228", features = ["derive"] } diff --git a/cosmic-theme/src/model/color.rs b/cosmic-theme/src/model/color.rs new file mode 100644 index 00000000..cd64ec7f --- /dev/null +++ b/cosmic-theme/src/model/color.rs @@ -0,0 +1,173 @@ +//! Color representation and serde helpers for the Cosmic theme + +use hex_color::HexColor; +use palette::{Srgb, Srgba}; +use serde::{Deserialize, Serialize}; + +/// A color in the Cosmic theme for serialization and deserialization +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ColorRepr { + /// A color represented as a hex string + #[serde(with = "hex_color::rgba")] + Hex(HexColor), + /// A color represented as an RGBA value + Rgba(Srgba), + /// A color represented as an RGB value + Rgb(Srgb), +} + +/// An optional color in the Cosmic theme for serialization and deserialization +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct ColorReprOption(Option); + +impl From for ColorRepr { + fn from(color: Srgb) -> Self { + let rgb_u8: Srgb = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgb_u8.red, + g: rgb_u8.green, + b: rgb_u8.blue, + a: 255, + }) + } +} + +impl From for ColorRepr { + fn from(color: Srgba) -> Self { + let rgba_u8: Srgba = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgba_u8.red, + g: rgba_u8.green, + b: rgba_u8.blue, + a: rgba_u8.alpha, + }) + } +} + +impl From for Srgb { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgb::::new(hex.r, hex.g, hex.b).into_format(), + ColorRepr::Rgb(rgb) => rgb, + ColorRepr::Rgba(rgba) => Srgb::new(rgba.red, rgba.green, rgba.blue), + } + } +} + +impl From for Srgba { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgba::::new(hex.r, hex.g, hex.b, hex.a).into_format(), + ColorRepr::Rgb(rgb) => Srgba::new(rgb.red, rgb.green, rgb.blue, 1.0), + ColorRepr::Rgba(rgba) => rgba, + } + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +/// A trait for converting between a color type and its representation for serialization and deserialization +pub trait ConvColorRepr: Sized { + /// Convert from a color representation to the color type + fn from_repr(repr: ColorRepr) -> Self; + /// Convert from the color type to its representation for serialization + fn to_repr(&self) -> ColorRepr; +} + +impl ConvColorRepr for Srgba { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +impl ConvColorRepr for Srgb { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +/// Serde helpers for serializing and deserializing colors in the Cosmic theme +pub mod color_serde { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + /// Serialize a color to a hex string + pub fn serialize(color: &T, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + let repr = color.to_repr(); + repr.serialize(serializer) + } + + /// Deserialize a color from a hex string or RGB/RGBA + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let repr = ColorRepr::deserialize(deserializer)?; + Ok(T::from_repr(repr)) + } + + /// Serde helpers for serializing and deserializing optional colors in the Cosmic theme + pub mod option { + use super::*; + + /// Serialize an optional color + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + match value { + Some(v) => super::serialize(v, serializer), + None => serializer.serialize_none(), + } + } + + /// Deserialize an optional color + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let opt = Option::::deserialize(deserializer)?; + Ok(opt.map(T::from_repr)) + } + } +} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 3852742b..4360f8f4 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::Srgba; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -95,75 +96,107 @@ pub struct CosmicPaletteInner { /// Utility Colors /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_red: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_green: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_orange: Srgba, /// Surface Grays /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_1: Srgba, /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_2: Srgba, /// System Neutrals /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_0: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_1: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_2: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_3: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_4: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_5: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_6: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_7: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_8: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_9: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_10: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_blue: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_indigo: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_purple: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_pink: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_red: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_orange: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_yellow: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_green: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_warm_grey: Srgba, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_warm_grey: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_orange: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_yellow: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_blue: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_purple: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_pink: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_indigo: Srgba, } diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index dce653e5..796ddab3 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::{Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; @@ -8,14 +9,18 @@ use crate::composite::over; #[must_use] pub struct Container { /// the color of the container + #[serde(with = "color_serde")] pub base: Srgba, /// the color of components in the container pub component: Component, /// the color of dividers in the container + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text in the container + #[serde(with = "color_serde")] pub on: Srgba, /// the color of @small_widget_container + #[serde(with = "color_serde")] pub small_widget: Srgba, } @@ -45,30 +50,42 @@ impl Container { #[must_use] pub struct Component { /// The base color of the widget + #[serde(with = "color_serde")] pub base: Srgba, /// The color of the widget when it is hovered + #[serde(with = "color_serde")] pub hover: Srgba, /// the color of the widget when it is pressed + #[serde(with = "color_serde")] pub pressed: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected_text: Srgba, /// the color of the widget when it is focused + #[serde(with = "color_serde")] pub focus: Srgba, /// the color of dividers for this widget + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text for this widget + #[serde(with = "color_serde")] pub on: Srgba, // the color of text with opacity 80 for this widget // pub text_opacity_80: Srgba, /// the color of the widget when it is disabled + #[serde(with = "color_serde")] pub disabled: Srgba, /// the color of text in the widget when it is disabled + #[serde(with = "color_serde")] pub on_disabled: Srgba, /// the color of the border for the widget + #[serde(with = "color_serde")] pub border: Srgba, /// the color of the border for the widget when it is disabled + #[serde(with = "color_serde")] pub disabled_border: Srgba, } diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index f48d1a8d..ff8eed3e 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -6,6 +6,7 @@ pub use mode::*; pub use spacing::*; pub use theme::*; +pub mod color; mod corner; mod cosmic_palette; mod density; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5db0f32c..53ea95b7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,10 +1,11 @@ use crate::{ Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, Spacing, ThemeMode, + color::{ColorRepr, ColorReprOption, color_serde, color_serde::option as color_serde_option}, composite::over, steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}, }; -use cosmic_config::{Config, CosmicConfigEntry}; +use cosmic_config::{Config, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}; use palette::{ IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, }; @@ -37,15 +38,8 @@ pub enum Layer { #[must_use] /// Cosmic Theme data structure with all colors and its name -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, -)] -#[version = 1] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, CosmicConfigEntry)] +#[version = 2] pub struct Theme { /// name of the theme pub name: String, @@ -98,13 +92,21 @@ pub struct Theme { /// enables blurred transparency pub is_frosted: bool, /// shade color for dialogs + #[serde(with = "color_serde")] + #[cosmic_config_entry(with = ColorRepr)] pub shade: Srgba, /// accent text colors /// If None, accent base color is the accent text color. + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent_text: Option, /// control tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub control_tint: Option, /// text tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, } @@ -739,7 +741,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - }; + } Self::dark_default() } @@ -748,10 +750,10 @@ impl Theme { pub fn preferred_theme() -> Self { let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); - if let Ok(desktop) = current_desktop { - if desktop.trim().to_lowercase().contains("gnome") { - return Self::gtk_prefer_colorscheme(); - } + if let Ok(desktop) = current_desktop + && desktop.trim().to_lowercase().contains("gnome") + { + return Self::gtk_prefer_colorscheme(); } Self::dark_default() @@ -766,15 +768,8 @@ impl From for Theme { #[must_use] /// Helper for building customized themes -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, - PartialEq, -)] -#[version = 1] +#[derive(Clone, Debug, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] +#[version = 2] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, @@ -783,22 +778,40 @@ pub struct ThemeBuilder { /// override corner radii for the builder pub corner_radii: CornerRadii, /// override neutral_tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub neutral_tint: Option, /// override bg_color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub bg_color: Option, /// override the primary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub primary_container_bg: Option, /// override the secontary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub secondary_container_bg: Option, /// override the text tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, /// override the accent color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent: Option, /// override the success color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub success: Option, /// override the warning color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub warning: Option, /// override the destructive color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency pub is_frosted: bool, // TODO handle @@ -807,6 +820,8 @@ pub struct ThemeBuilder { /// cosmic-comp active hint window outline width pub active_hint: u32, /// cosmic-comp custom window hint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, } diff --git a/src/command.rs b/src/command.rs index 1d6f635c..6bb16e8d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -65,7 +65,6 @@ pub fn file_transfer_send( /// Returns a list of file paths. #[cfg(feature = "xdg-portal")] pub fn file_transfer_receive(key: String) -> iced::Task>> { - dbg!(&key); iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; file_transfer.retrieve_files(&key).await From 99e196cc7929fe45b226268b578b0fbd475ebb0e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 13:22:43 -0400 Subject: [PATCH 04/26] chore: update ron files --- cosmic-theme/src/model/dark.ron | 2 +- cosmic-theme/src/model/light.ron | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 4453b8bf..ffec0818 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1 +1 @@ -Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) +Dark((name: "cosmic-dark",bright_red: "#FFA09AFF",bright_green: "#5EDB8CFF",bright_orange: "#FFA37DFF",gray_1: "#1B1B1BFF",gray_2: "#262626FF",neutral_0: "#000000FF",neutral_1: "#030303FF",neutral_2: "#161616FF",neutral_3: "#2E2E2EFF",neutral_4: "#484848FF",neutral_5: "#636363FF",neutral_6: "#808080FF",neutral_7: "#9E9E9EFF",neutral_8: "#BEBEBEFF",neutral_9: "#DEDEDEFF",neutral_10: "#FFFFFFFF",accent_blue: "#63D0DFFF",accent_indigo: "#A1C0EBFF",accent_purple: "#E79CFEFF",accent_pink: "#FF9CB1FF",accent_red: "#FDA1A0FF",accent_orange: "#FFAD00FF",accent_yellow: "#F7E062FF",accent_green: "#92CF9CFF",accent_warm_grey: "#CABAB4FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FFAD00FF",ext_yellow: "#FEDB40FF",ext_blue: "#48B9C7FF",ext_purple: "#CF7DFFFF",ext_pink: "#F93A83FF",ext_indigo: "#3E88FFFF",)) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 29b3ad65..c9fcc6ce 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1 +1 @@ -Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) +Light((name: "cosmic-light",bright_red: "#890418FF",bright_green: "#00572CFF",bright_orange: "#792C00FF",gray_1: "#D7D7D7FF",gray_2: "#E4E4E4FF",neutral_0: "#FFFFFFFF",neutral_1: "#DEDEDEFF",neutral_2: "#BEBEBEFF",neutral_3: "#9E9E9EFF",neutral_4: "#808080FF",neutral_5: "#636363FF",neutral_6: "#484848FF",neutral_7: "#2E2E2EFF",neutral_8: "#161616FF",neutral_9: "#030303FF",neutral_10: "#000000FF",accent_blue: "#00525AFF",accent_indigo: "#2E496DFF",accent_purple: "#68217CFF",accent_pink: "#86043AFF",accent_red: "#78292EFF",accent_orange: "#624000FF",accent_yellow: "#534800FF",accent_green: "#185529FF",accent_warm_grey: "#554742FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FBB86CFF",ext_yellow: "#F7E062FF",ext_blue: "#6ACAD8FF",ext_purple: "#D58CFFFF",ext_pink: "#FF9CDDFF",ext_indigo: "#95C4FCFF",)) From ded784a4e3559b95ddc178907ed57558b8699d18 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 2 Apr 2026 11:31:51 -0400 Subject: [PATCH 05/26] chore: update qt light default kcolorscheme this was likely off by one because of a rounding change from v1 --- ...e__output__qt_output__tests__light_default_kcolorscheme.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap index ae2bcb66..40363f95 100644 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -81,7 +81,7 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Selection] -BackgroundAlternate=108,149,152 +BackgroundAlternate=108,149,153 BackgroundNormal=0,82,90 DecorationFocus=0,82,90 DecorationHover=0,82,90 From 6caccaba337ed9bab21c5fe3c2aa7392e322e89c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 8 Apr 2026 16:13:31 -0600 Subject: [PATCH 06/26] fix: icon color when window is maximized --- src/theme/mod.rs | 2 +- src/theme/style/iced.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b7e85237..093bac05 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -307,7 +307,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 4633477d..aa6f4b33 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -43,7 +43,7 @@ pub mod application { iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), } } } From e287a789c1f33459d4a7ac737c2e7d4004e7e0e4 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 10 Apr 2026 20:53:43 -0600 Subject: [PATCH 07/26] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7fd263d9..fc6b4634 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 +Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 From 0e72508dcca7161376e86167242b24e0469e53ee Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 12 Apr 2026 18:50:19 +0200 Subject: [PATCH 08/26] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amadɣas Co-authored-by: Asier Saratsua Garmendia Co-authored-by: ButterflyOfFire Co-authored-by: Ettore Atalan Co-authored-by: Geeson Wan Co-authored-by: Hosted Weblate Co-authored-by: 麋麓 BigELK176 Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kab/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hant/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 2 +- i18n/eu/libcosmic.ftl | 0 i18n/kab/libcosmic.ftl | 33 +++++++++++++++++++++++++++++++++ i18n/ko/libcosmic.ftl | 21 ++++++++++++++------- i18n/zh-Hant/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 i18n/eu/libcosmic.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 238000f5..2d3704a6 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -6,7 +6,7 @@ links = Links developers = Entwickler(innen) designers = Designer(innen) artists = Künstler(innen) -translators = Übersetzer*innen +translators = Übersetzer(innen) documenters = Dokumentierer(innen) # Calendar january = Januar { $year } diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index e69de29b..6eac2bc7 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -0,0 +1,33 @@ +close = Mdel +license = Turagt +links = Iseɣwan +developers = Ineflayen +artists = Inaẓuren +translators = Imsuqlen +january = Yennayer { $year } +february = Fuṛar { $year } +march = Meɣres { $year } +april = Yebrir { $year } +may = Mayyu { $year } +june = Yunyu { $year } +july = Yulyu { $year } +august = Ɣuct { $year } +september = Ctembeṛ { $year } +october = Tubeṛ { $year } +november = Wambeṛ { $year } +december = Dujembeṛ { $year } +documenters = Imeskaren +monday = Arim +mon = Ari +tuesday = Aram +tue = Ara +wednesday = Ahad +wed = Aha +thursday = Amhad +thu = Amh +friday = Sem +fri = Sm +saturday = Sed +sat = Sd +sunday = Acer +sun = Ace diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index 8d499756..6cc0adbc 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,26 +2,33 @@ february = { $year }년 2월 close = 닫기 documenters = 문서 작성자 november = { $year }년 11월 -friday = 금 -tuesday = 화 +friday = 금요일 +tuesday = 화요일 may = { $year }년 5월 -wednesday = 수 +wednesday = 수요일 april = { $year }년 4월 -monday = 월 +monday = 월요일 translators = 번역가 artists = 아티스트 license = 라이선스 december = { $year }년 12월 -sunday = 일 +sunday = 일요일 links = 링크 march = { $year }년 3월 june = { $year }년 6월 -saturday = 토 +saturday = 토요일 august = { $year }년 8월 developers = 개발자 july = { $year }년 7월 -thursday = 목 +thursday = 목요일 september = { $year }년 9월 designers = 디자이너 october = { $year }년 10월 january = { $year }년 1월 +mon = 월 +tue = 화 +wed = 수 +thu = 목 +fri = 금 +sat = 토 +sun = 일 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl index e69de29b..8c9b201c 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -0,0 +1,34 @@ +close = 關閉 +developers = 開發人員 +designers = 設計人員 +artists = 美編設計 +translators = 翻譯人員 +documenters = 文件編輯人員 +january = { $year } 年 1 月 +monday = 星期一 +tuesday = 星期二 +wednesday = 星期三 +thursday = 星期四 +friday = 星期五 +saturday = 星期六 +sunday = 星期日 +mon = 週一 +tue = 週二 +wed = 週三 +thu = 週四 +fri = 週五 +sat = 週六 +sun = 週日 +license = 授權 +links = 連結 +february = { $year } 年 2 月 +march = { $year } 年 3 月 +april = { $year } 年 4 月 +may = { $year } 年 5 月 +june = { $year } 年 6 月 +july = { $year } 年 7 月 +august = { $year } 年 8 月 +september = { $year } 年 9 月 +october = { $year } 年 10 月 +november = { $year } 年 11 月 +december = { $year } 年 12 月 From 1b74c6f99900b97152ce6cf348ce8578843ab9af Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 13 Apr 2026 11:04:55 -0400 Subject: [PATCH 09/26] wip: blurred transparency --- cosmic-theme/src/model/theme.rs | 90 ++++++++++++++++++++++++++++++--- iced | 2 +- src/app/cosmic.rs | 88 ++++++++++++++++++++++++++++++-- src/core.rs | 7 +++ src/theme/style/iced.rs | 3 ++ 5 files changed, 177 insertions(+), 13 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 53ea95b7..56654480 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -90,7 +90,8 @@ pub struct Theme { /// cosmic-comp custom window hint color pub window_hint: Option, /// enables blurred transparency - pub is_frosted: bool, + /// If None, frosted effect is disabled. + pub frosted: Option, /// shade color for dialogs #[serde(with = "color_serde")] #[cosmic_config_entry(with = ColorRepr)] @@ -814,7 +815,7 @@ pub struct ThemeBuilder { #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency - pub is_frosted: bool, // TODO handle + pub frosted: Option, /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// cosmic-comp active hint window outline width @@ -840,7 +841,7 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - is_frosted: false, + frosted: None, // cosmic-comp theme settings gaps: (0, 8), active_hint: 3, @@ -986,9 +987,11 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, } = self; + let container_alpha = frosted.map_or(1.0, |f| f.alpha()); + let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1034,12 +1037,14 @@ impl ThemeBuilder { NonZeroUsize::new(100).unwrap(), ); - let bg = if let Some(bg_color) = bg_color { + let mut bg = if let Some(bg_color) = bg_color { bg_color } else { p_ref.gray_1 }; + bg.alpha = container_alpha; + let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); @@ -1070,11 +1075,12 @@ impl ThemeBuilder { ); let primary = { - let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) }; + container_bg.alpha = container_alpha; let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index: usize = color_index(container_bg, step_array.len()); @@ -1191,11 +1197,13 @@ impl ThemeBuilder { ), primary, secondary: { - let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { + let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg + { secondary_container_bg } else { get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; + container_bg.alpha = container_alpha; let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index = color_index(container_bg, step_array.len()); @@ -1347,7 +1355,7 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, accent_text, control_tint: neutral_tint, text_tint, @@ -1369,3 +1377,69 @@ impl ThemeBuilder { Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) } } + +#[repr(u8)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum BlurStrength { + ExtremelyLow = 1, + ExtremelyLow2, + VeryLow, + VeryLow2, + Low, + Low2, + Medium, + Medium2, + High, + High2, + VeryHigh, + VeryHigh2, + ExtremelyHigh, + ExtremelyHigh2, +} + +impl BlurStrength { + /// Get the alpha value corresponding to the blur strength + /// 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 { + match self { + Self::ExtremelyLow => 0.95, + Self::ExtremelyLow2 => 0.85, + Self::VeryLow => 0.8, + Self::VeryLow2 => 0.75, + Self::Low => 0.7, + Self::Low2 => 0.65, + Self::Medium => 0.6, + Self::Medium2 => 0.55, + Self::High => 0.5, + Self::High2 => 0.45, + Self::VeryHigh => 0.4, + Self::VeryHigh2 => 0.35, + Self::ExtremelyHigh => 0.2, + Self::ExtremelyHigh2 => 0.05, + } + } +} + +impl TryFrom for BlurStrength { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(BlurStrength::ExtremelyLow), + 2 => Ok(BlurStrength::ExtremelyLow2), + 3 => Ok(BlurStrength::VeryLow), + 4 => Ok(BlurStrength::VeryLow2), + 5 => Ok(BlurStrength::Low), + 6 => Ok(BlurStrength::Low2), + 7 => Ok(BlurStrength::Medium), + 8 => Ok(BlurStrength::Medium2), + 9 => Ok(BlurStrength::High), + 10 => Ok(BlurStrength::High2), + 11 => Ok(BlurStrength::VeryHigh), + 12 => Ok(BlurStrength::VeryHigh2), + 13 => Ok(BlurStrength::ExtremelyHigh), + 14 => Ok(BlurStrength::ExtremelyHigh2), + _ => Err(()), + } + } +} diff --git a/iced b/iced index 7fd263d9..46b65a3c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 +Subproject commit 46b65a3c3e2fb6f1627d117ca061743417806aaf diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 030ed041..6b7b04ba 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -773,10 +773,31 @@ impl Cosmic { if a.distance_squared(*t_inner.accent_color()) > 0.00001 { theme = Theme::system(Arc::new(t_inner.with_accent(a))); } - }; + } } + let new_blur = theme.cosmic().frosted.is_some(); THEME.lock().unwrap().set_theme(theme.theme_type); + + let core = self.app.core(); + if core.auto_blur { + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + return Task::batch(cmds); + } } Action::SystemThemeChange(keys, theme) => { @@ -809,6 +830,8 @@ impl Cosmic { theme }; new_theme.theme_type.prefer_dark(prefer_dark); + // TODO adjust theme container alphas to remove transparency? + // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -873,7 +896,7 @@ impl Cosmic { } } // Update radius for all tracked windows - for id in self.tracked_windows.iter() { + for id in &self.tracked_windows { cmds.push( corner_radius( *id, @@ -956,6 +979,9 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { + // TODO adjust theme container alphas to remove transparency? + // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? + let new_blur = new_theme.cosmic().frosted.is_some(); cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { @@ -1019,7 +1045,7 @@ impl Cosmic { } } // Update radius for all tracked windows - for id in self.tracked_windows.iter() { + for id in &self.tracked_windows { cmds.push( corner_radius( *id, @@ -1039,6 +1065,25 @@ impl Cosmic { ); } + let core = self.app.core(); + if core.auto_blur { + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + } + return Task::batch(cmds); } } @@ -1147,7 +1192,26 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + + if core.auto_blur { + let blur = if new_theme.cosmic().frosted.is_some() { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + } cosmic_theme.set_theme(new_theme.theme_type); + return Task::batch(cmds); } } } @@ -1263,8 +1327,24 @@ impl Cosmic { }; // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; - + let core = self.app.core(); + let blur_cmd = if core.auto_blur { + let blur = if t.frosted.is_some() { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + cmds.push(blur(id)); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + Task::batch(cmds) + } else { + Task::none() + }; return Task::batch([ + blur_cmd, corner_radius( id, if rounded { diff --git a/src/core.rs b/src/core.rs index 970a5351..2ea0890b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -101,6 +101,8 @@ pub struct Core { #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, + + pub(crate) auto_blur: bool, } impl Default for Core { @@ -161,6 +163,7 @@ impl Default for Core { menu_bars: HashMap::new(), #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, + auto_blur: true, } } } @@ -502,4 +505,8 @@ impl Core { pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } + + pub fn set_auto_blur(&mut self, auto_blur: bool) { + self.auto_blur = auto_blur; + } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 4633477d..3a57408a 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -565,6 +565,9 @@ impl iced_container::Catalog for Theme { Container::ContextDrawer => { let mut a = Container::primary(cosmic); + if let Some(Background::Color(ref mut color)) = a.background { + color.a = 1.; + } if cosmic.is_high_contrast { a.border.width = 1.; From 52116d2f36972c422a0953a4699e32d5eb30cdac Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 13 Apr 2026 14:07:31 -0600 Subject: [PATCH 10/26] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index fc6b4634..78caabba 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 +Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece From 46d9f0c3442189b446ffeff452c314fa6592da7e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 14 Apr 2026 11:53:33 -0700 Subject: [PATCH 11/26] widget/icon: Bundle icons on macOS, not just Windows --- Cargo.toml | 4 ++-- build.rs | 4 +++- src/widget/icon/bundle.rs | 6 +++--- src/widget/icon/named.rs | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83fe90f0..e090ad21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,12 +170,12 @@ cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus = { version = "5.14.0", default-features = false } -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } -[target.'cfg(not(unix))'.dependencies] +[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] # Used to embed bundled icons for non-unix platforms. phf = { version = "0.13.1", features = ["macros"] } diff --git a/build.rs b/build.rs index c69feaf5..4ce0aa9e 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,9 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() { + if env::var_os("CARGO_CFG_UNIX").is_none() + || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") + { generate_bundled_icons(); } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index 9d0877d0..bb6ce244 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,12 +4,12 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] pub fn get(icon_name: &str) -> Option { None } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] /// Get a bundled icon on non-unix platforms. pub fn get(icon_name: &str) -> Option { ICONS @@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option { .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 8405e080..dfd66cf5 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(not(windows))] + #[cfg(all(unix, not(target_os = "macos")))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(windows)] + #[cfg(any(not(unix), target_os = "macos"))] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows From d04aa41d6ad2dc60f8ec5e5dc4580744fceeb58d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 14 Apr 2026 19:14:08 -0400 Subject: [PATCH 12/26] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 46b65a3c..1bf1e333 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 46b65a3c3e2fb6f1627d117ca061743417806aaf +Subproject commit 1bf1e33317b8da299ffd9a620e7e0e099c0c3478 From 9d51e8fda4d4268a589e7d0bd5c1c24eba4cfdd0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 14 Apr 2026 23:44:01 -0400 Subject: [PATCH 13/26] feat(blur): better align with designs and remove transparency from theme when not on wayland --- cosmic-theme/src/model/theme.rs | 84 +++++++++++++++++--------- iced | 2 +- src/app/cosmic.rs | 104 +++++++++++++++++++++++++++++--- src/applet/mod.rs | 5 +- src/core.rs | 25 ++++++++ src/theme/mod.rs | 23 +++++++ src/theme/style/iced.rs | 18 +++++- src/theme/style/menu_bar.rs | 4 +- 8 files changed, 223 insertions(+), 42 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 56654480..0b2976c9 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -10,7 +10,7 @@ use palette::{ IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, }; use serde::{Deserialize, Serialize}; -use std::num::NonZeroUsize; +use std::{default, num::NonZeroUsize}; /// ID for the current dark `ThemeBuilder` config 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 pub window_hint: Option, /// enables blurred transparency - /// If None, frosted effect is disabled. - pub frosted: Option, + pub frosted: 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 #[serde(with = "color_serde")] #[cosmic_config_entry(with = ColorRepr)] @@ -815,7 +822,7 @@ pub struct ThemeBuilder { #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency - pub frosted: Option, + pub frosted: BlurStrength, /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// cosmic-comp active hint window outline width @@ -824,6 +831,14 @@ pub struct ThemeBuilder { #[serde(with = "color_serde_option")] #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, + /// 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 { @@ -841,11 +856,15 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - frosted: None, + frosted: BlurStrength::default(), // cosmic-comp theme settings gaps: (0, 8), active_hint: 3, window_hint: None, + frosted_windows: false, + frosted_system_interface: false, + frosted_panel: false, + frosted_applets: false, } } } @@ -988,9 +1007,13 @@ impl ThemeBuilder { active_hint, window_hint, frosted, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, } = 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_high_contrast = palette.is_high_contrast(); @@ -1080,7 +1103,7 @@ impl ThemeBuilder { } else { 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 base_index: usize = color_index(container_bg, step_array.len()); @@ -1203,7 +1226,7 @@ impl ThemeBuilder { } else { 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 base_index = color_index(container_bg, step_array.len()); @@ -1359,6 +1382,10 @@ impl ThemeBuilder { accent_text, control_tint: neutral_tint, text_tint, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, }; theme.spacing = spacing; 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)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { - ExtremelyLow = 1, + ExtremelyLow, ExtremelyLow2, VeryLow, VeryLow2, Low, Low2, + #[default] Medium, Medium2, 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: pub fn alpha(&self) -> f32 { match self { - Self::ExtremelyLow => 0.95, + Self::ExtremelyLow => 0.90, Self::ExtremelyLow2 => 0.85, Self::VeryLow => 0.8, Self::VeryLow2 => 0.75, @@ -1414,8 +1444,8 @@ impl BlurStrength { Self::High2 => 0.45, Self::VeryHigh => 0.4, Self::VeryHigh2 => 0.35, - Self::ExtremelyHigh => 0.2, - Self::ExtremelyHigh2 => 0.05, + Self::ExtremelyHigh => 0.25, + Self::ExtremelyHigh2 => 0.2, } } } @@ -1425,20 +1455,20 @@ impl TryFrom for BlurStrength { fn try_from(value: u8) -> Result { match value { - 1 => Ok(BlurStrength::ExtremelyLow), - 2 => Ok(BlurStrength::ExtremelyLow2), - 3 => Ok(BlurStrength::VeryLow), - 4 => Ok(BlurStrength::VeryLow2), - 5 => Ok(BlurStrength::Low), - 6 => Ok(BlurStrength::Low2), - 7 => Ok(BlurStrength::Medium), - 8 => Ok(BlurStrength::Medium2), - 9 => Ok(BlurStrength::High), - 10 => Ok(BlurStrength::High2), - 11 => Ok(BlurStrength::VeryHigh), - 12 => Ok(BlurStrength::VeryHigh2), - 13 => Ok(BlurStrength::ExtremelyHigh), - 14 => Ok(BlurStrength::ExtremelyHigh2), + 0 => Ok(BlurStrength::ExtremelyLow), + 1 => Ok(BlurStrength::ExtremelyLow2), + 2 => Ok(BlurStrength::VeryLow), + 3 => Ok(BlurStrength::VeryLow2), + 4 => Ok(BlurStrength::Low), + 5 => Ok(BlurStrength::Low2), + 6 => Ok(BlurStrength::Medium), + 7 => Ok(BlurStrength::Medium2), + 8 => Ok(BlurStrength::High), + 9 => Ok(BlurStrength::High2), + 10 => Ok(BlurStrength::VeryHigh), + 11 => Ok(BlurStrength::VeryHigh2), + 12 => Ok(BlurStrength::ExtremelyHigh), + 13 => Ok(BlurStrength::ExtremelyHigh2), _ => Err(()), } } diff --git a/iced b/iced index 1bf1e333..13b8d3ea 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1bf1e33317b8da299ffd9a620e7e0e099c0c3478 +Subproject commit 13b8d3eab67df7f40d3d9e932a9412f85ff8413c diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6b7b04ba..88ff025a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -115,8 +115,8 @@ where ( Self::new(model), Task::batch([ - command, iced_runtime::window::run_with_handle(id, init_windowing_system), + command, ]), ) } @@ -776,7 +776,17 @@ impl Cosmic { } } - 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); let core = self.app.core(); @@ -979,9 +989,19 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { - // TODO adjust theme container alphas to remove transparency? - // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? - let new_blur = 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(); + } + cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { @@ -1181,7 +1201,7 @@ impl Cosmic { if changed { core.theme_sub_counter += 1; - let new_theme = if is_dark { + let mut new_theme = if is_dark { crate::theme::system_dark() } else { crate::theme::system_light() @@ -1195,7 +1215,21 @@ impl Cosmic { let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); 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 } else { iced::window::disable_blur @@ -1316,7 +1350,7 @@ impl Cosmic { use iced_runtime::platform_specific::wayland::CornerRadius; 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 radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); let cur_rad = CornerRadius { @@ -1328,8 +1362,19 @@ impl Cosmic { // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; 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 = if t.frosted.is_some() { + let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur @@ -1343,6 +1388,7 @@ impl Cosmic { } else { Task::none() }; + let t = theme.cosmic(); return Task::batch([ blur_cmd, corner_radius( @@ -1365,7 +1411,45 @@ impl Cosmic { } 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() diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 48721e1c..6df99620 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -378,9 +378,11 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); 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 { text_color: Some(cosmic.background.on.into()), - background: Some(Color::from(cosmic.background.base).into()), + background: Some(Color::from(bg).into()), border: iced::Border { radius: corners.radius_m.into(), width: 1.0, @@ -565,6 +567,7 @@ pub fn run(flags: App::Flags) -> iced::Result { core.window.show_maximize = false; core.window.show_minimize = false; core.window.use_template = false; + core.app_type = crate::core::AppType::Applet; window_settings.decorations = false; window_settings.exit_on_close_request = true; diff --git a/src/core.rs b/src/core.rs index 2ea0890b..f1c3946d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -103,6 +103,18 @@ pub struct Core { pub(crate) sync_window_border_radii_to_theme: 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 { @@ -164,6 +176,7 @@ impl Default for Core { #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, auto_blur: true, + app_type: AppType::Window, } } } @@ -509,4 +522,16 @@ impl Core { pub fn set_auto_blur(&mut self, auto_blur: bool) { 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 + } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b7e85237..a04b105d 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -294,6 +294,29 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { 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 { diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 3a57408a..aa0d290a 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -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 }); 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), @@ -566,7 +580,7 @@ impl iced_container::Catalog for Theme { Container::ContextDrawer => { let mut a = Container::primary(cosmic); 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 { diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index ed0e657a..6e192bf3 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -65,10 +65,12 @@ impl StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); 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 { MenuBarStyle::Default => Appearance { - background: component.base.into(), + background: bg.into(), border_width: 1.0, bar_border_radius: cosmic.corner_radii.radius_xl, menu_border_radius: cosmic.corner_radii.radius_s.map(|x| x + 2.0), From 3d8d8915be516229bd215403e0a800ea80f618ae Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 14 Apr 2026 23:14:41 -0600 Subject: [PATCH 14/26] chore: enable ico and xpm image support for desktop feature --- Cargo.toml | 6 ++++++ src/app/mod.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e090ad21..d73da2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", + "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -141,9 +142,14 @@ css-color = "0.2.8" derive_setters = "0.1.9" futures = "0.3" image = { version = "0.25.10", default-features = false, features = [ + "ico", "jpeg", "png", ] } +image-extras = { version = "0.1.0", default-features = false, features = [ + "xpm", + "xbm", +], optional = true } libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5c0e95e4..42fa4b1b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -128,6 +128,9 @@ impl BootFn, crate::Action(settings: Settings, flags: App::Flags) -> iced::Result { + #[cfg(feature = "desktop")] + image_extras::register(); + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); From b814f54f67b3a51b231e6a2e69eb259b5a35325d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Apr 2026 15:06:07 -0400 Subject: [PATCH 15/26] refactor opaque fallback --- cosmic-theme/src/model/theme.rs | 177 +++++++++++++++++++++++++++++--- src/app/cosmic.rs | 99 +++++++++--------- src/app/mod.rs | 4 +- src/applet/mod.rs | 10 +- src/theme/mod.rs | 31 +----- src/theme/style/button.rs | 21 ++-- src/theme/style/dropdown.rs | 18 +++- src/theme/style/iced.rs | 118 +++++++++++++-------- src/theme/style/menu_bar.rs | 2 +- src/widget/card/style.rs | 22 ++-- src/widget/menu/menu_tree.rs | 2 +- src/widget/nav_bar.rs | 4 +- 12 files changed, 346 insertions(+), 162 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 0b2976c9..6076b9c8 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -44,11 +44,19 @@ pub struct Theme { /// name of the theme pub name: String, /// background element colors - pub background: Container, + pub(crate) background: Container, /// primary element colors - pub primary: Container, + pub(crate) primary: Container, /// secondary element colors - pub secondary: Container, + pub(crate) secondary: Container, + /// background element colors + pub(crate) transparent_background: Container, + /// primary element colors + pub(crate) transparent_primary: Container, + /// secondary element colors + pub(crate) transparent_secondary: Container, + /// button component styling + pub button: Component, /// accent element colors pub accent: Component, /// suggested element colors @@ -71,8 +79,6 @@ pub struct Theme { pub link_button: Component, /// text button element colors pub text_button: Component, - /// button component styling - pub button: Component, /// palette pub palette: CosmicPaletteInner, /// spacing @@ -180,6 +186,39 @@ impl Theme { todo!(); } + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent background based on whether blur is active + pub fn background(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_background + } else { + &self.background + } + } + + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent primary based on whether blur is active + pub fn primary(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_primary + } else { + &self.primary + } + } + + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent secondary based on whether blur is active + pub fn secondary(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_secondary + } else { + &self.secondary + } + } + #[must_use] #[allow(clippy::doc_markdown)] #[inline] @@ -1060,17 +1099,19 @@ impl ThemeBuilder { NonZeroUsize::new(100).unwrap(), ); - let mut bg = if let Some(bg_color) = bg_color { + let bg = if let Some(bg_color) = bg_color { bg_color } else { p_ref.gray_1 }; - bg.alpha = container_alpha; - let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); + let mut transparent_bg = bg; + transparent_bg.alpha = container_alpha; + let transparent_bg_steps_array = steps(transparent_bg, NonZeroUsize::new(100).unwrap()); + let mut component_hovered_overlay = if bg_index < 91 { control_steps_array[10] } else { @@ -1097,13 +1138,26 @@ impl ThemeBuilder { text_steps_array.as_deref(), ); + let transparent_bg_component = get_surface_color( + bg_index, + 8, + &transparent_bg_steps_array, + is_dark, + &p_ref.neutral_2, + ); + let on_transparent_bg_component = get_text( + color_index(transparent_bg_component, transparent_bg_steps_array.len()), + &transparent_bg_steps_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ); + let primary = { let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) }; - 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 base_index: usize = color_index(container_bg, step_array.len()); @@ -1146,6 +1200,45 @@ impl ThemeBuilder { is_high_contrast, ) }; + let transparent_primary = { + let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + primary_container_bg_color + } else { + get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) + }; + 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 base_index: usize = color_index(container_bg, step_array.len()); + let component_base = + get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]); + + Container::new( + Component::component( + component_base, + accent, + get_text( + color_index(component_base, step_array.len()), + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + container_bg, + get_text( + base_index, + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ) + }; let accent_text = if is_dark { (primary.base.relative_contrast(accent.color) < 4.).then(|| { @@ -1220,13 +1313,11 @@ impl ThemeBuilder { ), primary, secondary: { - let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg - { + let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; - 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 base_index = color_index(container_bg, step_array.len()); @@ -1386,6 +1477,67 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + transparent_background: Container::new( + Component::component( + transparent_bg_component, + accent, + on_transparent_bg_component, + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + transparent_bg, + get_text( + bg_index, + &transparent_bg_steps_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ), + transparent_primary, + transparent_secondary: { + let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg + { + secondary_container_bg + } else { + get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) + }; + 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 base_index = color_index(container_bg, step_array.len()); + let secondary_component = + get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]); + + Container::new( + Component::component( + secondary_component, + accent, + get_text( + color_index(secondary_component, step_array.len()), + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + container_bg, + get_text( + base_index, + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ) + }, }; theme.spacing = spacing; theme.corner_radii = corner_radii; @@ -1407,6 +1559,7 @@ impl ThemeBuilder { /// Actual blur radius is decided by cosmic-comp, /// but this represents the strength of the blur effect. +#[allow(missing_docs)] #[repr(u8)] #[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 88ff025a..e09ee958 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -784,10 +784,11 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; - if !new_blur { - theme = theme.into_opaque(); - } - THEME.lock().unwrap().set_theme(theme.theme_type); + theme.transparent = new_blur; + let mut guard = THEME.lock().unwrap(); + guard.set_theme(theme.theme_type); + guard.transparent = new_blur; + drop(guard); let core = self.app.core(); if core.auto_blur { @@ -810,12 +811,23 @@ impl Cosmic { } } - Action::SystemThemeChange(keys, theme) => { + Action::SystemThemeChange(keys, mut theme) => { let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark(); // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { return iced::Task::none(); } + // update transparent + 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, + } + }; + theme.transparent = new_blur; + let cmd = self.app.system_theme_update(&keys, theme.cosmic()); // Record the last-known system theme in event that the current theme is custom. self.app.core_mut().system_theme = theme.clone(); @@ -839,11 +851,12 @@ impl Cosmic { } else { theme }; + new_theme.transparent = new_blur; new_theme.theme_type.prefer_dark(prefer_dark); - // TODO adjust theme container alphas to remove transparency? - // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? cosmic_theme.set_theme(new_theme.theme_type); + cosmic_theme.transparent = new_blur; + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; @@ -954,6 +967,7 @@ impl Cosmic { } { return iced::Task::none(); } + let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; let core = self.app.core_mut(); @@ -982,6 +996,15 @@ impl Cosmic { } else { new_theme }; + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = new_theme.cosmic(); + match 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, + } + }; + new_theme.transparent = new_blur; core.system_theme = new_theme.clone(); { @@ -989,20 +1012,8 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { - 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(); - } - cosmic_theme.set_theme(new_theme.theme_type); + cosmic_theme.transparent = new_blur; #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; @@ -1087,7 +1098,7 @@ impl Cosmic { let core = self.app.core(); if core.auto_blur { - let blur = if new_blur { + let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur @@ -1206,6 +1217,18 @@ impl Cosmic { } else { crate::theme::system_light() }; + if let ThemeType::System { .. } = new_theme.theme_type { + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) + && { + let t = new_theme.cosmic(); + match 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, + } + }; + new_theme.transparent = new_blur; + } core.system_theme = new_theme.clone(); { let mut cosmic_theme = THEME.lock().unwrap(); @@ -1215,21 +1238,7 @@ impl Cosmic { let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); if core.auto_blur { - 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 { + let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur @@ -1370,9 +1379,7 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; - if !new_blur { - *theme = theme.into_opaque(); - } + theme.transparent = new_blur; let blur_cmd = if core.auto_blur { let blur = if new_blur { iced::window::enable_blur @@ -1412,6 +1419,7 @@ impl Cosmic { return iced_runtime::window::run_with_handle(id, init_windowing_system); } Action::WindowingSystemInitialized => { + // TODO do this after blur event confirms support instead of for all wayland windows let core = self.app.core(); let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { let t = core.system_theme.cosmic(); @@ -1423,17 +1431,8 @@ impl Cosmic { }; 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; - } + t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; + if core.auto_blur { let blur = if new_blur { iced::window::enable_blur diff --git a/src/app/mod.rs b/src/app/mod.rs index 5c0e95e4..fedbcef7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -820,7 +820,7 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - cosmic.background.base.into(), + cosmic.background(theme.transparent).base.into(), )), border: iced::Border { radius: [ @@ -849,7 +849,7 @@ impl ApplicationExt for App { container::Style { background: if content_container { Some(iced::Background::Color( - theme.cosmic().background.base.into(), + theme.cosmic().background(theme.transparent).base.into(), )) } else { None diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 6df99620..400229f4 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -227,7 +227,7 @@ impl Context { let icon = widget::icon(icon) .class(if symbolic { theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), + color: Some(theme.cosmic().background(theme.transparent).on.into()), })) } else { theme::Svg::default() @@ -378,18 +378,18 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; - let mut bg = cosmic.background.base; + let mut bg = cosmic.background(theme.transparent).base; bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); iced_widget::container::Style { - text_color: Some(cosmic.background.on.into()), + text_color: Some(cosmic.background(theme.transparent).on.into()), background: Some(Color::from(bg).into()), border: iced::Border { radius: corners.radius_m.into(), width: 1.0, - color: cosmic.background.divider.into(), + color: cosmic.background(theme.transparent).divider.into(), }, shadow: Shadow::default(), - icon_color: Some(cosmic.background.on.into()), + icon_color: Some(cosmic.background(theme.transparent).on.into()), snap: true, } }), diff --git a/src/theme/mod.rs b/src/theme/mod.rs index a04b105d..baac5e52 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -50,6 +50,7 @@ pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Compone pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background, + transparent: false, }); /// Currently-defined theme. @@ -213,6 +214,7 @@ impl ThemeType { pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, + pub transparent: bool, } impl Theme { @@ -283,9 +285,9 @@ impl Theme { /// can be used in a component that is intended to be a child of a `CosmicContainer` pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { - cosmic_theme::Layer::Background => &self.cosmic().background, - cosmic_theme::Layer::Primary => &self.cosmic().primary, - cosmic_theme::Layer::Secondary => &self.cosmic().secondary, + cosmic_theme::Layer::Background => &self.cosmic().background(self.transparent), + cosmic_theme::Layer::Primary => &self.cosmic().primary(self.transparent), + cosmic_theme::Layer::Secondary => &self.cosmic().secondary(self.transparent), } } @@ -294,29 +296,6 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { 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 { diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 0575ce67..b15bc364 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -128,20 +128,20 @@ pub fn appearance( let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); corner_radii = &cosmic.corner_radii.radius_0; } Button::AppletIcon => { let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); } Button::MenuFolder => { // Menu folders cannot be disabled, ignore customized icon and text color - let component = &cosmic.background.component; + let component = &cosmic.background(theme.transparent).component; let (background, _, _) = color(component); appearance.background = Some(Background::Color(background)); appearance.icon_color = Some(component.on.into()); @@ -150,11 +150,12 @@ pub fn appearance( } Button::ListItem => { corner_radii = &[0.0; 4]; - let (background, text, icon) = color(&cosmic.background.component); + let (background, text, icon) = color(&cosmic.background(theme.transparent).component); if selected { - appearance.background = - Some(Background::Color(cosmic.primary.component.hover.into())); + appearance.background = Some(Background::Color( + cosmic.primary(theme.transparent).component.hover.into(), + )); appearance.icon_color = Some(cosmic.accent.base.into()); appearance.text_color = Some(cosmic.accent_text_color().into()); } else { @@ -164,7 +165,7 @@ pub fn appearance( } } Button::MenuItem => { - let (background, text, icon) = color(&cosmic.background.component); + let (background, text, icon) = color(&cosmic.background(theme.transparent).component); appearance.background = Some(Background::Color(background)); appearance.icon_color = icon; appearance.text_color = text; @@ -280,6 +281,6 @@ impl Catalog for crate::Theme { } fn selection_background(&self) -> Background { - Background::Color(self.cosmic().primary.base.into()) + Background::Color(self.cosmic().primary(self.transparent).base.into()) } } diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs index cc89a399..56f5536e 100644 --- a/src/theme/style/dropdown.rs +++ b/src/theme/style/dropdown.rs @@ -13,18 +13,28 @@ impl dropdown::menu::StyleSheet for Theme { dropdown::menu::Appearance { text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background.component.base.into()), + background: Background::Color( + cosmic.background(self.transparent).component.base.into(), + ), border_width: 0.0, border_radius: cosmic.corner_radii.radius_m.into(), border_color: Color::TRANSPARENT, hovered_text_color: cosmic.on_bg_color().into(), - hovered_background: Background::Color(cosmic.primary.component.hover.into()), + hovered_background: Background::Color( + cosmic.primary(self.transparent).component.hover.into(), + ), selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color(cosmic.primary.component.hover.into()), + selected_background: Background::Color( + cosmic.primary(self.transparent).component.hover.into(), + ), - description_color: cosmic.primary.component.on_disabled.into(), + description_color: cosmic + .primary(self.transparent) + .component + .on_disabled + .into(), } } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index aa0d290a..83066b41 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -228,11 +228,11 @@ impl iced_checkbox::Catalog for Theme { }, Checkbox::Secondary => iced_checkbox::Style { background: Background::Color(if is_checked { - cosmic.background.component.base.into() + cosmic.background(self.transparent).component.base.into() } else { self.current_container().small_widget.into() }), - icon_color: cosmic.background.on.into(), + icon_color: cosmic.background(self.transparent).on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, @@ -413,11 +413,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn background(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.background.on)), - text_color: Some(Color::from(theme.background.on)), - background: Some(iced::Background::Color(theme.background.base.into())), + icon_color: Some(Color::from(theme.background(transparent).on)), + text_color: Some(Color::from(theme.background(transparent).on)), + background: Some(iced::Background::Color( + theme.background(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -428,11 +430,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn primary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.primary.on)), - text_color: Some(Color::from(theme.primary.on)), - background: Some(iced::Background::Color(theme.primary.base.into())), + icon_color: Some(Color::from(theme.primary(transparent).on)), + text_color: Some(Color::from(theme.primary(transparent).on)), + background: Some(iced::Background::Color( + theme.primary(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -443,11 +447,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn secondary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.secondary.on)), - text_color: Some(Color::from(theme.secondary.on)), - background: Some(iced::Background::Color(theme.secondary.base.into())), + icon_color: Some(Color::from(theme.secondary(transparent).on)), + text_color: Some(Color::from(theme.secondary(transparent).on)), + background: Some(iced::Background::Color( + theme.secondary(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -497,9 +503,11 @@ impl iced_container::Catalog for Theme { Container::Custom(f) => f(self), Container::WindowBackground => iced_container::Style { - icon_color: Some(Color::from(cosmic.background.on)), - text_color: Some(Color::from(cosmic.background.on)), - background: Some(iced::Background::Color(cosmic.background.base.into())), + icon_color: Some(Color::from(cosmic.background(self.transparent).on)), + text_color: Some(Color::from(cosmic.background(self.transparent).on)), + background: Some(iced::Background::Color( + cosmic.background(self.transparent).base.into(), + )), border: Border { radius: [ cosmic.corner_radii.radius_0[0], @@ -537,12 +545,13 @@ impl iced_container::Catalog for Theme { let (icon_color, text_color) = if *focused { ( Color::from(cosmic.accent_text_color()), - Color::from(cosmic.background.on), + Color::from(cosmic.background(self.transparent).on), ) } else { use crate::ext::ColorExt; - let unfocused_color = Color::from(cosmic.background.component.on) - .blend_alpha(cosmic.background.base.into(), 0.5); + let unfocused_color = + Color::from(cosmic.background(self.transparent).component.on) + .blend_alpha(cosmic.background(self.transparent).base.into(), 0.5); (unfocused_color, unfocused_color) }; @@ -552,7 +561,9 @@ impl iced_container::Catalog for Theme { background: if *transparent { None } else { - Some(iced::Background::Color(cosmic.background.base.into())) + Some(iced::Background::Color( + cosmic.background(self.transparent).base.into(), + )) }, border: Border { radius: [ @@ -578,23 +589,23 @@ impl iced_container::Catalog for Theme { } Container::ContextDrawer => { - let mut a = Container::primary(cosmic); + let mut a = Container::primary(cosmic, self.transparent); if let Some(Background::Color(ref mut color)) = a.background { color.a = (color.a + if cosmic.is_dark { 0.60 } else { 0.5 }).min(1.); } if cosmic.is_high_contrast { a.border.width = 1.; - a.border.color = cosmic.primary.divider.into(); + a.border.color = cosmic.primary(self.transparent).divider.into(); } a } - Container::Background => Container::background(cosmic), + Container::Background => Container::background(cosmic, self.transparent), - Container::Primary => Container::primary(cosmic), + Container::Primary => Container::primary(cosmic, self.transparent), - Container::Secondary => Container::secondary(cosmic), + Container::Secondary => Container::secondary(cosmic, self.transparent), Container::Dropdown => iced_container::Style { icon_color: None, @@ -626,10 +637,14 @@ impl iced_container::Catalog for Theme { match self.layer { cosmic_theme::Layer::Background => iced_container::Style { - icon_color: Some(Color::from(cosmic.background.component.on)), - text_color: Some(Color::from(cosmic.background.component.on)), + icon_color: Some(Color::from( + cosmic.background(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.background(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.background.component.base.into(), + cosmic.background(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -639,10 +654,14 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { - icon_color: Some(Color::from(cosmic.primary.component.on)), - text_color: Some(Color::from(cosmic.primary.component.on)), + icon_color: Some(Color::from( + cosmic.primary(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.primary(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.primary.component.base.into(), + cosmic.primary(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -652,10 +671,14 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { - icon_color: Some(Color::from(cosmic.secondary.component.on)), - text_color: Some(Color::from(cosmic.secondary.component.on)), + icon_color: Some(Color::from( + cosmic.secondary(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.secondary(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.secondary.component.base.into(), + cosmic.secondary(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -668,11 +691,13 @@ impl iced_container::Catalog for Theme { } Container::Dialog => iced_container::Style { - icon_color: Some(Color::from(cosmic.primary.on)), - text_color: Some(Color::from(cosmic.primary.on)), - background: Some(iced::Background::Color(cosmic.primary.base.into())), + icon_color: Some(Color::from(cosmic.primary(self.transparent).on)), + text_color: Some(Color::from(cosmic.primary(self.transparent).on)), + background: Some(iced::Background::Color( + cosmic.primary(self.transparent).base.into(), + )), border: Border { - color: cosmic.primary.divider.into(), + color: cosmic.primary(self.transparent).divider.into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, @@ -814,13 +839,15 @@ impl menu::Catalog for Theme { menu::Style { text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background.base.into()), + background: Background::Color(cosmic.background(self.transparent).base.into()), border: Border { radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color(cosmic.background.component.hover.into()), + selected_background: Background::Color( + cosmic.background(self.transparent).component.hover.into(), + ), shadow: Default::default(), } } @@ -858,7 +885,7 @@ impl pick_list::Catalog for Theme { match status { pick_list::Status::Active => appearance, pick_list::Status::Hovered => pick_list::Style { - background: Background::Color(cosmic.background.base.into()), + background: Background::Color(cosmic.background(self.transparent).base.into()), ..appearance }, pick_list::Status::Opened { is_hovered: _ } => appearance, @@ -1054,7 +1081,10 @@ impl progress_bar::Catalog for Theme { }, ) } else { - (theme.accent.base, theme.background.divider) + ( + theme.accent.base, + theme.background(self.transparent).divider, + ) }; let border = Border { radius: theme.corner_radii.radius_xl.into(), @@ -1527,7 +1557,7 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon: Color = cosmic.background.on.into(); + let icon: Color = cosmic.background(self.transparent).on.into(); // TODO do we need to add icon color back? match status { diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 6e192bf3..421d23d5 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -64,7 +64,7 @@ impl StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); - let component = &cosmic.background.component; + let component = &cosmic.background(self.transparent).component; let mut bg = component.base; bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs index 0e63e846..d8e95277 100644 --- a/src/widget/card/style.rs +++ b/src/widget/card/style.rs @@ -31,16 +31,26 @@ impl crate::widget::card::style::Catalog for crate::Theme { match self.layer { cosmic_theme::Layer::Background => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.background.component.hover.into()), - card_2: Background::Color(cosmic.background.component.pressed.into()), + card_1: Background::Color( + cosmic.background(self.transparent).component.hover.into(), + ), + card_2: Background::Color( + cosmic.background(self.transparent).component.pressed.into(), + ), }, cosmic_theme::Layer::Primary => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.primary.component.hover.into()), - card_2: Background::Color(cosmic.primary.component.pressed.into()), + card_1: Background::Color(cosmic.primary(self.transparent).component.hover.into()), + card_2: Background::Color( + cosmic.primary(self.transparent).component.pressed.into(), + ), }, cosmic_theme::Layer::Secondary => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.secondary.component.hover.into()), - card_2: Background::Color(cosmic.secondary.component.pressed.into()), + card_1: Background::Color( + cosmic.secondary(self.transparent).component.hover.into(), + ), + card_2: Background::Color( + cosmic.secondary(self.transparent).component.pressed.into(), + ), }, } } diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 41cf1dff..cb70d8db 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -230,7 +230,7 @@ pub fn menu_items< } fn key_style(theme: &crate::Theme) -> TextStyle { - let mut color = theme.cosmic().background.component.on; + let mut color = theme.cosmic().background(theme.transparent).component.on; color.alpha *= 0.75; TextStyle { color: Some(color.into()), diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index ad6f9206..1d57777d 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -173,7 +173,9 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { iced_widget::container::Style { icon_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()), - background: Some(Background::Color(cosmic.primary.base.into())), + background: Some(Background::Color( + cosmic.primary(theme.transparent).base.into(), + )), border: Border { width: 0.0, color: Color::TRANSPARENT, From 0fc4638af38d8edecf4b0bdc4e17e8e2bd2a2c22 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 15 Apr 2026 14:45:20 -0600 Subject: [PATCH 16/26] fix: register image_extras in run_single_instance too --- src/app/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 42fa4b1b..f78beac7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -197,6 +197,9 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { + #[cfg(feature = "desktop")] + image_extras::register(); + use std::collections::HashMap; let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); From 9cac422c245777e492094177b21b8a8be4ab7bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:03:47 +0200 Subject: [PATCH 17/26] fix(toggler): animate external changes --- src/widget/toggler.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 05371a17..b95b596e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -161,7 +161,10 @@ impl<'a, Message> Widget for Toggler<'a, } fn state(&self) -> tree::State { - tree::State::new(State::default()) + tree::State::new(State { + prev_toggled: self.is_toggled, + ..State::default() + }) } fn id(&self) -> Option { @@ -238,6 +241,14 @@ impl<'a, Message> Widget for Toggler<'a, return; }; let state = tree.state.downcast_mut::(); + + // animate external changes + if state.prev_toggled != self.is_toggled { + state.anim.changed(self.duration); + shell.request_redraw(); + state.prev_toggled = self.is_toggled; + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -246,6 +257,7 @@ impl<'a, Message> Widget for Toggler<'a, if mouse_over { shell.publish((on_toggle)(!self.is_toggled)); state.anim.changed(self.duration); + state.prev_toggled = !self.is_toggled; shell.capture_event(); } } @@ -430,4 +442,5 @@ pub fn next_to_each_other( pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, + prev_toggled: bool, } From 9b465a8b5c4d3bb75389bba49d6ee1cec8c26d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:34:25 +0200 Subject: [PATCH 18/26] feat(list_column): button list items --- src/theme/style/button.rs | 30 ++++-- src/widget/list/column.rs | 128 ---------------------- src/widget/list/list_column.rs | 188 +++++++++++++++++++++++++++++++++ src/widget/list/mod.rs | 4 +- src/widget/settings/item.rs | 94 +++++++++++++---- src/widget/settings/section.rs | 17 +-- 6 files changed, 298 insertions(+), 163 deletions(-) delete mode 100644 src/widget/list/column.rs create mode 100644 src/widget/list/list_column.rs diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 0575ce67..bb52d9a6 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem, + ListItem([f32; 4]), MenuFolder, MenuItem, MenuRoot, @@ -148,8 +148,8 @@ pub fn appearance( appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem => { - corner_radii = &[0.0; 4]; + Button::ListItem(radii) => { + corner_radii = radii; let (background, text, icon) = color(&cosmic.background.component); if selected { @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - appearance(self, focused, selected, false, style, move |component| { + let mut s = appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,7 +209,15 @@ impl Catalog for crate::Theme { }; (component.base.into(), text_color, text_color) - }) + }); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn disabled(&self, style: &Self::Class) -> Style { @@ -237,7 +245,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - appearance( + let mut s = appearance( self, focused || matches!(style, Button::Image), selected, @@ -256,7 +264,15 @@ impl Catalog for crate::Theme { (component.hover.into(), text_color, text_color) }, - ) + ); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs deleted file mode 100644 index 945b9140..00000000 --- a/src/widget/list/column.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::Padding; -use iced_widget::container::Catalog; - -use crate::{ - Apply, Element, theme, - widget::{container, divider, space::vertical}, -}; - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -#[must_use] -pub struct ListColumn<'a, Message> { - spacing: u16, - padding: Padding, - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - Self { - spacing: 0, - padding: Padding::from(0), - divider_padding: 16, - list_item_padding: [space_xxs, space_m].into(), - style: theme::Container::List, - children: Vec::with_capacity(4), - } - } -} - -impl<'a, Message: 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - #[allow(clippy::should_implement_trait)] - pub fn add(self, item: impl Into>) -> Self { - #[inline(never)] - fn inner<'a, Message: 'static>( - mut this: ListColumn<'a, Message>, - item: Element<'a, Message>, - ) -> ListColumn<'a, Message> { - if !this.children.is_empty() { - this.children.push( - container(divider::horizontal::default()) - .padding([0, this.divider_padding]) - .into(), - ); - } - - // Ensure a minimum height of 32. - let list_item = crate::widget::row![ - container(item).align_y(iced::Alignment::Center), - vertical().height(iced::Length::Fixed(32.)) - ] - .padding(this.list_item_padding) - .align_y(iced::Alignment::Center); - - this.children.push(list_item.into()); - this - } - - inner(self, item.into()) - } - - #[inline] - pub fn spacing(mut self, spacing: u16) -> Self { - self.spacing = spacing; - self - } - - /// Sets the style variant of this [`Circular`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - #[inline] - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - crate::widget::column::with_children(self.children) - .spacing(self.spacing) - .padding(self.padding) - .width(iced::Length::Fill) - .apply(container) - .padding([self.spacing, 0]) - .class(self.style) - .width(iced::Length::Fill) - .into() - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs new file mode 100644 index 00000000..89a87063 --- /dev/null +++ b/src/widget/list/list_column.rs @@ -0,0 +1,188 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::container::Catalog; +use crate::widget::{button, column, container, divider, row, space::vertical}; +use crate::{Apply, Element, theme}; +use iced::{Length, Padding}; + +/// A button list item for use in a [`ListColumn`]. +pub struct ListButton<'a, Message> { + content: Element<'a, Message>, + on_press: Option, + selected: bool, +} + +/// Creates a [`ListButton`] with the given content. +pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { + ListButton { + content: content.into(), + on_press: None, + selected: false, + } +} + +impl<'a, Message: 'static> ListButton<'a, Message> { + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + pub fn on_press_maybe(mut self, on_press: Option) -> Self { + self.on_press = on_press; + self + } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +pub enum ListItem<'a, Message> { + Element(Element<'a, Message>), + Button(ListButton<'a, Message>), +} + +/// A trait for types that can be added to a [`ListColumn`]. +pub trait IntoListItem<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message>; +} + +impl<'a, Message, T> IntoListItem<'a, Message> for T +where + T: Into>, +{ + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Element(self.into()) + } +} + +impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Button(self) + } +} + +#[must_use] +pub struct ListColumn<'a, Message> { + list_item_padding: Padding, + style: theme::Container<'a>, + children: Vec>, +} + +#[inline] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { + let cosmic_theme::Spacing { + space_xxs, space_m, .. + } = theme::spacing(); + + ListColumn { + list_item_padding: [space_xxs, space_m].into(), + style: theme::Container::List, + children: Vec::with_capacity(capacity), + } +} + +impl Default for ListColumn<'_, Message> { + fn default() -> Self { + with_capacity(4) + } +} + +impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Adds an element to the list column. + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children.push(item.into_list_item()); + self + } + + /// Sets the style variant of this [`ListColumn`]. + #[inline] + pub fn style(mut self, style: ::Class<'a>) -> Self { + self.style = style; + self + } + + pub fn list_item_padding(mut self, padding: impl Into) -> Self { + self.list_item_padding = padding.into(); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + let padding = self.list_item_padding; + let count = self.children.len(); + let last_index = count.saturating_sub(1); + let radius_s = theme::active().cosmic().radius_s(); + + // Ensure minimum height of 32 + let content_row = |content| { + row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) + }; + + self.children + .into_iter() + .enumerate() + .fold( + column::with_capacity((2 * count).saturating_sub(1)), + |mut col, (i, item)| { + if i > 0 { + col = col.push(divider::horizontal::default()); + } + + match item { + ListItem::Element(content) => { + col.push(content_row(content).padding(padding).width(Length::Fill)) + } + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + } + }, + ) + .width(Length::Fill) + .apply(container) + .class(self.style) + .into() + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { + match (first, last) { + (true, true) => radius, + (true, false) => [radius[0], radius[1], 0.0, 0.0], + (false, true) => [0.0, 0.0, radius[2], radius[3]], + (false, false) => [0.0, 0.0, 0.0, 0.0], + } +} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index c6e2051c..71eda086 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod column; +pub mod list_column; -pub use self::column::{ListColumn, list_column}; +pub use self::list_column::{ListButton, ListColumn, button, list_column}; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 349d93d8..a4092093 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use crate::{ Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, row, text}, + widget::{FlexRow, Row, column, container, flex_row, list, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; @@ -114,39 +114,95 @@ impl<'a, Message: 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - #[inline(never)] - fn control_(self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(4); - - if let Some(icon) = self.icon { - contents.push(icon); - } - + fn label(self) -> Element<'a, Message> { if let Some(description) = self.description { - let column = column::with_capacity(2) + column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill); - - contents.push(column.into()); + .width(Length::Fill) + .into() } else { - contents.push(text(self.title).width(Length::Fill).into()); + text(self.title).width(Length::Fill).into() } + } + #[inline(never)] + fn control_(mut self, widget: Element<'a, Message>) -> Vec> { + let mut contents = Vec::with_capacity(3); + if let Some(icon) = self.icon.take() { + contents.push(icon); + } + contents.push(self.label()); contents.push(widget); contents } + fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { + item_row(vec![widget.into(), self.label()]) + } + pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> Row<'a, Message, Theme> { - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), ) + .on_press(on_press) + } + + pub fn toggler_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) + } + + pub fn checkbox( + self, + is_checked: bool, + message: impl Fn(bool) -> Message + 'static, + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), + ) + .on_press(on_press) + } + + pub fn checkbox_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) } } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ab95b5ad..ee07c76d 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,16 +2,19 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; +use crate::widget::list_column::IntoListItem; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: 'static>() -> Section<'a, Message> { +pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } /// A section with a pre-defined list column. -pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { +pub fn with_column( + children: ListColumn<'_, Message>, +) -> Section<'_, Message> { Section { header: None, children, @@ -24,9 +27,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: 'static> Section<'a, Message> { +impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(mut self, title: impl Into>) -> Self { + pub fn title(self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -38,8 +41,8 @@ impl<'a, Message: 'static> Section<'a, Message> { /// Add a child element to the section's list column. #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl Into>) -> Self { - self.children = self.children.add(item.into()); + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children = self.children.add(item); self } @@ -61,7 +64,7 @@ impl<'a, Message: 'static> Section<'a, Message> { } } -impl<'a, Message: 'static> From> for Element<'a, Message> { +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) From 917af9fda204d027ad55380521041b6691f17895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:59:37 +0200 Subject: [PATCH 19/26] feat(radio): internal method for radio without label Also adds the related settings item builder. --- src/widget/radio.rs | 165 ++++++++++++++++++++++-------------- src/widget/settings/item.rs | 16 +++- 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 338c0a4e..c3f115c0 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::Theme; +use crate::{Theme, theme}; use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; @@ -92,7 +92,7 @@ where { is_selected: bool, on_click: Message, - label: Element<'a, Message, Theme, Renderer>, + label: Option>, width: Length, size: f32, spacing: f32, @@ -106,9 +106,6 @@ where /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: f32 = 16.0; - /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 8.0; - /// Creates a new [`Radio`] button. /// /// It expects: @@ -126,10 +123,29 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: label.into(), + label: Some(label.into()), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, + spacing: theme::spacing().space_xs as f32, + } + } + + /// Creates a new [`Radio`] button without a label. + /// + /// This is intended for internal use with the settings item builder, + /// where the label comes from the settings item title instead. + pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self + where + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + Radio { + is_selected: Some(value) == selected, + on_click: f(value), + label: None, + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: theme::spacing().space_xs as f32, } } @@ -161,11 +177,17 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - vec![Tree::new(&self.label)] + if let Some(label) = &self.label { + vec![Tree::new(label)] + } else { + vec![] + } } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.label)); + if let Some(label) = &mut self.label { + tree.diff_children(std::slice::from_mut(label)); + } } fn size(&self) -> Size { Size { @@ -180,16 +202,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - self.label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) + if let Some(label) = &mut self.label { + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + label + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } else { + layout::Node::new(Size::new(self.size, self.size)) + } } fn operate( @@ -199,12 +225,14 @@ where renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); + } } fn update( @@ -218,24 +246,25 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().update( + &mut tree.children[0], + event, + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } if !shell.is_event_captured() { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - shell.capture_event(); return; } @@ -253,13 +282,17 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let interaction = self.label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ); + let interaction = if let Some(label) = &self.label { + label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ) + } else { + mouse::Interaction::default() + }; if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -284,8 +317,6 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); - let mut children = layout.children(); - let custom_style = if is_mouse_over { theme.style( &(), @@ -302,16 +333,21 @@ where ) }; - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); + let (dot_bounds, label_layout) = if self.label.is_some() { + let mut children = layout.children(); + let dot_bounds = children.next().unwrap().bounds(); + (dot_bounds, children.next()) + } else { + (layout.bounds(), None) + }; - let size = bounds.width; + { + let size = dot_bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds, + bounds: dot_bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -326,8 +362,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + (size - dot_size) / 2.0, - y: bounds.y + (size - dot_size) / 2.0, + x: dot_bounds.x + (size - dot_size) / 2.0, + y: dot_bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -339,9 +375,8 @@ where } } - { - let label_layout = children.next().unwrap(); - self.label.as_widget().draw( + if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { + label.as_widget().draw( &tree.children[0], renderer, theme, @@ -361,7 +396,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_widget_mut().overlay( + self.label.as_mut()?.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -377,12 +412,14 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - self.label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); + if let Some(label) = &self.label { + label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); + } } } diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a4092093..11821335 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -103,7 +103,7 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: 'static> Item<'a, Message> { +impl<'a, Message: Clone + 'static> Item<'a, Message> { /// Assigns a control to the item. pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { item_row(self.control_(widget.into())) @@ -205,4 +205,18 @@ impl<'a, Message: 'static> Item<'a, Message> { ) .on_press_maybe(on_press) } + + pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> + where + V: Eq + Copy + 'static, + F: Fn(V) -> Message + 'static, + { + let on_press = f(value); + list::button( + self.control_start(crate::widget::radio::Radio::new_no_label( + value, selected, f, + )), + ) + .on_press(on_press) + } } From 3f9e93067b31d9ba81a4e3a28653b3380c61c352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:08:42 +0200 Subject: [PATCH 20/26] fix(item builder): remove unnecessary lifetime bound for radio --- src/widget/settings/item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 11821335..5abb464c 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -208,8 +208,8 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> where - V: Eq + Copy + 'static, - F: Fn(V) -> Message + 'static, + V: Eq + Copy, + F: Fn(V) -> Message, { let on_press = f(value); list::button( From c162a1f24a2b7fdf29286dfa807c4a1b4813ab7c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 9 Apr 2026 18:32:14 -0600 Subject: [PATCH 21/26] fix(animated-image): update frames and fix compilation errors --- src/widget/frames.rs | 63 +++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 056a55ba..a542cec6 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, - event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, + Widget, event, layout, renderer, window, }; -use iced_widget::image::{self, Handle}; +use iced_widget::image::{self, FilterMethod, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::png::PngDecoder; @@ -146,7 +146,7 @@ impl Frames { match image_type { ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), } } @@ -168,10 +168,10 @@ impl Frames { let first = frames.first().cloned().unwrap(); let total_bytes = frames .iter() - .map(|f| match f.handle.data() { - iced_core::image::Handle::Path(..) => 0, - iced_core::image::Handle::Bytes(_, b) => b.len(), - iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), + .map(|f| match &f.handle { + Handle::Path(..) => 0, + Handle::Bytes(_, b) => b.len(), + Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -324,7 +324,11 @@ where &self.frames.first.handle, self.width, self.height, + None, self.content_fit, + Rotation::default(), + false, + [0.0; 4], ) } @@ -371,37 +375,18 @@ where ) { let state = tree.state.downcast_ref::(); - // Pulled from iced_native::widget::::draw - // - // TODO: export iced_native::widget::image::draw as standalone function - { - let Size { width, height } = renderer.dimensions(&state.current.frame.handle); - let image_size = Size::new(width as f32, height as f32); - - let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); - - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); - }; - - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } - } + iced_widget::image::draw( + renderer, + layout, + &state.current.frame.handle, + None, + iced_core::border::Radius::default(), + self.content_fit, + FilterMethod::default(), + Rotation::default(), + 1.0, + 1.0, + ); } } From 3ca50dd7f6f12bb5376c252126b59652b375b70a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 16 Apr 2026 23:02:39 -0400 Subject: [PATCH 22/26] wip: corner radius v2 --- Cargo.toml | 2 +- iced | 2 +- src/app/cosmic.rs | 238 ++++++++++++++-------------------------------- src/core.rs | 39 ++++++++ 4 files changed, 115 insertions(+), 166 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83fe90f0..5ccaaf7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "a7d2d7a", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } diff --git a/iced b/iced index 13b8d3ea..4f6d52fc 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 13b8d3eab67df7f40d3d9e932a9412f85ff8413c +Subproject commit 4f6d52fce72749bb4421afe8d108f561c0bf77d2 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e09ee958..e3731f77 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -6,6 +6,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; +use crate::core::AppType; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -673,34 +674,13 @@ impl Cosmic { state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN); } if self.app.core().sync_window_border_radii_to_theme() { - use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; let theme = THEME.lock().unwrap(); - let t = theme.cosmic(); - let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - let cur_rad = CornerRadius { - top_left: radii[0].round() as u32, - top_right: radii[1].round() as u32, - bottom_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; let rounded = !self.app.core().window.sharp_corners; - return Task::batch([corner_radius( - id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard()]); + + let cur_rad = self.app.core().app_type.corners(&theme, rounded); + return Task::batch([corner_radius(id, Some(cur_rad)).discard()]); } } @@ -864,79 +844,26 @@ impl Cosmic { let t = cosmic_theme.cosmic(); - let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - let cur_rad = CornerRadius { - top_left: radii[0].round() as u32, - top_right: radii[1].round() as u32, - bottom_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; - let rounded = !self.app.core().window.sharp_corners; + + let cur_rad = self.app.core().app_type.corners(&cosmic_theme, rounded); + // Update radius for the main window let main_window_id = self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = vec![ - corner_radius( - main_window_id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ]; + let mut cmds = + vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { - if let SurfaceIdWrapper::Window(_) = surface_type { - cmds.push( - corner_radius( - *id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ); - } + let cur_rad = corners(*surface_type, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } // Update radius for all tracked windows for id in &self.tracked_windows { - cmds.push( - corner_radius( - *id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } return Task::batch(cmds); @@ -1016,19 +943,13 @@ impl Cosmic { cosmic_theme.transparent = new_blur; #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { - use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; let t = cosmic_theme.cosmic(); - let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - let cur_rad = CornerRadius { - top_left: radii[0].round() as u32, - top_right: radii[1].round() as u32, - bottom_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; let rounded = !self.app.core().window.sharp_corners; + let cur_rad = + self.app.core().app_type.corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self @@ -1036,64 +957,16 @@ impl Cosmic { .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = vec![ - corner_radius( - main_window_id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ]; + let mut cmds = + vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { - if let SurfaceIdWrapper::Window(_) = surface_type { - cmds.push( - corner_radius( - *id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ); - } + let cur_rad = corners(*surface_type, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } // Update radius for all tracked windows for id in &self.tracked_windows { - cmds.push( - corner_radius( - *id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } let core = self.app.core(); @@ -1387,32 +1260,33 @@ impl Cosmic { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); - cmds.push(blur(id)); - for id in &self.tracked_windows { - cmds.push(blur(*id)); + if !self.tracked_windows.contains(&id) { + cmds.push(blur(id)); } + Task::batch(cmds) } else { Task::none() }; + let corner_task = if let Some(s) = self.surface_views.get(&id) { + corner_radius(id, Some(corners(s.1, rounded, &theme))).discard() + } else if id + == self + .app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED) + || self.tracked_windows.contains(&id) + { + corner_radius(id, Some(self.app.core().app_type.corners(&theme, rounded))) + .discard() + } else { + Task::none() + }; let t = theme.cosmic(); return Task::batch([ blur_cmd, - corner_radius( - id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), + corner_task, iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } @@ -1537,3 +1411,39 @@ impl Cosmic { .discard() } } + +#[cfg(all(feature = "wayland", target_os = "linux"))] +fn corners( + surface_type: SurfaceIdWrapper, + rounded: bool, + theme: &Theme, +) -> iced_runtime::platform_specific::wayland::CornerRadius { + let theme = theme.cosmic(); + if let SurfaceIdWrapper::Popup(_) = surface_type { + let radius_m = theme.radius_m(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_m[0].round() as u32, + top_right: radius_m[1].round() as u32, + bottom_right: radius_m[2].round() as u32, + bottom_left: radius_m[3].round() as u32, + } + } else if let SurfaceIdWrapper::Window(_) = surface_type + && rounded + { + let radius_0 = theme.radius_0(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_0[0].round() as u32, + top_right: radius_0[1].round() as u32, + bottom_right: radius_0[2].round() as u32, + bottom_left: radius_0[3].round() as u32, + } + } else { + let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_s[0].round() as u32, + top_right: radius_s[1].round() as u32, + bottom_right: radius_s[2].round() as u32, + bottom_left: radius_s[3].round() as u32, + } + } +} diff --git a/src/core.rs b/src/core.rs index f1c3946d..e725547f 100644 --- a/src/core.rs +++ b/src/core.rs @@ -117,6 +117,45 @@ pub enum AppType { Applet, } +impl AppType { + /// Calculate suggested corners for each app type main window + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn corners( + &self, + theme: &Theme, + rounded: bool, + ) -> iced_runtime::platform_specific::wayland::CornerRadius { + let theme = theme.cosmic(); + if let Self::Applet = self { + let radius_l = theme.radius_l(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_l[0].round() as u32, + top_right: radius_l[1].round() as u32, + bottom_right: radius_l[2].round() as u32, + bottom_left: radius_l[3].round() as u32, + } + } else if let Self::Window = self + && rounded + { + let radius_0 = theme.radius_0(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_0[0].round() as u32, + top_right: radius_0[1].round() as u32, + bottom_right: radius_0[2].round() as u32, + bottom_left: radius_0[3].round() as u32, + } + } else { + let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_s[0].round() as u32, + top_right: radius_s[1].round() as u32, + bottom_right: radius_s[2].round() as u32, + bottom_left: radius_s[3].round() as u32, + } + } + } +} + impl Default for Core { fn default() -> Self { Self { From 8d7bcab258ba61dc8184d85b63a0e689aefd085c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:45:33 +0200 Subject: [PATCH 23/26] fix(list_column): add back `divider_padding` Also matches previous behavior of both paddings being applied to subsequent items, rather than globally. --- src/widget/list/list_column.rs | 101 ++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 89a87063..4ef3fc01 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -64,11 +64,19 @@ impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { } } +// Snapshots the padding values at the moment an item is added +struct ListEntry<'a, Message> { + item: ListItem<'a, Message>, + item_padding: Padding, + divider_padding: u16, +} + #[must_use] pub struct ListColumn<'a, Message> { list_item_padding: Padding, + divider_padding: u16, style: theme::Container<'a>, - children: Vec>, + children: Vec>, } #[inline] @@ -83,6 +91,7 @@ pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Me ListColumn { list_item_padding: [space_xxs, space_m].into(), + divider_padding: 0, style: theme::Container::List, children: Vec::with_capacity(capacity), } @@ -100,10 +109,14 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { Self::default() } - /// Adds an element to the list column. + /// Adds a [`ListItem`] to the [`ListColumn`]. #[allow(clippy::should_implement_trait)] pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(item.into_list_item()); + self.children.push(ListEntry { + item: item.into_list_item(), + item_padding: self.list_item_padding, + divider_padding: self.divider_padding, + }); self } @@ -119,53 +132,65 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { self } + #[inline] + pub fn divider_padding(mut self, padding: u16) -> Self { + self.divider_padding = padding; + self + } + #[must_use] pub fn into_element(self) -> Element<'a, Message> { - let padding = self.list_item_padding; let count = self.children.len(); let last_index = count.saturating_sub(1); let radius_s = theme::active().cosmic().radius_s(); + let mut col = column::with_capacity((2 * count).saturating_sub(1)); // Ensure minimum height of 32 let content_row = |content| { row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) }; - self.children - .into_iter() - .enumerate() - .fold( - column::with_capacity((2 * count).saturating_sub(1)), - |mut col, (i, item)| { - if i > 0 { - col = col.push(divider::horizontal::default()); - } + for ( + i, + ListEntry { + item, + item_padding, + divider_padding, + }, + ) in self.children.into_iter().enumerate() + { + if i > 0 { + col = col + .push(container(divider::horizontal::default()).padding([0, divider_padding])); + } - match item { - ListItem::Element(content) => { - col.push(content_row(content).padding(padding).width(Length::Fill)) - } - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - } - }, - ) - .width(Length::Fill) + col = match item { + ListItem::Element(content) => col.push( + content_row(content) + .padding(item_padding) + .width(Length::Fill), + ), + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(item_padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + }; + } + + col.width(Length::Fill) .apply(container) .class(self.style) .into() From c423ad1bfc25057922406c687f2ddc75ead5ab67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:19:23 +0200 Subject: [PATCH 24/26] improv(about): use `ListButton` --- src/widget/about.rs | 25 +++++++++++++++---------- src/widget/settings/section.rs | 11 ++++++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/widget/about.rs b/src/widget/about.rs index 148af02a..9b21e93a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,8 +1,9 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, space}, + widget::{self, list}, }; +use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -104,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { - widget::row::with_capacity(3) - .push(widget::text(name)) - .push(space::horizontal()) + let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { + color: Some(theme.cosmic().accent_text_color().into()), + }); + + let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { + widget::row::with_capacity(2) + .push(widget::text::body(name).width(Length::Fill)) .push_maybe( - (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), + (!url.is_empty()).then_some( + widget::icon::from_name("link-symbolic") + .icon() + .class(crate::theme::Svg::Custom(svg_accent.clone())), + ), ) .align_y(Alignment::Center) - .apply(widget::button::custom) - .class(crate::theme::Button::Link) + .apply(list::button) .on_press(on_url_press(url)) - .width(Length::Fill) - .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ee07c76d..3dddb1a1 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -3,7 +3,7 @@ use crate::Element; use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, text}; +use crate::widget::{ListColumn, column, list_column, text}; use std::borrow::Cow; /// A section within a settings view column. @@ -11,6 +11,11 @@ pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } +/// A section with a pre-defined list column of a given capacity. +pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { + with_column(list_column::with_capacity(capacity)) +} + /// A section with a pre-defined list column. pub fn with_column( children: ListColumn<'_, Message>, @@ -47,7 +52,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { } /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>>) -> Self { + pub fn add_maybe(self, item: Option>) -> Self { if let Some(item) = item { self.add(item) } else { @@ -58,7 +63,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Extends the [`Section`] with the given children. pub fn extend( self, - children: impl IntoIterator>>, + children: impl IntoIterator>, ) -> Self { children.into_iter().fold(self, Self::add) } From 01ab456610219ef51f41a192f5d68eebe19915db Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 17 Apr 2026 13:42:51 -0400 Subject: [PATCH 25/26] fix: blur only after blur event --- iced | 2 +- src/app/action.rs | 2 ++ src/app/cosmic.rs | 56 ++++++++++++++++++++--------------------------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/iced b/iced index 4f6d52fc..0a093b3a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 4f6d52fce72749bb4421afe8d108f561c0bf77d2 +Subproject commit 0a093b3ab0d5ad1b3ad6b457c1715880276e0ce1 diff --git a/src/app/action.rs b/src/app/action.rs index fb982acb..f894bd7b 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -64,6 +64,8 @@ pub enum Action { Unfocus(iced::window::Id), /// Windowing system initialized WindowingSystemInitialized, + /// Blur support enabled + BlurEnabled, /// Updates the window maximized state WindowMaximized(iced::window::Id, bool), /// Updates the tracked window geometry. diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e3731f77..c22b8dd6 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -95,6 +95,7 @@ pub struct Cosmic { >, pub tracked_windows: HashSet, pub opened_surfaces: HashMap, + blur_enabled: bool, } impl Cosmic @@ -461,6 +462,9 @@ where )) => { return Some(Action::WindowState(id, s)); } + wayland::Event::BlurEnabled => { + return Some(Action::BlurEnabled); + } _ => (), } } @@ -756,7 +760,7 @@ impl Cosmic { } } - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -798,7 +802,7 @@ impl Cosmic { return iced::Task::none(); } // update transparent - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -839,7 +843,6 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { - use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; let t = cosmic_theme.cosmic(); @@ -923,7 +926,7 @@ impl Cosmic { } else { new_theme }; - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = new_theme.cosmic(); match core.app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -945,8 +948,6 @@ impl Cosmic { if self.app.core().sync_window_border_radii_to_theme() { use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let t = cosmic_theme.cosmic(); - let rounded = !self.app.core().window.sharp_corners; let cur_rad = self.app.core().app_type.corners(&cosmic_theme, rounded); @@ -1008,7 +1009,7 @@ impl Cosmic { #[allow(clippy::used_underscore_binding)] _token, ), - ) + ); } #[cfg(not(all(feature = "wayland", target_os = "linux")))] @@ -1091,15 +1092,14 @@ impl Cosmic { crate::theme::system_light() }; if let ThemeType::System { .. } = new_theme.theme_type { - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) - && { - let t = new_theme.cosmic(); - match 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 new_blur = self.blur_enabled && { + let t = new_theme.cosmic(); + match 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, + } + }; new_theme.transparent = new_blur; } core.system_theme = new_theme.clone(); @@ -1229,22 +1229,14 @@ impl Cosmic { Action::Opened(id) => { #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { - use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; let mut theme = THEME.lock().unwrap(); - let t = theme.cosmic(); - let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - let cur_rad = CornerRadius { - top_left: radii[0].round() as u32, - top_right: radii[1].round() as u32, - bottom_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; + // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; let core = self.app.core(); - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -1260,9 +1252,7 @@ impl Cosmic { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); - if !self.tracked_windows.contains(&id) { - cmds.push(blur(id)); - } + cmds.push(blur(id)); Task::batch(cmds) } else { @@ -1283,7 +1273,6 @@ impl Cosmic { } else { Task::none() }; - let t = theme.cosmic(); return Task::batch([ blur_cmd, corner_task, @@ -1292,10 +1281,11 @@ impl Cosmic { } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - Action::WindowingSystemInitialized => { + Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows let core = self.app.core(); - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + self.blur_enabled = true; + let new_blur = self.blur_enabled && { let t = core.system_theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -1323,6 +1313,7 @@ impl Cosmic { return Task::batch(cmds); } } + _ => (), } iced::Task::none() @@ -1337,6 +1328,7 @@ impl Cosmic { surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), + blur_enabled: false, } } From 95756b1a576cf6dc9f6135cf1c66e1283bfc487f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:33:57 +0200 Subject: [PATCH 26/26] improv(circular): prevent caps from touching --- examples/application/Cargo.toml | 1 + examples/application/src/main.rs | 3 +- src/widget/progress_bar/circular.rs | 57 +++++++++++++++++------------ 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index c494238f..7a6083e0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -21,4 +21,5 @@ features = [ "single-instance", "surface-message", "multi-window", + "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index bceece6e..f6e571e0 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -200,7 +200,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(5) + widget::column::with_capacity(14) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -223,6 +223,7 @@ impl cosmic::Application for App { .on_clear(Message::Ignore), ) .push(widget::progress_bar::circular::Circular::new().size(50.0)) + .push(widget::progress_bar::circular::Circular::new().size(20.0)) .push( widget::progress_bar::linear::Linear::new() .girth(10.0) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 7e8177d6..fa8c38fe 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -15,8 +15,6 @@ use std::f32::consts::PI; use std::time::Duration; const MIN_ANGLE: Radians = Radians(PI / 8.0); -const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); -const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; #[must_use] pub struct Circular @@ -83,6 +81,12 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } + + fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { + let cap_angle = self.bar_height / track_radius; + let gap = MIN_ANGLE.0.max(cap_angle); + (gap - cap_angle, 2.0 * PI - gap * 2.0) + } } impl Default for Circular @@ -122,7 +126,7 @@ impl Default for Animation { } impl Animation { - fn next(&self, additional_rotation: u32, now: Instant) -> Self { + fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { match self { Self::Expanding { rotation, .. } => Self::Contracting { start: now, @@ -133,9 +137,9 @@ impl Animation { Self::Contracting { rotation, .. } => Self::Expanding { start: now, progress: 0.0, - rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( - (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, - )), + rotation: rotation.wrapping_add( + (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, + ), last: now, }, } @@ -157,6 +161,7 @@ impl Animation { &self, cycle_duration: Duration, rotation_duration: Duration, + wrap_angle: f32, now: Instant, ) -> Self { let elapsed = now.duration_since(self.start()); @@ -165,7 +170,7 @@ impl Animation { * (u32::MAX) as f32) as u32; match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), + elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), } } @@ -267,10 +272,13 @@ where return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - state.animation = - state - .animation - .timed_transition(self.cycle_duration, self.rotation_duration, *now); + let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.rotation_duration, + wrap_angle, + *now, + ); state.cache.clear(); shell.request_redraw(); @@ -380,22 +388,23 @@ where } else { let mut builder = canvas::path::Builder::new(); - let start = Radians(state.animation.rotation() * 2.0 * PI); + let start = state.animation.rotation() * 2.0 * PI; + let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); let (start_angle, end_angle) = match state.animation { Animation::Expanding { progress, .. } => ( start, - start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), + start + min_angle + wrap_angle * smootherstep(progress), ), Animation::Contracting { progress, .. } => ( - start + WRAP_ANGLE * (smootherstep(progress)), - start + MIN_ANGLE + WRAP_ANGLE, + start + wrap_angle * smootherstep(progress), + start + min_angle + wrap_angle, ), }; builder.arc(canvas::path::Arc { center: frame.center(), radius: track_radius, - start_angle, - end_angle, + start_angle: Radians(start_angle), + end_angle: Radians(end_angle), }); let bar_path = builder.build(); @@ -410,23 +419,23 @@ where let mut builder = canvas::path::Builder::new(); // get center of end of arc for rounded cap - let end_center = frame.center() - + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; + let end_center = + frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: end_center, radius: self.bar_height / 2.0, - start_angle: Radians(end_angle.0), - end_angle: Radians(end_angle.0 + PI), + start_angle: Radians(end_angle), + end_angle: Radians(end_angle + PI), }); // get center of start of arc for rounded cap let start_center = frame.center() - + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: start_center, radius: self.bar_height / 2.0, - start_angle: Radians(start_angle.0 - PI), - end_angle: Radians(start_angle.0), + start_angle: Radians(start_angle - PI), + end_angle: Radians(start_angle), }); let cap_path = builder.build();