diff --git a/Cargo.toml b/Cargo.toml index d73da2dc..5ccaaf7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,6 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", - "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -127,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 } @@ -142,14 +141,9 @@ 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 } @@ -176,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(all(unix, not(target_os = "macos")))'.dependencies] +[target.'cfg(unix)'.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(any(not(unix), target_os = "macos"))'.dependencies] +[target.'cfg(not(unix))'.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 4ce0aa9e..c69feaf5 100644 --- a/build.rs +++ b/build.rs @@ -3,9 +3,7 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() - || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") - { + if env::var_os("CARGO_CFG_UNIX").is_none() { generate_bundled_icons(); } } 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), } } } 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) + } + } } } 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/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/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/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",)) 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..6076b9c8 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,15 +1,16 @@ 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, }; 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"; @@ -37,24 +38,25 @@ 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, /// 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 @@ -77,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 @@ -96,15 +96,31 @@ pub struct Theme { /// cosmic-comp custom window hint color pub window_hint: Option, /// enables blurred transparency - pub is_frosted: bool, + 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)] 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, } @@ -170,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] @@ -739,7 +788,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - }; + } Self::dark_default() } @@ -748,10 +797,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 +815,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,31 +825,59 @@ 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 + pub frosted: BlurStrength, /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// 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, + /// 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 { @@ -825,11 +895,15 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - is_frosted: false, + 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, } } } @@ -971,9 +1045,15 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, } = self; + let container_alpha = frosted.alpha(); + let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1028,6 +1108,10 @@ impl ThemeBuilder { 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 { @@ -1054,8 +1138,22 @@ 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 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]) @@ -1102,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(|| { @@ -1332,10 +1469,75 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, accent_text, control_tint: neutral_tint, text_tint, + frosted_windows, + 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; @@ -1354,3 +1556,73 @@ impl ThemeBuilder { Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) } } + +/// 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 { + ExtremelyLow, + ExtremelyLow2, + VeryLow, + VeryLow2, + Low, + Low2, + #[default] + 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.90, + 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.25, + Self::ExtremelyHigh2 => 0.2, + } + } +} + +impl TryFrom for BlurStrength { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 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/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 diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 7a6083e0..c494238f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -21,5 +21,4 @@ features = [ "single-instance", "surface-message", "multi-window", - "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index f6e571e0..bceece6e 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(14) + widget::column::with_capacity(5) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -223,7 +223,6 @@ 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/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 2d3704a6..238000f5 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index 6eac2bc7..e69de29b 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -1,33 +0,0 @@ -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 6cc0adbc..8d499756 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,33 +2,26 @@ 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 8c9b201c..e69de29b 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -1,34 +0,0 @@ -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 月 diff --git a/iced b/iced index 78caabba..0a093b3a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece +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 030ed041..c22b8dd6 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"))] @@ -94,6 +95,7 @@ pub struct Cosmic { >, pub tracked_windows: HashSet, pub opened_surfaces: HashMap, + blur_enabled: bool, } impl Cosmic @@ -115,8 +117,8 @@ where ( Self::new(model), Task::batch([ - command, iced_runtime::window::run_with_handle(id, init_windowing_system), + command, ]), ) } @@ -460,6 +462,9 @@ where )) => { return Some(Action::WindowState(id, s)); } + wayland::Event::BlurEnabled => { + return Some(Action::BlurEnabled); + } _ => (), } } @@ -673,34 +678,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()]); } } @@ -773,18 +757,61 @@ impl Cosmic { if a.distance_squared(*t_inner.accent_color()) > 0.00001 { theme = Theme::system(Arc::new(t_inner.with_accent(a))); } - }; + } } - THEME.lock().unwrap().set_theme(theme.theme_type); + let new_blur = self.blur_enabled && { + 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 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 { + 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) => { + 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 = self.blur_enabled && { + 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(); @@ -808,89 +835,38 @@ impl Cosmic { } else { theme }; + new_theme.transparent = new_blur; new_theme.theme_type.prefer_dark(prefer_dark); 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; 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 .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.iter() { - 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(), - ); + for id in &self.tracked_windows { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } return Task::batch(cmds); @@ -921,6 +897,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(); @@ -949,6 +926,15 @@ impl Cosmic { } else { new_theme }; + 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(); { @@ -957,21 +943,14 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { 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; 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 @@ -979,64 +958,35 @@ 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.iter() { - 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(), - ); + for id in &self.tracked_windows { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + + let core = self.app.core(); + if core.auto_blur { + let blur = if cosmic_theme.transparent { + 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); @@ -1059,7 +1009,7 @@ impl Cosmic { #[allow(clippy::used_underscore_binding)] _token, ), - ) + ); } #[cfg(not(all(feature = "wayland", target_os = "linux")))] @@ -1136,18 +1086,48 @@ 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() }; + if let ThemeType::System { .. } = new_theme.theme_type { + 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(); { let mut cosmic_theme = THEME.lock().unwrap(); // 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 cosmic_theme.transparent { + 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); } } } @@ -1249,43 +1229,91 @@ 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 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 mut theme = THEME.lock().unwrap(); + // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; + let core = self.app.core(); + let new_blur = self.blur_enabled && { + 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 blur_cmd = 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()); + 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() + }; 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(), + blur_cmd, + corner_task, iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - _ => {} + Action::BlurEnabled => { + // TODO do this after blur event confirms support instead of for all wayland windows + let core = self.app.core(); + 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, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + let mut t = THEME.lock().unwrap(); + + t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; + + 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() @@ -1300,6 +1328,7 @@ impl Cosmic { surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), + blur_enabled: false, } } @@ -1374,3 +1403,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/app/mod.rs b/src/app/mod.rs index f78beac7..fedbcef7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -128,9 +128,6 @@ 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); @@ -197,9 +194,6 @@ 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(); @@ -826,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: [ @@ -855,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 48721e1c..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,16 +378,18 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; + 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()), - background: Some(Color::from(cosmic.background.base).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, } }), @@ -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/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 diff --git a/src/core.rs b/src/core.rs index 970a5351..e725547f 100644 --- a/src/core.rs +++ b/src/core.rs @@ -101,6 +101,59 @@ pub struct Core { #[cfg(all(feature = "wayland", target_os = "linux"))] 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 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 { @@ -161,6 +214,8 @@ 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, + app_type: AppType::Window, } } } @@ -502,4 +557,20 @@ 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; + } + + 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 093bac05..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), } } @@ -307,7 +309,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index bb52d9a6..b15bc364 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem([f32; 4]), + ListItem, MenuFolder, MenuItem, MenuRoot, @@ -128,33 +128,34 @@ 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()); appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem(radii) => { - corner_radii = radii; - let (background, text, icon) = color(&cosmic.background.component); + Button::ListItem => { + corner_radii = &[0.0; 4]; + 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; @@ -197,7 +198,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - let mut s = appearance(self, focused, selected, false, style, move |component| { + appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,15 +210,7 @@ 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 { @@ -245,7 +238,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - let mut s = appearance( + appearance( self, focused || matches!(style, Button::Image), selected, @@ -264,15 +257,7 @@ 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 { @@ -296,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 aa6f4b33..83066b41 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.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), } } } @@ -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() @@ -478,14 +484,30 @@ 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), 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], @@ -523,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) }; @@ -538,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: [ @@ -564,20 +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, @@ -609,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(), @@ -622,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(), @@ -635,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(), @@ -651,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(), }, @@ -797,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(), } } @@ -841,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, @@ -1037,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(), @@ -1510,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 ed0e657a..421d23d5 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -64,11 +64,13 @@ 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.); 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), diff --git a/src/widget/about.rs b/src/widget/about.rs index 9b21e93a..148af02a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,9 +1,8 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, list}, + widget::{self, space}, }; -use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -105,23 +104,19 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - 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)) + let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { + widget::row::with_capacity(3) + .push(widget::text(name)) + .push(space::horizontal()) .push_maybe( - (!url.is_empty()).then_some( - widget::icon::from_name("link-symbolic") - .icon() - .class(crate::theme::Svg::Custom(svg_accent.clone())), - ), + (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) .align_y(Alignment::Center) - .apply(list::button) + .apply(widget::button::custom) + .class(crate::theme::Button::Link) .on_press(on_url_press(url)) + .width(Length::Fill) + .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { 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/frames.rs b/src/widget/frames.rs index a542cec6..056a55ba 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, Rotation, Shell, Size, - Widget, event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + event, layout, renderer, window, }; -use iced_widget::image::{self, FilterMethod, Handle}; +use iced_widget::image::{self, 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 { - Handle::Path(..) => 0, - Handle::Bytes(_, b) => b.len(), - Handle::Rgba { pixels, .. } => pixels.len(), + .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(), }) .sum::() .try_into() @@ -324,11 +324,7 @@ where &self.frames.first.handle, self.width, self.height, - None, self.content_fit, - Rotation::default(), - false, - [0.0; 4], ) } @@ -375,18 +371,37 @@ where ) { let state = tree.state.downcast_ref::(); - 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, - ); + // 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); + } + } } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bb6ce244..9d0877d0 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(all(unix, not(target_os = "macos")))] +#[cfg(unix)] pub fn get(icon_name: &str) -> Option { None } -#[cfg(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] /// 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(any(not(unix), target_os = "macos"))] +#[cfg(not(unix))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index dfd66cf5..8405e080 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(all(unix, not(target_os = "macos")))] + #[cfg(not(windows))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(any(not(unix), target_os = "macos"))] + #[cfg(windows)] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 00000000..945b9140 --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,128 @@ +// 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 deleted file mode 100644 index 4ef3fc01..00000000 --- a/src/widget/list/list_column.rs +++ /dev/null @@ -1,213 +0,0 @@ -// 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) - } -} - -// 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>, -} - -#[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(), - divider_padding: 0, - 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 a [`ListItem`] to the [`ListColumn`]. - #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(ListEntry { - item: item.into_list_item(), - item_padding: self.list_item_padding, - divider_padding: self.divider_padding, - }); - 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 - } - - #[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 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) - }; - - 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])); - } - - 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() - } -} - -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 71eda086..c6e2051c 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 list_column; +pub mod column; -pub use self::list_column::{ListButton, ListColumn, button, list_column}; +pub use self::column::{ListColumn, list_column}; 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, diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index fa8c38fe..7e8177d6 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -15,6 +15,8 @@ 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 @@ -81,12 +83,6 @@ 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 @@ -126,7 +122,7 @@ impl Default for Animation { } impl Animation { - fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { + fn next(&self, additional_rotation: u32, now: Instant) -> Self { match self { Self::Expanding { rotation, .. } => Self::Contracting { start: now, @@ -137,9 +133,9 @@ impl Animation { Self::Contracting { rotation, .. } => Self::Expanding { start: now, progress: 0.0, - rotation: rotation.wrapping_add( - (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, - ), + rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( + (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, + )), last: now, }, } @@ -161,7 +157,6 @@ impl Animation { &self, cycle_duration: Duration, rotation_duration: Duration, - wrap_angle: f32, now: Instant, ) -> Self { let elapsed = now.duration_since(self.start()); @@ -170,7 +165,7 @@ impl Animation { * (u32::MAX) as f32) as u32; match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), + elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), } } @@ -272,13 +267,10 @@ where return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - 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.animation = + state + .animation + .timed_transition(self.cycle_duration, self.rotation_duration, *now); state.cache.clear(); shell.request_redraw(); @@ -388,23 +380,22 @@ where } else { let mut builder = canvas::path::Builder::new(); - let start = state.animation.rotation() * 2.0 * PI; - let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); + let start = Radians(state.animation.rotation() * 2.0 * PI); 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: Radians(start_angle), - end_angle: Radians(end_angle), + start_angle, + end_angle, }); let bar_path = builder.build(); @@ -419,23 +410,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.cos(), end_angle.sin()) * track_radius; + let end_center = frame.center() + + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; builder.arc(canvas::path::Arc { center: end_center, radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), + start_angle: Radians(end_angle.0), + end_angle: Radians(end_angle.0 + PI), }); // get center of start of arc for rounded cap let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; + + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; builder.arc(canvas::path::Arc { center: start_center, radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), + start_angle: Radians(start_angle.0 - PI), + end_angle: Radians(start_angle.0), }); let cap_path = builder.build(); diff --git a/src/widget/radio.rs b/src/widget/radio.rs index c3f115c0..338c0a4e 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::{Theme, theme}; +use crate::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: Option>, + label: Element<'a, Message, Theme, Renderer>, width: Length, size: f32, spacing: f32, @@ -106,6 +106,9 @@ 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: @@ -123,29 +126,10 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: Some(label.into()), + label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - 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, + spacing: Self::DEFAULT_SPACING, } } @@ -177,17 +161,11 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - if let Some(label) = &self.label { - vec![Tree::new(label)] - } else { - vec![] - } + vec![Tree::new(&self.label)] } fn diff(&mut self, tree: &mut Tree) { - if let Some(label) = &mut self.label { - tree.diff_children(std::slice::from_mut(label)); - } + tree.diff_children(std::slice::from_mut(&mut self.label)); } fn size(&self) -> Size { Size { @@ -202,20 +180,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - 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)) - } + 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) + }, + ) } fn operate( @@ -225,14 +199,12 @@ where renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - if let Some(label) = &mut self.label { - label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); - } + self.label.as_widget_mut().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); } fn update( @@ -246,25 +218,24 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - 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, - ); - } + self.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::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); + shell.capture_event(); return; } @@ -282,17 +253,13 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - 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() - }; + let interaction = self.label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ); if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -317,6 +284,8 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); + let mut children = layout.children(); + let custom_style = if is_mouse_over { theme.style( &(), @@ -333,21 +302,16 @@ where ) }; - 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 = dot_bounds.width; + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + let size = bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds: dot_bounds, + bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -362,8 +326,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: dot_bounds.x + (size - dot_size) / 2.0, - y: dot_bounds.y + (size - dot_size) / 2.0, + x: bounds.x + (size - dot_size) / 2.0, + y: bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -375,8 +339,9 @@ where } } - if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { - label.as_widget().draw( + { + let label_layout = children.next().unwrap(); + self.label.as_widget().draw( &tree.children[0], renderer, theme, @@ -396,7 +361,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_mut()?.as_widget_mut().overlay( + self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -412,14 +377,12 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some(label) = &self.label { - label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); - } + self.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 5abb464c..349d93d8 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, list, row, text}, + widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; @@ -103,7 +103,7 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: Clone + 'static> Item<'a, Message> { +impl<'a, Message: '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())) @@ -114,109 +114,39 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - fn label(self) -> Element<'a, Message> { + #[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); + } + if let Some(description) = self.description { - column::with_capacity(2) + let column = column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill) - .into() - } else { - text(self.title).width(Length::Fill).into() - } - } + .width(Length::Fill); - #[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(column.into()); + } else { + contents.push(text(self.title).width(Length::Fill).into()); } - 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, - ) -> 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), - ), + ) -> Row<'a, Message, Theme> { + 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) - } - - pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> - where - V: Eq + Copy, - F: Fn(V) -> Message, - { - let on_press = f(value); - list::button( - self.control_start(crate::widget::radio::Radio::new_no_label( - value, selected, f, - )), - ) - .on_press(on_press) } } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 3dddb1a1..ab95b5ad 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,24 +2,16 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; -use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, list_column, text}; +use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { +pub fn section<'a, Message: '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>, -) -> Section<'_, Message> { +pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { Section { header: None, children, @@ -32,9 +24,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: Clone + 'static> Section<'a, Message> { +impl<'a, Message: 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(self, title: impl Into>) -> Self { + pub fn title(mut self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -46,13 +38,13 @@ impl<'a, Message: Clone + '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 IntoListItem<'a, Message>) -> Self { - self.children = self.children.add(item); + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); self } /// 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 { @@ -63,13 +55,13 @@ 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) } } -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { +impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index b95b596e..05371a17 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -161,10 +161,7 @@ impl<'a, Message> Widget for Toggler<'a, } fn state(&self) -> tree::State { - tree::State::new(State { - prev_toggled: self.is_toggled, - ..State::default() - }) + tree::State::new(State::default()) } fn id(&self) -> Option { @@ -241,14 +238,6 @@ 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 { .. }) => { @@ -257,7 +246,6 @@ 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(); } } @@ -442,5 +430,4 @@ pub fn next_to_each_other( pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, - prev_toggled: bool, }