diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7897eb01..5a5258f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,26 +6,20 @@ on: - master pull_request: - jobs: format: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: + toolchain: nightly components: rustfmt - - name: Cargo cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - name: Format - run: cargo fmt -- --check + run: cargo +nightly fmt -- --check tests: needs: @@ -34,38 +28,28 @@ jobs: fail-fast: false matrix: test_args: - - --no-default-features --features "" # for cosmic-comp, don't remove! - - --no-default-features --features "winit_debug" - - --no-default-features --features "winit_tokio" - - --no-default-features --features "winit" - - --no-default-features --features "winit_wgpu" - - --no-default-features --features "wayland" - - --no-default-features --features "applet" - - --no-default-features --features "desktop,smol" - - --no-default-features --features "desktop,tokio" - - -p cosmic-theme - runs-on: ubuntu-22.04 + - --no-default-features --features "" # for cosmic-comp, don't remove! + - --no-default-features --features "winit_debug" + - --no-default-features --features "winit_tokio" + - --no-default-features --features "winit" + - --no-default-features --features "winit_wgpu" + - --no-default-features --features "wayland" + - --no-default-features --features "applet" + - --no-default-features --features "desktop,smol" + - --no-default-features --features "desktop,tokio" + - -p cosmic-theme + runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - - name: Get date for registry cache - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Cargo registry cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} - restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Test features run: cargo test ${{ matrix.test_args }} -- --test-threads=1 env: @@ -82,28 +66,18 @@ jobs: - "open-dialog" - "context-menu" - "nav-context" - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - - name: Get date for registry cache - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Cargo registry cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} - restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Check example run: cargo check -p "${{ matrix.examples }}" env: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index af5b059b..e51c552f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,13 +11,14 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - name: Install Rust nightly - uses: dtolnay/rust-toolchain@master + uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2026-04-27 + rustflags: "" - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation @@ -29,7 +30,7 @@ jobs: -p libcosmic \ --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc diff --git a/Cargo.toml b/Cargo.toml index 738e71fa..9948711a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,6 +173,7 @@ url = "2.5.8" zbus = { workspace = true, optional = true } float-cmp = "0.10.0" ron = { workspace = true, optional = true } +enumflags2 = "0.7.12" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] @@ -289,7 +290,7 @@ serde = "1.0" thiserror = "2.0" tracing = "0.1" tokio = "1.52" -zbus = {version = "5.15", default-features = false} +zbus = { version = "5.15", default-features = false } # Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores # [profile.*] blocks in non-root manifests, so this lives at the diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index cc19a91e..b49cf447 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -1,17 +1,32 @@ 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 let ast = syn::parse(input).unwrap(); - - // Build the trait implementation 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 +63,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 +118,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 2af498ff..1396b5cb 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -175,6 +175,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 .. @@ -193,9 +194,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 = @@ -205,6 +210,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 + }, }) } @@ -212,6 +224,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)); @@ -236,15 +252,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. @@ -254,6 +284,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 + }, }) } @@ -263,6 +300,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)); @@ -276,11 +317,27 @@ 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 + }, }) } /// Get data for the given application name and config version. + /// pub fn new_state(name: &str, version: u64) -> Result { pub fn new_data(name: &str, version: u64) -> Result { + Self::new_data_inner(name, version, true) + } + + pub fn new_data_inner( + name: &str, + version: u64, + look_for_previous: bool, + ) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -294,6 +351,13 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::new_data_inner(name, version - 1, false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -404,7 +468,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 b5bffa17..2907441f 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -16,6 +16,7 @@ no-default = [] [dependencies] palette = { workspace = true, features = ["serializing"] } +hex_color = { version = "3", features = ["serde"] } almost = "0.2" serde = { workspace = true, features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ diff --git a/cosmic-theme/src/model/color.rs b/cosmic-theme/src/model/color.rs new file mode 100644 index 00000000..fb4c2202 --- /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) => Some(v.to_repr()).serialize(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 19370dee..a7ac613f 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -5,6 +5,7 @@ pub use mode::*; pub use spacing::*; pub use theme::*; +pub mod color; mod corner; mod cosmic_palette; mod derivation; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 0c06005e..64f81f8a 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,3 +1,5 @@ +use crate::color::color_serde::option as color_serde_option; +use crate::color::{ColorRepr, ColorReprOption, color_serde}; use crate::composite::over; use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}; use crate::{ @@ -9,6 +11,7 @@ use palette::color_difference::Wcag21RelativeContrast; use palette::rgb::Rgb; use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; +use std::default; use std::num::NonZeroUsize; /// ID for the current dark `ThemeBuilder` config @@ -45,16 +48,24 @@ pub enum Layer { PartialEq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] -#[version = 1] +#[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 @@ -79,8 +90,6 @@ pub struct Theme { pub list_button: Component, /// text button element colors pub text_button: Component, - /// button component styling - pub button: Component, /// palette pub palette: CosmicPaletteInner, /// spacing @@ -97,16 +106,40 @@ pub struct Theme { pub active_hint: u32, /// cosmic-comp custom window hint color pub window_hint: Option, + #[serde(default)] /// enables blurred transparency - pub is_frosted: bool, + pub frosted: BlurStrength, + #[serde(default)] + /// frosted windows + pub frosted_windows: bool, + #[serde(default)] + /// frosted system interface + pub frosted_system_interface: bool, + #[serde(default)] + /// frosted panel + pub frosted_panel: bool, + #[serde(default)] + /// frosted applet popups + pub frosted_applets: bool, + #[serde(default)] + /// alpha map + pub alpha_map: AlphaMap, /// 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, } @@ -172,6 +205,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] @@ -741,7 +807,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - }; + } Self::dark_default() } @@ -750,10 +816,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() @@ -776,7 +842,7 @@ impl From for Theme { cosmic_config::cosmic_config_derive::CosmicConfigEntry, PartialEq, )] -#[version = 1] +#[version = 2] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, @@ -785,31 +851,67 @@ 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, + #[serde(default)] /// 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, + #[serde(default)] + /// frosted windows + pub frosted_windows: bool, + #[serde(default)] + /// frosted system interface + pub frosted_system_interface: bool, + #[serde(default)] + /// frosted panel + pub frosted_panel: bool, + #[serde(default)] + /// frosted applet popups + pub frosted_applets: bool, + #[serde(default)] + /// alpha map + pub alpha_map: AlphaMap, } impl Default for ThemeBuilder { @@ -827,11 +929,16 @@ 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, + alpha_map: AlphaMap::default(), } } } @@ -979,9 +1086,22 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, + alpha_map, } = self; + let container_alpha = alpha_map.blurred_alpha(frosted); + let actual_alpha = + if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) { + container_alpha + } else { + 1.0 + }; + let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1036,6 +1156,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 { @@ -1060,10 +1184,26 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, + ); + + 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(), + actual_alpha, ); 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]) @@ -1093,6 +1233,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + container_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1105,6 +1246,48 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + container_alpha, + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + 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(), + actual_alpha, + ), + 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(), + actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1178,6 +1361,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1214,6 +1398,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1226,6 +1411,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1238,14 +1424,24 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, ), - accent_button: Component::colored_button( - accent, - control_steps_array[1], - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), + accent_button: { + let step_array = steps(control_steps_array[5], NonZeroUsize::new(100).unwrap()); + + Component::colored_button( + accent, + control_steps_array[1], + get_text( + color_index(accent, step_array.len()), + &step_array, + &step_array[0], + text_steps_array.as_deref(), + 1., // accent is opaque anyway + ), + accent, + button_hovered_overlay, + button_pressed_overlay, + ) + }, button: Component::component( button_bg, accent, @@ -1349,10 +1545,79 @@ 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, + alpha_map, + transparent_background: Container::new( + Component::component( + transparent_bg_component, + accent, + on_transparent_bg_component, + component_hovered_overlay, + component_pressed_overlay, + 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(), + actual_alpha, + ), + 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(), + actual_alpha, + ), + component_hovered_overlay, + component_pressed_overlay, + is_high_contrast, + control_steps_array[8], + ), + container_bg, + get_text( + base_index, + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + actual_alpha, + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ) + }, }; theme.spacing = spacing; theme.corner_radii = corner_radii; @@ -1371,3 +1636,181 @@ 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(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum BlurStrength { + ExtremelyLow, + ExtremelyLow2, + VeryLow, + VeryLow2, + Low, + Low2, + Medium, + Medium2, + High, + High2, + VeryHigh, + VeryHigh2, + ExtremelyHigh, + ExtremelyHigh2, +} + +impl Default for BlurStrength { + fn default() -> Self { + Self::Medium + } +} + +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(()), + } + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AlphaMap { + pub extremely_low: f32, + pub extremely_low_2: f32, + pub very_low: f32, + pub very_low_2: f32, + pub low: f32, + pub low_2: f32, + pub medium: f32, + pub medium_2: f32, + pub high: f32, + pub high_2: f32, + pub very_high: f32, + pub very_high_2: f32, + pub extremely_high: f32, + pub extremely_high_2: f32, +} + +impl AlphaMap { + pub fn blurred_alpha(&self, blur: BlurStrength) -> f32 { + match blur { + BlurStrength::ExtremelyLow => self.extremely_low, + BlurStrength::ExtremelyLow2 => self.extremely_low_2, + BlurStrength::VeryLow => self.very_low, + BlurStrength::VeryLow2 => self.very_low_2, + BlurStrength::Low => self.low, + BlurStrength::Low2 => self.low_2, + BlurStrength::Medium => self.medium, + BlurStrength::Medium2 => self.medium_2, + BlurStrength::High => self.high, + BlurStrength::High2 => self.high_2, + BlurStrength::VeryHigh => self.very_high, + BlurStrength::VeryHigh2 => self.very_high_2, + BlurStrength::ExtremelyHigh => self.extremely_high, + BlurStrength::ExtremelyHigh2 => self.extremely_high_2, + } + } + + pub fn offset(self, offset: f32) -> Self { + let Self { + mut extremely_low, + mut extremely_low_2, + mut very_low, + mut very_low_2, + mut low, + mut low_2, + mut medium, + mut medium_2, + mut high, + mut high_2, + mut very_high, + mut very_high_2, + mut extremely_high, + mut extremely_high_2, + } = self; + extremely_low += offset; + extremely_low_2 += offset; + very_low += offset; + very_low_2 += offset; + low += offset; + low_2 += offset; + medium += offset; + medium_2 += offset; + high += offset; + high_2 += offset; + very_high += offset; + very_high_2 += offset; + extremely_high += offset; + extremely_high_2 += offset; + Self { + extremely_low, + extremely_low_2, + very_low, + very_low_2, + low, + low_2, + medium, + medium_2, + high, + high_2, + very_high, + very_high_2, + extremely_high, + extremely_high_2, + } + } + + pub fn guess_offset(&self) -> f32 { + let Self { + extremely_low, + extremely_high_2, + .. + } = self; + let default_low = Self::default().extremely_low; + let default_high = Self::default().extremely_high_2; + if *extremely_low > default_low { + 0.5 * (*extremely_low - default_low) / (1.0 - default_low) + 0.5 + } else if *extremely_high_2 < default_high { + 0.5 - (0.5 * (default_high - *extremely_high_2) / default_high) + } else { + 0.5 + } + } +} + +impl Default for AlphaMap { + fn default() -> Self { + Self { + extremely_low: 0.90, + extremely_low_2: 0.87692, + very_low: 0.85385, + very_low_2: 0.83076, + low: 0.80769, + low_2: 0.78461, + medium: 0.76154, + medium_2: 0.73846, + high: 0.71538, + high_2: 0.69231, + very_high: 0.66023, + very_high_2: 0.64615, + extremely_high: 0.62308, + extremely_high_2: 0.6, + } + } +} 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/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index d156722c..e595d8f8 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -81,6 +81,7 @@ pub fn get_text( step_array: &[Srgba], fallback: &Srgba, tint_array: Option<&[Srgba]>, + alpha: f32, ) -> Srgba { assert!(step_array.len() == 100); let step_array = if let Some(tint_array) = tint_array { @@ -90,11 +91,28 @@ pub fn get_text( step_array }; + let alpha_extra_steps = if alpha < 1.0 { + ((1. - alpha) * 100.0).round() as usize + } else { + 0 + }; let is_dark = base_index < 60; - let index = get_index(base_index, 70, step_array.len(), is_dark) - .or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) - .unwrap_or(if is_dark { 99 } else { 0 }); + let index = get_index( + base_index, + 70 + alpha_extra_steps, + step_array.len(), + is_dark, + ) + .or_else(|| { + get_index( + base_index, + 50 + alpha_extra_steps, + step_array.len(), + is_dark, + ) + }) + .unwrap_or(if is_dark { 99 } else { 0 }); *step_array.get(index).unwrap_or(fallback) } diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 8722a2a4..f77bf512 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -101,6 +101,7 @@ impl cosmic::Application for Window { Message::Surface(destroy_popup(id)) } else { Message::Surface(app_popup::( + |_| Default::default(), move |state: &mut Window| { let new_id = Id::unique(); state.popup = Some(new_id); diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index d4c75171..b5968461 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -22,4 +22,5 @@ features = [ "surface-message", "multi-window", "wgpu", + "wayland", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 05841f5b..af4200c9 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -7,7 +7,7 @@ use cosmic::app::Settings; use cosmic::iced::{Alignment, Length, Size}; use cosmic::prelude::*; use cosmic::widget::menu::{self, KeyBind}; -use cosmic::widget::nav_bar; +use cosmic::widget::{nav_bar, RcElementWrapper}; use cosmic::{executor, iced, widget, Core}; use std::collections::HashMap; use std::sync::LazyLock; @@ -43,7 +43,7 @@ pub enum Action { impl widget::menu::Action for Action { type Message = Message; - fn message(&self) -> Message { + fn message(&self) -> Self::Message { match self { Action::Hi => Message::Hi, Action::Hi2 => Message::Hi2, @@ -52,6 +52,25 @@ impl widget::menu::Action for Action { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Action2 { + Hi, + Hi2, + Hi3, +} + +impl widget::menu::Action for Action2 { + type Message = cosmic::Action; + + fn message(&self) -> Self::Message { + cosmic::Action::App(match self { + Action2::Hi => Message::Hi, + Action2::Hi2 => Message::Hi2, + Action2::Hi3 => Message::Hi3, + }) + } +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { @@ -84,6 +103,7 @@ pub enum Message { Hi2, Hi3, Tick, + ValueChanged(f32), } /// The [`App`] stores application-specific state. @@ -95,6 +115,7 @@ pub struct App { hidden: bool, keybinds: HashMap, progress: f32, + progress_slider: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -137,6 +158,7 @@ impl cosmic::Application for App { hidden: true, keybinds: HashMap::new(), progress: 0.0, + progress_slider: 0.0, }; let command = app.update_title(); @@ -155,6 +177,70 @@ impl cosmic::Application for App { self.update_title() } + fn nav_context_menu(&self) -> Option>>> { + Some(menu::nav_context( + &HashMap::new(), + vec![ + vec![cosmic::widget::menu::Item::Button( + "Hi", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + )], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 2", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 22", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 3", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 33", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 333", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 44", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 4444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 444444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 444444444444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + ], + )) + } + /// Handle application events here. fn update(&mut self, message: Self::Message) -> cosmic::app::Task { match message { @@ -185,6 +271,9 @@ impl cosmic::Application for App { Message::Tick => { self.progress = (self.progress + 0.01) % 1.0; } + Message::ValueChanged(value) => { + self.progress_slider = value; + } } Task::none() } @@ -201,7 +290,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(16) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -240,6 +329,16 @@ impl cosmic::Application for App { widget::progress_bar::linear::Linear::new() .girth(10.0) .progress(self.progress) + .width(Length::Fill), + ) + .push( + widget::slider(0.0..=1.0, self.progress_slider, Message::ValueChanged) + .step(0.001), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(self.progress_slider) .width(Length::Fill) .markers([0.25, 0.5, 0.75]) .segment_spacing(2), @@ -266,6 +365,7 @@ impl cosmic::Application for App { .progress(1.0) .width(Length::Fill), ) + .push(widget::button::suggested("asdf").on_press(Message::Ignore)) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 1992066f..875b83ff 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Size; -use cosmic::widget::{menu, nav_bar}; +use cosmic::widget::{menu, nav_bar, space}; use cosmic::{executor, iced, ApplicationExt, Element}; #[derive(Clone, Copy)] @@ -127,18 +127,20 @@ impl cosmic::Application for App { Some(&self.nav_model) } - /// The context menu to display for the given nav bar item ID. - fn nav_context_menu( - &self, - id: nav_bar::Id, - ) -> Option>>> { - Some(menu::items( + /// The context menu list. + fn nav_context_menu(&self) -> Option>>> { + Some(menu::nav_context( &HashMap::new(), - vec![ - menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), - menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), - menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), - ], + self.nav_model + .iter() + .map(|e| { + vec![ + menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(e)), + menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(e)), + menu::Item::Button("Delete", None, NavMenuAction::Delete(e)), + ] + }) + .collect(), )) } diff --git a/i18n/kmr/libcosmic_yoda.ftl b/i18n/kmr/libcosmic_yoda.ftl index e69de29b..a771f86d 100644 --- a/i18n/kmr/libcosmic_yoda.ftl +++ b/i18n/kmr/libcosmic_yoda.ftl @@ -0,0 +1,34 @@ +february = Reşemî { $year } +close = Bigire +documenters = Pêlbendvan +november = Mijdar { $year } +may = Gulan { $year } +april = Avrêl { $year } +translators = Wergêr +artists = Hunermend +license = Lîsans +december = Berfanbar { $year } +links = Girêdan +march = Adar { $year } +june = Pûşber { $year } +august = Tebax { $year } +developers = Pêşvebir +july = Tîrmeh { $year } +september = Îlon { $year } +designers = Nigarvan +october = Cotmeh { $year } +january = Çile { $year } +monday = Duşem +mon = Duş +tuesday = Sêşem +tue = Sêş +wednesday = Çarşem +wed = Çar +thursday = Pênçşem +thu = Pên +friday = Înî +fri = Înî +saturday = Şemî +sat = Şem +sunday = Yekşem +sun = Yek diff --git a/iced b/iced index d9fe129f..1a57634a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf +Subproject commit 1a57634add6e820c299f1c59664cd98098a9c348 diff --git a/src/app/action.rs b/src/app/action.rs index 166df16b..a3b9dd6a 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 86af0992..e29b9a99 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,22 +2,27 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use crate::core::Auto; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use crate::surface::action::LiveSettings; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use enumflags2::BitFlags; #[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] use iced::Application as IcedApplication; #[cfg(all(feature = "wayland", target_os = "linux"))] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -83,27 +88,33 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(all(feature = "wayland", target_os = "linux"))] pub surface_views: HashMap< window::Id, ( Option, SurfaceIdWrapper, - Box Fn(&'a App) -> Element<'a, crate::Action>>, + Box Fn(&'a App) -> crate::surface::action::LiveSettings>, + Option< + Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, ), >, - pub tracked_windows: HashSet, pub opened_surfaces: HashMap, + blur_enabled: bool, } impl Cosmic where T::Message: Send + 'static, { - pub fn init((core, flags): (Core, T::Flags)) -> (Self, iced::Task>) { - #[cfg(all(feature = "dbus-config", target_os = "linux"))] - let mut core = core; - + pub fn init( + (mut core, flags): (Core, T::Flags), + ) -> (Self, iced::Task>) { #[cfg(all(feature = "dbus-config", target_os = "linux"))] { use iced_futures::futures::executor::block_on; @@ -112,14 +123,21 @@ where let id = core.main_window_id().unwrap_or(window::Id::RESERVED); let (model, command) = T::init(core, flags); + let existing_theme = THEME.lock().unwrap(); + let blur = existing_theme.transparent; + drop(existing_theme); - ( - Self::new(model), - Task::batch([ - command, - iced_runtime::window::run_with_handle(id, init_windowing_system), - ]), - ) + let mut cmds = vec![ + iced_runtime::window::run_with_handle(id, init_windowing_system), + command, + ]; + + if blur { + cmds.push(crate::task::message(crate::action::cosmic( + Action::BlurEnabled, + ))); + } + (Self::new(model), Task::batch(cmds)) } #[cfg(not(feature = "multi-window"))] @@ -140,7 +158,7 @@ where #[cfg(feature = "surface-message")] match _surface_message { #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppSubsurface(settings, view) => { + crate::surface::Action::AppSubsurface(settings, _live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { @@ -148,7 +166,9 @@ where return Task::none(); }; - if let Some(view) = view.and_then(|view| { + let settings = settings(&mut self.app); + + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -161,22 +181,18 @@ where None } } - }) { - let settings = settings(&mut self.app); - - self.get_subsurface(settings, *view) - } else { - iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) - } + }); + self.get_subsurface(settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Subsurface(settings, view) => { + crate::surface::Action::Subsurface(settings, _live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for subsurface"); return Task::none(); }; + let settings = settings(); if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { + crate::surface::Action::AppPopup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for popup"); return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; - if let Some(view) = view.and_then(|view| { + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -218,13 +243,10 @@ where None } } - }) { - let settings = settings(&mut self.app); + }); + let settings = settings(&mut self.app); - self.get_popup(settings, *view) - } else { - iced_winit::commands::popup::get_popup(settings(&mut self.app)) - } + self.get_popup(settings, *live_settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyPopup(id) => { @@ -257,7 +279,7 @@ where iced::Task::none() } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Popup(settings, view) => { + crate::surface::Action::Popup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { @@ -265,6 +287,20 @@ where return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + let settings = settings(); + let live_settings = Box::new(move |_: &T| live_settings()); + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -276,15 +312,13 @@ where } } }) { - let settings = settings(); - - self.get_popup(settings, Box::new(move |_| view())) + self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) } else { - iced_winit::commands::popup::get_popup(settings()) + self.get_popup(settings, live_settings, None) } } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppWindow(id, settings, view) => { + crate::surface::Action::AppWindow(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() @@ -292,8 +326,19 @@ where tracing::error!("Invalid settings for AppWindow"); return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; - if let Some(view) = view.and_then(|view| { + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -305,25 +350,13 @@ where None } } - }) { - let settings = settings(&mut self.app); - self.tracked_windows.insert(id); + }); + let settings = settings(&mut self.app); - self.get_window(id, settings, *view) - } else { - let settings = settings(&mut self.app); - - self.tracked_windows.insert(id); - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open( - id, settings, channel, - )) - }) - .discard() - } + self.get_window(id, settings, *live_settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Window(id, settings, view) => { + crate::surface::Action::Window(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() @@ -332,6 +365,18 @@ where return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -344,14 +389,16 @@ where } }) { let settings = settings(); - self.tracked_windows.insert(id); - self.get_window(id, settings, Box::new(move |_| view())) + self.get_window( + id, + settings, + Box::new(move |_| live_settings()), + Some(Box::new(move |_| view())), + ) } else { let settings = settings(); - self.tracked_windows.insert(id); - iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, @@ -365,6 +412,96 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::AppLayerShell(settings, live_settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + return Task::none(); + }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + + let view = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + + Send + + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for layer surface: {err:?}"); + None + } + } + }); + + let settings = settings(&mut self.app); + + self.get_layer_shell(settings, *live_settings, view.map(|v| *v)) + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::LayerShell(settings, live_settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + return Task::none(); + }; + + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + let settings = settings(); + let live_settings = live_settings(); + let live_settings = Box::new(move |_app: &T| live_settings); + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for layer surface: {err:?}"); + None + } + } + }) { + self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) + } else { + self.get_layer_shell(settings, live_settings, None) + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::DestroyLayerShell(id) => { + iced_winit::commands::layer_surface::destroy_layer_surface(id) + } + crate::surface::Action::SyncLiveSettings(id) => { + if let Some((_, id, live_settings, _)) = self.surface_views.get(&id) { + let live_settings = live_settings(&self.app); + return self.apply_live_settings(*id, &live_settings); + } + Task::none() + } + _ => iced::Task::none(), } #[cfg(not(feature = "surface-message"))] @@ -460,6 +597,9 @@ where )) => { return Some(Action::WindowState(id, s)); } + wayland::Event::BlurEnabled => { + return Some(Action::BlurEnabled); + } _ => (), } } @@ -480,11 +620,12 @@ where .into_iter() .filter(cosmic_config::Error::is_err) { - if let cosmic_config::Error::GetKey(_, err) = &why - && err.kind() == std::io::ErrorKind::NotFound { + if let cosmic_config::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } + } tracing::error!(?why, "cosmic toolkit config update error"); } @@ -570,8 +711,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] - if let Some((_, _, v)) = self.surface_views.get(&id) { + if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } if self @@ -626,9 +766,9 @@ impl Cosmic { .app .core() .main_window_id() - .is_some_and(|main_id| main_id == _id) + .is_some_and(|main_id| main_id == id) { - self.app.core_mut().window.sharp_corners = _maximized; + self.app.core_mut().window.sharp_corners = maximized; } } @@ -671,35 +811,15 @@ impl Cosmic { self.app.core_mut().window.is_maximized = 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 rounded = !self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme(); + + let cur_rad = self.app.core().corners(&theme, rounded); + return Task::batch([corner_radius(id, cur_rad).discard()]); } } @@ -772,18 +892,70 @@ 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.clone()); + guard.transparent = new_blur; + drop(guard); + + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + let core = self.app.core(); + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + if core.blur(&theme, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + let overriden = wrapper.2(&self.app); + if core.blur(&theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { + cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_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(); @@ -807,89 +979,45 @@ 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 rounded = self.app.core().sync_window_border_radii_to_theme() + && !self.app.core().window.sharp_corners; - 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 cur_rad = self.app.core().corners(&cosmic_theme, rounded); - let rounded = !self.app.core().window.sharp_corners; // 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, 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(), - ); - } - } - // 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, - }) - }, + for (id, (_, surface_type, live_settings, _)) in &self.surface_views { + let overriden = live_settings(&self.app); + let cur_rad = if let Some(c) = overriden.corners { + Some(c) + } else { + corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, ) - .discard(), - ); + }; + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); } return Task::batch(cmds); @@ -920,6 +1048,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(); @@ -948,6 +1077,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(); { @@ -956,21 +1094,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 rounded = self.app.core().sync_window_border_radii_to_theme() + && !self.app.core().window.sharp_corners; + let cur_rad = self.app.core().corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self @@ -978,64 +1109,52 @@ 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, 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(), - ); - } - } - // 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, - }) - }, + for (id, (_, surface_type, live_settings, _)) in &self.surface_views + { + let overriden = live_settings(&self.app); + let cur_rad = if let Some(c) = overriden.corners { + Some(c) + } else { + corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, ) - .discard(), - ); + }; + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); + } + + let core = self.app.core(); + let blur = if cosmic_theme.transparent { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + + if core.blur(&cosmic_theme, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + let overriden = wrapper.2(&self.app); + if core.blur(&cosmic_theme, Some(wrapper.1)) + && overriden.blur.unwrap_or(true) + { + cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); + } } return Task::batch(cmds); @@ -1058,7 +1177,7 @@ impl Cosmic { #[allow(clippy::used_underscore_binding)] _token, ), - ) + ); } #[cfg(not(all(feature = "wayland", target_os = "linux")))] @@ -1078,10 +1197,9 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); - self.tracked_windows.remove(&id); } + self.surface_views.shrink_to(self.surface_views.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1135,18 +1253,57 @@ 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 core = self.app.core(); { 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); + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + let blur = if cosmic_theme.transparent { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + + if core.blur(&cosmic_theme, None) { + cmds.push(blur( + core.main_window_id().unwrap_or(window::Id::RESERVED), + )); + } + + for (id, wrapper, ..) in &self.surface_views { + let overriden = wrapper.2(&self.app); + if core.blur(&cosmic_theme, Some(wrapper.1)) + && overriden.blur.unwrap_or(true) + { + cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); + } + } + } cosmic_theme.set_theme(new_theme.theme_type); + return Task::batch(cmds); } } } @@ -1207,6 +1364,7 @@ impl Cosmic { parent, SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), _, + _, )) = self.surface_views.get(&f) { // If the parent is already focused, push the new focus @@ -1223,7 +1381,7 @@ impl Cosmic { cur = self .surface_views .get(&p) - .and_then(|(parent, _, _)| *parent); + .and_then(|(parent, _, _, _)| *parent); } parent_chain.reverse(); self.app.core_mut().focused_window = parent_chain; @@ -1247,44 +1405,135 @@ 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, - }; + // TODO do we need per window sharp corners? - let rounded = !self.app.core().window.sharp_corners; + let rounded = (!self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme()) + || self + .surface_views + .get(&id) + .is_some_and(|(_, surface_type, _, _)| { + matches!( + surface_type, + SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) + ) + }); + 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, + } + }; + + let wrapper = self.surface_views.get(&id).map(|s| s.1); + + // this will blur untracked windows as if they were the main window + let blur_cmd = if self.app.core().blur(&theme, wrapper) + && self + .surface_views + .get(&id) + .and_then(|s| s.2(&self.app).blur) + .unwrap_or(true) + { + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); + cmds.push(blur(id)); + + Task::batch(cmds) + } else { + Task::none() + }; + + let corner_task = if let Some((_, cur_rad)) = + self.surface_views.get(&id).map(|s| (s.1, &s.2)).and_then( + |(s, overriden)| { + let overriden = overriden(&self.app); + if let Some(c) = overriden.corners { + Some((s, c)) + } else { + corners(s, rounded, &theme, self.app.core().auto_corner_radius) + .map(|c| (s, c)) + } + }, + ) { + corner_radius(id, Some(cur_rad)).discard() + } else if id + == self + .app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED) + { + corner_radius(id, self.app.core().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); } - _ => {} + #[cfg(all(feature = "wayland", target_os = "linux"))] + Action::BlurEnabled => { + // TODO do this after blur event confirms support instead of for all wayland windows + self.blur_enabled = true; + let mut t = THEME.lock().unwrap(); + + let new_blur = self.blur_enabled && { + let t = t.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, + } + }; + + t.transparent = new_blur; + + self.app.core_mut().system_theme.transparent = new_blur; + { + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); + if self.app.core().blur(&t, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + let overriden = wrapper.2(&self.app); + if self.app.core().blur(&t, Some(wrapper.1)) + && overriden.blur.unwrap_or(true) + { + cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); + } + } + return Task::batch(cmds); + } + } + _ => (), } iced::Task::none() @@ -1295,34 +1544,136 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), - tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), + blur_enabled: false, } } + #[cfg(feature = "surface-message")] + /// Apply live setting overrides for a surface + pub fn apply_live_settings( + &mut self, + id_wrapper: SurfaceIdWrapper, + live_settings: &crate::surface::action::LiveSettings, + ) -> Task> { + let id = id_wrapper.inner(); + + let mut cmds = Vec::with_capacity(2); + let t = THEME.try_lock().unwrap(); + + if let Some(blur) = live_settings.blur { + let blur_cmd = if blur { + if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::enable_blur(id) + } else { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + iced_winit::commands::blur::blur( + id, + Some(vec![iced::Rectangle::new( + iced::Point::ORIGIN, + iced::Size::INFINITE, + )]), + ) + .discard() + } + #[cfg(not(all(feature = "wayland", target_os = "linux")))] + { + iced::window::enable_blur(id) + } + } + } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::disable_blur(id) + } else { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + iced_winit::commands::blur::blur(id, None).discard() + } + #[cfg(not(all(feature = "wayland", target_os = "linux")))] + { + iced::window::disable_blur(id) + } + }; + cmds.push(blur_cmd); + } else if self.app.core().blur(&t, Some(id_wrapper)) { + cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::enable_blur(id) + } else { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + iced_winit::commands::blur::blur( + id, + Some(vec![iced::Rectangle::new( + iced::Point::ORIGIN, + iced::Size::INFINITE, + )]), + ) + .discard() + } + #[cfg(not(all(feature = "wayland", target_os = "linux")))] + { + iced::window::enable_blur(id) + } + }); + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + if let Some(corners) = live_settings.corners { + cmds.push( + iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), + ); + } else { + let rounded = !self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme(); + if let Some(cur_rad) = + corners(id_wrapper, rounded, &t, self.app.core().auto_corner_radius) + { + cmds.push( + iced_winit::commands::corner_radius::corner_radius(id, Some(cur_rad)).discard(), + ); + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = + (id_wrapper, live_settings.padding) + { + cmds.push(iced_winit::commands::layer_surface::set_padding( + id, padding, + )); + } + Task::batch(cmds) + } + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_subsurface( &mut self, settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::Subsurface(settings.id), + &LiveSettings { + blur: Some(self.blur_enabled), + corners: None, + padding: None, + }, + ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Subsurface(settings.id), + Box::new(|_| LiveSettings::default()), view, ), ); - get_subsurface(settings) + Task::batch([live_settings_task, get_subsurface(settings)]) } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1330,21 +1681,27 @@ impl Cosmic { pub fn get_popup( &mut self, settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::popup::get_popup; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::Popup(settings.id), + &live_settings(&self.app), + ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Popup(settings.id), + live_settings, view, ), ); - get_popup(settings) + live_settings_task.chain(get_popup(settings)).discard() } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1353,23 +1710,107 @@ impl Cosmic { &mut self, id: iced::window::Id, settings: iced::window::Settings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::SurfaceIdWrapper; *self.opened_surfaces.entry(id).or_insert(0) += 1; + let live_settings_task = + self.apply_live_settings(SurfaceIdWrapper::Window(id), &live_settings(&self.app)); self.surface_views.insert( id, ( None, // TODO parent for window, platform specific option maybe? SurfaceIdWrapper::Window(id), + live_settings, view, ), ); - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) - }) - .discard() + Task::batch([ + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard(), + // We don't control window creation in the same way + live_settings_task, + ]) + } + + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn get_layer_shell( + &mut self, + settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, + >, + ) -> Task> { + use iced_winit::SurfaceIdWrapper; + use iced_winit::platform_specific::commands::layer_surface::get_layer_surface; + *self.opened_surfaces.entry(settings.id).or_insert(0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::LayerSurface(settings.id), + &live_settings(&self.app), + ); + self.surface_views.insert( + settings.id, + ( + None, // TODO parent for layer shell, platform specific option maybe? + SurfaceIdWrapper::LayerSurface(settings.id), + live_settings, + view, + ), + ); + Task::batch([live_settings_task, get_layer_surface(settings)]) } } + +#[cfg(all(feature = "wayland", target_os = "linux"))] +fn corners( + surface_type: SurfaceIdWrapper, + rounded: bool, + theme: &Theme, + auto_corner_radius: BitFlags, +) -> Option { + if !match surface_type { + SurfaceIdWrapper::Window(_) => auto_corner_radius.contains(Auto::Window), + SurfaceIdWrapper::LayerSurface(_) => auto_corner_radius.contains(Auto::System), + SurfaceIdWrapper::Popup(_) => auto_corner_radius.contains(Auto::Popup), + _ => false, + } { + return None; + } + let theme = theme.cosmic(); + Some(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 b2d82dcd..bf5da95e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -22,7 +22,7 @@ use crate::prelude::*; use crate::theme::THEME; use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::{Length, Subscription, theme, window}; +use iced::{Color, Length, Subscription, theme, window}; pub use settings::Settings; use std::borrow::Cow; use std::cell::RefCell; @@ -389,10 +389,22 @@ where let mut nav = crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) - .context_menu(self.nav_context_menu(self.core().nav_bar_context())) - .into_container() - .width(iced::Length::Shrink) - .height(iced::Length::Fill); + .context_menu(self.nav_context_menu()); + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + { + nav = nav + .window_id_maybe(self.core().main_window_id()) + .on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m))) + } + let mut nav = nav + .into_container() + .width(iced::Length::Shrink) + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); @@ -402,10 +414,7 @@ where } /// Shows a context menu for the active nav bar item. - fn nav_context_menu( - &self, - id: nav_bar::Id, - ) -> Option>>> { + fn nav_context_menu(&self) -> Option>>> { None } @@ -615,6 +624,7 @@ impl ApplicationExt for App { let content_container = core.window.content_container; let show_context = core.window.show_context; let nav_bar_active = core.nav_bar_active(); + let transparent_header = core.window.transparent_header; let focused = core .focus_chain() .iter() @@ -635,10 +645,8 @@ impl ApplicationExt for App { let mut widgets = Vec::with_capacity(3); // Insert nav bar onto the left side of the window. - let has_nav = if let Some(nav) = self - .nav_bar() - .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar"))) - { + let has_nav = if let Some(nav) = self.nav_bar() { + let nav = id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")); widgets.push( container(nav) .padding([ @@ -825,7 +833,11 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - cosmic.background.base.into(), + if transparent_header { + Color::TRANSPARENT + } else { + cosmic.background(theme.transparent).base.into() + }, )), border: iced::Border { radius: [ @@ -854,7 +866,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 e16b0303..37635bf4 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -221,7 +221,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() @@ -372,16 +372,17 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; + let mut bg = cosmic.background(theme.transparent).base; 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, } }), @@ -559,6 +560,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 c5c1c62b..fee60ec5 100644 --- a/src/command.rs +++ b/src/command.rs @@ -63,7 +63,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 f80954f9..8845d2af 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; +use enumflags2::{self, BitFlags, bitflags}; use iced::{Limits, Size, window}; use iced_core::window::Id; use palette::Srgba; @@ -38,11 +39,24 @@ pub struct Window { pub show_close: bool, pub show_maximize: bool, pub show_minimize: bool, + pub transparent_header: bool, pub is_maximized: bool, height: f32, width: f32, } +#[bitflags] +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Auto { + /// Automatically apply effect to regular windows + Window, + /// Automatically apply effect to popups + Popup, + /// Automatically apply effect to system interface elements (layer shell surfaces) + System, +} + /// COSMIC-specific application settings #[derive(Clone)] pub struct Core { @@ -97,8 +111,21 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub(crate) sync_window_border_radii_to_theme: bool, + pub(crate) auto_blur: BitFlags, + + pub(crate) auto_corner_radius: BitFlags, + + pub(crate) app_type: AppType, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AppType { + /// A regular application + Window, + /// A system application + System, + /// An applet + Applet, } impl Default for Core { @@ -141,6 +168,7 @@ impl Default for Core { show_minimize: true, show_window_menu: false, is_maximized: false, + transparent_header: false, height: 0., width: 0., }, @@ -156,8 +184,9 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(all(feature = "wayland", target_os = "linux"))] - sync_window_border_radii_to_theme: true, + auto_blur: Auto::System | Auto::Popup | Auto::Window, + auto_corner_radius: Auto::System | Auto::Popup | Auto::Window, + app_type: AppType::Window, } } } @@ -483,14 +512,112 @@ impl Core { crate::command::toggle_maximize(id) } - // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { - self.sync_window_border_radii_to_theme = sync; - } - #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn sync_window_border_radii_to_theme(&self) -> bool { - self.sync_window_border_radii_to_theme + match self.app_type { + AppType::Window => self.auto_corner_radius.contains(Auto::Window), + AppType::System => self.auto_corner_radius.contains(Auto::System), + AppType::Applet => false, + } + } + + pub fn set_auto_blur(&mut self, auto_blur: BitFlags) { + self.auto_blur = auto_blur; + } + + pub fn auto_blur(&self) -> BitFlags { + self.auto_blur + } + + pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags) { + self.auto_corner_radius = auto_corner_radius; + } + + pub fn auto_corner_radius(&self) -> BitFlags { + self.auto_corner_radius + } + + pub fn set_app_type(&mut self, app_type: AppType) { + self.app_type = app_type; + } + + pub fn app_type(&self) -> AppType { + self.app_type + } + + #[must_use] + pub fn blur( + &self, + theme: &Theme, + surface_id_wrapper: Option, + ) -> bool { + use iced_winit::SurfaceIdWrapper; + let theme = theme.cosmic(); + match surface_id_wrapper { + Some(SurfaceIdWrapper::LayerSurface(_)) => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + Some(SurfaceIdWrapper::Window(_)) => { + theme.frosted_windows && self.auto_blur.contains(Auto::Window) + } + Some(SurfaceIdWrapper::Popup(_)) + if matches!(self.app_type, AppType::Window | AppType::System) => + { + theme.frosted_windows && self.auto_blur.contains(Auto::Popup) + } + Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => { + theme.frosted_applets && self.auto_blur.contains(Auto::Popup) + } + None => match self.app_type { + AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window), + AppType::System => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + AppType::Applet => false, + }, + _ => false, + } + } + + /// Calculate suggested corners for each app type main window + #[must_use] + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn corners( + &self, + theme: &Theme, + rounded: bool, + ) -> Option { + if !self.sync_window_border_radii_to_theme() { + return None; + } + let theme = theme.cosmic(); + let ret = if let AppType::Applet = self.app_type { + 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 AppType::Window = self.app_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, + } + }; + Some(ret) } } diff --git a/src/surface/action.rs b/src/surface/action.rs index 16816fff..2d86b007 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -5,6 +5,10 @@ use super::Action; use crate::Application; use iced::window; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use iced_runtime::platform_specific::wayland::CornerRadius; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use std::any::Any; use std::sync::Arc; @@ -29,16 +33,38 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action { #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] -pub fn app_window( - settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, +pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { + Action::DestroyLayerShell(id) +} + +#[derive(Debug, Default, Copy, Clone)] +pub struct LiveSettings { + #[cfg(all(feature = "wayland", target_os = "linux"))] + /// Override the surface padding value for the surface type. + pub padding: Option, + #[cfg(all(feature = "wayland", target_os = "linux"))] + /// Override the default corner radius value for the surface type. + pub corners: Option, + /// Override the default blur setting for the surface type. + pub blur: Option, +} + +#[cfg(all(feature = "wayland", target_os = "linux"))] +type BoxedView = Option< + Box< + dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> + + Send + + Sync + + 'static, >, +>; + +#[cfg(all(feature = "wayland", target_os = "linux"))] +#[must_use] +pub fn app_window( + live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, + settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, + view: BoxedView, ) -> (window::Id, Action) { let id = window::Id::unique(); @@ -46,11 +72,16 @@ pub fn app_window( Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + ( id, Action::AppWindow( id, Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -63,6 +94,7 @@ pub fn app_window( #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_window( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> window::Settings + Send + Sync + 'static, view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -73,11 +105,15 @@ pub fn simple_window( let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + ( id, Action::Window( id, Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> @@ -95,18 +131,12 @@ pub fn simple_window( #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_popup( + live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync + 'static, - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> Action { let boxed: Box< dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -116,8 +146,13 @@ pub fn app_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::AppPopup( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -128,7 +163,7 @@ pub fn app_popup( /// Used to create a subsurface message from within a widget. #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] -pub fn simple_subsurface( +pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync @@ -147,6 +182,7 @@ pub fn simple_subsurface( Action::Subsurface( Arc::new(boxed), + Arc::new(Box::new(LiveSettings::default)), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -158,6 +194,7 @@ pub fn simple_subsurface( #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_popup( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync @@ -174,8 +211,12 @@ pub fn simple_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -197,14 +238,7 @@ pub fn subsurface( + Sync + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> Action { let boxed: Box< dyn Fn( @@ -219,6 +253,82 @@ pub fn subsurface( Action::AppSubsurface( Arc::new(boxed), + Arc::new(Box::new(|_: &App| LiveSettings::default())), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} + +#[cfg(all(feature = "wayland", target_os = "linux"))] +#[must_use] +pub fn simple_layer_shell( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, + settings: impl Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> Action { + let boxed: Box< + dyn Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::LayerShell( + Arc::new(boxed), + Arc::new(boxed_live), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) +} + +#[cfg(all(feature = "wayland", target_os = "linux"))] +#[must_use] +pub fn app_layer_shell( + live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, + settings: impl Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. + view: BoxedView, +) -> Action { + let boxed: Box< + dyn Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + + Action::AppLayerShell( + Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) diff --git a/src/surface/corner_radius.rs b/src/surface/corner_radius.rs new file mode 100644 index 00000000..1eda578d --- /dev/null +++ b/src/surface/corner_radius.rs @@ -0,0 +1,86 @@ +use iced::Rectangle; +use iced_runtime::platform_specific::wayland::CornerRadius; + +#[must_use] +pub fn rounded_rect_strips(rect: Rectangle, radius: CornerRadius) -> Vec> { + let mut out = Vec::new(); + + let w = rect.width.max(0.0); + let h = rect.height.max(0.0); + + if w <= 0.0 || h <= 0.0 { + return out; + } + + let tl = radius.top_left as i32; + let tr = radius.top_right as i32; + let bl = radius.bottom_left as i32; + let br = radius.bottom_right as i32; + + let max_top = tl.max(tr); + let max_bottom = bl.max(br); + + let center_y = rect.y + max_top as f32; + let center_h = (h as i32 - max_top - max_bottom).max(0) as f32; + + if center_h > 0.0 { + out.push(Rectangle { + x: rect.x, + y: center_y, + width: w, + height: center_h, + }); + } + + for y in 0..max_top { + let left = if y < tl { circle_inset(tl, y) } else { 0 }; + + let right = if y < tr { circle_inset(tr, y) } else { 0 }; + + let strip_x = rect.x + left as f32; + let strip_w = (w as i32 - left - right).max(0) as f32; + + if strip_w > 0.0 { + out.push(Rectangle { + x: strip_x, + y: rect.y + y as f32, + width: strip_w, + height: 1.0, + }); + } + } + + for y in 0..max_bottom { + let cy = max_bottom - 1 - y; + let left = if cy < bl { circle_inset(bl, cy) } else { 0 }; + + let right = if cy < br { circle_inset(br, cy) } else { 0 }; + + let strip_x = rect.x + left as f32; + let strip_w = (w as i32 - left - right).max(0) as f32; + + if strip_w > 0.0 { + out.push(Rectangle { + x: strip_x, + y: rect.y + h - max_bottom as f32 + y as f32, + width: strip_w, + height: 1.0, + }); + } + } + + out +} + +fn circle_inset(radius: i32, y: i32) -> i32 { + if radius <= 0 { + return 0; + } + + let fy = y as f32 + 0.5; + let r = radius as f32; + + let x = r - (r * r - (r - fy) * (r - fy)).sqrt(); + + x.floor() as i32 +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 307b5275..d34d684f 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -2,36 +2,28 @@ // SPDX-License-Identifier: MPL-2.0 pub mod action; +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub mod corner_radius; use iced::{Limits, Size, Task}; -use std::future::Future; +use std::any::Any; use std::sync::Arc; +type BoxedSetting = Arc>; + /// Ignore this message in your application. It will be intercepted. #[derive(Clone)] pub enum Action { /// Create a subsurface with a view function accepting the App as a parameter - AppSubsurface( - std::sync::Arc>, - Option>>, - ), + AppSubsurface(BoxedSetting, BoxedSetting, Option), /// Create a subsurface with a view function - Subsurface( - std::sync::Arc>, - Option>>, - ), + Subsurface(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroySubsurface(iced::window::Id), /// Create a popup with a view function accepting the App as a parameter - AppPopup( - std::sync::Arc>, - Option>>, - ), + AppPopup(BoxedSetting, BoxedSetting, Option), /// Create a popup - Popup( - std::sync::Arc>, - Option>>, - ), + Popup(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), /// Destroys the global tooltip popup subsurface @@ -40,18 +32,29 @@ pub enum Action { /// Create a window with a view function accepting the App as a parameter AppWindow( iced::window::Id, - std::sync::Arc>, - Option>>, + BoxedSetting, + BoxedSetting, + Option, ), /// Create a window with a view function Window( iced::window::Id, - std::sync::Arc>, - Option>>, + BoxedSetting, + BoxedSetting, + Option, ), /// Destroy a window DestroyWindow(iced::window::Id), + /// Create a layer shell surface with a view function accepting the App as a parameter + AppLayerShell(BoxedSetting, BoxedSetting, Option), + + /// Create a layer shell surface with a view function + LayerShell(BoxedSetting, BoxedSetting, Option), + + /// Destroy a layer shell surface + DestroyLayerShell(iced::window::Id), + /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -62,28 +65,45 @@ pub enum Action { size: Size, }, Ignore, + SyncLiveSettings(iced::window::Id), Task(Arc Task + Send + Sync>), } +pub fn surface_task(action: Action) -> Task> { + crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) +} + impl std::fmt::Debug for Action { #[cold] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::AppSubsurface(arg0, arg1) => f + Self::AppSubsurface(arg0, arg1, arg2) => f .debug_tuple("AppSubsurface") .field(arg0) .field(arg1) + .field(arg2) + .finish(), + Self::Subsurface(arg0, arg1, arg2) => f + .debug_tuple("Subsurface") + .field(arg0) + .field(arg1) + .field(arg2) .finish(), - Self::Subsurface(arg0, arg1) => { - f.debug_tuple("Subsurface").field(arg0).field(arg1).finish() - } Self::DestroySubsurface(arg0) => { f.debug_tuple("DestroySubsurface").field(arg0).finish() } - Self::AppPopup(arg0, arg1) => { - f.debug_tuple("AppPopup").field(arg0).field(arg1).finish() - } - Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), + Self::AppPopup(arg0, arg1, arg2) => f + .debug_tuple("AppPopup") + .field(arg0) + .field(arg1) + .field(arg2) + .finish(), + Self::Popup(arg0, arg1, arg2) => f + .debug_tuple("Popup") + .field(arg0) + .field(arg1) + .field(arg2) + .finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { @@ -97,20 +117,38 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), - Self::AppWindow(id, arg0, arg1) => f + Self::AppWindow(id, arg0, arg1, arg2) => f .debug_tuple("AppWindow") .field(id) .field(arg0) .field(arg1) + .field(arg2) .finish(), - Self::Window(id, arg0, arg1) => f + Self::Window(id, arg0, arg1, arg2) => f .debug_tuple("Window") .field(id) .field(arg0) .field(arg1) + .field(arg2) .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), + Self::AppLayerShell(arg, arg1, arg2) => f + .debug_tuple("AppLayerShell") + .field(arg) + .field(arg1) + .field(arg2) + .finish(), + Self::LayerShell(arg, arg1, arg2) => f + .debug_tuple("LayerShell") + .field(arg) + .field(arg1) + .field(arg2) + .finish(), + Self::DestroyLayerShell(arg0) => { + f.debug_tuple("DestroyLayerShell").field(arg0).finish() + } + Self::SyncLiveSettings(arg0) => f.debug_tuple("SyncLiveSettings").field(arg0).finish(), } } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index a5ae5d4a..1fd19cf3 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,9 +7,9 @@ pub mod portal; pub mod style; -use cosmic_config::{CosmicConfigEntry, config_subscription}; +use ::iced::Alignment; +use cosmic_config::CosmicConfigEntry; use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; -use iced_futures::Subscription; use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, LazyLock, Mutex}; pub use style::*; @@ -46,6 +46,8 @@ 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, + list_item_position: None, }); /// Currently-defined theme. @@ -82,34 +84,6 @@ pub fn is_high_contrast() -> bool { active_type().is_high_contrast() } -// /// Watches for changes to the system's theme preference. -// #[cold] -// pub fn subscription(is_dark: bool) -> Subscription { -// config_subscription::<_, crate::cosmic_theme::Theme>( -// ( -// std::any::TypeId::of::(), -// is_dark, -// ), -// if is_dark { -// cosmic_theme::DARK_THEME_ID -// } else { -// cosmic_theme::LIGHT_THEME_ID -// } -// .into(), -// crate::cosmic_theme::Theme::VERSION, -// ) -// .map(|res| { -// for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) { -// tracing::error!( -// ?error, -// "error while watching system theme preference changes" -// ); -// } - -// Theme::system(Arc::new(res.config)) -// }) -// } - pub fn system_dark() -> Theme { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { return Theme::dark(); @@ -209,6 +183,9 @@ impl ThemeType { pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, + pub transparent: bool, + /// Only meaningful for widgets that must be in a list. Otherwise it should be ignored. + pub list_item_position: Option<(Alignment, usize)>, } impl Theme { @@ -279,9 +256,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), } } @@ -290,6 +267,14 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } + + #[inline] + /// Clone theme with a list position + pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self { + let mut new = self.clone(); + new.list_item_position = position; + new + } } impl LayeredTheme for Theme { diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index f84c9e97..723d1b6b 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -126,20 +126,20 @@ pub fn appearance( let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); corner_radii = &cosmic.corner_radii.radius_0; } Button::AppletIcon => { let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); } Button::MenuFolder => { // Menu folders cannot be disabled, ignore customized icon and text color - let component = &cosmic.background.component; + let component = &cosmic.background(theme.transparent).component; let (background, _, _) = color(component); appearance.background = Some(Background::Color(background)); appearance.icon_color = Some(component.on.into()); @@ -151,8 +151,9 @@ pub fn appearance( let (background, text, icon) = color(&cosmic.list_button); 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 { @@ -162,7 +163,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; @@ -278,6 +279,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 8f9fa461..38030e4b 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -224,11 +224,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 }, @@ -386,9 +386,11 @@ pub enum Container<'a> { WindowBackground, Background, Card, - ContextDrawer, + ContextDrawer { + transparent: bool, + }, Custom(Box iced_container::Style + 'a>), - Dialog, + Dialog(bool), Dropdown, HeaderBar { focused: bool, @@ -409,11 +411,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() @@ -424,11 +428,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() @@ -439,11 +445,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() @@ -474,14 +482,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], @@ -519,12 +543,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) }; @@ -534,7 +559,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: [ @@ -559,21 +586,21 @@ impl iced_container::Catalog for Theme { } } - Container::ContextDrawer => { - let mut a = Container::primary(cosmic); + Container::ContextDrawer { transparent } => { + let mut a = Container::primary(cosmic, self.transparent && *transparent); 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, @@ -605,10 +632,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(), @@ -618,10 +649,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(), @@ -631,10 +666,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(), @@ -646,12 +685,17 @@ 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())), + Container::Dialog(is_overlay) => iced_container::Style { + 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 && !is_overlay).base.into(), + )), border: Border { - color: cosmic.primary.divider.into(), + color: cosmic + .primary(self.transparent && !is_overlay) + .divider + .into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, @@ -793,13 +837,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(), } } @@ -837,7 +883,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, @@ -1033,7 +1079,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(), @@ -1513,7 +1562,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 db6e0ba1..37e14486 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -31,7 +31,7 @@ pub trait StyleSheet { type Style: Default; /// Produces the [`Appearance`] of a menu bar and its menus. - fn appearance(&self, style: &Self::Style) -> Appearance; + fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance; } /// The style of a menu bar and its menus @@ -54,7 +54,7 @@ impl From Appearance> for MenuBarStyle { impl StyleSheet for fn(&Theme) -> Appearance { type Style = Theme; - fn appearance(&self, style: &Self::Style) -> Appearance { + fn appearance(&self, style: &Self::Style, _is_overlay: bool) -> Appearance { (self)(style) } } @@ -62,13 +62,14 @@ impl StyleSheet for fn(&Theme) -> Appearance { impl StyleSheet for Theme { type Style = MenuBarStyle; - fn appearance(&self, style: &Self::Style) -> Appearance { + fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance { let cosmic = self.cosmic(); - let component = &cosmic.background.component; + let component = &cosmic.background(!is_overlay && self.transparent).component; + let bg = component.base; 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, @@ -76,7 +77,7 @@ impl StyleSheet for Theme { background_expand: [1; 4], path: component.hover.into(), }, - MenuBarStyle::Custom(c) => c.appearance(self), + MenuBarStyle::Custom(c) => c.appearance(self, is_overlay), } } } diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index e6b83d7b..72113b87 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -6,6 +6,7 @@ //! //! A [`Button`] has some local [`State`]. +use iced::Alignment; use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; @@ -448,7 +449,7 @@ impl<'a, Message: 'a + Clone> Widget let state = tree.state.downcast_ref::(); - let styling = if !is_enabled { + let mut styling = if !is_enabled { theme.disabled(&self.style) } else if is_mouse_over { if state.is_pressed { @@ -470,6 +471,24 @@ impl<'a, Message: 'a + Clone> Widget theme.active(state.is_focused, self.selected, &self.style) }; + if matches!( + self.style, + crate::theme::Button::MenuItem | crate::theme::Button::MenuFolder + ) { + match theme.list_item_position { + Some((Alignment::Start, _)) => { + styling.border_radius = + styling.border_radius.bottom(theme.cosmic().radius_0()[3]); + } + Some((Alignment::End, _)) => { + styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]); + } + Some((Alignment::Center, _)) => {} + None => { + styling.border_radius = theme.cosmic().radius_0().into(); + } + } + } let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); 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/cards.rs b/src/widget/cards.rs index 4d6770fa..5c8852a8 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -2,7 +2,7 @@ use std::time::Duration; use crate::anim; -use crate::widget::card::style::Style; +use crate::theme::THEME; use crate::widget::icon::{self, Handle}; use crate::widget::{button, column, row, text}; use float_cmp::approx_eq; @@ -19,7 +19,6 @@ const TOP_SPACING: u16 = 4; const VERTICAL_SPACING: f32 = 8.0; const PADDING: u16 = 16; const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; -const BG_CARD_BORDER_RADIUS: f32 = 8.0; const BG_CARD_MARGIN_STEP: f32 = 8.0; /// get an expandable stack of cards @@ -93,7 +92,7 @@ where /// Get an expandable stack of cards #[allow(clippy::too_many_arguments)] pub fn new( - _id: widget::Id, + id: widget::Id, card_inner_elements: Vec>, on_clear_all: Message, on_show_more: Option, @@ -112,7 +111,7 @@ where Self { can_show_more, - id: Id::unique(), + id, show_less_button: { let mut show_less_children = Vec::with_capacity(3); if let Some(source) = show_less_icon { @@ -337,14 +336,15 @@ where for i in 1..self.elements.len().min(3) { // height must be 16px for 8px padding // but we only want 4px visible + let guard = THEME.lock().unwrap(); + let radius_xs = guard.cosmic().radius_xs(); let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; - let node = - Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) - .translate(Vector::new( - margin, - size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, - )); + let node = Node::new(Size::new(width - 2.0 * margin, radius_xs[0] * 2.0)) + .translate(Vector::new( + margin, + size.height - radius_xs[0] * 2.0 + BG_CARD_VISIBLE_HEIGHT, + )); size.height += BG_CARD_VISIBLE_HEIGHT; children.push(node); } @@ -424,22 +424,19 @@ where _ = tree_children.next(); } + let radius_xs = theme.cosmic().radius_xs(); // Draw first to appear behind if fully_unexpanded { + use crate::widget::card::style::Catalog as _; let card_layout = layout.next().unwrap(); - let appearance = Style::default(); + let appearance = theme.default(); let bg_layout = layout.collect::>(); - for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { + for (i, layout) in (0..2).zip(bg_layout).rev() { renderer.fill_quad( Quad { bounds: layout.bounds(), border: Border { - radius: Radius::from([ - 0.0, - 0.0, - BG_CARD_BORDER_RADIUS, - BG_CARD_BORDER_RADIUS, - ]), + radius: Radius::from([0.0, 0.0, radius_xs[2], radius_xs[3]]), ..Default::default() }, shadow: Shadow::default(), diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 879cc604..3ec2ff35 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -5,13 +5,12 @@ use std::borrow::Cow; use std::rc::Rc; -use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::Element; use crate::theme::iced::Slider; -use crate::theme::{Button, THEME}; +use crate::theme::{Button, THEME, Theme}; use crate::widget::button::Catalog; use crate::widget::segmented_button::Entity; use crate::widget::{container, slider}; @@ -39,37 +38,6 @@ use super::{Icon, button, segmented_control, text, text_input, tooltip}; #[doc(inline)] pub use ColorPickerModel as Model; -// TODO is this going to look correct enough? -pub static HSV_RAINBOW: LazyLock> = LazyLock::new(|| { - (0u16..8) - .map(|h| { - Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( - RgbHue::new(f32::from(h) * 360.0 / 7.0), - 1.0, - 1.0, - ))) - }) - .collect() -}); - -fn hsv_rainbow(low_hue: f32, high_hue: f32) -> Vec { - let mut colors = Vec::new(); - let steps: u8 = 7; - let step_size = (high_hue - low_hue) / f32::from(steps); - for i in 0..=steps { - let hue = low_hue + step_size * f32::from(i); - colors.push(ColorStop { - color: Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( - RgbHue::new(hue), - 1.0, - 1.0, - ))), - offset: f32::from(i) / f32::from(steps), - }); - } - colors -} - const MAX_RECENT: usize = 20; #[derive(Debug, Clone)] @@ -301,23 +269,67 @@ where copy_to_clipboard_label: T, copied_to_clipboard_label: T, ) -> ColorPicker<'a, Message> { - fn rail_backgrounds(hue: f32) -> (Background, Background) { - let low_range = hsv_rainbow(0., hue); - let high_range = hsv_rainbow(hue, 360.); - - ( - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(low_range), - )), - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(high_range), - )), - ) - } - let on_update = self.on_update; let spacing = THEME.lock().unwrap().cosmic().spacing; + let color_slider_style = Rc::new(move |t: &Theme| { + let cosmic = t.cosmic(); + let mut a = slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + a.rail.backgrounds = ( + // active track + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { + let offset; + let hue = self.active_color.hue.into_positive_degrees(); + let new_hue: f32; + if hue <= f32::from(index) * 360.0 / 7.0 { + offset = 1.0; + new_hue = hue; + } else { + offset = (f32::from(index) / 7.0) * (360.0 / hue); + new_hue = f32::from(index) * 360.0 / 7.0; + } + ColorStop { + color: Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), + )), + offset, + } + })), + )), + // inactive track + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { + let offset; + let hue = self.active_color.hue.into_positive_degrees(); + let new_hue: f32; + if hue >= f32::from(index) * 360.0 / 7.0 { + offset = 0.0; + new_hue = hue; + } else { + offset = + ((f32::from(index) / 7.0) - (hue / 360.0)) / (1.0 - (hue / 360.0)); + new_hue = f32::from(index) * 360.0 / 7.0; + } + ColorStop { + color: Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), + )), + offset, + } + })), + )), + ); + a.rail.width = 8.0; + a.handle.background = Background::Color(Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(self.active_color.hue, 1.0, 1.0), + ))); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }); + let mut inner = column![ // segmented buttons segmented_control::horizontal(self.model) @@ -332,56 +344,22 @@ where .width(self.width) .height(self.height), slider( - 0.001..=359.99, + 0.0..=359.99, self.active_color.hue.into_positive_degrees(), move |v| { let mut new = self.active_color; new.hue = v.into(); on_update(ColorPickerUpdate::ActiveColor(new)) - } + }, ) .on_release(on_update(ColorPickerUpdate::ActionFinished)) .class(Slider::Custom { - active: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - let hue = self.active_color.hue.into_positive_degrees(); - a.rail.backgrounds = rail_backgrounds(hue); - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), - hovered: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - let hue = self.active_color.hue.into_positive_degrees(); - a.rail.backgrounds = rail_backgrounds(hue); - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), - dragging: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - let hue = self.active_color.hue.into_positive_degrees(); - a.rail.backgrounds = rail_backgrounds(hue); - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), + active: color_slider_style.clone(), + hovered: color_slider_style.clone(), + dragging: color_slider_style, }) + .step(4.0 / 17.0) + .shift_step(64.0 / 17.0) .width(self.width), text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 9a7448eb..b743c4eb 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -31,6 +31,24 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { on_close: Message, max_width: f32, ) -> Element<'a, Message> + where + Drawer: Into>, + { + Self::new_inner_overlay( + title, actions, header, footer, drawer, on_close, max_width, false, + ) + } + + pub fn new_inner_overlay( + title: Option>, + actions: Option>, + header: Option>, + footer: Option>, + drawer: Drawer, + on_close: Message, + max_width: f32, + overlay: bool, + ) -> Element<'a, Message> where Drawer: Into>, { @@ -43,6 +61,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer: Element<'a, Message>, on_close: Message, max_width: f32, + overlay: bool, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxs, @@ -105,7 +124,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { container( LayerContainer::new(pane) .layer(cosmic_theme::Layer::Primary) - .class(crate::style::Container::ContextDrawer) + .class(crate::style::Container::ContextDrawer { + transparent: !overlay, + }) .width(Length::Fill) .height(Length::Fill) .max_width(max_width), @@ -124,6 +145,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer.into(), on_close, max_width, + overlay, ) } @@ -142,7 +164,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { Content: Into>, Drawer: Into>, { - let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); + let drawer = Self::new_inner_overlay( + title, actions, header, footer, drawer, on_close, max_width, true, + ); ContextDrawer { id: None, diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 9256c954..0d3ba1fb 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -79,8 +79,10 @@ impl ContextMenu<'_, Message> { my_state: &mut LocalState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::destroy_popup; - use crate::widget::menu::Menu; + use crate::surface::action::{LiveSettings, destroy_popup}; + use crate::theme::THEME; + use crate::widget::menu::{Menu, StyleSheet as _}; + use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -183,8 +185,22 @@ impl ContextMenu<'_, Message> { ..Default::default() }; let parent = self.window_id; + let t = THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; + shell.publish((self.on_surface_action.as_ref().unwrap())( crate::surface::action::simple_popup( + move || LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, move || SctkPopupSettings { parent, id, diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 72ab11f4..4ee027bd 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -18,6 +18,7 @@ pub struct Dialog<'a, Message> { height: Option, max_width: Option, max_height: Option, + is_overlay: bool, } impl Default for Dialog<'_, Message> { @@ -40,6 +41,7 @@ impl<'a, Message> Dialog<'a, Message> { height: None, max_width: None, max_height: None, + is_overlay: true, } } @@ -97,6 +99,11 @@ impl<'a, Message> Dialog<'a, Message> { self.max_width = Some(max_width.into()); self } + + pub fn is_overlay(mut self, is_overlay: bool) -> Self { + self.is_overlay = is_overlay; + self + } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { @@ -157,7 +164,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes let mut container = widget::container( widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), ) - .class(style::Container::Dialog) + .class(style::Container::Dialog(dialog.is_overlay)) .padding(space_m) .width(dialog.width.unwrap_or(Length::Fixed(570.0))); diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 3cc9fa71..7b706478 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -6,7 +6,7 @@ use iced_core::window; use crate::Element; use crate::widget::{Id, Widget, container}; use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent}; -use iced::{Event, Length, Point, Rectangle, Vector, event, mouse, overlay}; +use iced::{Event, Length, Point, Rectangle, Vector, mouse, overlay}; use iced_core::widget::{Tree, tree}; use iced_core::{self, Clipboard, Shell, layout, renderer}; diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index ebcf175a..2fa742e9 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -579,6 +579,8 @@ pub fn update< use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; + + use crate::surface::action::LiveSettings; let bounds = layout.bounds(); let anchor_rect = Rectangle { x: bounds.x as i32, @@ -605,6 +607,7 @@ pub fn update< let on_surface_action_clone = on_surface_action.clone(); let translation = layout.virtual_offset(); let get_popup_action = surface::action::simple_popup::( + || LiveSettings::default(), move || { SctkPopupSettings { parent, diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 6b0d13ce..7c149203 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -1,4 +1,4 @@ -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Id, Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 3958b03c..91a6729c 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -1,7 +1,7 @@ use crate::Theme; use cosmic_theme::LayeredTheme; use iced::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 9e4204cc..41892f69 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -3,7 +3,7 @@ use crate::widget::container::Catalog; use crate::widget::space::vertical; -use crate::widget::{button, column, container, divider, row}; +use crate::widget::{DndDestination, DndSource, button, column, container, divider, row}; use crate::{Apply, Element, theme}; use iced::{Length, Padding}; @@ -11,14 +11,27 @@ use iced::{Length, Padding}; pub struct ListButton<'a, Message> { content: Element<'a, Message>, on_press: Option, + dnd_source_builder: Option>>, + dnd_destination_builder: Option>>, selected: bool, } +/// Builds a DndSource, wrapping an element +pub type DndSourceBuilder<'a, Message> = + dyn FnOnce( + Element<'a, Message>, + ) -> DndSource<'a, Message, Box>; +/// Builds a DndDestination, wrapping an element +pub type DndDestinationBuilder<'a, Message> = + dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>; + /// 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, + dnd_source_builder: None, + dnd_destination_builder: None, selected: false, } } @@ -38,6 +51,19 @@ impl<'a, Message: 'static> ListButton<'a, Message> { self.selected = selected; self } + + pub fn with_dnd_source(mut self, builder: Box>) -> Self { + self.dnd_source_builder = Some(builder); + self + } + + pub fn with_dnd_destination( + mut self, + builder: Box>, + ) -> Self { + self.dnd_destination_builder = Some(builder); + self + } } pub enum ListItem<'a, Message> { @@ -175,8 +201,10 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { content, on_press, selected, - }) => col.push( - content_row(content) + dnd_source_builder, + dnd_destination_builder, + }) => { + let mut button: Element<'a, Message> = content_row(content) .apply(button::custom) .padding(item_padding) .width(Length::Fill) @@ -186,8 +214,18 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { radius_s, i == 0, i == last_index, - ))), - ), + ))) + .into(); + if let Some(builder) = dnd_source_builder { + button = builder(button).into(); + } + + if let Some(builder) = dnd_destination_builder { + button = builder(button).into(); + } + + col.push(button) + } }; } diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 9d4ce4b1..17d47cdd 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -66,10 +66,13 @@ mod menu_bar; pub(crate) use menu_bar::MenuBarState; pub use menu_bar::{MenuBar, menu_bar as bar}; +pub mod menu_column; + mod menu_inner; mod menu_tree; pub use menu_tree::{ MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, + nav_context, }; pub use crate::style::menu_bar::{Appearance, StyleSheet}; diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index b764fde7..2202125f 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -146,6 +146,7 @@ impl fmt::Display for KeyBind { write!(f, "{:?} + ", modifier)?; } match &self.key { + Key::Character(c) if c.as_str() == " " => write!(f, "Space"), Key::Character(c) => write!(f, "{}", c.to_uppercase()), Key::Named(named) => write!(f, "{:?}", named), other => write!(f, "{:?}", other), @@ -216,6 +217,7 @@ mod test { }; assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); + assert!(format!("{}", bind) == String::from("Ctrl + Space")) } #[test] diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 3843c349..01b91b59 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -18,10 +18,8 @@ use crate::Renderer; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; use crate::widget::RcWrapper; -use crate::widget::dropdown::menu::{self, State}; use crate::widget::menu::menu_inner::init_root_menu; -use iced::event::Status; use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::layout::{Limits, Node}; @@ -218,7 +216,7 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), - window_id: window::Id::NONE, + window_id: window::Id::RESERVED, #[cfg(all( feature = "multi-window", feature = "wayland", @@ -370,7 +368,9 @@ where my_state: &mut MenuBarState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::destroy_popup; + use crate::surface::action::{LiveSettings, destroy_popup}; + use crate::theme::THEME; + use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -485,7 +485,22 @@ where ..Default::default() }; let parent = self.window_id; + + let t = THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; + shell.publish((surface_action)(crate::surface::action::simple_popup( + move || LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, move || SctkPopupSettings { parent, id, @@ -694,7 +709,20 @@ where // draw path highlight if self.path_highlight.is_some() { - let styling = theme.appearance(&self.style); + let mut is_overlay = true; + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && self.on_surface_action.is_some() + && self.window_id != window::Id::NONE + { + is_overlay = true; + }; + let styling = theme.appearance(&self.style, is_overlay); if let Some(active) = state.active_root.first() { let active_bounds = layout .children() @@ -738,7 +766,7 @@ where tree: &'b mut Tree, layout: Layout<'b>, _renderer: &Renderer, - viewport: &Rectangle, + _viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( diff --git a/src/widget/menu/menu_column.rs b/src/widget/menu/menu_column.rs new file mode 100644 index 00000000..3e7455d6 --- /dev/null +++ b/src/widget/menu/menu_column.rs @@ -0,0 +1,411 @@ +//! Distribute content vertically. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::Event; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + layout, mouse, overlay, renderer, +}; + +#[allow(missing_debug_implementations)] +#[must_use] +pub struct MenuColumn<'a, Message, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Renderer> MenuColumn<'a, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`MenuColumn`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`MenuColumn`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`MenuColumn`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`MenuColumn`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`MenuColumn`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`MenuColumn::width`] or [`MenuColumn::height`] accordingly. + pub fn from_vec(children: Vec>) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align: Alignment::Start, + clip: false, + children, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`MenuColumn`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`MenuColumn`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`MenuColumn`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`MenuColumn`]. + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`MenuColumn`] . + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`MenuColumn`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an element to the [`MenuColumn`]. + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + + self.children.push(child); + self + } + + /// Adds an element to the [`MenuColumn`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`MenuColumn`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl Default for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer: iced::core::Renderer> + FromIterator> + for MenuColumn<'a, Message, Renderer> +{ + fn from_iter>>( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.max_width(self.max_width); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &mut self.children, + &mut tree.children, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget_mut().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + for (((_i, child), state), c_layout) in self + .children + .iter_mut() + .enumerate() + .zip(&mut tree.children) + .zip(layout.children()) + { + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), c_layout)| { + child.as_widget().mouse_interaction( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + + for (i, ((child, state), c_layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + .enumerate() + { + let t = theme.with_list_item_position(if self.children.len() == 1 { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == self.children.len() - 1 { + Some((Alignment::End, i)) + } else { + None + }); + child.as_widget().draw( + state, + renderer, + &t, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes( + c_layout.with_virtual_offset(layout.virtual_offset()), + state, + cursor, + ) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, + ) { + for ((e, c_layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, crate::Theme, Renderer> +where + Message: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(column: MenuColumn<'a, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c534c5b7..d5e50b92 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -15,7 +15,7 @@ use super::menu_tree::MenuTree; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; -use iced::window; +use iced::{Alignment, window}; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_widget::core::layout::{Limits, Node}; use iced_widget::core::mouse::{self, Cursor}; @@ -524,6 +524,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ); let node_size = children_node.size(); intrinsic_size.height += node_size.height; + intrinsic_size.width = intrinsic_size.width.max(node_size.width); nodes.push(children_node); @@ -561,7 +562,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell: &mut Shell<'_, Message>, ) -> Option<(usize, MenuState)> { use event::Event::{Mouse, Touch}; - use event::Status::{Captured, Ignored}; + use mouse::Button::Left; use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; @@ -579,7 +580,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - let menu_roots = match &mut self.menu_roots { + let _menu_roots = match &mut self.menu_roots { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; @@ -740,7 +741,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Rectangle::new(Point::ORIGIN, Size::INFINITE) }; - let styling = theme.appearance(&self.style); + let styling = theme.appearance(&self.style, self.is_overlay); let roots = active_root.iter().skip(1).fold( &self.menu_roots[active_root[0]].children, |mt, next_active_root| &mt[*next_active_root].children, @@ -815,13 +816,27 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .children() .nth(active.saturating_sub(start_index)) { + let i = active.saturating_sub(start_index); + let mut rad = styling.menu_border_radius; + let rad_0 = theme.cosmic().radius_0(); + if start_index != end_index { + if 0 == i { + rad[0] = rad_0[0]; + rad[1] = rad_0[1]; + } else if i == end_index - start_index { + rad[2] = rad_0[2]; + rad[3] = rad_0[3]; + } else { + rad = rad_0; + } + } let path_quad = renderer::Quad { bounds: active_layout .bounds() .intersection(&viewport) .unwrap_or_default(), border: Border { - radius: styling.menu_border_radius.into(), + radius: rad.into(), ..Default::default() }, shadow: Shadow::default(), @@ -834,12 +849,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // draw item menu_roots[start_index..=end_index] .iter() + .enumerate() .zip(children_layout.children()) - .for_each(|(mt, clo)| { + .for_each(|((i, mt), clo)| { + let t = theme.with_list_item_position( + if start_index == end_index { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == end_index - start_index { + Some((Alignment::End, i)) + } else { + None + }, + ); + mt.item.draw( &state.tree.children[active_root[0]].children[mt.index], r, - theme, + &t, style, clo, view_cursor, @@ -949,7 +977,7 @@ impl Widget, cursor: mouse::Cursor, @@ -967,21 +995,25 @@ impl Widget Widget Widget( ) where Message: Clone, { - use event::Status::{Captured, Ignored}; + use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index efcf9800..bd4f1e6c 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()), @@ -401,3 +401,19 @@ pub fn menu_items< }) .collect() } + +/// Create a menu tree from a widget and a vector of sub trees +pub fn nav_context< + A: MenuAction, + L: Into> + From<&'static str> + 'static, + Message: 'static + std::clone::Clone, +>( + key_binds: &HashMap, + children: Vec>>, +) -> Vec> { + let menus = children + .into_iter() + .map(|m| MenuItem::::Folder(L::from(""), m)); + let root = vec![MenuItem::::Folder(L::from(""), menus.collect())]; + menu_items(key_binds, root) +} diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index b7391abb..6f9d0210 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -8,7 +8,7 @@ use apply::Apply; use iced::clipboard::dnd::DndAction; use iced::clipboard::mime::AllowedMimeTypes; -use iced::{Background, Length}; +use iced::{Background, Length, window}; use iced_core::{Border, Color, Shadow}; use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; @@ -55,7 +55,7 @@ where } #[must_use] -pub struct NavBar<'a, Message> { +pub struct NavBar<'a, Message: Clone + 'static> { segmented_button: segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>, } @@ -133,6 +133,41 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { self.segmented_button = self.segmented_button.on_dnd_leave(handler); self } + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux" + ))] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.segmented_button = self.segmented_button.with_positioner(positioner); + self + } + + #[must_use] + pub fn window_id(mut self, id: window::Id) -> Self { + self.segmented_button = self.segmented_button.window_id(id); + self + } + + #[must_use] + pub fn window_id_maybe(mut self, id: Option) -> Self { + self.segmented_button = self.segmented_button.window_id_maybe(id); + + self + } + + #[must_use] + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.segmented_button = self.segmented_button.on_surface_action(handler); + self + } } impl<'a, Message: Clone + 'static> From> @@ -172,7 +207,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/popover.rs b/src/widget/popover.rs index 138d5a19..fb904248 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -4,7 +4,7 @@ //! A container which displays an overlay when a popup widget is attached. use iced::widget; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, @@ -401,6 +401,26 @@ where && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { + // Swallow new presses outside the popup, but still forward other + // events so an interaction started inside it (such as a selection + // drag) receives its release once the cursor leaves the bounds. + let is_press = matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch(touch::Event::FingerPressed { .. }) + ); + if !is_press { + self.content.as_widget_mut().update( + self.tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &layout.bounds(), + ); + } shell.capture_event(); return; } diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index a9d52831..500ba4ac 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -7,30 +7,21 @@ const LAG: f32 = 0.1; #[derive(Default)] pub struct Progress { pub current: f32, - target: Option, last: Option, } impl Progress { - /// Smoothly chases `target` using exponential decay. + /// Chases `target` using exponential decay. /// Returns `true` if a redraw should be requested. pub fn update(&mut self, target: f32, now: Instant) -> bool { // Don't animate on start let Some(last) = self.last else { self.current = target; - self.target = Some(target); self.last = Some(now); return false; }; - // Sync animation clock when target changes - if self.target != Some(target) { - self.target = Some(target); - self.last = Some(now); - return true; - } - - let dt = (now - last).as_secs_f32(); + let dt = (now - last).as_secs_f32().min(1.0 / 60.0); self.last = Some(now); let diff = target - self.current; diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index b8e92453..6c06e2f7 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -3,7 +3,7 @@ use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; -use iced::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; +use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use std::time::Duration; @@ -196,54 +196,13 @@ where }; let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; + let track_color = custom_style.track_color; + let bar_color = custom_style.bar_color; - let mut draw_quad = |x: f32, - width: f32, - color: iced::Color, - mut border: iced::Border, - is_track: bool, - total_progress_width: f32| { - let mut height = bounds.height; - if !is_track { - // For progress that is at the end of completion - if total_progress_width > bounds.width - radius { - let border_radius = - radius.min(bounds.height / 2.0) - (bounds.width - total_progress_width); - border.radius.top_right = border_radius; - border.radius.bottom_right = border_radius; - } else { - border.radius.top_right = 0.0; - border.radius.bottom_right = 0.0; - } - - // For indeterminate mode or when progress has just started - if x < radius.min(bounds.height / 2.0) { - let border_radius = radius.min(bounds.height / 2.0) - x; - border.radius.top_left = border_radius; - border.radius.bottom_left = border_radius; - - if total_progress_width < radius.min(bounds.height / 2.0) { - height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) - + total_progress_width * 2.0; - } - } else { - border.radius.top_left = 0.0; - border.radius.bottom_left = 0.0; - } - - if x > bounds.width - radius.min(bounds.height / 2.0) { - height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + width * 2.0; - } - } - + let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x + x, - y: bounds.y + (bounds.height - height) / 2.0, - width, - height, - }, + bounds: rect, border, snap: true, ..renderer::Quad::default() @@ -251,21 +210,22 @@ where color, ); }; + let to_rect = |x: f32, width: f32| Rectangle { + x: bounds.x + x * bounds.width, + y: bounds.y, + width: width * bounds.width, + height: bounds.height, + }; + // determinate progress bar if self.progress.is_some() { let current_p = state.progress.current; let len = self.markers.len(); let spacing = self.segment_spacing; + let gap = spacing / bounds.width; + let drawable = 1.0 - gap * len as f32; let radius_inner = radius.min(spacing); - let gap = if len != 0 { - spacing / bounds.width - } else { - 0.0 - }; - let drawable = 1.0 - gap * len as f32; - - let mut absolute_width = 0.0; for i in 0..=len { let (seg_lo, r_left) = if i == 0 { (0.0, radius) @@ -279,62 +239,45 @@ where }; let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; + let rect = to_rect(x_start, x_width); + let segment_radius = [r_left, r_right, r_right, r_left].into(); - let mut segment_radius = if i == 0 && len == 0 { - [r_left, r_right, r_right, r_left].into() - } else if i == 0 { - [r_left, 0.0, 0.0, r_left].into() - } else if i == len { - [0.0, r_right, r_right, 0.0].into() - } else { - [0.0, 0.0, 0.0, 0.0].into() + let border = Border { + width: border_width, + color: border_color, + radius: segment_radius, }; - // draw track segment - draw_quad( - x_start * bounds.width, - x_width * bounds.width, - custom_style.track_color, - iced::Border { - width: border_width, - color: border_color, - radius: segment_radius, - }, - true, - bounds.width, - ); - - // draw bar segment - if current_p > seg_lo { - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - absolute_width += x_width * fill + if i == 0 { 0.0 } else { gap }; - segment_radius = [r_left, r_right, r_right, r_left].into(); - draw_quad( - x_start * bounds.width, - x_width * fill * bounds.width, - custom_style.bar_color, - iced::Border { - radius: segment_radius, - ..iced::Border::default() - }, - false, - absolute_width * bounds.width, - ); + // empty segment + if current_p < seg_lo { + draw_quad(renderer, rect, border, track_color); + } + // filled segment + else if current_p > seg_hi { + draw_quad(renderer, rect, border, bar_color); + } + // partially filled segment + else { + let fill = (current_p - seg_lo) / (seg_hi - seg_lo); + draw_quad(renderer, rect, border, track_color); + renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| { + draw_quad(renderer, rect, border, bar_color); + }); } } - } else { + } + // indeterminate progress bar + else { // draw track draw_quad( - 0.0, - bounds.width, - custom_style.track_color, - iced::Border { + renderer, + bounds, + Border { width: border_width, color: border_color, radius: radius.into(), }, - true, - bounds.width, + track_color, ); // draw bar @@ -346,27 +289,17 @@ where let start = bar_start % 1.0; let right_width = (1.0 - start).min(length); let left_width = length - right_width; - let border = iced::Border { + let border = Border { radius: radius.into(), - ..iced::Border::default() + ..Border::default() }; - draw_quad( - start * bounds.width, - right_width * bounds.width, - custom_style.bar_color, - border, - false, - (right_width + start) * bounds.width, - ); - draw_quad( - 0.0, - left_width * bounds.width, - custom_style.bar_color, - border, - false, - left_width * bounds.width, - ); + renderer.with_layer(to_rect(start, right_width), |renderer| { + draw_quad(renderer, bounds, border, bar_color); + }); + renderer.with_layer(to_rect(0.0, left_width), |renderer| { + draw_quad(renderer, bounds, border, bar_color); + }); } } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index f8e174e1..edf5a3c7 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,7 +1,7 @@ //! Create choices using radio buttons. use crate::{Theme, theme}; use iced::border; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::tree::Tree; use iced_core::{ Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 839fa9a7..8863309b 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -5,7 +5,7 @@ use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index c8925cc2..b30bb848 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -1,7 +1,7 @@ //! Responsive Container, which will notify of size changes. use iced::{Limits, Size}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5fd67649..62205264 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -21,7 +21,7 @@ pub struct Horizontal; /// Horizontal implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Horizontal, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5458cd0a..26cc2bea 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -20,7 +20,7 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message> = /// Vertical implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> SegmentedButton<'_, Vertical, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Vertical, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5e0b4f7f..49778367 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -38,6 +38,7 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; +use std::sync::Arc; use std::time::{Duration, Instant}; thread_local! { @@ -107,7 +108,7 @@ pub trait SegmentedVariant { /// A conjoined group of items that function together as a button. #[derive(Setters)] #[must_use] -pub struct SegmentedButton<'a, Variant, SelectionMode, Message> +pub struct SegmentedButton<'a, Variant, SelectionMode, Message: Clone + 'static> where Model: Selectable, SelectionMode: Default, @@ -193,14 +194,27 @@ where pub(super) tab_drag: Option>, #[setters(skip)] pub(super) on_drop_hint: Option) -> Message + 'static>>, + #[setters(skip)] pub(super) on_reorder: Option Message + 'static>>, #[setters(skip)] + window_id: window::Id, + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux" + ))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + #[setters(skip)] + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, + /// Defines the implementation of this struct variant: PhantomData, } -impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> +impl<'a, Variant, SelectionMode, Message: Clone + 'static> + SegmentedButton<'a, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, @@ -248,6 +262,14 @@ where tab_drag: None, on_drop_hint: None, on_reorder: None, + window_id: window::Id::RESERVED, + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux" + ))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_surface_action: None, } } @@ -311,12 +333,7 @@ where where Message: Clone + 'static, { - self.context_menu = context_menu.map(|menus| { - vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), - menus, - )] - }); + self.context_menu = context_menu; if let Some(ref mut context_menu) = self.context_menu { context_menu.iter_mut().for_each(menu::Tree::set_index); @@ -878,6 +895,267 @@ where |id| id.0, ) } + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux" + ))] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } + + #[must_use] + pub fn window_id(mut self, id: window::Id) -> Self { + self.window_id = id; + self + } + + #[must_use] + pub fn window_id_maybe(mut self, id: Option) -> Self { + if let Some(id) = id { + self.window_id = id; + } + self + } + + #[must_use] + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.on_surface_action = Some(Arc::new(handler)); + self + } + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + #[allow(clippy::too_many_lines)] + fn create_popup<'b>( + &mut self, + layout: Layout<'b>, + view_cursor: mouse::Cursor, + renderer: &'b Renderer, + shell: &'b mut Shell<'_, Message>, + viewport: &'b Rectangle, + tree: &'b mut Tree, + ) { + let state = tree.state.downcast_mut::(); + let my_state = state.menu_state.clone(); + + if self.window_id != window::Id::NONE { + use crate::surface::action::{LiveSettings, destroy_popup}; + use crate::widget::menu::StyleSheet; + use iced_runtime::platform_specific::wayland::CornerRadius; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + + let Some(surface_action) = self.on_surface_action.as_ref() else { + return; + }; + + let id = my_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.get(&self.window_id).copied() { + // close existing popups + state.menu_states.clear(); + state.active_root.clear(); + shell.publish(surface_action(destroy_popup(id))); + state.view_cursor = view_cursor; + id + } else { + window::Id::unique() + } + }); + let Some(entity) = state.show_context else { + return; + }; + + let Some((mut bounds, i)) = self + .variant_bounds(state, layout.bounds()) + .enumerate() + .find_map(|(i, item)| match item { + ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + _ => None, + }) + else { + return; + }; + + assert!( + self.context_menu + .as_ref() + .is_none_or(|m| m[0].children.len() == self.model.len()), + "model length must match the number of context menus" + ); + let menu = self + .context_menu + .as_mut() + .map(|m| m[0].children[i].clone()) + .unwrap(); + + bounds.x = state.context_cursor.x; + bounds.y = state.context_cursor.y; + + let mut popup_menu: menu::Menu<'static, _> = menu::Menu { + tree: my_state.clone(), + menu_roots: std::borrow::Cow::Owned(vec![menu]), + bounds_expand: 0, + menu_overlays_parent: false, + close_condition: CloseCondition { + leave: false, + click_outside: true, + click_inside: true, + }, + item_width: ItemWidth::Uniform(240), + item_height: ItemHeight::Dynamic(40), + bar_bounds: bounds, + main_offset: 0, + cross_offset: 0, + root_bounds_list: vec![bounds], + path_highlight: Some(PathHighlight::MenuActive), + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), + position: Point::new(0., 0.), + is_overlay: false, + window_id: id, + depth: 0, + on_surface_action: self.on_surface_action.clone(), + }; + + menu::init_root_menu( + &mut popup_menu, + renderer, + shell, + view_cursor.position().unwrap(), + viewport.size(), + Vector::new(0., 0.), + bounds, + 0., // TODO offset? + ); + let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| { + state.popup_id.insert(self.window_id, id); + (state + .menu_states + .iter() + .find(|s| s.index.is_none()) + .map(|s| s.menu_bounds.parent_bounds) + .map_or_else( + || { + let bounds = layout.bounds(); + Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: 1, + height: 1, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: 1, + height: 1, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) + }); + + let menu_node = + popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.)); + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: + cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, + gravity, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + + let t = THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; + + /// Used to create a popup message from within a widget. + #[cfg(all(feature = "wayland", target_os = "linux"))] + #[must_use] + pub fn simple_popup( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, + settings: impl Fn() + -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + view: Option crate::Element<'static, Message> + Send + Sync + 'static>, + ) -> crate::surface::Action { + use std::any::Any; + + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + + crate::surface::Action::Popup( + Arc::new(boxed), + Arc::new(boxed_live), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) + } + shell.publish((surface_action)(simple_popup( + move || LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, + move || SctkPopupSettings { + parent, + id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill)) + }), + ))); + } + } } impl Widget @@ -990,10 +1268,10 @@ where mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &iced::Rectangle, + viewport: &iced::Rectangle, ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); @@ -1398,6 +1676,41 @@ where state.unfocus(); } + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + if is_pressed(event) + && let Some(_on_context) = self.on_context.as_ref() + { + let (was_open, id) = state.menu_state.inner.with_data_mut(|data| { + let was_open = data.open; + data.reset(); + data.open = false; + data.view_cursor = cursor_position; + let root = data.popup_id.remove(&self.window_id); + data.popup_id.clear(); + (was_open, root) + }); + if let Some(w) = id + && let Some(surface_action) = self.on_surface_action.as_ref() + && was_open + { + use crate::surface::action::destroy_popup; + + shell.publish((surface_action)(destroy_popup(w))); + return; + } + } + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); @@ -1456,9 +1769,28 @@ where shell.publish(on_context(key)); shell.capture_event(); + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + self.create_popup( + layout, + cursor_position, + renderer, + shell, + viewport, + tree, + ); + } return; } - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event { @@ -1576,6 +1908,33 @@ where } } + if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event))) + && let Some(_id) = state + .menu_state + .inner + .with_data_mut(|ms| ms.popup_id.remove(&self.window_id)) + { + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.capture_event(); + + shell.publish(surface_action(crate::surface::action::destroy_popup(_id))); + } + state.show_context = None; + + state.menu_state.inner.with_data_mut(|data| { + // Clear stale MenuBounds from any previous context menu before opening a new one. + data.reset(); + data.open = false; + data.view_cursor = cursor_position; + }); + } + if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -2193,19 +2552,43 @@ where _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "surface-message" + ))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) && self.on_surface_action.is_some() + && self.window_id != window::Id::NONE + { + return None; + } + let state = tree.state.downcast_mut::(); let menu_state = state.menu_state.clone(); let entity = state.show_context?; - let mut bounds = - self.variant_bounds(state, layout.bounds()) - .find_map(|item| match item { - ItemBounds::Button(e, bounds) if e == entity => Some(bounds), - _ => None, - })?; + let (mut bounds, i) = self + .variant_bounds(state, layout.bounds()) + .enumerate() + .find_map(|(i, item)| match item { + ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + _ => None, + })?; - let context_menu = self.context_menu.as_mut()?; + assert!( + self.context_menu + .as_ref() + .is_none_or(|m| m[0].children.len() == self.model.len()) + ); + let menu = self + .context_menu + .as_mut() + .map(|m| m[0].children[i].clone())?; if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. @@ -2223,7 +2606,7 @@ where Some( crate::widget::menu::Menu { tree: menu_state, - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), + menu_roots: std::borrow::Cow::Owned(vec![menu]), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -2509,6 +2892,7 @@ mod tests { where Model: Selectable, SelectionMode: Default, + Message: Clone, { const VERTICAL: bool = false; diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_control.rs index 046956c7..cecc3eb1 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_control.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -37,7 +37,7 @@ where /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where diff --git a/src/widget/tab_bar.rs b/src/widget/tab_bar.rs index a08128b4..358f1d4b 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/tab_bar.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model supplied by the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -35,7 +35,7 @@ where /// /// The data for the widget comes from a model that is maintained the application. /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 730fae25..cecd7ae7 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1811,9 +1811,22 @@ pub fn update<'a, Message: Clone + 'static>( focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Check if Ctrl/Command+A/C/V/X was pressed. - if state.keyboard_modifiers.command() { - match key.to_latin(*physical_key) { + // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard + let clip_key = match key.as_ref() { + keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => { + Some('v') + } + keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => { + Some('c') + } + keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => { + Some('x') + } + _ if modifiers.command() => key.to_latin(*physical_key), + _ => None, + }; + { + match clip_key { Some('c') => { if !is_secure && let Some((start, end)) = state.cursor.selection(value) { diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 922c1433..03bdf1bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -4,7 +4,7 @@ use iced::{Limits, Size}; use iced_core::layout::Node; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::renderer::{self}; use iced_core::widget::Operation; use iced_core::widget::tree::Tree; @@ -152,7 +152,7 @@ where translation, ) } else { - let bounds = layout.bounds(); + let _bounds = layout.bounds(); Some(overlay::Element::new(Box::new(ToasterOverlay::new( &mut state.children[1], diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2254759e..6d17503c 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,18 +1,17 @@ //! Show toggle controls using togglers. -use std::time::{Duration, Instant}; +use std::time::Duration; use crate::{Element, anim}; use iced_core::renderer::{self, Renderer}; use iced_core::widget::{self, Tree, tree}; use iced_core::{ - Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, - event, layout, mouse, text, touch, window, + Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, layout, mouse, text, touch, window, }; use iced_widget::Id; use iced_widget::toggler::Status; -pub use iced_widget::toggler::{Catalog, Style}; +pub use iced_widget::toggler::Catalog; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -260,7 +259,7 @@ impl<'a, Message> Widget for Toggler<'a, shell.capture_event(); } } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(_now)) => { state.anim.anim_done(self.duration); if state.anim.last_change.is_some() { shell.request_redraw(); @@ -370,7 +369,7 @@ impl<'a, Message> Widget for Toggler<'a, }, style.background, ); - let mut t = state.anim.t(self.duration, self.is_toggled); + let t = state.anim.t(self.duration, self.is_toggled); let toggler_foreground_bounds = Rectangle { x: bounds.x diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index 203a2010..310491cb 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -13,25 +13,20 @@ use std::time::Duration; use iced::Task; use iced_runtime::core::widget::Id; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, - Vector, Widget, layout, mouse, overlay, renderer, svg, touch, + Vector, Widget, layout, mouse, overlay, renderer, touch, }; +use iced_runtime::platform_specific::wayland::CornerRadius; + +use crate::surface::action::LiveSettings; +use crate::theme::THEME; pub use super::{Catalog, Style}; -/// Internally defines different button widget variants. -enum Variant { - Normal, - Image { - close_icon: svg::Handle, - on_remove: Option, - }, -} - /// A generic button which emits a message when pressed. #[allow(missing_debug_implementations)] #[must_use] @@ -306,7 +301,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } let content_layout = layout.children().next().unwrap(); - let state = tree.state.downcast_ref::(); + let _state = tree.state.downcast_ref::(); let styling = theme.style(&self.style); @@ -521,8 +516,26 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); + + let theme = THEME.lock().unwrap(); + + let corners = theme.cosmic().corner_radii.radius_s; + let boxed_live: Box< + dyn Fn() -> LiveSettings + Send + Sync + 'static, + > = Box::new(move || LiveSettings { + corners: Some(CornerRadius { + top_left: corners[0] as u32, + top_right: corners[1] as u32, + bottom_left: corners[3] as u32, + bottom_right: corners[2] as u32, + }), + ..Default::default() + }); + let boxed_live: Box = + Box::new(boxed_live); crate::surface::Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< @@ -553,9 +566,25 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( + 'static, > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); + let theme = THEME.lock().unwrap(); + + let corners = theme.cosmic().corner_radii.radius_s; + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(move || LiveSettings { + corners: Some(CornerRadius { + top_left: corners[0] as u32, + top_right: corners[1] as u32, + bottom_left: corners[3] as u32, + bottom_right: corners[2] as u32, + }), + ..Default::default() + }); + let boxed_live: Box = + Box::new(boxed_live); let sm = crate::surface::Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index aee06e5a..27f75a3d 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::thread::{self, ThreadId}; use crate::Element; -use iced::{Length, Rectangle, Size, event}; +use iced::{Length, Rectangle, Size}; use iced_core::id::Id; use iced_core::widget::tree; use iced_core::{Widget, widget};