diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c90bffa..9070c89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,11 +41,11 @@ jobs: fail-fast: false matrix: features: - - 'winit_softbuffer debug' - - 'winit_softbuffer tokio' - - winit_softbuffer + - 'winit_tiny_skia debug' + - 'winit_tiny_skia tokio' + - winit_tiny_skia - winit_wgpu - - softbuffer + - tiny_skia - wayland - applet runs-on: ubuntu-22.04 diff --git a/Cargo.toml b/Cargo.toml index 379f60c..a8cb3b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,16 +7,16 @@ edition = "2021" name = "cosmic" [features] -default = ["dyrend", "winit", "tokio"] +default = ["tiny_skia", "winit", "tokio", "a11y"] debug = ["iced/debug"] -softbuffer = ["iced/softbuffer", "iced_softbuffer"] -dyrend = ["iced/dyrend"] -wayland = ["iced/wayland", "iced/dyrend", "iced_sctk"] +a11y = ["iced/a11y", "iced_accessibility"] +tiny_skia = ["iced/tiny-skia", "iced_tiny_skia"] +wayland = ["iced/wayland", "iced_sctk", "sctk",] wgpu = ["iced/wgpu", "iced_wgpu"] tokio = ["dep:tokio", "iced/tokio"] winit = ["iced/winit", "iced_winit"] -applet = ["cosmic-panel-config", "sctk", "wayland"] -winit_softbuffer = ["winit", "softbuffer"] +applet = ["cosmic-panel-config", "wayland", "ron", "serde"] +winit_tiny_skia = ["winit", "tiny_skia"] winit_wgpu = ["winit", "wgpu"] [dependencies] @@ -25,37 +25,40 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.6.1" tokio = { version = "1.24.2", optional = true } -cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true } -sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "389a4f2" } +cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", branch = "bg_jammy", optional = true } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", optional = true, tag = "themed-pointer"} slotmap = "1.0.6" fraction = "0.13.0" +cosmic-config = { path = "cosmic-config" } +ron = { version = "0.8", optional = true } +serde = { version = "1.0", optional = true } [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.2" [dependencies.cosmic-theme] -git = "https://github.com/pop-os/cosmic-theme.git" +path = "cosmic-theme" [dependencies.iced] path = "iced" default-features = false -features = ["image", "svg"] +features = ["image", "svg", "lazy"] + +[dependencies.iced_runtime] +path = "iced/runtime" [dependencies.iced_core] path = "iced/core" -[dependencies.iced_lazy] -path = "iced/lazy" +[dependencies.iced_widget] +path = "iced/widget" -[dependencies.iced_native] -path = "iced/native" +[dependencies.iced_accessibility] +path = "iced/accessibility" -[dependencies.iced_softbuffer] -path = "iced/softbuffer" optional = true - -[dependencies.iced_dyrend] -path = "iced/dyrend" +[dependencies.iced_tiny_skia] +path = "iced/tiny_skia" optional = true [dependencies.iced_style] @@ -73,13 +76,11 @@ optional = true path = "iced/wgpu" optional = true -[dependencies.iced_glow] -path = "iced/glow" -optional = true - [workspace] members = [ "cosmic-config", + "cosmic-config-derive", + "cosmic-theme", "examples/*", ] exclude = [ diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml new file mode 100644 index 0000000..44f960e --- /dev/null +++ b/cosmic-config-derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cosmic-config-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs new file mode 100644 index 0000000..88984fa --- /dev/null +++ b/cosmic-config-derive/src/lib.rs @@ -0,0 +1,85 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{self}; + +#[proc_macro_derive(CosmicConfigEntry)] +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 impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + // let generics = &ast.generics; + + // Get the fields of the struct + let fields = match ast.data { + syn::Data::Struct(ref data_struct) => match data_struct.fields { + syn::Fields::Named(ref fields) => &fields.named, + _ => unimplemented!("Only named fields are supported"), + }, + _ => unimplemented!("Only structs are supported"), + }; + + let write_each_config_field = fields.iter().map(|field| { + let field_name = &field.ident; + quote! { + config.set(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 config.get::<#field_type>(stringify!(#field_name)) { + Ok(#field_name) => default.#field_name = #field_name, + Err(e) => errors.push(e), + } + } + }); + + // // Get the existing where clause or create a new one if it doesn't exist + // let mut where_clause = ast + // .generics + // .where_clause + // .clone() + // .unwrap_or_else(|| parse_quote!(where)); + + // // Add your additional constraints to the where clause + // // Here, we add the constraint 'T: Debug' to all generic parameters + // for param in ast.generics.params.iter() { + // where_clause + // .predicates + // .push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned)); + // } + + let gen = quote! { + impl CosmicConfigEntry for #name { + fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { + let tx = config.transaction(); + #(#write_each_config_field)* + tx.commit() + } + + fn get_entry(config: &Config) -> Result, Self)> { + let mut default = Self::default(); + let mut errors = Vec::new(); + + #(#get_each_config_field)* + + if errors.is_empty() { + Ok(default) + } else { + Err((errors, default)) + } + } + } + }; + + gen.into() +} diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 38cf3e4..7393faf 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -3,10 +3,19 @@ name = "cosmic-config" version = "0.1.0" edition = "2021" +[features] +default = ["macro", "subscription"] +macro = ["cosmic-config-derive"] +subscription = ["iced_futures"] + [dependencies] atomicwrites = "0.4.0" calloop = { version = "0.10.5", optional = true } -dirs = "4.0.0" -notify = "5.1.0" +dirs = "5.0.1" +notify = "6.0.0" ron = "0.8.0" serde = "1.0.152" +cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } +iced = { path = "../iced/", optional = true } +iced_futures = { path = "../iced/futures/", optional = true } + diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index e4b237a..a7b2be6 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,12 +1,21 @@ -use notify::Watcher; +#[cfg(feature = "subscription")] +use iced_futures::futures::channel::mpsc; +#[cfg(feature = "subscription")] +use iced_futures::subscription; +use notify::{RecommendedWatcher, Watcher}; use serde::{de::DeserializeOwned, Serialize}; use std::{ + borrow::Cow, fs, + hash::Hash, io::Write, path::{Path, PathBuf}, sync::Mutex, }; +#[cfg(feature = "macro")] +pub use cosmic_config_derive; + #[cfg(feature = "calloop")] pub mod calloop; @@ -251,3 +260,123 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { Ok(()) } } + +#[cfg(feature = "subscription")] +pub enum ConfigState { + Init(Cow<'static, str>, u64), + Waiting(T, RecommendedWatcher, mpsc::Receiver<()>, Config), + Failed, +} + +#[cfg(feature = "subscription")] +pub enum ConfigUpdate { + Update(T), + UpdateError(T, Vec), + Failed, +} + +pub trait CosmicConfigEntry +where + Self: Sized, +{ + fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; + fn get_entry(config: &Config) -> Result, Self)>; +} + +#[cfg(feature = "subscription")] +pub fn config_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::unfold( + id, + ConfigState::Init(config_id, config_version), + move |state| start_listening_loop(id, state), + ) +} + +#[cfg(feature = "subscription")] +async fn start_listening< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + state: ConfigState, +) -> ( + Option<(I, Result, T)>)>, + ConfigState, +) { + use iced_futures::futures::{future::pending, StreamExt}; + + match state { + ConfigState::Init(config_id, version) => { + let (tx, rx) = mpsc::channel(100); + let config = match Config::new(&config_id, version) { + Ok(c) => c, + Err(_) => return (None, ConfigState::Failed), + }; + let watcher = match config.watch(move |_helper, _keys| { + let mut tx = tx.clone(); + let _ = tx.try_send(()); + }) { + Ok(w) => w, + Err(_) => return (None, ConfigState::Failed), + }; + + match T::get_entry(&config) { + Ok(t) => ( + Some((id, Ok(t.clone()))), + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + Some((id, Err((errors, t.clone())))), + ConfigState::Waiting(t, watcher, rx, config), + ), + } + } + ConfigState::Waiting(old, watcher, mut rx, config) => match rx.next().await { + Some(_) => match T::get_entry(&config) { + Ok(t) => ( + if t != old { + Some((id, Ok(t.clone()))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + Err((errors, t)) => ( + if t != old { + Some((id, Err((errors, t.clone())))) + } else { + None + }, + ConfigState::Waiting(t, watcher, rx, config), + ), + }, + + None => (None, ConfigState::Failed), + }, + ConfigState::Failed => pending().await, + } +} + +#[cfg(feature = "subscription")] +async fn start_listening_loop< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + mut state: ConfigState, +) -> ((I, Result, T)>), ConfigState) { + loop { + let (update, new_state) = start_listening(id, state).await; + state = new_state; + if let Some(update) = update { + return (update, state); + } + } +} diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml new file mode 100644 index 0000000..7f745b6 --- /dev/null +++ b/cosmic-theme/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cosmic-theme" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.docs.rs] +features = ["test_all_features"] +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = [] +no-default = [] +contrast-derivation = ["float-cmp"] +theme-from-image = ["kmeans_colors", "contrast-derivation", "float-cmp", "image"] +hex-color = ["hex"] + +[dependencies] +palette = {version = "0.6", features = ["serializing"] } +anyhow = "1.0" +hex = {version = "0.4.3", optional = true} +kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } +image = {version = "0.24.1", optional = true } +float-cmp = { version = "0.9.0", optional = true } +serde = { version = "1.0.129", features = ["derive"] } +ron = "0.8" +lazy_static = "1.4.0" +csscolorparser = {version = "0.6.2", features = ["serde"]} +directories = { git = "https://github.com/edfloreshz/directories-rs", version = "4.0.1" } +cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription"] } + diff --git a/cosmic-theme/README.md b/cosmic-theme/README.md new file mode 100644 index 0000000..1a1aebe --- /dev/null +++ b/cosmic-theme/README.md @@ -0,0 +1 @@ +# WIP \ No newline at end of file diff --git a/cosmic-theme/src/color_picker/exact.rs b/cosmic-theme/src/color_picker/exact.rs new file mode 100644 index 0000000..2e29c26 --- /dev/null +++ b/cosmic-theme/src/color_picker/exact.rs @@ -0,0 +1,170 @@ +use super::ColorPicker; +use crate::{Selection, ThemeConstraints}; +use anyhow::{anyhow, bail, Result}; +use float_cmp::approx_eq; +use palette::{Clamp, IntoColor, Lch, RelativeContrast, Srgba}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +/// Implementation of a Cosmic color chooser which exactly meets constraints +#[derive(Debug, Default, Clone)] +pub struct Exact { + selection: Selection, + constraints: ThemeConstraints, +} + +impl Exact +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// create a new Exact color picker + pub fn new(selection: Selection, constraints: ThemeConstraints) -> Self { + Self { + selection, + constraints, + } + } +} + +impl ColorPicker for Exact +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn get_constraints(&self) -> ThemeConstraints { + self.constraints + } + + fn get_selection(&self) -> Selection { + self.selection.clone() + } + + fn pick_color_graphic( + &self, + color: C, + contrast: f32, + grayscale: bool, + lighten: Option, + ) -> (C, Option) { + let mut err = None; + + let res = self.pick_color(color.clone(), Some(contrast), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("Graphic contrast {} failed: {}", contrast, e)); + } + + let res = self.pick_color(color.clone(), None, grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(e); + } + + // return same color if no other color possible + (color, err) + } + + fn pick_color_text( + &self, + color: C, + grayscale: bool, + lighten: Option, + ) -> (C, Option) { + let mut err = None; + + // AAA + let res = self.pick_color(color.clone(), Some(7.0), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("AAA text contrast failed: {}", e)); + } + + // AA + let res = self.pick_color(color.clone(), Some(4.5), grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(anyhow!("AA text contrast failed: {}", e)); + } + + let res = self.pick_color(color.clone(), None, grayscale, lighten); + if let Ok(c) = res { + return (c, err); + } else if let Err(e) = res { + err = Some(e); + } + + (color, err) + } + + fn pick_color( + &self, + color: C, + contrast: Option, + grayscale: bool, + lighten: Option, + ) -> Result { + let srgba: Srgba = color.clone().into(); + let mut lch_color: Lch = srgba.into_color(); + + // set to grayscale + if grayscale { + lch_color.chroma = 0.0; + } + + // lighten or darken + // TODO closed form solution using Lch color space contrast formula? + // for now do binary search... + + if let Some(contrast) = contrast { + let (min, max) = match lighten { + Some(b) if b => (lch_color.l, 100.0), + Some(_) => (0.0, lch_color.l), + None => (0.0, 100.0), + }; + let (mut l, mut r) = (min, max); + + for _ in 0..100 { + let cur_guess_lightness = (l + r) / 2.0; + let mut cur_guess = lch_color; + cur_guess.l = cur_guess_lightness; + let cur_contrast = srgba.get_contrast_ratio(&cur_guess.into_color()); + let contrast_dir = contrast > cur_contrast; + let lightness_dir = lch_color.l < cur_guess.l; + if approx_eq!(f32, contrast, cur_contrast, ulps = 4) { + lch_color = cur_guess; + break; + // TODO fix + } else if lightness_dir && contrast_dir || !lightness_dir && !contrast_dir { + l = cur_guess_lightness; + } else { + r = cur_guess_lightness; + } + } + + // clamp to valid value in range + lch_color.clamp_self(); + + // verify contrast + let actual_contrast = srgba.get_contrast_ratio(&lch_color.into_color()); + if !approx_eq!(f32, contrast, actual_contrast, ulps = 4) { + bail!( + "Failed to derive color with contrast {} from {:?}", + contrast, + color + ); + } + + Ok(C::from(lch_color.into_color())) + } else { + // maximize contrast if no constraint is given + if lch_color.l > 50.0 { + Ok(C::from(palette::named::BLACK.into_format().into_color())) + } else { + Ok(C::from(palette::named::WHITE.into_format().into_color())) + } + } + } +} diff --git a/cosmic-theme/src/color_picker/mod.rs b/cosmic-theme/src/color_picker/mod.rs new file mode 100644 index 0000000..b5bf4ee --- /dev/null +++ b/cosmic-theme/src/color_picker/mod.rs @@ -0,0 +1,280 @@ +use crate::{Component, Container, ContainerType, Derivation, Selection, Theme, ThemeConstraints}; +use anyhow::{anyhow, Result}; +use palette::{IntoColor, Lcha, Shade, Srgba}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +pub use exact::*; +mod exact; + +// TODO derive palette from Selection? +/// Color picker derives colors and theme elements +pub trait ColorPicker< + C: Into + From + Clone + fmt::Debug + Default + Serialize + DeserializeOwned, +> +{ + /// try to derive a color with a given contrast, grayscale setting, and lightness direction + fn pick_color( + &self, + color: C, + contrast: Option, + grayscale: bool, + lighten: Option, + ) -> Result; + + /// try to derive a text color with a given grayscale setting, and lightness direction + fn pick_color_text( + &self, + color: C, + grayscale: bool, + lighten: Option, + ) -> (C, Option); + + /// try to derive a graphic color with a given contrast, grayscale setting, and lightness direction + fn pick_color_graphic( + &self, + color: C, + contrast: f32, + grayscale: bool, + lighten: Option, + ) -> (C, Option); + + /// get the selection for this color picker + fn get_selection(&self) -> Selection; + + /// get the constraints for this color picker + fn get_constraints(&self) -> ThemeConstraints; + + /// derive a theme from the selection and constraints + fn theme_derivation(&self) -> Derivation> { + let mut theme_errors = Vec::new(); + + let Derivation { + derived: background, + errors: mut errs, + } = self.container_derivation(ContainerType::Background); + theme_errors.append(&mut errs); + + let Derivation { + derived: primary, + errors: mut errs, + } = self.container_derivation(ContainerType::Primary); + theme_errors.append(&mut errs); + + let Derivation { + derived: secondary, + mut errors, + } = self.container_derivation(ContainerType::Secondary); + theme_errors.append(&mut errors); + + let Derivation { + derived: accent, + mut errors, + } = self.widget_derivation(self.get_selection().accent); + theme_errors.append(&mut errors); + + let Derivation { + derived: destructive, + mut errors, + } = self.widget_derivation(self.get_selection().destructive); + theme_errors.append(&mut errors); + + let Derivation { + derived: warning, + mut errors, + } = self.widget_derivation(self.get_selection().warning); + theme_errors.append(&mut errors); + + let Derivation { + derived: success, + mut errors, + } = self.widget_derivation(self.get_selection().success); + theme_errors.append(&mut errors); + + Derivation { + derived: Theme::new( + background, + primary, + secondary, + accent, + destructive, + warning, + success, + ), + errors: theme_errors, + } + } + + /// derive a container element + fn container_derivation(&self, container_type: ContainerType) -> Derivation> { + let selection = self.get_selection(); + let constraints = self.get_constraints(); + + let mut errors = Vec::new(); + + let Selection { + background, + primary_container, + secondary_container, + .. + } = selection; + + let ThemeConstraints { + elevated_contrast_ratio, + divider_contrast_ratio, + divider_gray_scale, + lighten, + .. + } = constraints; + + let container = match container_type { + ContainerType::Background => background, + ContainerType::Primary => primary_container, + ContainerType::Secondary => secondary_container, + }; + let (container_divider, err) = self.pick_color_graphic( + container.clone(), + divider_contrast_ratio, + divider_gray_scale, + Some(lighten), + ); + if let Some(e) = err { + errors.push(e); + }; + + let (container_fg, err) = self.pick_color_text(container.clone(), true, None); + if let Some(err) = err { + let err = anyhow!("{} => \"container text\" failed: {}", container_type, err); + errors.push(err); + }; + + // TODO revisit this and adjust constraints for transparency + let mut container_fg_opacity_80: Srgba = container_fg.clone().into(); + container_fg_opacity_80.alpha *= 0.8; + + let (component_default, err) = self.pick_color_graphic( + container.clone(), + elevated_contrast_ratio, + false, + Some(lighten), + ); + if let Some(e) = err { + let err = anyhow!( + "{} => \"container component\" failed: {}", + container_type, + e + ); + errors.push(err); + }; + + let Derivation { + derived: container_component, + errors: errs, + } = self.widget_derivation(component_default); + for e in errs { + let err = anyhow!( + "{} => \"container component derivation\" failed: {}", + container_type, + e + ); + errors.push(err); + } + + Derivation { + derived: Container { + base: container, + divider: container_divider, + on: container_fg, + component: container_component, + }, + errors, + } + } + + /// derive a widget + fn widget_derivation(&self, default: C) -> Derivation> { + let ThemeConstraints { + divider_contrast_ratio, + divider_gray_scale, + lighten, + .. + } = self.get_constraints(); + + let mut errors = Vec::new(); + + let rgba: Srgba = default.clone().into(); + let lch = Lcha { + color: rgba.color.into_color(), + alpha: rgba.alpha, + }; + + // TODO define constraints for different states... + // & add color self methods and errors if these fail + let hover = if lighten { + lch.lighten(0.1) + } else { + lch.darken(0.1) + }; + + let pressed = if lighten { + hover.lighten(0.1) + } else { + hover.darken(0.1) + }; + let pressed = C::from(Srgba { + color: pressed.color.into_color(), + alpha: pressed.alpha, + }); + + // TODO is this actually a different color? or just outlined? + let selected = default.clone(); + + let mut disabled: Srgba = default.clone().into(); + disabled.alpha = 0.5; + + let (divider, error) = self.pick_color_graphic( + pressed.clone(), + divider_contrast_ratio, + divider_gray_scale, + Some(lighten), + ); + if let Some(error) = error { + errors.push(error); + } + + let (text, error) = self.pick_color_text(pressed.clone(), true, None); + if let Some(error) = error { + errors.push(error); + } + + let (selected_text, error) = self.pick_color_text(selected.clone(), true, None); + if let Some(error) = error { + errors.push(error); + } + + let mut text_opacity_80: Srgba = text.clone().into(); + text_opacity_80.alpha = 0.8; + + let mut disabled_fg = text.clone().into(); + disabled_fg.alpha = 0.5; + + Derivation { + derived: Component { + base: default, + hover: C::from(Srgba { + color: hover.color.into_color(), + alpha: hover.alpha, + }), + pressed, + selected: selected.clone(), + selected_text: selected_text, + focus: selected.clone(), // FIXME + divider, + on: text, + disabled: disabled.into(), + on_disabled: disabled_fg.into(), + }, + errors, + } + } +} diff --git a/cosmic-theme/src/config/mod.rs b/cosmic-theme/src/config/mod.rs new file mode 100644 index 0000000..a558fcf --- /dev/null +++ b/cosmic-theme/src/config/mod.rs @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MPL-2.0-only + +use crate::{util::CssColor, Theme, NAME, THEME_DIR}; +use anyhow::{bail, Context, Result}; +use directories::{BaseDirsExt, ProjectDirsExt}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + fmt, + fs::File, + io::{prelude::*, BufReader}, + path::PathBuf, +}; + +/// Cosmic Theme config +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// whether high contrast mode is activated + pub is_high_contrast: bool, + /// active + pub is_dark: bool, + /// Selected light theme name + pub light: String, + /// Selected dark theme name + pub dark: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + is_dark: true, + light: "cosmic-light".to_string(), + dark: "cosmic-dark".to_string(), + is_high_contrast: false, + } + } +} + +/// name of the config file +pub const CONFIG_NAME: &str = "config"; + +impl Config { + /// create a new cosmic theme config + pub fn new(is_dark: bool, high_contrast: bool, light: String, dark: String) -> Self { + Self { + is_dark, + light, + dark, + is_high_contrast: high_contrast, + } + } + + /// save the cosmic theme config + pub fn save(&self) -> Result<()> { + let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) + .context("Failed to find project directory.")?; + if let Ok(path) = xdg_dirs.place_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { + let mut f = File::create(path)?; + let ron = ron::ser::to_string_pretty(&self, Default::default())?; + f.write_all(ron.as_bytes())?; + Ok(()) + } else { + bail!("failed to save theme config") + } + } + + /// init the config directory + pub fn init() -> anyhow::Result { + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + let res = Ok(base_dirs.create_config_directory(NAME)?); + Theme::::init()?; + + if Self::load().is_ok() { + res + } else { + Self::default().save()?; + Theme::dark_default().save()?; + Theme::light_default().save()?; + res + } + } + + /// load the cosmic theme config + pub fn load() -> Result { + let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) + .context("Failed to find project directory.")?; + let path = xdg_dirs.config_dir(); + std::fs::create_dir_all(&path)?; + let path = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))); + if path.is_none() { + let s = Self::default(); + s.save()?; + } + if let Some(path) = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { + let mut f = File::open(&path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(ron::from_str(s.as_str())?) + } else { + anyhow::bail!("Failed to load config") + } + } + + /// get the name of the active theme + pub fn active_name(&self) -> Option { + if self.is_dark && self.dark.is_empty() { + Some(self.dark.clone()) + } else if !self.is_dark && !self.light.is_empty() { + Some(self.light.clone()) + } else { + None + } + // if *high_contrast { + // if let Some(palette) = palette.take() { + // // TODO enforce high contrast constraints + // *palette = palette.to_high_contrast(); + // todo!() + // } + // } + } + + /// get the active theme + pub fn get_active(&self) -> anyhow::Result> { + let active = match self.active_name() { + Some(n) => n, + _ => anyhow::bail!("No configured active overrides"), + }; + let css_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let css_dirs = directories::ProjectDirs::from_path(PathBuf::from(css_path)) + .context("Failed to find project directory.")?; + let active_theme_path = match css_dirs.find_data_file(format!("{active}.ron")) { + Some(p) => p, + _ => anyhow::bail!("Could not find theme"), + }; + match File::open(active_theme_path) { + Ok(active_theme_file) => { + let reader = BufReader::new(active_theme_file); + Ok(ron::de::from_reader::<_, Theme>(reader)?) + } + Err(_) => { + if self.is_dark { + Ok(Theme::dark_default()) + } else { + Ok(Theme::light_default()) + } + } + } + } + + /// set the name of the active light theme + pub fn set_active_light(new: &str) -> Result<()> { + let mut self_ = Self::load()?; + + self_.light = new.to_string(); + + self_.save() + } + + /// set the name of the active dark theme + pub fn set_active_dark(new: &str) -> Result<()> { + let mut self_ = Self::load()?; + + self_.dark = new.to_string(); + + self_.save() + } +} + +impl From<(Theme, Theme)> for Config +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((light, dark): (Theme, Theme)) -> Self { + Self { + light: light.name, + dark: dark.name, + is_dark: true, + is_high_contrast: false, + } + } +} + +impl From> for Config +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from(t: Theme) -> Self { + Self { + light: t.clone().name, + dark: t.name, + is_dark: true, + is_high_contrast: true, + } + } +} diff --git a/cosmic-theme/src/hex_color.rs b/cosmic-theme/src/hex_color.rs new file mode 100644 index 0000000..bf04f21 --- /dev/null +++ b/cosmic-theme/src/hex_color.rs @@ -0,0 +1,35 @@ +use hex::encode; +use palette::{Pixel, Srgba}; +use std::fmt; + +/// Wrapper type for Hex color strings +#[derive(Debug, Clone)] +pub struct Hex { + hex_string: String, +} + +impl> From for Hex { + fn from(c: C) -> Self { + let srgba: Srgba = c.into(); + let hex_string = encode::<[u8; 4]>(Srgba::into_raw(srgba.into_format())); + Hex { hex_string } + } +} + +impl Into for Hex { + fn into(self) -> String { + self.hex_string + } +} + +impl fmt::Display for Hex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{}", self) + } +} + +/// Create a hex String from an Srgba +pub fn hex_from_rgba(rgba: &Srgba) -> String { + let hex = encode::<[u8; 4]>(Srgba::into_raw(rgba.into_format())); + format!("#{hex}") +} diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs new file mode 100644 index 0000000..efa4025 --- /dev/null +++ b/cosmic-theme/src/lib.rs @@ -0,0 +1,79 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)] + +//! Cosmic theme library. +//! +//! Provides utilities for creating custom cosmic themes. +//! + +#[cfg(feature = "contrast-derivation")] +pub use color_picker::*; +pub use config::*; +#[cfg(feature = "hex-color")] +pub use hex_color::*; +pub use model::*; +pub use output::*; +pub use theme_provider::*; +#[cfg(feature = "contrast-derivation")] +mod color_picker; +mod config; +#[cfg(feature = "hex-color")] +mod hex_color; +mod model; +mod output; +mod theme_provider; +/// utilities +pub mod util; + +/// name of cosmic theme +pub const NAME: &'static str = "com.system76.CosmicTheme"; +/// Name of the theme directory +pub const THEME_DIR: &str = "themes"; +/// name of the palette directory +pub const PALETTE_DIR: &str = "palettes"; + +pub use palette; + +/// theme derivation from an image +#[cfg(feature = "theme-from-image")] +pub mod theme_from_image { + use image::EncodableLayout; + use kmeans_colors::{get_kmeans_hamerly, Kmeans, Sort}; + use palette::{rgb::Srgba, Pixel}; + use palette::{IntoColor, Lab}; + use std::path::Path; + + /// Create a palette from an image + /// The palette is sorted by how often a color occurs in the image, most often first + pub fn theme_from_image>(path: P) -> Option> { + // calculate kmeans colors from file + // let pixbuf = Pixbuf::from_file(path); + let img = image::open(path); + match img { + Ok(img) => { + let lab: Vec = Srgba::from_raw_slice(img.to_rgba8().into_raw().as_bytes()) + .iter() + .map(|x| x.color.into_format().into_color()) + .collect(); + + let mut result = Kmeans::new(); + + // TODO random seed + for i in 0..2 { + let run_result = get_kmeans_hamerly(5, 20, 5.0, false, &lab, i as u64); + if run_result.score < result.score { + result = run_result; + } + } + let mut res = Lab::sort_indexed_colors(&result.centroids, &result.indices); + res.sort_unstable_by(|a, b| (b.percentage).partial_cmp(&a.percentage).unwrap()); + let colors: Vec = res.iter().map(|x| x.centroid.into_color()).collect(); + Some(colors) + } + Err(err) => { + eprintln!("{}", err); + None + } + } + } +} diff --git a/cosmic-theme/src/model/constraint.rs b/cosmic-theme/src/model/constraint.rs new file mode 100644 index 0000000..4513249 --- /dev/null +++ b/cosmic-theme/src/model/constraint.rs @@ -0,0 +1,26 @@ +/// Cosmic theme custom constraints which are used to pick colors +#[derive(Copy, Clone, Debug)] +pub struct ThemeConstraints { + /// requested contrast ratio for elevated surfaces + pub elevated_contrast_ratio: f32, + /// requested contrast ratio for dividers + pub divider_contrast_ratio: f32, + /// requested contrast ratio for text + pub text_contrast_ratio: f32, + /// gray scale or color for dividers + pub divider_gray_scale: bool, + /// elevated surfaces are lightened or darkened + pub lighten: bool, +} + +impl Default for ThemeConstraints { + fn default() -> Self { + Self { + elevated_contrast_ratio: 1.1, + divider_contrast_ratio: 1.51, + text_contrast_ratio: 7.0, + divider_gray_scale: true, + lighten: true, + } + } +} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs new file mode 100644 index 0000000..622627c --- /dev/null +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -0,0 +1,252 @@ +use std::{ + fmt, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +use anyhow::Context; +use directories::{BaseDirsExt, ProjectDirsExt}; +use lazy_static::lazy_static; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{util::CssColor, NAME, PALETTE_DIR}; + +lazy_static! { + /// built in light palette + pub static ref LIGHT_PALETTE: CosmicPalette = + ron::from_str(include_str!("light.ron")).unwrap(); + /// built in dark palette + pub static ref DARK_PALETTE: CosmicPalette = + ron::from_str(include_str!("dark.ron")).unwrap(); +} + +/// Palette type +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum CosmicPalette { + /// Dark mode + Dark(CosmicPaletteInner), + /// Light mode + Light(CosmicPaletteInner), + /// High contrast light mode + HighContrastLight(CosmicPaletteInner), + /// High contrast dark mode + HighContrastDark(CosmicPaletteInner), +} + +impl AsRef> for CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_ref(&self) -> &CosmicPaletteInner { + match self { + CosmicPalette::Dark(p) => p, + CosmicPalette::Light(p) => p, + CosmicPalette::HighContrastLight(p) => p, + CosmicPalette::HighContrastDark(p) => p, + } + } +} + +impl CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// check if the palette is dark + pub fn is_dark(&self) -> bool { + match self { + CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true, + CosmicPalette::Light(_) | CosmicPalette::HighContrastLight(_) => false, + } + } + + /// check if the palette is high_contrast + pub fn is_high_contrast(&self) -> bool { + match self { + CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true, + CosmicPalette::Light(_) | CosmicPalette::Dark(_) => false, + } + } +} + +impl Default for CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn default() -> Self { + CosmicPalette::Dark(Default::default()) + } +} + +/// The palette for Cosmic Theme, from which all color properties are derived +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct CosmicPaletteInner { + /// name of the palette + pub name: String, + + /// basic palette + /// blue: colors used for various points of emphasis in the UI + pub blue: C, + /// red: colors used for various points of emphasis in the UI + pub red: C, + /// green: colors used for various points of emphasis in the UI + pub green: C, + /// yellow: colors used for various points of emphasis in the UI + pub yellow: C, + + /// surface grays + /// colors used for three levels of surfaces in the UI + pub gray_1: C, + /// colors used for three levels of surfaces in the UI + pub gray_2: C, + /// colors used for three levels of surfaces in the UI + pub gray_3: C, + + /// System Neutrals + /// A wider spread of dark colors for more general use. + pub neutral_1: C, + /// A wider spread of dark colors for more general use. + pub neutral_2: C, + /// A wider spread of dark colors for more general use. + pub neutral_3: C, + /// A wider spread of dark colors for more general use. + pub neutral_4: C, + /// A wider spread of dark colors for more general use. + pub neutral_5: C, + /// A wider spread of dark colors for more general use. + pub neutral_6: C, + /// A wider spread of dark colors for more general use. + pub neutral_7: C, + /// A wider spread of dark colors for more general use. + pub neutral_8: C, + /// A wider spread of dark colors for more general use. + pub neutral_9: C, + /// A wider spread of dark colors for more general use. + pub neutral_10: C, + + /// Extended Color Palette + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_warm_grey: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_orange: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_yellow: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_blue: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_purple: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_pink: C, + /// Colors used for themes, app icons, illustrations, and other brand purposes. + pub ext_indigo: C, + + /// Potential Accent Color Combos + pub accent_warm_grey: C, + /// Potential Accent Color Combos + pub accent_orange: C, + /// Potential Accent Color Combos + pub accent_yellow: C, + /// Potential Accent Color Combos + pub accent_purple: C, + /// Potential Accent Color Combos + pub accent_pink: C, + /// Potential Accent Color Combos + pub accent_indigo: C, +} + +impl From> for CosmicPaletteInner { + fn from(p: CosmicPaletteInner) -> Self { + CosmicPaletteInner { + name: p.name, + blue: p.blue.into(), + red: p.red.into(), + green: p.green.into(), + yellow: p.yellow.into(), + gray_1: p.gray_1.into(), + gray_2: p.gray_2.into(), + gray_3: p.gray_3.into(), + neutral_1: p.neutral_1.into(), + neutral_2: p.neutral_2.into(), + neutral_3: p.neutral_3.into(), + neutral_4: p.neutral_4.into(), + neutral_5: p.neutral_5.into(), + neutral_6: p.neutral_6.into(), + neutral_7: p.neutral_7.into(), + neutral_8: p.neutral_8.into(), + neutral_9: p.neutral_9.into(), + neutral_10: p.neutral_10.into(), + ext_warm_grey: p.ext_warm_grey.into(), + ext_orange: p.ext_orange.into(), + ext_yellow: p.ext_yellow.into(), + ext_blue: p.ext_blue.into(), + ext_purple: p.ext_purple.into(), + ext_pink: p.ext_pink.into(), + ext_indigo: p.ext_indigo.into(), + accent_warm_grey: p.accent_warm_grey.into(), + accent_orange: p.accent_orange.into(), + accent_yellow: p.accent_yellow.into(), + accent_purple: p.accent_purple.into(), + accent_pink: p.accent_pink.into(), + accent_indigo: p.accent_indigo.into(), + } + } +} + +impl CosmicPalette +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// name of the palette + pub fn name(&self) -> &str { + match &self { + CosmicPalette::Dark(p) => &p.name, + CosmicPalette::Light(p) => &p.name, + CosmicPalette::HighContrastLight(p) => &p.name, + CosmicPalette::HighContrastDark(p) => &p.name, + } + } + /// save the theme to the theme directory + pub fn save(&self) -> anyhow::Result<()> { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + let ron_name = format!("{}.ron", self.name()); + + if let Ok(p) = ron_dirs.place_config_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?; + } else { + anyhow::bail!("Failed to write RON theme."); + } + Ok(()) + } + + /// init the theme directory + pub fn init() -> anyhow::Result { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + Ok(base_dirs.create_config_directory(ron_path)?) + } + + /// load a theme by name + pub fn load_from_name(name: &str) -> anyhow::Result { + let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + + let ron_name = format!("{}.ron", name); + if let Some(p) = ron_dirs.find_config_file(ron_name) { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } else { + anyhow::bail!("Failed to write RON theme."); + } + } + + /// load a theme by path + pub fn load(p: &dyn AsRef) -> anyhow::Result { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } +} diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron new file mode 100644 index 0000000..d8d0ba8 --- /dev/null +++ b/cosmic-theme/src/model/dark.ron @@ -0,0 +1,95 @@ +Dark ( + ( + name: "cosmic-dark", + blue: ( + c: "#94EBEB", + ), + red: ( + c: "#FFB5B5", + ), + green: ( + c: "#ACF7D2", + ), + yellow: ( + c: "#FFF19E", + ), + gray_1: ( + c: "#1E1E1E", + ), + gray_2: ( + c: "#292929", + ), + gray_3: ( + c: "#2E2E2E", + ), + neutral_1: ( + c: "#000000", + ), + neutral_2: ( + c: "#272727", + ), + neutral_3: ( + c: "#424242", + ), + neutral_4: ( + c: "#5D5D5D", + ), + neutral_5: ( + c: "#787878", + ), + neutral_6: ( + c: "#939393", + ), + neutral_7: ( + c: "#AEAEAE", + ), + neutral_8: ( + c: "#C9C9C9", + ), + neutral_9: ( + c: "#E4E4E4", + ), + neutral_10: ( + c: "#FFFFFF", + ), + ext_warm_grey: ( + c: "#9B8E8A", + ), + ext_orange: ( + c: "#FFAD00", + ), + ext_yellow: ( + c: "#FEDB40", + ), + ext_blue: ( + c: "#48B9C7", + ), + ext_purple: ( + c: "#CF7DFF", + ), + ext_pink: ( + c: "#F93A83", + ), + ext_indigo: ( + c: "#3E88FF", + ), + accent_warm_grey: ( + c: "#554742", + ), + accent_orange: ( + c: "#AF5C02", + ), + accent_yellow: ( + c: "#966800", + ), + accent_purple: ( + c: "#813FFF", + ), + accent_pink: ( + c: "#F93A83", + ), + accent_indigo: ( + c: "#3E88FF", + ), + ) +) \ No newline at end of file diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs new file mode 100644 index 0000000..da5f1ec --- /dev/null +++ b/cosmic-theme/src/model/derivation.rs @@ -0,0 +1,480 @@ +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt; + +use crate::{util::over, CosmicPalette}; + +/// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Container { + /// the color of the container + pub base: C, + /// the color of components in the container + pub component: Component, + /// the color of dividers in the container + pub divider: C, + /// the color of text in the container + pub on: C, +} + +impl Container +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// convert to srgba + pub fn into_srgba(self) -> Container { + Container { + base: self.base.into(), + component: self.component.into_srgba(), + divider: self.divider.into(), + on: self.on.into(), + } + } + + pub(crate) fn new( + palette: CosmicPalette, + container_type: ComponentType, + bg: C, + on_bg: C, + ) -> Self { + let mut divider_c: Srgba = on_bg.clone().into(); + divider_c.alpha = 0.2; + + let divider = over(divider_c.clone(), bg.clone()); + Self { + base: bg, + component: (palette, container_type).into(), + divider: divider.into(), + on: on_bg, + } + } +} + +impl From<(CosmicPalette, ContainerType)> for Container +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((p, t): (CosmicPalette, ContainerType)) -> Self { + match (p, t) { + (CosmicPalette::Dark(p), ContainerType::Background) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_7.clone(), + ), + (CosmicPalette::Dark(p), ContainerType::Primary) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::Dark(p), ContainerType::Secondary) => Self::new( + CosmicPalette::Dark(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Background) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Primary) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::HighContrastDark(p), ContainerType::Secondary) => Self::new( + CosmicPalette::HighContrastDark(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Background) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Primary) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::Light(p), ContainerType::Secondary) => Self::new( + CosmicPalette::Light(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_8.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Background) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Background, + p.gray_1.clone(), + p.neutral_10.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Primary) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Primary, + p.gray_2.clone(), + p.neutral_9.clone(), + ), + (CosmicPalette::HighContrastLight(p), ContainerType::Secondary) => Self::new( + CosmicPalette::HighContrastLight(p.clone()), + ComponentType::Secondary, + p.gray_3.clone(), + p.neutral_9.clone(), + ), + } + } +} + +/// The type of the container +#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)] +pub enum ContainerType { + /// Background type + Background, + /// Primary type + Primary, + /// Secondary type + Secondary, +} + +impl Default for ContainerType { + fn default() -> Self { + Self::Background + } +} + +impl fmt::Display for ContainerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ContainerType::Background => write!(f, "Background"), + ContainerType::Primary => write!(f, "Primary Container"), + ContainerType::Secondary => write!(f, "Secondary Container"), + } + } +} + +/// The colors for a widget of the Cosmic theme +#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)] +pub struct Component { + /// The base color of the widget + pub base: C, + /// The color of the widget when it is hovered + pub hover: C, + /// the color of the widget when it is pressed + pub pressed: C, + /// the color of the widget when it is selected + pub selected: C, + /// the color of the widget when it is selected + pub selected_text: C, + /// the color of the widget when it is focused + pub focus: C, + /// the color of dividers for this widget + pub divider: C, + /// the color of text for this widget + pub on: C, + // the color of text with opacity 80 for this widget + // pub text_opacity_80: C, + /// the color of the widget when it is disabled + pub disabled: C, + /// the color of text in the widget when it is disabled + pub on_disabled: C, +} + +impl Component +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// get @hover_state_color + pub fn hover_state_color(&self) -> Srgba { + self.hover.clone().into() + } + /// get @pressed_state_color + pub fn pressed_state_color(&self) -> Srgba { + self.pressed.clone().into() + } + /// get @selected_state_color + pub fn selected_state_color(&self) -> Srgba { + self.selected.clone().into() + } + /// get @selected_state_text_color + pub fn selected_state_text_color(&self) -> Srgba { + self.selected_text.clone().into() + } + /// get @focus_color + pub fn focus_color(&self) -> Srgba { + self.focus.clone().into() + } + /// convert to srgba + pub fn into_srgba(self) -> Component { + Component { + base: self.base.into(), + hover: self.hover.into(), + pressed: self.pressed.into(), + selected: self.selected.into(), + selected_text: self.selected_text.into(), + focus: self.focus.into(), + divider: self.divider.into(), + on: self.on.into(), + disabled: self.disabled.into(), + on_disabled: self.on_disabled.into(), + } + } + + /// helper for producing a component from a base color a neutral and an accent + pub fn colored_component(base: C, neutral: C, accent: C) -> Self { + let neutral = neutral.clone().into(); + let mut neutral_05 = neutral.clone(); + let mut neutral_10 = neutral.clone(); + let mut neutral_20 = neutral.clone(); + neutral_05.alpha = 0.05; + neutral_10.alpha = 0.1; + neutral_20.alpha = 0.2; + + let base: Srgba = base.into(); + let mut base_50 = base.clone(); + base_50.alpha = 0.5; + + let on_20 = neutral.clone(); + let mut on_50 = on_20.clone(); + + on_50.alpha = 0.5; + + Component { + base: base.clone().into(), + hover: over(neutral_10, base).into(), + pressed: over(neutral_20, base).into(), + selected: over(neutral_10, base).into(), + selected_text: accent.clone(), + divider: on_20.into(), + on: neutral.into(), + disabled: base_50.into(), + on_disabled: on_50.into(), + focus: accent, + } + } + + /// helper for producing a component color theme + pub fn component( + base: C, + component_state_overlay: C, + base_overlay: C, + base_overlay_alpha: f32, + accent: C, + on_component: C, + is_high_contrast: bool, + ) -> Self { + let component_state_overlay = component_state_overlay.clone().into(); + let mut component_state_overlay_10 = component_state_overlay.clone(); + let mut component_state_overlay_20 = component_state_overlay.clone(); + component_state_overlay_10.alpha = 0.1; + component_state_overlay_20.alpha = 0.2; + + let base = base.into(); + let mut base_overlay = base_overlay.into(); + base_overlay.alpha = base_overlay_alpha; + let base = over(base_overlay, base); + let mut base_50 = base.clone(); + base_50.alpha = 0.5; + + let mut on_20 = on_component.clone().into(); + let mut on_50 = on_20.clone(); + + on_20.alpha = 0.2; + on_50.alpha = 0.5; + + Component { + base: base.clone().into(), + hover: over(component_state_overlay_10, base).into(), + pressed: over(component_state_overlay_20, base).into(), + selected: over(component_state_overlay_10, base).into(), + selected_text: accent.clone(), + focus: accent.clone(), + divider: if is_high_contrast { + on_50.clone().into() + } else { + on_20.into() + }, + on: on_component.clone(), + disabled: base_50.into(), + on_disabled: on_50.into(), + } + } +} + +/// Derived theme element from a palette and constraints +#[derive(Debug)] +pub struct Derivation { + /// Derived theme element + pub derived: E, + /// Derivation errors (Failed constraints) + pub errors: Vec, +} + +pub(crate) enum ComponentType { + Background, + Primary, + Secondary, + Destructive, + Warning, + Success, + Accent, +} + +impl From<(CosmicPalette, ComponentType)> for Component +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from((p, t): (CosmicPalette, ComponentType)) -> Self { + match (p, t) { + (CosmicPalette::Dark(p), ComponentType::Background) => Self::component( + p.gray_1, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_8, + false, + ), + + (CosmicPalette::Dark(p), ComponentType::Primary) => Self::component( + p.gray_2, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_8, + false, + ), + + (CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component( + p.gray_3, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + false, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component( + p.gray_1, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component( + p.gray_2, + p.neutral_1, + p.neutral_10, + 0.08, + p.blue, + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component( + p.gray_3, + p.neutral_1, + p.neutral_10.clone(), + 0.08, + p.blue, + p.neutral_10, + true, + ), + + (CosmicPalette::Light(p), ComponentType::Background) => Component::component( + p.gray_1.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.75, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::Light(p), ComponentType::Primary) => Component::component( + p.gray_2.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.9, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::Light(p), ComponentType::Secondary) => Component::component( + p.gray_3.clone(), + p.neutral_1.clone(), + p.neutral_1, + 1.0, + p.blue.clone(), + p.neutral_8, + false, + ), + (CosmicPalette::HighContrastLight(p), ComponentType::Background) => { + Component::component( + p.gray_1.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.75, + p.blue.clone(), + p.neutral_9, + true, + ) + } + (CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component( + p.gray_2.clone(), + p.neutral_1.clone(), + p.neutral_1, + 0.9, + p.blue.clone(), + p.neutral_9, + true, + ), + (CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => { + Component::component( + p.gray_3.clone(), + p.neutral_1.clone(), + p.neutral_1, + 1.0, + p.blue.clone(), + p.neutral_9, + true, + ) + } + + (CosmicPalette::Dark(p), ComponentType::Destructive) + | (CosmicPalette::Light(p), ComponentType::Destructive) + | (CosmicPalette::HighContrastLight(p), ComponentType::Destructive) + | (CosmicPalette::HighContrastDark(p), ComponentType::Destructive) => { + Component::colored_component(p.red.clone(), p.neutral_1.clone(), p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Warning) + | (CosmicPalette::Light(p), ComponentType::Warning) + | (CosmicPalette::HighContrastLight(p), ComponentType::Warning) + | (CosmicPalette::HighContrastDark(p), ComponentType::Warning) => { + Component::colored_component(p.yellow.clone(), p.neutral_1, p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Success) + | (CosmicPalette::Light(p), ComponentType::Success) + | (CosmicPalette::HighContrastLight(p), ComponentType::Success) + | (CosmicPalette::HighContrastDark(p), ComponentType::Success) => { + Component::colored_component(p.green.clone(), p.neutral_1, p.blue.clone()) + } + + (CosmicPalette::Dark(p), ComponentType::Accent) + | (CosmicPalette::Light(p), ComponentType::Accent) + | (CosmicPalette::HighContrastDark(p), ComponentType::Accent) + | (CosmicPalette::HighContrastLight(p), ComponentType::Accent) => { + Component::colored_component(p.blue.clone(), p.neutral_1, p.blue.clone()) + } + } + } +} diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron new file mode 100644 index 0000000..92951bb --- /dev/null +++ b/cosmic-theme/src/model/light.ron @@ -0,0 +1,95 @@ +Light ( + ( + name: "cosmic-light", + blue: ( + c: "#00496D", + ), + red: ( + c: "#A0252B", + ), + green: ( + c: "#3B6E43", + ), + yellow: ( + c: "#966800", + ), + gray_1: ( + c: "#DEDEDE", + ), + gray_2: ( + c: "#E9E9E9", + ), + gray_3: ( + c: "#F4F4F4", + ), + neutral_1: ( + c: "#FFFFFF", + ), + neutral_2: ( + c: "#E4E4E4", + ), + neutral_3: ( + c: "#C9C9C9", + ), + neutral_4: ( + c: "#AEAEAE", + ), + neutral_5: ( + c: "#939393", + ), + neutral_6: ( + c: "#787878", + ), + neutral_7: ( + c: "#5D5D5D", + ), + neutral_8: ( + c: "#424242", + ), + neutral_9: ( + c: "#272727", + ), + neutral_10: ( + c: "#000000", + ), + ext_warm_grey: ( + c: "#9B8E8A", + ), + ext_orange: ( + c: "#FBB86C", + ), + ext_yellow: ( + c: "#F7E062", + ), + ext_blue: ( + c: "#6ACAD8", + ), + ext_purple: ( + c: "#D58CFF", + ), + ext_pink: ( + c: "#FF9CDD", + ), + ext_indigo: ( + c: "#95C4FC", + ), + accent_warm_grey: ( + c: "#ADA29E", + ), + accent_orange: ( + c: "#FFD7A1", + ), + accent_yellow: ( + c: "#FFF19E", + ), + accent_purple: ( + c: "#D58CFF", + ), + accent_pink: ( + c: "#FF9CDD", + ), + accent_indigo: ( + c: "#95C4FC", + ), + ) +) \ No newline at end of file diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs new file mode 100644 index 0000000..684df0b --- /dev/null +++ b/cosmic-theme/src/model/mod.rs @@ -0,0 +1,14 @@ +#[cfg(feature = "contrast-derivation")] +pub use constraint::*; +pub use cosmic_palette::*; +pub use derivation::*; +#[cfg(feature = "contrast-derivation")] +pub use selection::*; +pub use theme::*; +#[cfg(feature = "contrast-derivation")] +mod constraint; +mod cosmic_palette; +mod derivation; +#[cfg(feature = "contrast-derivation")] +mod selection; +mod theme; diff --git a/cosmic-theme/src/model/selection.rs b/cosmic-theme/src/model/selection.rs new file mode 100644 index 0000000..a4120c4 --- /dev/null +++ b/cosmic-theme/src/model/selection.rs @@ -0,0 +1,99 @@ +use palette::{named, IntoColor, Lch, Srgba}; +use std::convert::TryFrom; + +/// A Selection is a group of colors from which a cosmic palette can be derived +#[derive(Copy, Clone, Debug, Default)] +pub struct Selection { + /// base background container color + pub background: C, + /// base primary container color + pub primary_container: C, + /// base secondary container color + pub secondary_container: C, + /// base accent color + pub accent: C, + /// custom accent color (overrides base) + pub accent_fg: Option, + /// custom accent nav handle text color (overrides base) + pub accent_nav_handle_fg: Option, + /// base destructive element color + pub destructive: C, + /// base destructive element color + pub warning: C, + /// base destructive element color + pub success: C, +} + +// vector should be in order of most common +impl TryFrom> for Selection +where + C: Clone + From, +{ + type Error = anyhow::Error; + + fn try_from(mut colors: Vec) -> Result { + if colors.len() < 8 { + anyhow::bail!("length of inputted vector must be at least 8.") + } else { + let lch_colors: Vec = colors + .iter() + .map(|x| { + let srgba: Srgba = x.clone().into(); + srgba.color.into_format().into_color() + }) + .collect(); + + let red_lch: Lch = named::CRIMSON.into_format().into_color(); + let mut reddest_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - red_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[reddest_i].hue.to_degrees().abs() + - red_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + reddest_i = i; + } + } + + let yellow_lch: Lch = named::YELLOW.into_format().into_color(); + let mut yellow_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - yellow_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[yellow_i].hue.to_degrees().abs() + - yellow_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + yellow_i = i; + } + } + + let green_lch: Lch = named::GREEN.into_format().into_color(); + let mut green_i = 1; + for (i, c) in lch_colors[1..].iter().enumerate() { + let d_cur = (c.hue.to_degrees() - green_lch.hue.to_degrees()).abs(); + let reddest_d = (lch_colors[green_i].hue.to_degrees().abs() + - green_lch.hue.to_degrees().abs()) + .abs(); + if d_cur < reddest_d { + green_i = i; + } + } + + let red = colors.remove(reddest_i); + let green = colors.remove(green_i); + let yellow = colors.remove(yellow_i); + + Ok(Self { + background: colors[0].into(), + primary_container: colors[1].into(), + secondary_container: colors[3].into(), + accent: colors[2].into(), + accent_fg: Some(colors[2].into()), + accent_nav_handle_fg: Some(colors[2].into()), + destructive: red.into(), + warning: yellow.into(), + success: green.into(), + }) + } + } +} diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs new file mode 100644 index 0000000..f31b0cd --- /dev/null +++ b/cosmic-theme/src/model/theme.rs @@ -0,0 +1,431 @@ +use crate::{ + util::CssColor, Component, ComponentType, Container, ContainerType, CosmicPalette, + CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, +}; +use anyhow::Context; +use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; +use directories::{BaseDirsExt, ProjectDirsExt}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + fmt, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +/// Theme layer type +pub enum Layer { + /// Background layer + #[default] + Background, + /// Primary Layer + Primary, + /// Secondary Layer + Secondary, +} + +/// Cosmic Theme data structure with all colors and its name +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Theme { + /// name of the theme + pub name: String, + /// background element colors + pub background: Container, + /// primary element colors + pub primary: Container, + /// secondary element colors + pub secondary: Container, + /// accent element colors + pub accent: Component, + /// suggested element colors + pub success: Component, + /// destructive element colors + pub destructive: Component, + /// warning element colors + pub warning: Component, + /// palette + pub palette: CosmicPaletteInner, + /// is dark + pub is_dark: bool, + /// is high contrast + pub is_high_contrast: bool, +} + +impl CosmicConfigEntry for Theme { + fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { + let self_ = self.clone(); + // TODO do as transaction + let tx = config.transaction(); + + tx.set("name", self_.name)?; + tx.set("background", self_.background)?; + tx.set("primary", self_.primary)?; + tx.set("secondary", self_.secondary)?; + tx.set("accent", self_.accent)?; + tx.set("success", self_.success)?; + tx.set("destructive", self_.destructive)?; + tx.set("warning", self_.warning)?; + tx.set("palette", self_.palette)?; + tx.set("is_dark", self_.is_dark)?; + tx.set("is_high_contrast", self_.is_high_contrast)?; + + tx.commit() + } + + fn get_entry(config: &Config) -> Result, Self)> { + let mut default = Self::default(); + let mut errors = Vec::new(); + + match config.get::("name") { + Ok(name) => default.name = name, + Err(e) => errors.push(e), + } + match config.get::>("background") { + Ok(background) => default.background = background, + Err(e) => errors.push(e), + } + match config.get::>("primary") { + Ok(primary) => default.primary = primary, + Err(e) => errors.push(e), + } + match config.get::>("secondary") { + Ok(secondary) => default.secondary = secondary, + Err(e) => errors.push(e), + } + match config.get::>("accent") { + Ok(accent) => default.accent = accent, + Err(e) => errors.push(e), + } + match config.get::>("success") { + Ok(success) => default.success = success, + Err(e) => errors.push(e), + } + match config.get::>("destructive") { + Ok(destructive) => default.destructive = destructive, + Err(e) => errors.push(e), + } + match config.get::>("warning") { + Ok(warning) => default.warning = warning, + Err(e) => errors.push(e), + } + match config.get::>("palette") { + Ok(palette) => default.palette = palette, + Err(e) => errors.push(e), + } + match config.get::("is_dark") { + Ok(is_dark) => default.is_dark = is_dark, + Err(e) => errors.push(e), + } + match config.get::("is_high_contrast") { + Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast, + Err(e) => errors.push(e), + } + + if errors.is_empty() { + Ok(default) + } else { + Err((errors, default)) + } + } +} + +impl Default for Theme { + fn default() -> Self { + Theme::::dark_default().into_srgba() + } +} + +impl Default for Theme { + fn default() -> Self { + Self::dark_default() + } +} + +/// Trait for layered themes +pub trait LayeredTheme { + /// Set the layer of the theme + fn set_layer(&mut self, layer: Layer); +} + +// TODO better eq check +impl PartialEq for Theme +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Theme where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned +{ +} + +impl Theme { + /// version of the theme + pub fn version() -> u32 { + 1 + } + + /// id of the theme + pub fn id() -> &'static str { + NAME + } +} + +impl Theme +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + /// Convert the theme to a high-contrast variant + pub fn to_high_contrast(&self) -> Self { + todo!(); + } + + /// save the theme to the theme directory + pub fn save(&self) -> anyhow::Result<()> { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + let ron_name = format!("{}.ron", &self.name); + + if let Ok(p) = ron_dirs.place_config_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?; + } else { + anyhow::bail!("Failed to write RON theme."); + } + Ok(()) + } + + /// init the theme directory + pub fn init() -> anyhow::Result { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; + Ok(base_dirs.create_config_directory(ron_path)?) + } + + /// load a theme by name + pub fn load_from_name(name: &str) -> anyhow::Result { + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let ron_dirs = directories::ProjectDirs::from_path(ron_path) + .context("Failed to get project directories.")?; + + let ron_name = format!("{}.ron", name); + if let Some(p) = ron_dirs.find_config_file(ron_name) { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } else { + anyhow::bail!("Failed to write RON theme."); + } + } + + /// load a theme by path + pub fn load(p: &dyn AsRef) -> anyhow::Result { + let f = File::open(p)?; + Ok(ron::de::from_reader(f)?) + } + + // TODO convenient getter functions for each named color variable + /// get @accent_color + pub fn accent_color(&self) -> Srgba { + self.accent.base.clone().into() + } + /// get @success_color + pub fn success_color(&self) -> Srgba { + self.success.base.clone().into() + } + /// get @destructive_color + pub fn destructive_color(&self) -> Srgba { + self.destructive.base.clone().into() + } + /// get @warning_color + pub fn warning_color(&self) -> Srgba { + self.warning.base.clone().into() + } + + // Containers + /// get @bg_color + pub fn bg_color(&self) -> Srgba { + self.background.base.clone().into() + } + /// get @bg_component_color + pub fn bg_component_color(&self) -> Srgba { + self.background.component.base.clone().into() + } + /// get @primary_container_color + pub fn primary_container_color(&self) -> Srgba { + self.primary.base.clone().into() + } + /// get @primary_component_color + pub fn primary_component_color(&self) -> Srgba { + self.primary.component.base.clone().into() + } + /// get @secondary_container_color + pub fn secondary_container_color(&self) -> Srgba { + self.secondary.base.clone().into() + } + /// get @secondary_component_color + pub fn secondary_component_color(&self) -> Srgba { + self.secondary.component.base.clone().into() + } + + // Text + /// get @on_bg_color + pub fn on_bg_color(&self) -> Srgba { + self.background.on.clone().into() + } + /// get @on_bg_component_color + pub fn on_bg_component_color(&self) -> Srgba { + self.background.component.on.clone().into() + } + /// get @on_primary_color + pub fn on_primary_container_color(&self) -> Srgba { + self.primary.on.clone().into() + } + /// get @on_primary_component_color + pub fn on_primary_component_color(&self) -> Srgba { + self.primary.component.on.clone().into() + } + /// get @on_secondary_color + pub fn on_secondary_container_color(&self) -> Srgba { + self.secondary.on.clone().into() + } + /// get @on_secondary_component_color + pub fn on_secondary_component_color(&self) -> Srgba { + self.secondary.component.on.clone().into() + } + /// get @accent_text_color + pub fn accent_text_color(&self) -> Srgba { + self.accent.base.clone().into() + } + /// get @success_text_color + pub fn success_text_color(&self) -> Srgba { + self.success.base.clone().into() + } + /// get @warning_text_color + pub fn warning_text_color(&self) -> Srgba { + self.warning.base.clone().into() + } + /// get @destructive_text_color + pub fn destructive_text_color(&self) -> Srgba { + self.destructive.base.clone().into() + } + /// get @on_accent_color + pub fn on_accent_color(&self) -> Srgba { + self.accent.on.clone().into() + } + /// get @on_success_color + pub fn on_success_color(&self) -> Srgba { + self.success.on.clone().into() + } + /// get @oon_warning_color + pub fn on_warning_color(&self) -> Srgba { + self.warning.on.clone().into() + } + /// get @on_destructive_color + pub fn on_destructive_color(&self) -> Srgba { + self.destructive.on.clone().into() + } + + // Borders and Dividers + /// get @bg_divider + pub fn bg_divider(&self) -> Srgba { + self.background.divider.clone().into() + } + /// get @bg_component_divider + pub fn bg_component_divider(&self) -> Srgba { + self.background.component.divider.clone().into() + } + /// get @primary_container_divider + pub fn primary_container_divider(&self) -> Srgba { + self.primary.divider.clone().into() + } + /// get @primary_component_divider + pub fn primary_component_divider(&self) -> Srgba { + self.primary.component.divider.clone().into() + } + /// get @secondary_container_divider + pub fn secondary_container_divider(&self) -> Srgba { + self.secondary.divider.clone().into() + } + /// get @secondary_component_divider + pub fn secondary_component_divider(&self) -> Srgba { + self.secondary.component.divider.clone().into() + } + + /// get @window_header_bg + pub fn window_header_bg(&self) -> Srgba { + self.background.base.clone().into() + } +} + +impl Theme { + /// get the built in light theme + pub fn light_default() -> Self { + LIGHT_PALETTE.clone().into() + } + + /// get the built in dark theme + pub fn dark_default() -> Self { + DARK_PALETTE.clone().into() + } + + /// get the built in high contrast dark theme + pub fn high_contrast_dark_default() -> Self { + CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into() + } + + /// get the built in high contrast light theme + pub fn high_contrast_light_default() -> Self { + CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into() + } + + /// convert to srgba + pub fn into_srgba(self) -> Theme { + Theme { + name: self.name, + background: self.background.into_srgba(), + primary: self.primary.into_srgba(), + secondary: self.secondary.into_srgba(), + accent: self.accent.into_srgba(), + success: self.success.into_srgba(), + destructive: self.destructive.into_srgba(), + warning: self.warning.into_srgba(), + palette: self.palette.into(), + is_dark: self.is_dark, + is_high_contrast: self.is_high_contrast, + } + } +} + +impl From> for Theme +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn from(p: CosmicPalette) -> Self { + let is_dark = p.is_dark(); + let is_high_contrast = p.is_high_contrast(); + Self { + name: p.name().to_string(), + background: (p.clone(), ContainerType::Background).into(), + primary: (p.clone(), ContainerType::Primary).into(), + secondary: (p.clone(), ContainerType::Secondary).into(), + accent: (p.clone(), ComponentType::Accent).into(), + success: (p.clone(), ComponentType::Success).into(), + destructive: (p.clone(), ComponentType::Destructive).into(), + warning: (p.clone(), ComponentType::Warning).into(), + palette: match p { + CosmicPalette::Dark(p) => p.into(), + CosmicPalette::Light(p) => p.into(), + CosmicPalette::HighContrastLight(p) => p.into(), + CosmicPalette::HighContrastDark(p) => p.into(), + }, + is_dark, + is_high_contrast, + } + } +} diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs new file mode 100644 index 0000000..43fb498 --- /dev/null +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -0,0 +1,187 @@ +use crate::{ + model::{Accent, Container, ContainerType, Destructive, Widget}, + Hex, Theme, NAME, +}; +use anyhow::{bail, Result}; +use palette::Srgba; +use serde::{de::DeserializeOwned, Serialize}; +use std::{fmt, fs::File, io::prelude::*, path::PathBuf}; + +pub(crate) const CSS_DIR: &'static str = "css"; +pub(crate) const THEME_DIR: &'static str = "themes"; + +/// Trait for outputting the Theme as Gtk4CSS +pub trait Gtk4Output { + /// turn the theme into css + fn as_css(&self) -> String; + /// Serialize the theme as RON and write the CSS to the appropriate directories + /// Should be written in the XDG data directory for cosmic-theme + fn write(&self) -> Result<()>; +} + +impl Gtk4Output for Theme +where + C: Clone + + fmt::Debug + + Default + + Into + + Into + + From + + Serialize + + DeserializeOwned, +{ + fn as_css(&self) -> String { + let Self { + background, + primary, + secondary, + accent, + destructive, + .. + } = self; + let mut css = String::new(); + + css.push_str(&background.as_css()); + css.push_str(&primary.as_css()); + css.push_str(&secondary.as_css()); + css.push_str(&accent.as_css()); + css.push_str(&destructive.as_css()); + + css + } + + fn write(&self) -> Result<()> { + // TODO sass -> css + let ron_str = ron::ser::to_string_pretty(self, Default::default())?; + let css_str = self.as_css(); + + let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect(); + let css_path: PathBuf = [NAME, CSS_DIR].iter().collect(); + + let ron_dirs = xdg::BaseDirectories::with_prefix(ron_path)?; + let css_dirs = xdg::BaseDirectories::with_prefix(css_path)?; + + let ron_name = format!("{}.ron", &self.name); + let css_name = format!("{}.css", &self.name); + + if let Ok(p) = ron_dirs.place_data_file(ron_name) { + let mut f = File::create(p)?; + f.write_all(ron_str.as_bytes())?; + } else { + bail!("Failed to write RON theme.") + } + + if let Ok(p) = css_dirs.place_data_file(css_name) { + let mut f = File::create(p)?; + f.write_all(css_str.as_bytes())?; + } else { + bail!("Failed to write RON theme.") + } + + Ok(()) + } +} + +/// Trait for converting theme data into gtk4 CSS +pub trait AsGtk4Css +where + C: Copy + Into + From, +{ + /// function for converting theme data into gtk4 CSS + fn as_css(&self) -> String; +} + +impl AsGtk4Css for Container +where + C: Copy + Clone + fmt::Debug + Default + Into + From + fmt::Display, +{ + fn as_css(&self) -> String { + let Self { + prefix, + container, + container_component, + container_divider, + container_fg, + .. + } = self; + + let prefix_lower = match prefix { + ContainerType::Background => "background", + ContainerType::Primary => "primary", + ContainerType::Secondary => "secondary", + }; + let component = widget_gtk4_css(prefix_lower, container_component); + + format!( + r#" +@define-color {prefix_lower}_container #{{{container}}}; +@define-color {prefix_lower}_container_divider #{{{container_divider}}}; +@define-color {prefix_lower}_container_fg #{{{container_fg}}}; +{component} +"# + ) + } +} + +impl AsGtk4Css for Accent +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_css(&self) -> String { + let Accent { + accent, + accent_fg, + accent_nav_handle_fg, + suggested, + } = self; + let suggested = widget_gtk4_css("suggested", suggested); + + format!( + r#" +@define-color accent #{{{accent}}}; +@define-color accent_fg #{{{accent_fg}}}; +@define-color accent_nav_handle_fg #{{{accent_nav_handle_fg}}}; +{suggested} +"# + ) + } +} + +impl AsGtk4Css for Destructive +where + C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, +{ + fn as_css(&self) -> String { + let Destructive { destructive } = &self; + widget_gtk4_css("destructive", destructive) + } +} + +fn widget_gtk4_css( + prefix: &str, + Widget { + base, + hover, + pressed, + focused, + divider, + text, + text_opacity_80, + disabled, + disabled_fg, + }: &Widget, +) -> String { + format!( + r#" +@define-color {prefix}_widget_base #{{{base}}}; +@define-color {prefix}_widget_hover #{{{hover}}}; +@define-color {prefix}_widget_pressed #{{{pressed}}}; +@define-color {prefix}_widget_focused #{{{focused}}}; +@define-color {prefix}_widget_divider #{{{divider}}}; +@define-color {prefix}_widget_fg #{{{text}}}; +@define-color {prefix}_widget_fg_opacity_80 #{{{text_opacity_80}}}; +@define-color {prefix}_widget_disabled #{{{disabled}}}; +@define-color {prefix}_widget_disabled_fg #{{{disabled_fg}}}; +"# + ) +} diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs new file mode 100644 index 0000000..3130762 --- /dev/null +++ b/cosmic-theme/src/output/mod.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "gtk4-theme")] +/// Module for outputting the Cosmic gtk4 theme type as CSS +pub mod gtk4_output; +#[cfg(feature = "gtk4-theme")] +pub use gtk4_output::*; + +#[cfg(feature = "ron-serialization")] +pub use ron::*; diff --git a/cosmic-theme/src/theme_provider/mod.rs b/cosmic-theme/src/theme_provider/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cosmic-theme/src/theme_provider/mod.rs @@ -0,0 +1 @@ + diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs new file mode 100644 index 0000000..afbf98c --- /dev/null +++ b/cosmic-theme/src/util.rs @@ -0,0 +1,59 @@ +use csscolorparser::Color; +use palette::Srgba; +use serde::{Deserialize, Serialize}; + +/// utility wrapper for serializing and deserializing colors with arbitrary CSS +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct CssColor { + c: Color, +} + +impl From for CssColor { + fn from(c: Srgba) -> Self { + Self { + c: Color { + r: c.red as f64, + g: c.green as f64, + b: c.blue as f64, + a: c.alpha as f64, + }, + } + } +} + +impl Into for CssColor { + fn into(self) -> Srgba { + Srgba::new( + self.c.r as f32, + self.c.g as f32, + self.c.b as f32, + self.c.a as f32, + ) + } +} + +/// straight alpha "A over B" operator on non-linear srgba +pub fn over, B: Into>(a: A, b: B) -> Srgba { + let a = a.into(); + let b = b.into(); + let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0); + let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + + Srgba::new(o_r, o_g, o_b, o_a) +} + +fn alpha_over(a: f32, b: f32) -> f32 { + a + b * (1.0 - a) +} + +fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 { + a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha +} diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index 277ec5f..f6d3da3 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] } +libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "tiny_skia", "a11y"] } diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 920428b..1665118 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -2,19 +2,18 @@ // SPDX-License-Identifier: MPL-2.0 use cosmic::{ - iced::{self, wayland::window::set_mode_window, Alignment, Application, Command, Length}, + iced::{self, wayland::window::set_mode_window, Application, Command, Length}, iced::{ wayland::window::{start_drag_window, toggle_maximize}, - widget::{ - column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, - }, + widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider}, + window, Color, }, - iced_native::window, + iced_style::application, theme::{self, Theme}, widget::{ button, header_bar, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, - scrollable, segmented_button, settings, toggler, IconSource, + scrollable, segmented_button, segmented_selection, settings, toggler, IconSource, }, Element, ElementExt, }; @@ -118,6 +117,7 @@ pub struct Window { show_maximize: bool, exit: bool, rectangle_tracker: Option>, + pub selection: segmented_button::SingleSelectModel, } impl Window { @@ -176,6 +176,8 @@ pub enum Message { InputChanged, Rectangle(RectangleUpdate), NavBar(segmented_button::Entity), + Ignore, + Selection(segmented_button::Entity), } impl Application for Window { @@ -189,6 +191,11 @@ impl Application for Window { .nav_bar_toggled(true) .show_maximize(true) .show_minimize(true); + window.selection = segmented_button::Model::builder() + .insert(|b| b.text("Choice A").activate()) + .insert(|b| b.text("Choice B")) + .insert(|b| b.text("Choice C")) + .build(); window.slider_value = 50.0; // window.theme = Theme::Light; window.pick_list_selected = Some("Option 1"); @@ -240,9 +247,9 @@ impl Application for Window { Message::ToggleNavBarCondensed => { self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed } - Message::Drag => return start_drag_window(window::Id::new(0)), - Message::Minimize => return set_mode_window(window::Id::new(0), window::Mode::Hidden), - Message::Maximize => return toggle_maximize(window::Id::new(0)), + Message::Drag => return start_drag_window(window::Id(0)), + Message::Minimize => return set_mode_window(window::Id(0), window::Mode::Hidden), + Message::Maximize => return toggle_maximize(window::Id(0)), Message::RowSelected(row) => println!("Selected row {row}"), Message::InputChanged => {} Message::Rectangle(r) => match r { @@ -253,6 +260,8 @@ impl Application for Window { self.rectangle_tracker.replace(t); } }, + Message::Ignore => {} + Message::Selection(key) => self.selection.activate(key), } Command::none() @@ -302,7 +311,7 @@ impl Application for Window { widgets.push(nav_bar.debug(self.debug)); } - if !(self.is_condensed() && nav_bar_toggled) { + if !nav_bar_toggled { let secondary = button(ButtonTheme::Secondary) .text("Secondary") .on_press(Message::ButtonPressed); @@ -363,20 +372,23 @@ impl Application for Window { .add(settings::item( "Slider", slider(0.0..=100.0, self.slider_value, Message::SliderChanged) - .width(Length::Units(250)), + .width(Length::Fixed(250.0)), )) .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Units(250)) - .height(Length::Units(4)), + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), + )) + .add(settings::item( + "Segmented Button", + segmented_selection::horizontal(&self.selection) + .on_activate(Message::Selection), )) .into(), ]) .into(); - let mut widgets: Vec> = Vec::with_capacity(2); - widgets.push( scrollable(row![ horizontal_space(Length::Fill), @@ -391,6 +403,7 @@ impl Application for Window { .padding([0, 8, 8, 8]) .width(Length::Fill) .height(Length::Fill) + .style(theme::Container::Background) .into(); column(vec![header, content]).into() @@ -408,6 +421,13 @@ impl Application for Window { Message::Close } fn subscription(&self) -> iced::Subscription { - rectangle_tracker_subscription(0).map(|(i, e)| Message::Rectangle(e)) + rectangle_tracker_subscription(0).map(|(_, e)| Message::Rectangle(e)) + } + + fn style(&self) -> ::Style { + cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance { + background_color: Color::TRANSPARENT, + text_color: theme.cosmic().on_bg_color().into(), + })) } } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index db0cb60..feb41ae 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,6 +8,8 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.13.0" -libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_softbuffer"] } +libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_tiny_skia", "a11y"] } once_cell = "1.15" -slotmap = "1.0.6" \ No newline at end of file +slotmap = "1.0.6" +env_logger = "0.10" +log = "0.4.17" diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index 1d1f2bf..5700a59 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -4,9 +4,15 @@ use cosmic::{iced::Application, settings}; mod window; +use env_logger::Env; pub use window::*; pub fn main() -> cosmic::iced::Result { + let env = Env::default() + .filter_or("MY_LOG_LEVEL", "info") + .write_style_or("MY_LOG_STYLE", "always"); + + env_logger::init_from_env(env); settings::set_default_icon_theme("Pop"); let mut settings = settings(); settings.window.min_size = Some((600, 300)); diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 50b5490..d5977a1 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -1,25 +1,34 @@ /// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 use cosmic::{ - iced::widget::{self, button, column, container, horizontal_space, row, text}, + cosmic_config::config_subscription, + font::load_fonts, iced::{self, Application, Command, Length, Subscription}, - iced_native::{subscription, window}, - iced_winit::window::{close, drag, minimize, toggle_maximize}, + iced::{ + subscription, + widget::{self, column, container, horizontal_space, row, text}, + window::{self, close, drag, minimize, toggle_maximize}, + }, keyboard_nav, - theme::{self, Theme, COSMIC_DARK, COSMIC_LIGHT}, + theme::{self, CosmicTheme, CosmicThemeCss, Theme}, widget::{ header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings, warning, IconSource, }, Element, ElementExt, }; -use once_cell::sync::Lazy; +use log::error; use std::{ - sync::atomic::{AtomicU32, Ordering}, + borrow::Cow, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, vec, }; -static BTN: Lazy = Lazy::new(button::Id::unique); +// XXX The use of button is removed because it assigns the same ID to multiple buttons, causing a crash when a11y is enabled... +// static BTN: Lazy = Lazy::new(|| id::Id::new("BTN")); mod bluetooth; @@ -151,6 +160,7 @@ pub struct Window { warning_message: String, scale_factor: f64, scale_factor_string: String, + system_theme: Arc, } impl Window { @@ -194,6 +204,8 @@ pub enum Message { ToggleNavBar, ToggleNavBarCondensed, ToggleWarning, + FontsLoaded, + SystemTheme(CosmicTheme), } impl From for Message { @@ -237,7 +249,7 @@ impl Window { )) .padding(0) .style(theme::Button::Link) - .id(BTN.clone()) + // .id(BTN.clone()) .on_press(Message::from(page)), row!( text(sub_page.title()).size(30), @@ -282,7 +294,7 @@ impl Window { .padding(0) .style(theme::Button::Transparent) .on_press(Message::from(sub_page.into_page())) - .id(BTN.clone()) + // .id(BTN.clone()) .into() } @@ -341,7 +353,7 @@ impl Application for Window { window.insert_page(Page::Accessibility); window.insert_page(Page::Applications); - (window, Command::none()) + (window, load_fonts().map(|_| Message::FontsLoaded)) } fn title(&self) -> String { @@ -371,6 +383,16 @@ impl Application for Window { Subscription::batch(vec![ window_break.map(|_| Message::CondensedViewToggle), keyboard_nav::subscription().map(Message::KeyboardNav), + config_subscription::<_, CosmicThemeCss>(0, Cow::from("com.system76.CosmicTheme"), 1) + .map(|(_, update)| match update { + Ok(t) => Message::SystemTheme(t.into_srgba()), + Err((errors, t)) => { + for error in errors { + error!("{:?}", error); + } + Message::SystemTheme(t.into_srgba()) + } + }), ]) } @@ -391,7 +413,13 @@ impl Application for Window { Some(demo::Output::Debug(debug)) => self.debug = debug, Some(demo::Output::ScalingFactor(factor)) => self.set_scale_factor(factor), Some(demo::Output::ThemeChanged(theme)) => { - self.theme = theme; + self.theme = match theme { + demo::ThemeVariant::Light => Theme::light(), + demo::ThemeVariant::Dark => Theme::dark(), + demo::ThemeVariant::HighContrastDark => Theme::dark_hc(), + demo::ThemeVariant::HighContrastLight => Theme::light_hc(), + demo::ThemeVariant::Custom => Theme::custom(self.system_theme.clone()), + }; } Some(demo::Output::ToggleWarning) => self.toggle_warning(), None => (), @@ -405,10 +433,10 @@ impl Application for Window { Message::ToggleNavBarCondensed => { self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed } - Message::Drag => return drag(window::Id::new(0)), - Message::Close => return close(window::Id::new(0)), - Message::Minimize => return minimize(window::Id::new(0), true), - Message::Maximize => return toggle_maximize(window::Id::new(0)), + Message::Drag => return drag(), + Message::Close => return close(), + Message::Minimize => return minimize(true), + Message::Maximize => return toggle_maximize(), Message::InputChanged => {} @@ -420,6 +448,10 @@ impl Application for Window { _ => (), }, Message::ToggleWarning => self.toggle_warning(), + Message::FontsLoaded => {} + Message::SystemTheme(t) => { + self.system_theme = Arc::new(t); + } } ret } @@ -545,17 +577,21 @@ impl Application for Window { .padding([0, 8, 8, 8]) .width(Length::Fill) .height(Length::Fill) + .style(theme::Container::Background) .into(); let warning = warning(&self.warning_message) .on_close(Message::ToggleWarning) .into(); if self.show_warning { - column(vec![ + column![ header, - warning, - iced::widget::vertical_space(Length::Units(12)).into(), - content, - ]) + container(column(vec![ + warning, + iced::widget::vertical_space(Length::Fixed(12.0)).into(), + content, + ])) + .style(theme::Container::Background) + ] .into() } else { column(vec![header, content]).into() @@ -567,6 +603,6 @@ impl Application for Window { } fn theme(&self) -> Theme { - self.theme + self.theme.clone() } } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index acfad0b..243ba07 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -1,9 +1,9 @@ use apply::Apply; use cosmic::{ cosmic_theme, - iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider, text, text_input}, - iced::{Alignment, Length}, - theme::{self, Button as ButtonTheme, Theme}, + iced::widget::{checkbox, column, pick_list, progress_bar, radio, slider, text, text_input}, + iced::{id, Alignment, Length}, + theme::{self, Button as ButtonTheme, Theme, ThemeType}, widget::{ button, container, icon, segmented_button, segmented_selection, settings, spin_button, toggler, view_switcher, @@ -15,6 +15,33 @@ use once_cell::sync::Lazy; use super::{Page, Window}; +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] +pub enum ThemeVariant { + Light, + Dark, + HighContrastDark, + HighContrastLight, + Custom, +} + +impl From<&ThemeType> for ThemeVariant { + fn from(theme: &ThemeType) -> Self { + match theme { + ThemeType::Light => ThemeVariant::Light, + ThemeType::Dark => ThemeVariant::Dark, + ThemeType::HighContrastDark => ThemeVariant::HighContrastDark, + ThemeType::HighContrastLight => ThemeVariant::HighContrastLight, + ThemeType::Custom(_) => ThemeVariant::Custom, + } + } +} + +impl From for ThemeVariant { + fn from(theme: ThemeType) -> Self { + ThemeVariant::from(&theme) + } +} + pub enum DemoView { TabA, TabB, @@ -29,7 +56,7 @@ pub enum MultiOption { OptionD, OptionE, } -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); +static INPUT_ID: Lazy = Lazy::new(id::Id::unique); #[derive(Clone, Debug)] pub enum Message { @@ -44,7 +71,7 @@ pub enum Message { Selection(segmented_button::Entity), SliderChanged(f32), SpinButton(spin_button::Message), - ThemeChanged(Theme), + ThemeChanged(ThemeVariant), ToggleWarning, TogglerToggled(bool), ViewSwitcher(segmented_button::Entity), @@ -54,7 +81,7 @@ pub enum Message { pub enum Output { Debug(bool), ScalingFactor(f32), - ThemeChanged(Theme), + ThemeChanged(ThemeVariant), ToggleWarning, } @@ -151,20 +178,21 @@ impl State { pub(super) fn view<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { let choose_theme = [ - Theme::light(), - Theme::dark(), - Theme::light_hc(), - Theme::dark_hc(), + ThemeVariant::Light, + ThemeVariant::Dark, + ThemeVariant::HighContrastLight, + ThemeVariant::HighContrastLight, + ThemeVariant::Custom, ] - .iter() + .into_iter() .fold( - row![].spacing(10).align_items(Alignment::Center), + column![].spacing(10).align_items(Alignment::Center), |row, theme| { row.push(radio( - format!("{:?}", theme.theme_type), - *theme, - if window.theme == *theme { - Some(*theme) + format!("{:?}", theme), + theme, + if ThemeVariant::from(&window.theme.theme_type) == theme { + Some(theme) } else { None }, @@ -253,13 +281,14 @@ impl State { .add(settings::item( "Slider", slider(0.0..=100.0, self.slider_value, Message::SliderChanged) - .width(Length::Units(250)), + .width(Length::Fixed(250.0)) + .height(38), )) .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Units(250)) - .height(Length::Units(4)), + .width(Length::Fixed(250.0)) + .height(Length::Fixed(4.0)), )) .add(settings::item_row(vec![checkbox( "Checkbox", @@ -401,8 +430,8 @@ impl State { text_input( "Type to search apps or type “?” for more options...", &self.entry_value, - Message::InputChanged, ) + .on_input(Message::InputChanged) // .on_submit(Message::Activate(None)) .padding(8) .size(20) diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 7f3ddad..f20722f 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -219,10 +219,10 @@ impl State { for image_path in chunk.iter() { image_row.push(if image_path.ends_with(".svg") { svg(svg::Handle::from_path(image_path)) - .width(Length::Units(150)) + .width(Length::Fixed(150.0)) .into() } else { - image(image_path).width(Length::Units(150)).into() + image(image_path).width(Length::Fixed(150.0)).into() }); } image_column.push(row(image_row).spacing(16).into()); @@ -234,7 +234,7 @@ impl State { horizontal_space(Length::Fill), container( image("/usr/share/backgrounds/pop/kate-hazen-COSMIC-desktop-wallpaper.png") - .width(Length::Units(300)) + .width(Length::Fixed(300.0)) ) .padding(4) .style(theme::Container::Background), diff --git a/examples/cosmic/src/window/editor.rs b/examples/cosmic/src/window/editor.rs index c1423e2..e272050 100644 --- a/examples/cosmic/src/window/editor.rs +++ b/examples/cosmic/src/window/editor.rs @@ -1,7 +1,6 @@ use apply::Apply; use cosmic::iced::widget::{horizontal_space, row, scrollable}; -use cosmic::iced::Length; -use cosmic::iced_winit::Alignment; +use cosmic::iced::{Alignment, Length}; use cosmic::widget::{button, segmented_button, view_switcher}; use cosmic::{theme, Element}; use slotmap::Key; diff --git a/iced b/iced index a9d0b3d..2a3b577 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a9d0b3d84555d1852d5d3a73edbf32e014dff20b +Subproject commit 2a3b5770b9f9c700d4aeb6398ab6c917024ce6cc diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 60e4fc1..585695d 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,16 +1,16 @@ -use cosmic_panel_config::{PanelAnchor, PanelSize}; +use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use iced::{ alignment::{Horizontal, Vertical}, wayland::InitialSurface, widget::{self, Container}, Color, Element, Length, Rectangle, Settings, }; -use iced_core::BorderRadius; -use iced_native::command::platform_specific::wayland::{ +use iced_core::layout::Limits; +use iced_style::{button::StyleSheet, container::Appearance}; +use iced_widget::runtime::command::platform_specific::wayland::{ popup::{SctkPopupSettings, SctkPositioner}, window::SctkWindowSettings, }; -use iced_style::{button::StyleSheet, container::Appearance}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; use crate::{theme::Button, Renderer}; @@ -19,14 +19,15 @@ pub use cosmic_panel_config; const APPLET_PADDING: u32 = 8; +#[must_use] pub fn applet_button_theme() -> Button { Button::Custom { active: Box::new(|t| iced_style::button::Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: 0.0, ..t.active(&Button::Text) }), hover: Box::new(|t| iced_style::button::Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: 0.0, ..t.hovered(&Button::Text) }), } @@ -36,6 +37,8 @@ pub fn applet_button_theme() -> Button { pub struct CosmicAppletHelper { pub size: Size, pub anchor: PanelAnchor, + pub background: CosmicPanelBackground, + pub output_name: String, } #[derive(Clone, Debug)] @@ -51,18 +54,24 @@ impl Default for CosmicAppletHelper { size: Size::PanelSize( std::env::var("COSMIC_PANEL_SIZE") .ok() - .and_then(|size| size.parse::().ok()) + .and_then(|size| ron::from_str(size.as_str()).ok()) .unwrap_or(PanelSize::S), ), anchor: std::env::var("COSMIC_PANEL_ANCHOR") .ok() - .and_then(|size| size.parse::().ok()) + .and_then(|size| ron::from_str(size.as_str()).ok()) .unwrap_or(PanelAnchor::Top), + background: std::env::var("COSMIC_PANEL_BACKGROUND") + .ok() + .and_then(|size| ron::from_str(size.as_str()).ok()) + .unwrap_or(CosmicPanelBackground::ThemeDefault), + output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(), } } } impl CosmicAppletHelper { + #[must_use] pub fn suggested_size(&self) -> (u16, u16) { match &self.size { Size::PanelSize(size) => match size { @@ -87,18 +96,20 @@ impl CosmicAppletHelper { } #[must_use] + #[allow(clippy::cast_precision_loss)] pub fn window_settings_with_flags(&self, flags: F) -> Settings { let (width, height) = self.suggested_size(); let width = u32::from(width); let height = u32::from(height); Settings { initial_surface: InitialSurface::XdgWindow(SctkWindowSettings { - iced_settings: iced_native::window::Settings { - size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2), - min_size: Some((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)), - max_size: Some((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)), - ..Default::default() - }, + size: (width + APPLET_PADDING * 2, height + APPLET_PADDING * 2), + size_limits: Limits::NONE + .min_height(height as f32 + APPLET_PADDING as f32 * 2.0) + .max_height(height as f32 + APPLET_PADDING as f32 * 2.0) + .min_width(width as f32 + APPLET_PADDING as f32 * 2.0) + .max_width(width as f32 + APPLET_PADDING as f32 * 2.0), + resizable: None, ..Default::default() }), ..crate::settings_with_flags(flags) @@ -124,7 +135,7 @@ impl CosmicAppletHelper { &self, content: impl Into>, ) -> Container<'a, Message, Renderer> { - let (valign, halign) = match self.anchor { + let (vertical_align, horizontal_align) = match self.anchor { PanelAnchor::Left => (Vertical::Center, Horizontal::Left), PanelAnchor::Right => (Vertical::Center, Horizontal::Right), PanelAnchor::Top => (Vertical::Top, Horizontal::Center), @@ -135,22 +146,23 @@ impl CosmicAppletHelper { crate::theme::Container::custom(|theme| Appearance { text_color: Some(theme.cosmic().background.on.into()), background: Some(Color::from(theme.cosmic().background.base).into()), - border_radius: 12.0, + border_radius: 12.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }), )) .width(Length::Shrink) .height(Length::Shrink) - .align_x(halign) - .align_y(valign) + .align_x(horizontal_align) + .align_y(vertical_align) } #[must_use] + #[allow(clippy::cast_possible_wrap)] pub fn get_popup_settings( &self, - parent: iced_native::window::Id, - id: iced_native::window::Id, + parent: iced_core::window::Id, + id: iced_core::window::Id, size: Option<(u32, u32)>, width_padding: Option, height_padding: Option, diff --git a/src/executor/multi.rs b/src/executor/multi.rs index a6f0731..18cb823 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -7,7 +7,7 @@ use std::future::Future; pub struct Executor(tokio::runtime::Runtime); #[cfg(feature = "tokio")] -impl iced_native::Executor for Executor { +impl iced::Executor for Executor { fn new() -> Result { Ok(Self( tokio::runtime::Builder::new_multi_thread() diff --git a/src/executor/single.rs b/src/executor/single.rs index e293fc1..1ffa052 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -7,7 +7,7 @@ use std::future::Future; pub struct Executor(tokio::runtime::Runtime); #[cfg(feature = "tokio")] -impl iced_native::Executor for Executor { +impl iced::Executor for Executor { fn new() -> Result { // Current thread executor requires calling `block_on` to actually run // futures. Main thread is busy with things other than running futures, diff --git a/src/font.rs b/src/font.rs index e3f9698..8a2a4cc 100644 --- a/src/font.rs +++ b/src/font.rs @@ -2,18 +2,24 @@ // SPDX-License-Identifier: MPL-2.0 pub use iced::Font; - -pub const FONT: Font = Font::External { - name: "Fira Sans Regular", - bytes: include_bytes!("../res/Fira/FiraSans-Regular.otf"), +use iced::{ + font::{load, Error}, + Command, }; -pub const FONT_LIGHT: Font = Font::External { - name: "Fira Sans Light", - bytes: include_bytes!("../res/Fira/FiraSans-Light.otf"), -}; +pub const FONT: Font = Font::with_name("Fira Sans Regular"); +pub const FONT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Regular.otf"); -pub const FONT_SEMIBOLD: Font = Font::External { - name: "Fira Sans SemiBold", - bytes: include_bytes!("../res/Fira/FiraSans-SemiBold.otf"), -}; +pub const FONT_LIGHT: Font = Font::with_name("Fira Sans Light"); +pub const FONT_LIGHT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Light.otf"); + +pub const FONT_SEMIBOLD: Font = Font::with_name("Fira Sans SemiBold"); +pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); + +pub fn load_fonts() -> Command> { + Command::batch(vec![ + load(FONT_DATA), + load(FONT_LIGHT_DATA), + load(FONT_SEMIBOLD_DATA), + ]) +} diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 814bcc4..af4703e 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -3,7 +3,7 @@ use iced::{ keyboard::{self, KeyCode}, mouse, subscription, Command, Event, Subscription, }; -use iced_native::widget::{operation, Id, Operation}; +use iced_core::widget::{operation, Id, Operation}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Message { @@ -14,7 +14,6 @@ pub enum Message { Search, } -#[must_use] pub fn subscription() -> Subscription { subscription::events_with(|event, status| match (event, status) { // Focus @@ -61,7 +60,6 @@ pub fn subscription() -> Subscription { } /// Unfocuses any actively-focused widget. -#[must_use] pub fn unfocus() -> Command { Command::::widget(unfocus_operation()) } diff --git a/src/lib.rs b/src/lib.rs index db47026..aa04d56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,16 +3,18 @@ #![allow(clippy::module_name_repetitions)] +pub use cosmic_config; pub use cosmic_theme; pub use iced; -pub use iced_lazy; -pub use iced_native; +pub use iced_runtime; #[cfg(feature = "wayland")] pub use iced_sctk; pub use iced_style; +pub use iced_widget; #[cfg(feature = "winit")] pub use iced_winit; - +#[cfg(feature = "wayland")] +pub use sctk; #[cfg(feature = "applet")] pub mod applet; pub mod executor; diff --git a/src/settings.rs b/src/settings.rs index 7fb35a2..9f9b20f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -27,11 +27,8 @@ pub fn settings() -> iced::Settings { #[must_use] pub fn settings_with_flags(flags: Flags) -> iced::Settings { iced::Settings { - default_font: match font::FONT { - iced::Font::Default => None, - iced::Font::External { bytes, .. } => Some(bytes), - }, - default_text_size: 18, + default_font: font::FONT, + default_text_size: 18.0, ..iced::Settings::with_flags(flags) } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 51462d6..2fef40a 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,12 +7,13 @@ mod segmented_button; use std::hash::Hash; use std::hash::Hasher; use std::rc::Rc; +use std::sync::Arc; pub use self::segmented_button::SegmentedButton; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; -use iced_core::BorderRadius; +use iced_core::renderer::BorderRadius; use iced_style::application; use iced_style::button; use iced_style::checkbox; @@ -25,18 +26,18 @@ use iced_style::radio; use iced_style::rule; use iced_style::scrollable; use iced_style::slider; +use iced_style::slider::Rail; use iced_style::svg; -use iced_style::text; use iced_style::text_input; use iced_style::toggler; use iced_core::{Background, Color}; use palette::Srgba; -type CosmicColor = ::palette::rgb::Srgba; -type CosmicComponent = cosmic_theme::Component; -type CosmicTheme = cosmic_theme::Theme; -type CosmicThemeCss = cosmic_theme::Theme; +pub type CosmicColor = ::palette::rgb::Srgba; +pub type CosmicComponent = cosmic_theme::Component; +pub type CosmicTheme = cosmic_theme::Theme; +pub type CosmicThemeCss = cosmic_theme::Theme; lazy_static::lazy_static! { pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba(); @@ -57,16 +58,17 @@ lazy_static::lazy_static! { }; } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] pub enum ThemeType { #[default] Dark, Light, HighContrastDark, HighContrastLight, + Custom(Arc), } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, @@ -80,6 +82,7 @@ impl Theme { ThemeType::Light => &COSMIC_LIGHT, ThemeType::HighContrastDark => &COSMIC_HC_DARK, ThemeType::HighContrastLight => &COSMIC_HC_LIGHT, + ThemeType::Custom(ref t) => t.as_ref(), } } @@ -115,6 +118,14 @@ impl Theme { } } + #[must_use] + pub fn custom(theme: Arc) -> Self { + Self { + theme_type: ThemeType::Custom(theme), + ..Default::default() + } + } + /// get current container /// can be used in a component that is intended to be a child of a `CosmicContainer` #[must_use] @@ -218,8 +229,8 @@ impl button::StyleSheet for Theme { let component = style.cosmic(self); button::Appearance { border_radius: match style { - Button::Link => BorderRadius::from(0.0), - _ => BorderRadius::from(24.0), + Button::Link => 0.0, + _ => 24.0, }, background: match style { Button::Link | Button::Text => None, @@ -301,7 +312,7 @@ impl checkbox::StyleSheet for Theme { } else { palette.background.base.into() }), - checkmark_color: palette.accent.on.into(), + icon_color: palette.accent.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -318,7 +329,7 @@ impl checkbox::StyleSheet for Theme { } else { palette.background.base.into() }), - checkmark_color: palette.background.on.into(), + icon_color: palette.background.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: neutral_7.into(), @@ -330,7 +341,7 @@ impl checkbox::StyleSheet for Theme { } else { palette.background.base.into() }), - checkmark_color: palette.success.on.into(), + icon_color: palette.success.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -347,7 +358,7 @@ impl checkbox::StyleSheet for Theme { } else { palette.background.base.into() }), - checkmark_color: palette.destructive.on.into(), + icon_color: palette.destructive.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -374,7 +385,7 @@ impl checkbox::StyleSheet for Theme { } else { neutral_10.into() }), - checkmark_color: palette.accent.on.into(), + icon_color: palette.accent.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -391,7 +402,7 @@ impl checkbox::StyleSheet for Theme { } else { neutral_10.into() }), - checkmark_color: self.current_container().on.into(), + icon_color: self.current_container().on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -408,7 +419,7 @@ impl checkbox::StyleSheet for Theme { } else { neutral_10.into() }), - checkmark_color: palette.success.on.into(), + icon_color: palette.success.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -425,7 +436,7 @@ impl checkbox::StyleSheet for Theme { } else { neutral_10.into() }), - checkmark_color: palette.destructive.on.into(), + icon_color: palette.destructive.on.into(), border_radius: 4.0, border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { @@ -474,6 +485,7 @@ pub enum Container { Secondary, #[default] Transparent, + HeaderBar, Custom(Box container::Appearance>), } @@ -496,7 +508,18 @@ impl container::StyleSheet for Theme { container::Appearance { text_color: Some(Color::from(palette.background.on)), background: Some(iced::Background::Color(palette.background.base.into())), - border_radius: 2.0, + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::HeaderBar => { + let palette = self.cosmic(); + + container::Appearance { + text_color: Some(Color::from(palette.background.on)), + background: Some(iced::Background::Color(palette.background.base.into())), + border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -507,7 +530,7 @@ impl container::StyleSheet for Theme { container::Appearance { text_color: Some(Color::from(palette.primary.on)), background: Some(iced::Background::Color(palette.primary.base.into())), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -518,7 +541,7 @@ impl container::StyleSheet for Theme { container::Appearance { text_color: Some(Color::from(palette.secondary.on)), background: Some(iced::Background::Color(palette.secondary.base.into())), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -538,11 +561,15 @@ impl slider::StyleSheet for Theme { //TODO: no way to set rail thickness slider::Appearance { - rail_colors: ( - cosmic.accent.base.into(), - //TODO: no way to set color before/after slider - Color::TRANSPARENT, - ), + rail: Rail { + colors: ( + cosmic.accent.base.into(), + //TODO: no way to set color before/after slider + Color::TRANSPARENT, + ), + width: 4.0, + }, + handle: slider::Handle { shape: slider::HandleShape::Circle { radius: 10.0 }, color: cosmic.accent.base.into(), @@ -610,7 +637,8 @@ impl pick_list::StyleSheet for Theme { border_radius: 24.0, border_width: 0.0, border_color: Color::TRANSPARENT, - icon_size: 0.7, + // icon_size: 0.7, // TODO: how to replace + handle_color: cosmic.on_bg_color().into(), } } @@ -856,7 +884,11 @@ impl scrollable::StyleSheet for Theme { } } - fn hovered(&self, _style: &Self::Style) -> scrollable::Scrollbar { + fn hovered( + &self, + _style: &Self::Style, + _is_mouse_over_scrollbar: bool, + ) -> scrollable::Scrollbar { let theme = self.cosmic(); scrollable::Scrollbar { @@ -948,7 +980,7 @@ pub enum Text { Default, Color(Color), // TODO: Can't use dyn Fn since this must be copy - Custom(fn(&Theme) -> text::Appearance), + Custom(fn(&Theme) -> iced_widget::text::Appearance), } impl From for Text { @@ -957,16 +989,16 @@ impl From for Text { } } -impl text::StyleSheet for Theme { +impl iced_widget::text::StyleSheet for Theme { type Style = Text; - fn appearance(&self, style: Self::Style) -> text::Appearance { + fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance { match style { - Text::Accent => text::Appearance { + Text::Accent => iced_widget::text::Appearance { color: Some(self.cosmic().accent.base.into()), }, - Text::Default => text::Appearance { color: None }, - Text::Color(c) => text::Appearance { color: Some(c) }, + Text::Default => iced_widget::text::Appearance { color: None }, + Text::Color(c) => iced_widget::text::Appearance { color: Some(c) }, Text::Custom(f) => f(self), } } @@ -995,12 +1027,14 @@ impl text_input::StyleSheet for Theme { border_radius: 8.0, border_width: 1.0, border_color: self.current_container().component.divider.into(), + icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), border_radius: 24.0, border_width: 0.0, border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), }, } } @@ -1016,12 +1050,14 @@ impl text_input::StyleSheet for Theme { border_radius: 8.0, border_width: 1.0, border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), border_radius: 24.0, border_width: 0.0, border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), }, } } @@ -1037,12 +1073,14 @@ impl text_input::StyleSheet for Theme { border_radius: 8.0, border_width: 1.0, border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), border_radius: 24.0, border_width: 0.0, border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), }, } } @@ -1065,4 +1103,12 @@ impl text_input::StyleSheet for Theme { palette.accent.base.into() } + + fn disabled_color(&self, _style: &Self::Style) -> Color { + todo!() + } + + fn disabled(&self, _style: &Self::Style) -> text_input::Appearance { + todo!() + } } diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs index 1e1f53c..3212341 100644 --- a/src/theme/segmented_button.rs +++ b/src/theme/segmented_button.rs @@ -3,7 +3,7 @@ use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; -use iced_core::{Background, BorderRadius}; +use iced_core::{renderer::BorderRadius, Background}; use palette::{rgb::Rgb, Alpha}; #[derive(Default)] @@ -141,7 +141,7 @@ impl StyleSheet for Theme { mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use iced_core::{Background, BorderRadius}; + use iced_core::{renderer::BorderRadius, Background}; use palette::{rgb::Rgb, Alpha}; pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { @@ -222,7 +222,7 @@ pub fn hover( mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use iced_core::{Background, BorderRadius}; + use iced_core::{renderer::BorderRadius, Background}; use palette::{rgb::Rgb, Alpha}; pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 682c0c2..0a0df14 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -1,13 +1,13 @@ use iced::widget::Container; use iced::Size; -use iced_native::alignment; -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::{Operation, Tree}; -use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; +use iced_core::alignment; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::Tree; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; pub use iced_style::container::{Appearance, StyleSheet}; @@ -27,7 +27,7 @@ where #[allow(missing_debug_implementations)] pub struct AspectRatio<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { ratio: f32, @@ -36,7 +36,7 @@ where impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { fn constrain_limits(&self, size: Size) -> Size { @@ -55,7 +55,7 @@ where impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { /// Creates an empty [`Container`]. @@ -92,14 +92,14 @@ where /// Sets the maximum width of the [`Container`]. #[must_use] - pub fn max_width(mut self, max_width: u32) -> Self { + pub fn max_width(mut self, max_width: f32) -> Self { self.container = self.container.max_width(max_width); self } /// Sets the maximum height of the [`Container`] in pixels. #[must_use] - pub fn max_height(mut self, max_height: u32) -> Self { + pub fn max_height(mut self, max_height: f32) -> Self { self.container = self.container.max_height(max_height); self } @@ -142,14 +142,14 @@ where impl<'a, Message, Renderer> Widget for AspectRatio<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { fn children(&self) -> Vec { self.container.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } @@ -169,8 +169,16 @@ where self.container.layout(renderer, &custom_limits) } - fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation) { - self.container.operate(tree, layout, operation); + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, + ) { + self.container.operate(tree, layout, renderer, operation); } fn on_event( @@ -228,7 +236,7 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -241,7 +249,7 @@ impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: 'a + iced_native::Renderer, + Renderer: 'a + iced_core::Renderer, Renderer::Theme: StyleSheet, { fn from(column: AspectRatio<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 62e58d8..84da98e 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -1,13 +1,13 @@ use cosmic_theme::LayeredTheme; use iced::widget::Container; -use iced_native::alignment; -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::{Operation, Tree}; -use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; +use iced_core::alignment; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::Tree; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; pub use iced_style::container::{Appearance, StyleSheet}; pub fn container<'a, Message: 'static, T>( @@ -25,7 +25,7 @@ where #[allow(missing_debug_implementations)] pub struct LayerContainer<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { layer: Option, @@ -34,7 +34,7 @@ where impl<'a, Message, Renderer> LayerContainer<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, ::Style: std::convert::From, { @@ -83,14 +83,14 @@ where /// Sets the maximum width of the [`LayerContainer`]. #[must_use] - pub fn max_width(mut self, max_width: u32) -> Self { + pub fn max_width(mut self, max_width: f32) -> Self { self.container = self.container.max_width(max_width); self } /// Sets the maximum height of the [`LayerContainer`] in pixels. #[must_use] - pub fn max_height(mut self, max_height: u32) -> Self { + pub fn max_height(mut self, max_height: f32) -> Self { self.container = self.container.max_height(max_height); self } @@ -133,14 +133,14 @@ where impl<'a, Message, Renderer> Widget for LayerContainer<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { fn children(&self) -> Vec { self.container.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } @@ -156,8 +156,16 @@ where self.container.layout(renderer, limits) } - fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation) { - self.container.operate(tree, layout, operation); + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, + ) { + self.container.operate(tree, layout, renderer, operation); } fn on_event( @@ -222,7 +230,7 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -235,7 +243,7 @@ impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: 'a + iced_native::Renderer, + Renderer: 'a + iced_core::Renderer, Renderer::Theme: StyleSheet + Clone + cosmic_theme::LayeredTheme, { fn from(column: LayerContainer<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index eadebea..64e4bc3 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,6 +5,7 @@ use crate::{theme, Element}; use apply::Apply; use derive_setters::Setters; use iced::{self, widget, Length}; +use iced_core::renderer::BorderRadius; use std::borrow::Cow; #[must_use] @@ -74,12 +75,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }); let mut widget = widget::row(packed) - .height(Length::Units(50)) + .height(Length::Fixed(50.0)) .padding(8) .spacing(8) .apply(widget::container) + .style(crate::theme::Container::HeaderBar) .center_y() - .apply(widget::mouse_listener); + .apply(widget::mouse_area); if let Some(message) = self.on_drag.clone() { widget = widget.on_press(message); diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 3810be7..7691f73 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -71,7 +71,7 @@ impl<'a> IconSource<'a> { let handle = if let Some(path) = icon { svg::Handle::from_path(path) } else { - eprintln!("svg icon '{:?}' size {} not found", self, size); + eprintln!("svg icon '{self:?}' size {size} not found"); svg::Handle::from_memory(Vec::new()) }; @@ -79,7 +79,7 @@ impl<'a> IconSource<'a> { } else if let Some(icon) = icon { Handle::Image(icon.into()) } else { - eprintln!("icon '{:?}' size {} not found", self, size); + eprintln!("icon '{self:?}' size {size} not found"); Handle::Image(image::Handle::from_memory(Vec::new())) } } @@ -90,7 +90,13 @@ impl<'a> IconSource<'a> { } /// Get a handle to a raster image from memory. - pub fn raster_from_memory(bytes: impl Into>) -> Self { + pub fn raster_from_memory( + bytes: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync + + 'static, + ) -> Self { IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes))) } @@ -98,7 +104,11 @@ impl<'a> IconSource<'a> { pub fn raster_from_pixels( width: u32, height: u32, - pixels: impl Into>, + pixels: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync + + 'static, ) -> Self { IconSource::Handle(Handle::Image(image::Handle::from_pixels( width, height, pixels, @@ -165,7 +175,7 @@ impl From for IconSource<'static> { } /// A lazily-generated icon. -#[derive(Hash, Setters)] +#[derive(Setters)] pub struct Icon<'a> { #[setters(skip)] source: IconSource<'a>, @@ -181,6 +191,33 @@ pub struct Icon<'a> { force_svg: bool, } +// XXX Hopefully this will be enough precision +impl Hash for Icon<'_> { + #[allow(clippy::cast_possible_truncation)] + fn hash(&self, state: &mut H) { + self.source.hash(state); + self.theme.hash(state); + self.style.hash(state); + self.size.hash(state); + self.content_fit.hash(state); + self.force_svg.hash(state); + match self.width { + Some(Length::Fill) => 0.hash(state), + Some(Length::Shrink) => 1.hash(state), + Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), + Some(Length::FillPortion(p)) => p.hash(state), + None => 2.hash(state), + } + match self.height { + Some(Length::Fill) => 0.hash(state), + Some(Length::Shrink) => 1.hash(state), + Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), + Some(Length::FillPortion(p)) => p.hash(state), + None => 2.hash(state), + } + } +} + /// A lazily-generated icon. #[must_use] pub fn icon<'a>(source: impl Into>, size: u16) -> Icon<'a> { @@ -199,8 +236,8 @@ pub fn icon<'a>(source: impl Into>, size: u16) -> Icon<'a> { impl<'a> Icon<'a> { fn raster_element(&self, handle: image::Handle) -> Element<'static, Message> { Image::new(handle) - .width(self.width.unwrap_or(Length::Units(self.size))) - .height(self.height.unwrap_or(Length::Units(self.size))) + .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) + .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) .content_fit(self.content_fit) .into() } @@ -208,8 +245,8 @@ impl<'a> Icon<'a> { fn svg_element(&self, handle: svg::Handle) -> Element<'static, Message> { svg::Svg::::new(handle) .style(self.style.clone()) - .width(self.width.unwrap_or(Length::Units(self.size))) - .height(self.height.unwrap_or(Length::Units(self.size))) + .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) + .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) .content_fit(self.content_fit) .into() } @@ -228,7 +265,7 @@ impl<'a> Icon<'a> { let mut source = IconSource::Name(Cow::Borrowed("")); std::mem::swap(&mut source, &mut self.source); - iced_lazy::lazy(hash, move || -> Element { + iced::widget::lazy(hash, move |_| -> Element { match source.load(self.size, self.theme.as_deref(), self.force_svg) { Handle::Svg(handle) => self.svg_element(handle), Handle::Image(handle) => self.raster_element(handle), diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 0a1f5c3..c626138 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -63,7 +63,7 @@ pub fn style(theme: &crate::Theme) -> iced::widget::container::Appearance { iced::widget::container::Appearance { text_color: Some(container.on.into()), background: Some(Background::Color(container.base.into())), - border_radius: 8.0, + border_radius: 8.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 3ee2263..2a23646 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -45,7 +45,7 @@ pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance { iced_style::container::Appearance { text_color: Some(cosmic.on_bg_color().into()), background: Some(Background::Color(cosmic.primary.base.into())), - border_radius: 8.0, + border_radius: 8.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 09e1482..64670a1 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -3,13 +3,13 @@ //! A widget showing a popup in an overlay positioned relative to another widget. -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::{Operation, Tree}; -use iced_native::{Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget}; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::{Operation, OperationOutputWrapper, Tree}; +use iced_core::{Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget}; use std::cell::RefCell; pub use iced_style::container::{Appearance, StyleSheet}; @@ -43,15 +43,15 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget for Popover<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { fn children(&self) -> Vec { vec![Tree::new(&self.content), Tree::new(&*self.popup.borrow())] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&[&self.content, &self.popup.borrow()]) + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut [&mut self.content, &mut self.popup.borrow_mut()]); } fn width(&self) -> Length { @@ -66,10 +66,16 @@ where self.content.as_widget().layout(renderer, limits) } - fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation) { + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { self.content .as_widget() - .operate(&mut tree.children[0], layout, operation) + .operate(&mut tree.children[0], layout, renderer, operation); } fn on_event( @@ -128,11 +134,11 @@ where layout, cursor_position, viewport, - ) + ); } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, @@ -155,7 +161,7 @@ where impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: 'static, - Renderer: iced_native::Renderer + 'static, + Renderer: iced_core::Renderer + 'static, Renderer::Theme: StyleSheet, { fn from(popover: Popover<'a, Message, Renderer>) -> Self { @@ -171,7 +177,7 @@ struct Overlay<'a, 'b, Message, Renderer> { impl<'a, 'b, Message, Renderer> overlay::Overlay for Overlay<'a, 'b, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, { fn layout(&self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { // Position is set to the center bottom of the lower widget @@ -186,11 +192,16 @@ where node } - fn operate(&mut self, layout: Layout<'_>, operation: &mut dyn Operation) { + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { self.content .borrow() .as_widget() - .operate(self.tree, layout, operation) + .operate(self.tree, layout, renderer, operation); } fn on_event( @@ -246,6 +257,6 @@ where layout, cursor_position, &bounds, - ) + ); } } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index e30d4a8..3d9f378 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -4,14 +4,14 @@ use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; -use iced_native::alignment; -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::{Operation, Tree}; -use iced_native::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; +use iced_core::alignment; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::Tree; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget}; use std::{fmt::Debug, hash::Hash}; pub use iced_style::container::{Appearance, StyleSheet}; @@ -44,7 +44,7 @@ where #[allow(missing_debug_implementations)] pub struct RectangleTrackingContainer<'a, Message, Renderer, I> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { tx: UnboundedSender<(I, Rectangle)>, @@ -54,7 +54,7 @@ where impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { @@ -93,14 +93,14 @@ where /// Sets the maximum width of the [`Container`]. #[must_use] - pub fn max_width(mut self, max_width: u32) -> Self { + pub fn max_width(mut self, max_width: f32) -> Self { self.container = self.container.max_width(max_width); self } /// Sets the maximum height of the [`Container`] in pixels. #[must_use] - pub fn max_height(mut self, max_height: u32) -> Self { + pub fn max_height(mut self, max_height: f32) -> Self { self.container = self.container.max_height(max_height); self } @@ -144,7 +144,7 @@ where impl<'a, Message, Renderer, I> Widget for RectangleTrackingContainer<'a, Message, Renderer, I> where - Renderer: iced_native::Renderer, + Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { @@ -152,7 +152,7 @@ where self.container.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } @@ -168,8 +168,16 @@ where self.container.layout(renderer, limits) } - fn operate(&self, tree: &mut Tree, layout: Layout<'_>, operation: &mut dyn Operation) { - self.container.operate(tree, layout, operation); + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, + ) { + self.container.operate(tree, layout, renderer, operation); } fn on_event( @@ -229,7 +237,7 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -242,7 +250,7 @@ impl<'a, Message, Renderer, I> From where Message: 'a, - Renderer: 'a + iced_native::Renderer, + Renderer: 'a + iced_core::Renderer, Renderer::Theme: StyleSheet, I: 'a + Hash + Copy + Send + Sync + Debug, { diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 80e6ff3..224f8d6 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -26,44 +26,51 @@ pub enum State { async fn start_listening( id: I, - state: State, -) -> (Option<(I, RectangleUpdate)>, State) { - match state { - State::Ready => { - let (tx, rx) = unbounded(); + mut state: State, +) -> ((I, RectangleUpdate), State) { + loop { + let (update, new_state) = match state { + State::Ready => { + let (tx, rx) = unbounded(); - ( - Some((id, RectangleUpdate::Init(RectangleTracker { tx }))), - State::Waiting(rx, HashMap::new()), - ) - } - State::Waiting(mut rx, mut map) => match rx.next().await { - Some(u) => { - if let Some(prev) = map.get(&u.0) { - let new = u.1; - if prev.width != new.width - || prev.height != new.height - || prev.x != new.x - || prev.y != new.y - { - map.insert(u.0, new); - return ( + ( + Some((id, RectangleUpdate::Init(RectangleTracker { tx }))), + State::Waiting(rx, HashMap::new()), + ) + } + State::Waiting(mut rx, mut map) => match rx.next().await { + Some(u) => { + if let Some(prev) = map.get(&u.0) { + let new = u.1; + if (prev.width - new.width).abs() > 0.1 + || (prev.height - new.height).abs() > 0.1 + || (prev.x - new.x).abs() > 0.1 + || (prev.y - new.y).abs() > 0.1 + { + map.insert(u.0, new); + ( + Some((id, RectangleUpdate::Rectangle(u))), + State::Waiting(rx, map), + ) + } else { + (None, State::Waiting(rx, map)) + } + } else { + map.insert(u.0, u.1); + ( Some((id, RectangleUpdate::Rectangle(u))), State::Waiting(rx, map), - ); + ) } - } else { - map.insert(u.0, u.1); - return ( - Some((id, RectangleUpdate::Rectangle(u))), - State::Waiting(rx, map), - ); } - (None, State::Waiting(rx, map)) - } - None => (None, State::Finished), - }, - State::Finished => iced::futures::future::pending().await, + None => (None, State::Finished), + }, + State::Finished => iced::futures::future::pending().await, + }; + state = new_state; + if let Some(u) = update { + return (u, state); + } } } diff --git a/src/widget/scrollable.rs b/src/widget/scrollable.rs index 26f75d6..202d09e 100644 --- a/src/widget/scrollable.rs +++ b/src/widget/scrollable.rs @@ -8,6 +8,6 @@ pub fn scrollable<'a, Message>( element: impl Into>, ) -> widget::Scrollable<'a, Message, Renderer> { widget::scrollable(element) - .scrollbar_width(8) - .scroller_width(8) + // .scrollbar_width(8) TODO add these back + // .scroller_width(8) } diff --git a/src/widget/search/field.rs b/src/widget/search/field.rs index 89aa61d..2cfbe2b 100644 --- a/src/widget/search/field.rs +++ b/src/widget/search/field.rs @@ -11,7 +11,7 @@ use apply::Apply; /// A search field for COSMIC applications. pub fn field( - id: iced::widget::text_input::Id, + id: iced_core::id::Id, phrase: &str, on_change: fn(String) -> Message, on_clear: Message, @@ -29,7 +29,7 @@ pub fn field( /// A search field for COSMIC applications. #[must_use] pub struct Field<'a, Message: 'static + Clone> { - id: iced::widget::text_input::Id, + id: iced_core::id::Id, phrase: &'a str, on_change: fn(String) -> Message, on_clear: Message, @@ -38,7 +38,8 @@ pub struct Field<'a, Message: 'static + Clone> { impl<'a, Message: 'static + Clone> Field<'a, Message> { pub fn into_element(mut self) -> crate::Element<'a, Message> { - let mut input = iced::widget::text_input("", self.phrase, self.on_change) + let mut input = iced::widget::text_input("", self.phrase) + .on_input(self.on_change) .style(crate::theme::TextInput::Search) .width(Length::Fill) .id(self.id); @@ -52,8 +53,8 @@ impl<'a, Message: 'static + Clone> Field<'a, Message> { input, clear_button().on_press(self.on_clear) ) - .width(Length::Units(300)) - .height(Length::Units(38)) + .width(Length::Fixed(300.0)) + .height(Length::Fixed(38.0)) .padding([0, 16]) .spacing(8) .align_items(iced::Alignment::Center) @@ -84,7 +85,7 @@ fn active_style(theme: &crate::Theme) -> container::Appearance { iced::widget::container::Appearance { text_color: Some(cosmic.palette.neutral_9.into()), background: Some(Background::Color(neutral_7.into())), - border_radius: 24.0, + border_radius: 24.0.into(), border_width: 2.0, border_color: cosmic.accent.focus.into(), } diff --git a/src/widget/search/model.rs b/src/widget/search/model.rs index 27cafca..632c05b 100644 --- a/src/widget/search/model.rs +++ b/src/widget/search/model.rs @@ -6,14 +6,13 @@ use crate::iced; /// A model for managing the state of a search widget. pub struct Model { - pub input_id: iced::widget::text_input::Id, + pub input_id: iced_core::id::Id, pub phrase: String, pub state: State, } impl Model { /// Focuses the search field. - #[must_use] pub fn focus(&mut self) -> crate::iced::Command { self.state = State::Active; iced::widget::text_input::focus(self.input_id.clone()) @@ -29,7 +28,7 @@ impl Model { impl Default for Model { fn default() -> Self { Self { - input_id: iced::widget::text_input::Id::unique(), + input_id: iced_core::id::Id::unique(), phrase: String::with_capacity(32), state: State::Inactive, } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 8fff084..008e848 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -8,7 +8,7 @@ use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; -use iced_native::layout; +use iced_core::layout; /// Horizontal [`SegmentedButton`]. pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> = @@ -25,10 +25,10 @@ pub fn horizontal( model: &Model, ) -> SegmentedButton where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Model: Selectable, { @@ -38,10 +38,10 @@ where impl<'a, SelectionMode, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, @@ -49,8 +49,8 @@ where type Renderer = Renderer; fn variant_appearance( - theme: &::Theme, - style: &<::Theme as StyleSheet>::Style, + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, ) -> super::Appearance { theme.horizontal(style) } @@ -85,7 +85,7 @@ where } let size = limits - .height(Length::Units(height as u16)) + .height(Length::Fixed(height)) .resolve(Size::new(width, height)); layout::Node::new(size) diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 5cc2e1a..ea91b78 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use iced_core::{Background, BorderRadius, Color}; +use iced_core::{renderer::BorderRadius, Background, Color}; /// Appearance of the segmented button. #[derive(Default, Clone, Copy)] diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 04f3ccc..3a65409 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -8,7 +8,7 @@ use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; -use iced_native::layout; +use iced_core::layout; /// A type marker defining the vertical variant of a [`SegmentedButton`]. pub struct Vertical; @@ -25,10 +25,10 @@ pub fn vertical( model: &Model, ) -> SegmentedButton where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, @@ -39,10 +39,10 @@ where impl<'a, SelectionMode, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, @@ -50,8 +50,8 @@ where type Renderer = Renderer; fn variant_appearance( - theme: &::Theme, - style: &<::Theme as StyleSheet>::Style, + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, ) -> super::Appearance { theme.vertical(style) } @@ -86,7 +86,7 @@ where } let size = limits - .height(Length::Units(height as u16)) + .height(Length::Fixed(height)) .resolve(Size::new(width, height)); layout::Node::new(size) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e480157..58756b3 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -10,9 +10,10 @@ use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length, Point, Rectangle, Size, }; -use iced_core::BorderRadius; -use iced_native::widget::{self, operation, tree, Operation}; -use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; +use iced_core::renderer::BorderRadius; +use iced_core::text::{LineHeight, Shaping}; +use iced_core::widget::{self, operation, tree}; +use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use std::marker::PhantomData; /// State that is maintained by each individual widget. @@ -46,15 +47,15 @@ impl operation::Focusable for LocalState { /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { - type Renderer: iced_native::Renderer; + type Renderer: iced_core::Renderer; /// Get the appearance for this variant of the widget. fn variant_appearance( - theme: &::Theme, - style: &<::Theme as StyleSheet>::Style, + theme: &::Theme, + style: &<::Theme as StyleSheet>::Style, ) -> super::Appearance where - ::Theme: StyleSheet; + ::Theme: StyleSheet; /// Calculates the bounds for the given button by its position. fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; @@ -67,10 +68,10 @@ pub trait SegmentedVariant { #[derive(Setters)] pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Model: Selectable, SelectionMode: Default, @@ -91,13 +92,13 @@ where /// Spacing between icon and text in button. pub(super) button_spacing: u16, /// Desired font for active tabs. - pub(super) font_active: Renderer::Font, + pub(super) font_active: Option, /// Desired font for hovered tabs. - pub(super) font_hovered: Renderer::Font, + pub(super) font_hovered: Option, /// Desired font for inactive tabs. - pub(super) font_inactive: Renderer::Font, + pub(super) font_inactive: Option, /// Size of the font. - pub(super) font_size: u16, + pub(super) font_size: f32, /// Size of icon pub(super) icon_size: u16, /// Desired width of the widget. @@ -106,6 +107,8 @@ where pub(super) height: Length, /// Desired spacing between items. pub(super) spacing: u16, + /// LineHeight of the font. + pub(super) line_height: LineHeight, /// Style to draw the widget in. #[setters(into)] pub(super) style: ::Style, @@ -122,10 +125,10 @@ where impl<'a, Variant, SelectionMode, Message, Renderer> SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, Model: Selectable, @@ -141,14 +144,15 @@ where button_padding: [4, 4, 4, 4], button_height: 32, button_spacing: 4, - font_active: Renderer::Font::default(), - font_hovered: Renderer::Font::default(), - font_inactive: Renderer::Font::default(), - font_size: 17, + font_active: None, + font_hovered: None, + font_inactive: None, + font_size: 17.0, icon_size: 16, height: Length::Shrink, width: Length::Fill, spacing: 0, + line_height: LineHeight::default(), style: ::Style::default(), on_activate: None, on_close: None, @@ -212,6 +216,7 @@ where pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; + let font = renderer.default_font(); for key in self.model.order.iter().copied() { let mut button_width = 0.0f32; @@ -219,7 +224,14 @@ where // Add text to measurement if text was given. if let Some(text) = self.model.text(key) { - let (w, h) = renderer.measure(text, self.font_size, Default::default(), bounds); + let (w, h) = renderer.measure( + text, + self.font_size, + self.line_height, + font, + bounds, + Shaping::Advanced, + ); button_width = w; button_height = h; @@ -253,10 +265,10 @@ where impl<'a, Variant, SelectionMode, Message, Renderer> Widget for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer, + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, Model: Selectable, @@ -379,7 +391,10 @@ where &self, tree: &mut Tree, _layout: Layout<'_>, - operation: &mut dyn Operation, + _renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, ) { let state = tree.state.downcast_mut::(); operation.focusable(state, self.id.as_ref().map(|id| &id.0)); @@ -392,7 +407,7 @@ where cursor_position: iced::Point, _viewport: &iced::Rectangle, _renderer: &Renderer, - ) -> iced_native::mouse::Interaction { + ) -> iced_core::mouse::Interaction { let bounds = layout.bounds(); if bounds.contains(cursor_position) { @@ -402,15 +417,15 @@ where .contains(cursor_position) { return if self.model.items[key].enabled { - iced_native::mouse::Interaction::Pointer + iced_core::mouse::Interaction::Pointer } else { - iced_native::mouse::Interaction::Idle + iced_core::mouse::Interaction::Idle }; } } } - iced_native::mouse::Interaction::Idle + iced_core::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] @@ -418,7 +433,7 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &::Theme, + theme: &::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: iced::Point, @@ -458,6 +473,7 @@ where } else { (appearance.inactive, &self.font_inactive) }; + let font = font.unwrap_or_else(|| renderer.default_font()); let button_appearance = if nth == 0 { status_appearance.first @@ -536,7 +552,7 @@ where unimplemented!() } icon::Handle::Svg(handle) => { - iced_native::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds); + iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds); } } @@ -550,14 +566,16 @@ where bounds.y = y; // Draw the text in this button. - renderer.fill_text(iced_native::text::Text { + renderer.fill_text(iced_core::text::Text { content: text, - size: f32::from(self.font_size), + size: self.font_size, bounds, color: status_appearance.text_color, - font: font.clone(), + font, horizontal_alignment, vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, }); } @@ -575,7 +593,7 @@ where unimplemented!() } icon::Handle::Svg(handle) => { - iced_native::svg::Renderer::draw( + iced_core::svg::Renderer::draw( renderer, handle, Some(status_appearance.text_color), @@ -588,11 +606,11 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, _tree: &'b mut Tree, - _layout: iced_native::Layout<'_>, + _layout: iced_core::Layout<'_>, _renderer: &Renderer, - ) -> Option> { + ) -> Option> { None } } @@ -601,10 +619,10 @@ impl<'a, Variant, SelectionMode, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: iced_native::Renderer - + iced_native::text::Renderer - + iced_native::image::Renderer - + iced_native::svg::Renderer + Renderer: iced_core::Renderer + + iced_core::text::Renderer + + iced_core::image::Renderer + + iced_core::svg::Renderer + 'a, Renderer::Theme: StyleSheet, SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>: @@ -624,7 +642,6 @@ where } /// A command that focuses a segmented item stored in a widget. -#[must_use] pub fn focus(id: Id) -> Command { Command::widget(operation::focusable::focus(id.0)) } diff --git a/src/widget/segmented_selection.rs b/src/widget/segmented_selection.rs index 0b6a205..bc2320e 100644 --- a/src/widget/segmented_selection.rs +++ b/src/widget/segmented_selection.rs @@ -25,7 +25,7 @@ where .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) - .font_active(crate::font::FONT_SEMIBOLD) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } /// A selection of multiple choices appearing as a conjoined button. @@ -45,5 +45,5 @@ where .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) - .font_active(crate::font::FONT_SEMIBOLD) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs index d994ee8..c069422 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -46,43 +46,41 @@ impl<'a, Message: 'static> SpinButton<'a, Message> { icon("list-remove-symbolic", 24) .style(theme::Svg::Symbolic) .apply(container) - .width(Length::Fill) - .height(Length::Fill) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) .align_x(Horizontal::Center) .align_y(Vertical::Center) .apply(button) - .width(Length::Fill) - .height(Length::Fill) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) .style(theme::Button::Text) .on_press(model::Message::Decrement), text(label) .vertical_alignment(Vertical::Center) .apply(container) - .width(Length::Fill) - .height(Length::Fill) .align_x(Horizontal::Center) .align_y(Vertical::Center), icon("list-add-symbolic", 24) .style(theme::Svg::Symbolic) .apply(container) - .width(Length::Fill) - .height(Length::Fill) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) .align_x(Horizontal::Center) .align_y(Vertical::Center) .apply(button) - .width(Length::Fill) - .height(Length::Fill) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)) .style(theme::Button::Text) .on_press(model::Message::Increment), ] - .width(Length::Fill) - .height(Length::Units(32)) + .width(Length::Shrink) + .height(Length::Fixed(32.0)) + .spacing(4.0) .align_items(Alignment::Center), ) - .padding([4, 4]) .align_y(Vertical::Center) - .width(Length::Units(95)) - .height(Length::Units(32)) + .width(Length::Shrink) + .height(Length::Fixed(32.0)) .style(theme::Container::custom(container_style)) .apply(Element::from) .map(on_change) @@ -104,7 +102,7 @@ fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance { iced_style::container::Appearance { text_color: Some(basic.palette.neutral_10.into()), background: Some(Background::Color(neutral_10.into())), - border_radius: 24.0, + border_radius: 24.0.into(), border_width: 0.0, border_color: accent.base.into(), } diff --git a/src/widget/text.rs b/src/widget/text.rs index c868f9a..d235708 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -7,7 +7,7 @@ pub use iced::widget::Text; /// [`Text`]: widget::Text pub fn text<'a, Renderer>(text: impl Into>) -> Text<'a, Renderer> where - Renderer: iced_native::text::Renderer, + Renderer: iced_core::text::Renderer, Renderer::Theme: iced::widget::text::StyleSheet, { Text::new(text) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 004f81d..12e9d1a 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -9,7 +9,7 @@ pub fn toggler<'a, Message>( is_checked: bool, f: impl Fn(bool) -> Message + 'a, ) -> widget::Toggler<'a, Message, Renderer> { - widget::Toggler::new(is_checked, label, f) + widget::Toggler::new(label, is_checked, f) .size(24) .spacing(12) .width(Length::Shrink) diff --git a/src/widget/view_switcher.rs b/src/widget/view_switcher.rs index 7ff711c..85163e2 100644 --- a/src/widget/view_switcher.rs +++ b/src/widget/view_switcher.rs @@ -25,7 +25,7 @@ where .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) - .font_active(crate::font::FONT_SEMIBOLD) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } /// A collection of tabs for developing a tabbed interface. @@ -45,5 +45,5 @@ where .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) - .font_active(crate::font::FONT_SEMIBOLD) + .font_active(Some(crate::font::FONT_SEMIBOLD)) } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index b64315f..f46fa45 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -64,11 +64,12 @@ impl<'a, Message: 'static + Clone> From> for Element<'a, Me } } +#[must_use] pub fn warning_container(theme: &Theme) -> widget::container::Appearance { widget::container::Appearance { text_color: Some(theme.cosmic().warning.on.into()), background: Some(Background::Color(theme.cosmic().warning_color().into())), - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }