From e056e8c830780be92a96cf176680d9202d6734a1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 30 May 2023 12:03:15 -0400 Subject: [PATCH 0001/1276] Cosmic advanced text (#103) * wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced --- .github/workflows/ci.yml | 8 +- Cargo.toml | 47 +- cosmic-config-derive/Cargo.toml | 12 + cosmic-config-derive/src/lib.rs | 85 ++++ cosmic-config/Cargo.toml | 13 +- cosmic-config/src/lib.rs | 131 ++++- cosmic-theme/Cargo.toml | 32 ++ cosmic-theme/README.md | 1 + cosmic-theme/src/color_picker/exact.rs | 170 +++++++ cosmic-theme/src/color_picker/mod.rs | 280 +++++++++++ cosmic-theme/src/config/mod.rs | 196 ++++++++ cosmic-theme/src/hex_color.rs | 35 ++ cosmic-theme/src/lib.rs | 79 +++ cosmic-theme/src/model/constraint.rs | 26 + cosmic-theme/src/model/cosmic_palette.rs | 252 ++++++++++ cosmic-theme/src/model/dark.ron | 95 ++++ cosmic-theme/src/model/derivation.rs | 480 +++++++++++++++++++ cosmic-theme/src/model/light.ron | 95 ++++ cosmic-theme/src/model/mod.rs | 14 + cosmic-theme/src/model/selection.rs | 99 ++++ cosmic-theme/src/model/theme.rs | 431 +++++++++++++++++ cosmic-theme/src/output/gtk4_output.rs | 187 ++++++++ cosmic-theme/src/output/mod.rs | 8 + cosmic-theme/src/theme_provider/mod.rs | 1 + cosmic-theme/src/util.rs | 59 +++ examples/cosmic-sctk/Cargo.toml | 2 +- examples/cosmic-sctk/src/window.rs | 52 +- examples/cosmic/Cargo.toml | 6 +- examples/cosmic/src/main.rs | 6 + examples/cosmic/src/window.rs | 78 ++- examples/cosmic/src/window/demo.rs | 69 ++- examples/cosmic/src/window/desktop.rs | 6 +- examples/cosmic/src/window/editor.rs | 3 +- iced | 2 +- src/applet/mod.rs | 52 +- src/executor/multi.rs | 2 +- src/executor/single.rs | 2 +- src/font.rs | 30 +- src/keyboard_nav.rs | 4 +- src/lib.rs | 8 +- src/settings.rs | 7 +- src/theme/mod.rs | 114 +++-- src/theme/segmented_button.rs | 6 +- src/widget/aspect_ratio.rs | 46 +- src/widget/cosmic_container.rs | 44 +- src/widget/header_bar.rs | 6 +- src/widget/icon.rs | 57 ++- src/widget/list/column.rs | 2 +- src/widget/nav_bar.rs | 2 +- src/widget/popover.rs | 49 +- src/widget/rectangle_tracker/mod.rs | 44 +- src/widget/rectangle_tracker/subscription.rs | 73 +-- src/widget/scrollable.rs | 4 +- src/widget/search/field.rs | 13 +- src/widget/search/model.rs | 5 +- src/widget/segmented_button/horizontal.rs | 24 +- src/widget/segmented_button/style.rs | 2 +- src/widget/segmented_button/vertical.rs | 24 +- src/widget/segmented_button/widget.rs | 111 +++-- src/widget/segmented_selection.rs | 4 +- src/widget/spin_button/mod.rs | 30 +- src/widget/text.rs | 2 +- src/widget/toggler.rs | 2 +- src/widget/view_switcher.rs | 4 +- src/widget/warning.rs | 3 +- 65 files changed, 3431 insertions(+), 405 deletions(-) create mode 100644 cosmic-config-derive/Cargo.toml create mode 100644 cosmic-config-derive/src/lib.rs create mode 100644 cosmic-theme/Cargo.toml create mode 100644 cosmic-theme/README.md create mode 100644 cosmic-theme/src/color_picker/exact.rs create mode 100644 cosmic-theme/src/color_picker/mod.rs create mode 100644 cosmic-theme/src/config/mod.rs create mode 100644 cosmic-theme/src/hex_color.rs create mode 100644 cosmic-theme/src/lib.rs create mode 100644 cosmic-theme/src/model/constraint.rs create mode 100644 cosmic-theme/src/model/cosmic_palette.rs create mode 100644 cosmic-theme/src/model/dark.ron create mode 100644 cosmic-theme/src/model/derivation.rs create mode 100644 cosmic-theme/src/model/light.ron create mode 100644 cosmic-theme/src/model/mod.rs create mode 100644 cosmic-theme/src/model/selection.rs create mode 100644 cosmic-theme/src/model/theme.rs create mode 100644 cosmic-theme/src/output/gtk4_output.rs create mode 100644 cosmic-theme/src/output/mod.rs create mode 100644 cosmic-theme/src/theme_provider/mod.rs create mode 100644 cosmic-theme/src/util.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c90bffac..9070c891 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 379f60ca..a8cb3b4e 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 00000000..44f960ec --- /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 00000000..88984fa7 --- /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 38cf3e41..7393fafc 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 e4b237a1..a7b2be63 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 00000000..7f745b64 --- /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 00000000..1a1aebed --- /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 00000000..2e29c265 --- /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 00000000..b5bf4ee7 --- /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 00000000..a558fcf5 --- /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 00000000..bf04f216 --- /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 00000000..efa4025b --- /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 00000000..45132494 --- /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 00000000..622627c1 --- /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 00000000..d8d0ba8f --- /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 00000000..da5f1ec2 --- /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 00000000..92951bb7 --- /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 00000000..684df0b8 --- /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 00000000..a4120c48 --- /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 00000000..f31b0cdd --- /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 00000000..43fb498c --- /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 00000000..31307629 --- /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 00000000..8b137891 --- /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 00000000..afbf98c6 --- /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 277ec5f2..f6d3da38 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 920428b0..1665118b 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 db0cb60e..feb41ae5 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 1d1f2bfe..5700a590 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 50b54904..d5977a11 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 acfad0b3..243ba077 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 7f3ddadd..f20722fb 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 c1423e29..e272050d 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 a9d0b3d8..2a3b5770 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 60e4fc1c..585695d4 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 a6f07318..18cb8234 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 e293fc1d..1ffa0529 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 e3f96983..8a2a4cc6 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 814bcc48..af4703e4 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 db470260..aa04d562 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 7fb35a2f..9f9b20fd 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 51462d67..2fef40a7 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 1e1f53c3..32123414 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 682c0c25..0a0df145 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 62e58d85..84da98e6 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 eadebeab..64e4bc3d 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 3810be73..7691f733 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 0a1f5c36..c6261383 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 3ee2263d..2a236468 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 09e1482a..64670a1c 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 e30d4a85..3d9f378c 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 80e6ff3c..224f8d6c 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 26f75d60..202d09e2 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 89aa61d0..2cfbe2b5 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 27cafcaa..632c05b6 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 8fff084b..008e848d 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 5cc2e1a7..ea91b785 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 04f3ccc0..3a65409b 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 e4801574..58756b38 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 0b6a2059..bc2320e5 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 d994ee88..c069422e 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 c868f9a6..d2357081 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 004f81da..12e9d1a1 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 7ff711c7..85163e2f 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 b64315f3..f46fa45f 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, } From f06a81ccf9fdeaef0033bfc07aa493ff8675f420 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 30 May 2023 13:50:08 -0400 Subject: [PATCH 0002/1276] chore: update deps --- Cargo.toml | 2 +- iced | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8cb3b4e..91e4a096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ 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", branch = "bg_jammy", optional = true } +cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", rev = "6d2c228", 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" diff --git a/iced b/iced index 2a3b5770..0f90f496 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2a3b5770b9f9c700d4aeb6398ab6c917024ce6cc +Subproject commit 0f90f49692c1773abe1d161c7e60f6f64f621f2f From 31f7e97d5bf4860be5afd406209eed733f736f04 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 30 May 2023 23:46:49 +0200 Subject: [PATCH 0003/1276] fix: incorrect font weights, sizes, line heights --- examples/cosmic-sctk/src/window.rs | 3 ++- examples/cosmic/src/window.rs | 10 +++++----- iced | 2 +- src/font.rs | 25 ++++++++++++++++++++++--- src/widget/header_bar.rs | 3 +-- src/widget/segmented_button/widget.rs | 2 +- src/widget/settings/item.rs | 8 ++++---- src/widget/settings/section.rs | 5 +---- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 1665118b..4a3976d0 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -367,7 +367,8 @@ impl Application for Window { vec!["Option 1", "Option 2", "Option 3", "Option 4"], self.pick_list_selected, Message::PickListSelected, - ), + ) + .text_size(14.0), )) .add(settings::item( "Slider", diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index d5977a11..d25e053e 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -225,7 +225,7 @@ impl Window { } fn page_title(&self, page: Page) -> Element { - row!(text(page.title()).size(30), horizontal_space(Length::Fill),).into() + row!(text(page.title()).size(28), horizontal_space(Length::Fill),).into() } fn is_condensed(&self) -> bool { @@ -245,14 +245,14 @@ impl Window { column!( iced::widget::Button::new(row!( icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink), - text(page.title()).size(16), + text(page.title()).size(14), )) .padding(0) .style(theme::Button::Link) // .id(BTN.clone()) .on_press(Message::from(page)), row!( - text(sub_page.title()).size(30), + text(sub_page.title()).size(28), horizontal_space(Length::Fill), ), ) @@ -276,8 +276,8 @@ impl Window { .style(theme::Svg::Symbolic) .into(), column!( - text(sub_page.title()).size(18), - text(sub_page.description()).size(12), + text(sub_page.title()).size(14), + text(sub_page.description()).size(10), ) .spacing(2) .into(), diff --git a/iced b/iced index 0f90f496..cb7f2b56 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 0f90f49692c1773abe1d161c7e60f6f64f621f2f +Subproject commit cb7f2b566bde194a0aaed869f5f903939a979c03 diff --git a/src/font.rs b/src/font.rs index 8a2a4cc6..e971a3b4 100644 --- a/src/font.rs +++ b/src/font.rs @@ -6,14 +6,33 @@ use iced::{ font::{load, Error}, Command, }; +use iced_core::font::Family; + +pub const FONT: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Normal, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; -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_LIGHT: Font = Font::with_name("Fira Sans Light"); +pub const FONT_LIGHT: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Light, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; + 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: Font = Font { + family: Family::Name("Fira Sans"), + weight: iced_core::font::Weight::Semibold, + stretch: iced_core::font::Stretch::Normal, + monospaced: false, +}; + pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); pub fn load_fonts() -> Command> { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 64e4bc3d..51fec134 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,6 @@ 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] @@ -99,7 +98,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { std::mem::swap(&mut title, &mut self.title); super::text(title) - .size(18) + .size(16) .font(crate::font::FONT_SEMIBOLD) .apply(widget::container) .center_x() diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 58756b38..272bb217 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -147,7 +147,7 @@ where font_active: None, font_hovered: None, font_inactive: None, - font_size: 17.0, + font_size: 14.0, icon_size: 16, height: Length::Shrink, width: Length::Fill, diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 078c322f..83eebdc0 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -15,7 +15,7 @@ pub fn item<'a, Message: 'static>( widget: impl Into>, ) -> Row<'a, Message, Renderer> { item_row(vec![ - text(title).size(20).into(), + text(title).into(), horizontal_space(iced::Length::Fill).into(), widget.into(), ]) @@ -65,12 +65,12 @@ impl<'a, Message: 'static> Item<'a, Message> { } if let Some(description) = self.description { - let title = text(self.title).size(20); - let desc = text(description).size(14); + let title = text(self.title); + let desc = text(description).size(10); contents.push(column!(title, desc).spacing(2).into()); } else { - contents.push(text(self.title).size(20).into()); + contents.push(text(self.title).into()); } contents.push(horizontal_space(iced::Length::Fill).into()); diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 511c2c16..59fa7ce8 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -31,10 +31,7 @@ impl<'a, Message: 'static> Section<'a, Message> { impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { - let title = text(data.title) - .size(20) - .font(crate::font::FONT_SEMIBOLD) - .into(); + let title = text(data.title).font(crate::font::FONT_SEMIBOLD).into(); column(vec![title, data.children.into_element()]) .spacing(8) From ce713d9da76b94a82a62ef93c579a5cd7844102b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 31 May 2023 10:28:15 -0400 Subject: [PATCH 0004/1276] refactor: move applet helpers to cosmic-applets repo --- .github/workflows/ci.yml | 1 - Cargo.toml | 4 - src/applet/mod.rs | 200 --------------------------------------- src/lib.rs | 2 - 4 files changed, 207 deletions(-) delete mode 100644 src/applet/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9070c891..b641a1b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,6 @@ jobs: - winit_wgpu - tiny_skia - wayland - - applet runs-on: ubuntu-22.04 steps: - name: Checkout sources diff --git a/Cargo.toml b/Cargo.toml index 91e4a096..23bcdc71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ 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", "wayland", "ron", "serde"] winit_tiny_skia = ["winit", "tiny_skia"] winit_wgpu = ["winit", "wgpu"] @@ -25,13 +24,10 @@ 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", rev = "6d2c228", 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" diff --git a/src/applet/mod.rs b/src/applet/mod.rs deleted file mode 100644 index 585695d4..00000000 --- a/src/applet/mod.rs +++ /dev/null @@ -1,200 +0,0 @@ -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::layout::Limits; -use iced_style::{button::StyleSheet, container::Appearance}; -use iced_widget::runtime::command::platform_specific::wayland::{ - popup::{SctkPopupSettings, SctkPositioner}, - window::SctkWindowSettings, -}; -use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; - -use crate::{theme::Button, Renderer}; - -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: 0.0, - ..t.active(&Button::Text) - }), - hover: Box::new(|t| iced_style::button::Appearance { - border_radius: 0.0, - ..t.hovered(&Button::Text) - }), - } -} - -#[derive(Debug, Clone)] -pub struct CosmicAppletHelper { - pub size: Size, - pub anchor: PanelAnchor, - pub background: CosmicPanelBackground, - pub output_name: String, -} - -#[derive(Clone, Debug)] -pub enum Size { - PanelSize(PanelSize), - // (width, height) - Hardcoded((u16, u16)), -} - -impl Default for CosmicAppletHelper { - fn default() -> Self { - Self { - size: Size::PanelSize( - std::env::var("COSMIC_PANEL_SIZE") - .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| 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 { - PanelSize::XL => (64, 64), - PanelSize::L => (36, 36), - PanelSize::M => (24, 24), - PanelSize::S => (16, 16), - PanelSize::XS => (12, 12), - }, - Size::Hardcoded((width, height)) => (*width, *height), - } - } - - // Set the default window size. Helper for application init with hardcoded size. - pub fn window_size(&mut self, width: u16, height: u16) { - self.size = Size::Hardcoded((width, height)); - } - - #[must_use] - pub fn window_settings(&self) -> Settings { - self.window_settings_with_flags(F::default()) - } - - #[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 { - 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) - } - } - - #[must_use] - pub fn icon_button<'a, Message: 'static>( - &self, - icon_name: &'a str, - ) -> widget::Button<'a, Message, Renderer> { - crate::widget::button(crate::theme::Button::Text) - .icon( - crate::theme::Svg::Symbolic, - icon_name, - self.suggested_size().0, - ) - .padding(8) - } - - // TODO popup container which tracks the size of itself and requests the popup to resize to match - pub fn popup_container<'a, Message: 'static>( - &self, - content: impl Into>, - ) -> Container<'a, Message, Renderer> { - 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), - PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), - }; - - Container::::new(Container::::new(content).style( - 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.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }), - )) - .width(Length::Shrink) - .height(Length::Shrink) - .align_x(horizontal_align) - .align_y(vertical_align) - } - - #[must_use] - #[allow(clippy::cast_possible_wrap)] - pub fn get_popup_settings( - &self, - parent: iced_core::window::Id, - id: iced_core::window::Id, - size: Option<(u32, u32)>, - width_padding: Option, - height_padding: Option, - ) -> SctkPopupSettings { - let (width, height) = self.suggested_size(); - let pixel_offset = 8; - let (offset, anchor, gravity) = match self.anchor { - PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), - PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left), - PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom), - PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top), - }; - SctkPopupSettings { - parent, - id, - positioner: SctkPositioner { - anchor, - gravity, - offset, - size, - anchor_rect: Rectangle { - x: 0, - y: 0, - width: width_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(width), - height: height_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(height), - }, - reactive: true, - constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y - ..Default::default() - }, - parent_size: None, - grab: true, - } - } -} diff --git a/src/lib.rs b/src/lib.rs index aa04d562..bfbde8af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,8 +15,6 @@ pub use iced_widget; pub use iced_winit; #[cfg(feature = "wayland")] pub use sctk; -#[cfg(feature = "applet")] -pub mod applet; pub mod executor; pub mod font; pub mod keyboard_nav; From 5765053ad70d7b0fded56fe9e9c73649509a03ae Mon Sep 17 00:00:00 2001 From: Brock <58987761+13r0ck@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:12:57 -0600 Subject: [PATCH 0005/1276] Expose internal iced crates for cosmic-time (#110) This allows cosmic-time to use all imports via libcosmic, so the versions of iced will not collide. --- Cargo.toml | 7 +++++++ src/lib.rs | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 23bcdc71..14fa8454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ tiny_skia = ["iced/tiny-skia", "iced_tiny_skia"] wayland = ["iced/wayland", "iced_sctk", "sctk",] wgpu = ["iced/wgpu", "iced_wgpu"] tokio = ["dep:tokio", "iced/tokio"] +smol = ["iced/smol"] winit = ["iced/winit", "iced_winit"] winit_tiny_skia = ["winit", "tiny_skia"] winit_wgpu = ["winit", "wgpu"] @@ -43,12 +44,18 @@ features = ["image", "svg", "lazy"] [dependencies.iced_runtime] path = "iced/runtime" +[dependencies.iced_renderer] +path = "iced/renderer" + [dependencies.iced_core] path = "iced/core" [dependencies.iced_widget] path = "iced/widget" +[dependencies.iced_futures] +path = "iced/futures" + [dependencies.iced_accessibility] path = "iced/accessibility" diff --git a/src/lib.rs b/src/lib.rs index bfbde8af..4dc92191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,9 @@ pub use cosmic_config; pub use cosmic_theme; pub use iced; +pub use iced_core; +pub use iced_futures; +pub use iced_renderer; pub use iced_runtime; #[cfg(feature = "wayland")] pub use iced_sctk; From cf2818c4a163caaa6a627e7aa6bcd40e455c7a6a Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld <4404502+Drakulix@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:52:40 +0200 Subject: [PATCH 0006/1276] cosmic-config: Don't pull all of iced (#111) --- cosmic-config/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 7393fafc..84174405 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -16,6 +16,6 @@ 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 } +iced = { path = "../iced/", default-features = false, optional = true } +iced_futures = { path = "../iced/futures/", default-features = false, optional = true } From 31d7c75098a9ea4857bc16321306aa40ed14eac2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 6 Jun 2023 14:55:41 -0400 Subject: [PATCH 0007/1276] fix: update iced to fix Id reuse --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index cb7f2b56..f2647b96 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit cb7f2b566bde194a0aaed869f5f903939a979c03 +Subproject commit f2647b96fbf06c785ec7e7d7a161eaebb03d4d77 From 6699aa475646fd044b5233dc918e3b0e3604c8ed Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 9 Jun 2023 17:13:01 -0400 Subject: [PATCH 0008/1276] fix: derive PartialEq for Theme --- cosmic-theme/src/model/cosmic_palette.rs | 4 ++-- cosmic-theme/src/model/derivation.rs | 4 ++-- cosmic-theme/src/model/theme.rs | 17 +---------------- cosmic-theme/src/util.rs | 2 +- src/theme/mod.rs | 4 ++-- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 622627c1..45a95561 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -23,7 +23,7 @@ lazy_static! { } /// Palette type -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub enum CosmicPalette { /// Dark mode Dark(CosmicPaletteInner), @@ -80,7 +80,7 @@ where } /// The palette for Cosmic Theme, from which all color properties are derived -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct CosmicPaletteInner { /// name of the palette pub name: String, diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index da5f1ec2..c030dd34 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -5,7 +5,7 @@ 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)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub struct Container { /// the color of the container pub base: C, @@ -160,7 +160,7 @@ impl fmt::Display for ContainerType { } /// The colors for a widget of the Cosmic theme -#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize, Eq)] pub struct Component { /// The base color of the widget pub base: C, diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index f31b0cdd..51412998 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -27,7 +27,7 @@ pub enum Layer { } /// Cosmic Theme data structure with all colors and its name -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Theme { /// name of the theme pub name: String, @@ -149,21 +149,6 @@ pub trait LayeredTheme { 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 { diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs index afbf98c6..bb264c8e 100644 --- a/cosmic-theme/src/util.rs +++ b/cosmic-theme/src/util.rs @@ -3,7 +3,7 @@ use palette::Srgba; use serde::{Deserialize, Serialize}; /// utility wrapper for serializing and deserializing colors with arbitrary CSS -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] pub struct CssColor { c: Color, } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 2fef40a7..bf1074b9 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -58,7 +58,7 @@ lazy_static::lazy_static! { }; } -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default)] pub enum ThemeType { #[default] Dark, @@ -68,7 +68,7 @@ pub enum ThemeType { Custom(Arc), } -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, From a8a2e4ad26fad231db232d3ac2823011e5d31d04 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 12 Jun 2023 12:08:14 -0400 Subject: [PATCH 0009/1276] feat: theme helper methods --- Cargo.toml | 1 + cosmic-theme/src/model/theme.rs | 2 +- src/theme/mod.rs | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 14fa8454..8a2285d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/cl slotmap = "1.0.6" fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } +tracing = "0.1" [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.2" diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 51412998..284dd6c6 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -151,7 +151,7 @@ pub trait LayeredTheme { impl Theme { /// version of the theme - pub fn version() -> u32 { + pub fn version() -> u64 { 1 } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index bf1074b9..231d5faa 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -11,9 +11,13 @@ use std::sync::Arc; pub use self::segmented_button::SegmentedButton; +use cosmic_config::config_subscription; +use cosmic_config::CosmicConfigEntry; +use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; use iced_core::renderer::BorderRadius; +use iced_futures::Subscription; use iced_style::application; use iced_style::button; use iced_style::checkbox; @@ -1112,3 +1116,42 @@ impl text_input::StyleSheet for Theme { todo!() } } + +pub fn theme() -> Theme { + let Ok(helper) = crate::cosmic_config::Config::new( + crate::cosmic_theme::NAME, + crate::cosmic_theme::Theme::::version(), + ) else { + return crate::theme::Theme::dark(); + }; + let t = crate::cosmic_theme::Theme::get_entry(&helper).map_or_else( + |(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme.into_srgba() + }, + crate::cosmic_theme::Theme::into_srgba, + ); + crate::theme::Theme::custom(Arc::new(t)) +} + +pub fn theme_subscription(id: u64) -> Subscription { + config_subscription::>( + id, + crate::cosmic_theme::NAME.into(), + crate::cosmic_theme::Theme::::version(), + ) + .map(|(_, res)| { + let theme = res.map_or_else( + |(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme.into_srgba() + }, + crate::cosmic_theme::Theme::into_srgba, + ); + crate::theme::Theme::custom(Arc::new(theme)) + }) +} From 804b183492eb8d270b62ade3e6863638ec4f3814 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld <4404502+Drakulix@users.noreply.github.com> Date: Tue, 13 Jun 2023 19:11:19 +0200 Subject: [PATCH 0010/1276] chore: Update submodule (#115) --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f2647b96..978e9fe8 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f2647b96fbf06c785ec7e7d7a161eaebb03d4d77 +Subproject commit 978e9fe8142bb301ce27ad23ce0afb12ef8918a7 From 850968715c16f3800fa899fe939dbf45d1e1a9b8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 15 Jun 2023 11:16:32 -0400 Subject: [PATCH 0011/1276] udpate iced to use latest rebase --- Cargo.toml | 8 +-- cosmic-theme/Cargo.toml | 2 +- examples/cosmic-sctk/Cargo.toml | 2 +- examples/cosmic/Cargo.toml | 2 +- iced | 2 +- src/theme/mod.rs | 81 +++++++++++++++++---------- src/theme/segmented_button.rs | 6 +- src/widget/aspect_ratio.rs | 6 +- src/widget/cosmic_container.rs | 6 +- src/widget/popover.rs | 12 ++-- src/widget/rectangle_tracker/mod.rs | 6 +- src/widget/segmented_button/style.rs | 2 +- src/widget/segmented_button/widget.rs | 25 ++++----- 13 files changed, 87 insertions(+), 73 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8a2285d1..30775ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,23 +7,21 @@ edition = "2021" name = "cosmic" [features] -default = ["tiny_skia", "winit", "tokio", "a11y"] +default = ["winit", "tokio", "a11y"] debug = ["iced/debug"] 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"] smol = ["iced/smol"] winit = ["iced/winit", "iced_winit"] -winit_tiny_skia = ["winit", "tiny_skia"] winit_wgpu = ["winit", "wgpu"] [dependencies] apply = "0.3.0" derive_setters = "0.1.5" lazy_static = "1.4.0" -palette = "0.6.1" +palette = "0.7" tokio = { version = "1.24.2", optional = true } sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", optional = true, tag = "themed-pointer"} slotmap = "1.0.6" @@ -61,9 +59,9 @@ path = "iced/futures" path = "iced/accessibility" optional = true + [dependencies.iced_tiny_skia] path = "iced/tiny_skia" -optional = true [dependencies.iced_style] path = "iced/style" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7f745b64..b53e3387 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -17,7 +17,7 @@ theme-from-image = ["kmeans_colors", "contrast-derivation", "float-cmp", "image" hex-color = ["hex"] [dependencies] -palette = {version = "0.6", features = ["serializing"] } +palette = {version = "0.7", 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 } diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index f6d3da38..e614bb88 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", "tiny_skia", "a11y"] } +libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index feb41ae5..5e97c675 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.13.0" -libcosmic = { path = "../..", default-features = false, features = ["debug", "winit_tiny_skia", "a11y"] } +libcosmic = { path = "../..", default-features = false, features = ["debug", "winit", "a11y"] } once_cell = "1.15" slotmap = "1.0.6" env_logger = "0.10" diff --git a/iced b/iced index 978e9fe8..c308e226 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 978e9fe8142bb301ce27ad23ce0afb12ef8918a7 +Subproject commit c308e2266867a7bb4dfadb2dc8bdf310b7c87ee8 diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 231d5faa..5ab9cde1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -4,6 +4,7 @@ pub mod expander; mod segmented_button; +use std::f32::consts::PI; use std::hash::Hash; use std::hash::Hasher; use std::rc::Rc; @@ -16,7 +17,9 @@ use cosmic_config::CosmicConfigEntry; use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; -use iced_core::renderer::BorderRadius; +use iced_core::gradient::Linear; +use iced_core::BorderRadius; +use iced_core::Radians; use iced_futures::Subscription; use iced_style::application; use iced_style::button; @@ -233,8 +236,8 @@ impl button::StyleSheet for Theme { let component = style.cosmic(self); button::Appearance { border_radius: match style { - Button::Link => 0.0, - _ => 24.0, + Button::Link => 0.0.into(), + _ => 24.0.into(), }, background: match style { Button::Link | Button::Text => None, @@ -317,7 +320,7 @@ impl checkbox::StyleSheet for Theme { palette.background.base.into() }), icon_color: palette.accent.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.accent.base @@ -334,7 +337,7 @@ impl checkbox::StyleSheet for Theme { palette.background.base.into() }), icon_color: palette.background.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: neutral_7.into(), text_color: None, @@ -346,7 +349,7 @@ impl checkbox::StyleSheet for Theme { palette.background.base.into() }), icon_color: palette.success.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.success.base @@ -363,7 +366,7 @@ impl checkbox::StyleSheet for Theme { palette.background.base.into() }), icon_color: palette.destructive.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.destructive.base @@ -390,7 +393,7 @@ impl checkbox::StyleSheet for Theme { neutral_10.into() }), icon_color: palette.accent.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.accent.base @@ -407,7 +410,7 @@ impl checkbox::StyleSheet for Theme { neutral_10.into() }), icon_color: self.current_container().on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { self.current_container().base @@ -424,7 +427,7 @@ impl checkbox::StyleSheet for Theme { neutral_10.into() }), icon_color: palette.success.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.success.base @@ -441,7 +444,7 @@ impl checkbox::StyleSheet for Theme { neutral_10.into() }), icon_color: palette.destructive.on.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { palette.destructive.base @@ -519,10 +522,17 @@ impl container::StyleSheet for Theme { } Container::HeaderBar => { let palette = self.cosmic(); + let mut header_top = palette.background.base; + let header_bottom = palette.background.base; + header_top.alpha = 0.8; container::Appearance { text_color: Some(Color::from(palette.background.on)), - background: Some(iced::Background::Color(palette.background.base.into())), + background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( + Linear::new(Radians(3.0 * PI / 2.0)) + .add_stop(0.0, header_top.into()) + .add_stop(1.0, header_bottom.into()), + ))), border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]), border_width: 0.0, border_color: Color::TRANSPARENT, @@ -572,6 +582,7 @@ impl slider::StyleSheet for Theme { Color::TRANSPARENT, ), width: 4.0, + border_radius: 2.0.into(), }, handle: slider::Handle { @@ -616,7 +627,7 @@ impl menu::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.base.into()), border_width: 0.0, - border_radius: 16.0, + border_radius: 16.0.into(), border_color: Color::TRANSPARENT, selected_text_color: cosmic.on_bg_color().into(), // TODO doesn't seem to be specified @@ -638,7 +649,7 @@ impl pick_list::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Color::TRANSPARENT.into(), placeholder_color: cosmic.on_bg_color().into(), - border_radius: 24.0, + border_radius: 24.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, // icon_size: 0.7, // TODO: how to replace @@ -772,6 +783,16 @@ impl pane_grid::StyleSheet for Theme { width: 2.0, }) } + + fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { + let theme = self.cosmic(); + pane_grid::Appearance { + background: Background::Color(theme.bg_color().into()), + border_width: 2.0, + border_color: theme.bg_divider().into(), + border_radius: 0.0.into(), + } + } } /* @@ -802,17 +823,17 @@ impl progress_bar::StyleSheet for Theme { ProgressBar::Primary => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.accent.base).into(), - border_radius: 2.0, + border_radius: 2.0.into(), }, ProgressBar::Success => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.success.base).into(), - border_radius: 2.0, + border_radius: 2.0.into(), }, ProgressBar::Danger => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.destructive.base).into(), - border_radius: 2.0, + border_radius: 2.0.into(), }, ProgressBar::Custom(f) => f(self), } @@ -845,19 +866,19 @@ impl rule::StyleSheet for Theme { Rule::Default => rule::Appearance { color: self.current_container().divider.into(), width: 1, - radius: 0.0, + radius: 0.0.into(), fill_mode: rule::FillMode::Full, }, Rule::LightDivider => rule::Appearance { color: self.current_container().divider.into(), width: 1, - radius: 0.0, + radius: 0.0.into(), fill_mode: rule::FillMode::Padded(10), }, Rule::HeavyDivider => rule::Appearance { color: self.current_container().divider.into(), width: 4, - radius: 4.0, + radius: 4.0.into(), fill_mode: rule::FillMode::Full, }, Rule::Custom(f) => f(self), @@ -876,12 +897,12 @@ impl scrollable::StyleSheet for Theme { background: Some(Background::Color( self.current_container().component.base.into(), )), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: self.current_container().component.divider.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -899,12 +920,12 @@ impl scrollable::StyleSheet for Theme { background: Some(Background::Color( self.current_container().component.hover.into(), )), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: theme.accent.base.into(), - border_radius: 4.0, + border_radius: 4.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -1028,14 +1049,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0, + border_radius: 8.0.into(), 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_radius: 24.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), @@ -1051,14 +1072,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0, + border_radius: 8.0.into(), 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_radius: 24.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), @@ -1074,14 +1095,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0, + border_radius: 8.0.into(), 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_radius: 24.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs index 32123414..1e1f53c3 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::{renderer::BorderRadius, Background}; +use iced_core::{Background, BorderRadius}; 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::{renderer::BorderRadius, Background}; + use iced_core::{Background, BorderRadius}; 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::{renderer::BorderRadius, Background}; + use iced_core::{Background, BorderRadius}; 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 0a0df145..bf000006 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -186,7 +186,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -206,7 +206,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -221,7 +221,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, ) { self.container.draw( diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 84da98e6..3214b7fa 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -173,7 +173,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -193,7 +193,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -208,7 +208,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let theme = if let Some(layer) = self.layer { diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 64670a1c..2135f7f6 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -83,7 +83,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -103,7 +103,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -123,7 +123,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -208,7 +208,7 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -227,7 +227,7 @@ where fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -246,7 +246,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, ) { let bounds = layout.bounds(); self.content.borrow().as_widget().draw( diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 3d9f378c..6b84e5d2 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -185,7 +185,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -205,7 +205,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -220,7 +220,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let _ = self.tx.unbounded_send((self.id, layout.bounds())); diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index ea91b785..5cc2e1a7 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::{renderer::BorderRadius, Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// Appearance of the segmented button. #[derive(Default, Clone, Copy)] diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 272bb217..cee18b34 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -10,9 +10,9 @@ use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length, Point, Rectangle, Size, }; -use iced_core::renderer::BorderRadius; use iced_core::text::{LineHeight, Shaping}; use iced_core::widget::{self, operation, tree}; +use iced_core::BorderRadius; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use std::marker::PhantomData; @@ -303,7 +303,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor_position: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -311,10 +311,10 @@ where let bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - if bounds.contains(cursor_position) { + if cursor_position.is_over(bounds) { for (nth, key) in self.model.order.iter().copied().enumerate() { let bounds = self.variant_button_bounds(bounds, nth); - if bounds.contains(cursor_position) { + if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. state.hovered = key; @@ -322,13 +322,11 @@ where // If marked as closable, show a close icon. if self.model.items[key].closable { if let Some(on_close) = self.on_close.as_ref() { - if close_bounds( + if cursor_position.is_over(close_bounds( bounds, f32::from(self.icon_size), self.button_padding, - ) - .contains(cursor_position) - { + )) { if let Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, )) @@ -404,18 +402,15 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: iced::Point, + cursor_position: mouse::Cursor, _viewport: &iced::Rectangle, _renderer: &Renderer, ) -> iced_core::mouse::Interaction { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor_position.is_over(bounds) { for (nth, key) in self.model.order.iter().copied().enumerate() { - if self - .variant_button_bounds(bounds, nth) - .contains(cursor_position) - { + if cursor_position.is_over(self.variant_button_bounds(bounds, nth)) { return if self.model.items[key].enabled { iced_core::mouse::Interaction::Pointer } else { @@ -436,7 +431,7 @@ where theme: &::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: iced::Point, + _cursor_position: mouse::Cursor, _viewport: &iced::Rectangle, ) { let state = tree.state.downcast_ref::(); From 78a3a1f29a61354b7b7acf6b85e1c8a17e3eef72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 15 Jun 2023 11:38:25 -0400 Subject: [PATCH 0012/1276] fix CI --- .github/workflows/ci.yml | 7 +++---- Cargo.toml | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b641a1b6..247a23ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,11 +41,10 @@ jobs: fail-fast: false matrix: features: - - 'winit_tiny_skia debug' - - 'winit_tiny_skia tokio' - - winit_tiny_skia + - 'winit_debug' + - 'winit_tokio' + - winit - winit_wgpu - - tiny_skia - wayland runs-on: ubuntu-22.04 steps: diff --git a/Cargo.toml b/Cargo.toml index 30775ed9..1051dae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ wgpu = ["iced/wgpu", "iced_wgpu"] tokio = ["dep:tokio", "iced/tokio"] smol = ["iced/smol"] winit = ["iced/winit", "iced_winit"] +winit_tokio = ["iced/winit", "iced_winit", "tokio"] +winit_debug = ["iced/winit", "iced_winit", "debug"] winit_wgpu = ["winit", "wgpu"] [dependencies] From bf456a08ee81aaebcbcea87d94079c87dc032e28 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 15 Jun 2023 13:06:30 -0400 Subject: [PATCH 0013/1276] feat: animated togglers in the cosmic_sctk example --- Cargo.toml | 3 ++ examples/cosmic-sctk/Cargo.toml | 1 + examples/cosmic-sctk/src/window.rs | 67 +++++++++++++++++++++++++++--- examples/cosmic/Cargo.toml | 2 +- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1051dae5..771a2994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,3 +90,6 @@ members = [ exclude = [ "iced", ] + +[patch."https://github.com/pop-os/libcosmic"] +libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index e614bb88..3eca05a7 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="262545f", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 4a3976d0..3ab239e8 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -6,8 +6,9 @@ use cosmic::{ iced::{ wayland::window::{start_drag_window, toggle_maximize}, widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider}, - window, Color, + window, Color, Event, }, + iced_futures::Subscription, iced_style::application, theme::{self, Theme}, widget::{ @@ -17,12 +18,16 @@ use cosmic::{ }, Element, ElementExt, }; +use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use std::{ sync::atomic::{AtomicU32, Ordering}, vec, }; use theme::Button as ButtonTheme; +static DEBUG_TOGGLER: Lazy = Lazy::new(id::Toggler::unique); +static TOGGLER: Lazy = Lazy::new(id::Toggler::unique); + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Page { Demo, @@ -118,6 +123,7 @@ pub struct Window { exit: bool, rectangle_tracker: Option>, pub selection: segmented_button::SingleSelectModel, + timeline: Timeline, } impl Window { @@ -178,6 +184,29 @@ pub enum Message { NavBar(segmented_button::Entity), Ignore, Selection(segmented_button::Entity), + Tick(Instant), +} + +impl Window { + fn update_togglers(&mut self) { + let timeline = &mut self.timeline; + + let chain = if self.toggler_value { + chain::Toggler::on(TOGGLER.clone(), 1.) + } else { + chain::Toggler::off(TOGGLER.clone(), 1.) + }; + timeline.set_chain(chain); + + let chain = if self.debug { + chain::Toggler::on(DEBUG_TOGGLER.clone(), 1.) + } else { + chain::Toggler::off(DEBUG_TOGGLER.clone(), 1.) + }; + timeline.set_chain(chain); + + timeline.start(); + } } impl Application for Window { @@ -233,14 +262,20 @@ impl Application for Window { } } Message::Page(page) => self.page = page, - Message::Debug(debug) => self.debug = debug, + Message::Debug(debug) => { + self.debug = debug; + self.update_togglers(); + } Message::ThemeChanged(theme) => self.theme = theme, Message::ButtonPressed => {} Message::SliderChanged(value) => self.slider_value = value, Message::CheckboxToggled(value) => { self.checkbox_value = value; } - Message::TogglerToggled(value) => self.toggler_value = value, + Message::TogglerToggled(value) => { + self.toggler_value = value; + self.update_togglers(); + } Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::Close => self.exit = true, Message::ToggleNavBar => self.nav_bar_toggled = !self.nav_bar_toggled, @@ -262,6 +297,7 @@ impl Application for Window { }, Message::Ignore => {} Message::Selection(key) => self.selection.activate(key), + Message::Tick(now) => self.timeline.now(now), } Command::none() @@ -325,7 +361,14 @@ impl Application for Window { settings::view_section("Debug") .add(settings::item( "Debug layout", - toggler(String::from("Debug layout"), self.debug, Message::Debug), + container(anim!( + //toggler + DEBUG_TOGGLER, + &self.timeline, + String::from("Debug layout"), + self.debug, + |_chain, enable| { Message::Debug(enable) }, + )), )) .into(), settings::view_section("Buttons") @@ -359,7 +402,14 @@ impl Application for Window { settings::view_section("Controls") .add(settings::item( "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled), + anim!( + //toggler + TOGGLER, + &self.timeline, + None, + self.toggler_value, + |_chain, enable| { Message::TogglerToggled(enable) }, + ), )) .add(settings::item( "Pick List (TODO)", @@ -422,7 +472,12 @@ impl Application for Window { Message::Close } fn subscription(&self) -> iced::Subscription { - rectangle_tracker_subscription(0).map(|(_, e)| Message::Rectangle(e)) + Subscription::batch(vec![ + rectangle_tracker_subscription(0).map(|(_, e)| Self::Message::Rectangle(e)), + self.timeline + .as_subscription() + .map(|(_, instant)| Self::Message::Tick(instant)), + ]) } fn style(&self) -> ::Style { diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 5e97c675..894b1291 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -9,7 +9,7 @@ publish = false apply = "0.3.0" fraction = "0.13.0" libcosmic = { path = "../..", default-features = false, features = ["debug", "winit", "a11y"] } -once_cell = "1.15" +once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" From 944e77405f94f7494c171f75f79c590812fba15f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 15 Jun 2023 19:37:17 -0400 Subject: [PATCH 0014/1276] update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index c308e226..4b58fee5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c308e2266867a7bb4dfadb2dc8bdf310b7c87ee8 +Subproject commit 4b58fee5f68d7a9a2f9fc14ab441cff58555e630 From ce685b5aebabafafedf3480b1a41a68c3afd34ec Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 16 Jun 2023 12:30:22 -0400 Subject: [PATCH 0015/1276] fix(iced): always draw new surfaces --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 4b58fee5..681fee4d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 4b58fee5f68d7a9a2f9fc14ab441cff58555e630 +Subproject commit 681fee4d28d4e3b489bc5269fce985cf7791a020 From 42d7baf0d5cb14ab476120be9dfcaea9bd1d0be4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 21 Jun 2023 17:38:19 -0400 Subject: [PATCH 0016/1276] update iced: fixes scaling and autosize surface size issues --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 681fee4d..d36f1e98 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 681fee4d28d4e3b489bc5269fce985cf7791a020 +Subproject commit d36f1e98f5e6e6cb7da49bf52b21c4aa08666b16 From e3f30a1b5c57bd288879b685bef4d033119dac4a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 23 Jun 2023 23:39:48 +0200 Subject: [PATCH 0017/1276] feat(widget): add FlexRow widget --- src/widget/flex_row.rs | 132 +++++++++++++++++++++++++++++++++++++++++ src/widget/mod.rs | 3 + 2 files changed, 135 insertions(+) create mode 100644 src/widget/flex_row.rs diff --git a/src/widget/flex_row.rs b/src/widget/flex_row.rs new file mode 100644 index 00000000..586d44ce --- /dev/null +++ b/src/widget/flex_row.rs @@ -0,0 +1,132 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::Element; +use apply::Apply; +use derive_setters::Setters; +use iced::widget::{column, row}; +use iced_core::{alignment, Length, Size}; + +/// Responsively generates rows and columns of widgets based on its dimmensions. +#[derive(Setters)] +pub struct FlexRow<'a, Message> { + #[setters(skip)] + generator: Box (u16, Vec>) + 'a>, + /// Sets the space between each column of items. + column_spacing: u16, + /// Sets the space between each item in a row. + row_spacing: u16, + /// Sets the max number of items per row. + max_items: Option, + /// Sets the horizontal alignment of the [`FlexRow`]. + align_x: alignment::Horizontal, + /// Sets the vertical alignment of the [`FlexRow`]. + align_y: alignment::Vertical, + /// Sets the width of the [`FlexRow`]. + width: Length, + /// Sets the height of the [`FlexRow`]. + height: Length, +} + +/// Responsively generates rows and columns of widgets based on its dimmensions. +/// +/// The `generator` input is a closure which must return the max width of all +/// elements created, and a `Vec` containing the generated elements. +/// +/// ## Example +/// +/// Suppose that there is a `COLOR_VALUE` variable which contains an array of +/// color values, and a `color_button` function which creates an `Element` from +/// a color. +/// +/// We already know beforehand that our color buttons will have a fixed width +/// of `70`, so the `generator` closure returns this with a `Vec` of our color +/// button widgets. +/// +/// ```ignore +/// use iced_core::{alignment, Length}; +/// +/// let generator = |_size| { +/// let elements = COLOR_VALUES.iter() +/// .cloned() +/// .map(color_button) +/// .collect::>(); +/// +/// (70, elements) +/// }; +/// +/// cosmic::widget::flex_row(generator) +/// .column_spacing(12) +/// .row_spacing(16) +/// .width(Length::Fill) +/// .align_x(alignment::Horizontal::Center) +/// .into() +/// ``` +pub fn flex_row<'a, Message: 'static>( + generator: impl Fn(Size) -> (u16, Vec>) + 'a, +) -> FlexRow<'a, Message> { + FlexRow { + generator: Box::new(generator), + column_spacing: 4, + row_spacing: 4, + max_items: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + width: Length::Shrink, + height: Length::Shrink, + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(container: FlexRow<'a, Message>) -> Self { + iced::widget::responsive(move |size| { + let (item_width, mut elements) = (container.generator)(size); + + let mut items_per_row = flex_row_items( + size.width, + f32::from(item_width), + f32::from(container.row_spacing), + ) as usize; + + if let Some(max_items) = container.max_items { + items_per_row = items_per_row.max(max_items as usize); + } + + let mut elements_column = Vec::with_capacity(elements.len() / items_per_row); + + let mut iterator = elements.drain(..); + + while let Some(element) = iterator.next() { + let mut elements_row = Vec::with_capacity(items_per_row); + elements_row.push(element); + + for element in iterator.by_ref().take(items_per_row - 1) { + elements_row.push(element); + } + + elements_column.push(row(elements_row).spacing(container.row_spacing).into()); + } + + column(elements_column) + .spacing(container.column_spacing) + .apply(iced::widget::container) + .align_x(container.align_x) + .align_y(container.align_y) + .width(container.width) + .height(container.height) + .into() + }) + .into() + } +} + +#[allow(clippy::cast_precision_loss)] +fn flex_row_items(available: f32, item_width: f32, spacing: f32) -> u32 { + let mut items = 2; + + while available >= (item_width + spacing) * items as f32 - spacing { + items += 1; + } + + items - 1 +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index fab55020..1a6b3c17 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -8,6 +8,9 @@ pub mod aspect_ratio; mod button; pub use button::*; +pub mod flex_row; +pub use flex_row::{flex_row, FlexRow}; + mod header_bar; pub use header_bar::{header_bar, HeaderBar}; From b0db23a16923f7fb33faca870f1151951a494fb2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sat, 24 Jun 2023 13:25:23 +0200 Subject: [PATCH 0018/1276] perf(flexrow): provide reusable vec for storing elements --- Cargo.toml | 2 +- src/widget/flex_row.rs | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 771a2994..247744b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "cosmic" default = ["winit", "tokio", "a11y"] debug = ["iced/debug"] a11y = ["iced/a11y", "iced_accessibility"] -wayland = ["iced/wayland", "iced_sctk", "sctk",] +wayland = ["iced/wayland", "iced_sctk", "sctk"] wgpu = ["iced/wgpu", "iced_wgpu"] tokio = ["dep:tokio", "iced/tokio"] smol = ["iced/smol"] diff --git a/src/widget/flex_row.rs b/src/widget/flex_row.rs index 586d44ce..36f7aa7a 100644 --- a/src/widget/flex_row.rs +++ b/src/widget/flex_row.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use std::cell::RefCell; + use crate::Element; use apply::Apply; use derive_setters::Setters; @@ -11,7 +13,7 @@ use iced_core::{alignment, Length, Size}; #[derive(Setters)] pub struct FlexRow<'a, Message> { #[setters(skip)] - generator: Box (u16, Vec>) + 'a>, + generator: Box>, Size) -> u16 + 'a>, /// Sets the space between each column of items. column_spacing: u16, /// Sets the space between each item in a row. @@ -31,7 +33,7 @@ pub struct FlexRow<'a, Message> { /// Responsively generates rows and columns of widgets based on its dimmensions. /// /// The `generator` input is a closure which must return the max width of all -/// elements created, and a `Vec` containing the generated elements. +/// elements created, while storing elements in the provided `Vec`. /// /// ## Example /// @@ -40,22 +42,23 @@ pub struct FlexRow<'a, Message> { /// a color. /// /// We already know beforehand that our color buttons will have a fixed width -/// of `70`, so the `generator` closure returns this with a `Vec` of our color -/// button widgets. +/// of `70`, so we store elements in the provided `Vec` and return `70`. /// /// ```ignore /// use iced_core::{alignment, Length}; /// -/// let generator = |_size| { -/// let elements = COLOR_VALUES.iter() +/// let flex_row = cosmic::widget::flex_row(|vec, _size| { +/// let elements = DEFAULT_COLORS +/// .iter() /// .cloned() -/// .map(color_button) -/// .collect::>(); +/// .map(color_button); /// -/// (70, elements) -/// }; +/// vec.extend(elements); /// -/// cosmic::widget::flex_row(generator) +/// 70 +/// }); +/// +/// flex_row /// .column_spacing(12) /// .row_spacing(16) /// .width(Length::Fill) @@ -63,7 +66,7 @@ pub struct FlexRow<'a, Message> { /// .into() /// ``` pub fn flex_row<'a, Message: 'static>( - generator: impl Fn(Size) -> (u16, Vec>) + 'a, + generator: impl Fn(&mut Vec>, Size) -> u16 + 'a, ) -> FlexRow<'a, Message> { FlexRow { generator: Box::new(generator), @@ -79,8 +82,11 @@ pub fn flex_row<'a, Message: 'static>( impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(container: FlexRow<'a, Message>) -> Self { + let elements = RefCell::new(Vec::new()); + iced::widget::responsive(move |size| { - let (item_width, mut elements) = (container.generator)(size); + let mut elements = elements.borrow_mut(); + let item_width = (container.generator)(&mut elements, size); let mut items_per_row = flex_row_items( size.width, From 1562a80245d9d8058e11603e34d0b2e234f20805 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 26 Jun 2023 19:33:24 +0200 Subject: [PATCH 0019/1276] chore(iced): configurable natural scrolling --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d36f1e98..b227beb2 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d36f1e98f5e6e6cb7da49bf52b21c4aa08666b16 +Subproject commit b227beb26085be2d22ea9bc2e97c0a92994eb776 From b5a5e01de45fe38e1724657c8a56f26b92601b81 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 27 Jun 2023 13:00:53 -0400 Subject: [PATCH 0020/1276] update iced --- examples/cosmic-sctk/Cargo.toml | 2 +- iced | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index 3eca05a7..10490282 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="262545f", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="39c96ac", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/iced b/iced index b227beb2..67120473 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b227beb26085be2d22ea9bc2e97c0a92994eb776 +Subproject commit 671204737990a5d780b95e0ec261f9cd884a7dd1 From 456b2ddcd569782748e0f42533f247512a6425fe Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 3 Jul 2023 14:33:25 -0700 Subject: [PATCH 0021/1276] config: Ignore some filesystem events (#125) APIs like inotify can provide notification for some events that don't change the contents of a file. It makes sense to ignore those here, even if the expected way to write is through the config API. This reduces the number of events on the directory received by a single `:w` in vim from 12 to 8. And reduces a `touch` from 2 events to 1. Atomic writes through the config API only result in 1 event per-setting, before and after this change. --- cosmic-config/src/lib.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index a7b2be63..20e03a55 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -2,7 +2,10 @@ use iced_futures::futures::channel::mpsc; #[cfg(feature = "subscription")] use iced_futures::subscription; -use notify::{RecommendedWatcher, Watcher}; +use notify::{ + event::{EventKind, ModifyKind}, + RecommendedWatcher, Watcher, +}; use serde::{de::DeserializeOwned, Serialize}; use std::{ borrow::Cow, @@ -137,8 +140,16 @@ impl Config { let mut watcher = notify::recommended_watcher(move |event_res: Result| { // println!("{:#?}", event_res); - match event_res { + match &event_res { Ok(event) => { + match &event.kind { + EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { + // Data not mutated + return; + } + _ => {} + } + let mut keys = Vec::new(); for path in event.paths.iter() { match path.strip_prefix(&watch_config.user_path) { From 598bfaa6111c38633cbcd9d875ef8384108fb40d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 7 Jul 2023 16:39:22 -0400 Subject: [PATCH 0022/1276] feat: icon default fallbacks --- examples/cosmic/src/window/demo.rs | 20 +++++--- src/widget/icon.rs | 73 +++++++++++++++++++-------- src/widget/segmented_button/widget.rs | 4 +- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 243ba077..095f2671 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -3,7 +3,7 @@ use cosmic::{ cosmic_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}, + theme::{self, Button as ButtonTheme, ThemeType}, widget::{ button, container, icon, segmented_button, segmented_selection, settings, spin_button, toggler, view_switcher, @@ -417,11 +417,19 @@ impl State { .padding(8) .width(Length::Fill) .into(), - container(text("Primary container with some text").size(24)) - .layer(cosmic_theme::Layer::Primary) - .padding(8) - .width(Length::Fill) - .into(), + container(column![ + text( + "Primary container with some text and a couple icons testing default fallbacks" + ) + .size(24), + icon("microphone-sensitivity-high-symbolic-test", 24) + .style(cosmic::theme::Svg::SymbolicActive), + icon("microphone-sensitivity-high-symbolic-test", 16).default_fallbacks(false) + ]) + .layer(cosmic_theme::Layer::Primary) + .padding(8) + .width(Length::Fill) + .into(), container(text("Secondary container with some text").size(24)) .layer(cosmic_theme::Layer::Secondary) .padding(8) diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 7691f733..9e31eea8 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -13,6 +13,7 @@ use std::{ borrow::Cow, collections::hash_map::DefaultHasher, ffi::OsStr, hash::Hash, hash::Hasher, path::Path, path::PathBuf, }; +use tracing::error; #[derive(Clone, Debug, Hash)] pub enum Handle { @@ -30,30 +31,30 @@ pub enum IconSource<'a> { impl<'a> IconSource<'a> { /// Loads the icon as either an image or svg [`Handle`]. #[must_use] - pub fn load(&self, size: u16, theme: Option<&str>, svg: bool) -> Handle { - let name_path_buffer: Option; + pub fn load( + &self, + size: u16, + theme: Option<&str>, + svg: bool, + default_fallbacks: bool, + ) -> Handle { + let mut name_path_buffer: Option; let icon: Option<&Path> = match self { IconSource::Handle(handle) => return handle.clone(), IconSource::Path(ref path) => Some(path), #[cfg(unix)] IconSource::Name(ref name) => { - let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { - let default_theme: &str = &default_theme.borrow(); - freedesktop_icons::lookup(name) - .with_size(size) - .with_theme(theme.unwrap_or(default_theme)) - .with_cache() - .find() - }); - - name_path_buffer = if icon.is_none() { - freedesktop_icons::lookup(name) - .with_size(size) - .with_cache() - .find() - } else { - icon - }; + name_path_buffer = None; + if let Some(path) = load_icon(name, size, theme) { + name_path_buffer = Some(path); + } else if default_fallbacks { + for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { + if let Some(path) = load_icon(name, size, theme) { + name_path_buffer = Some(path); + break; + } + } + } name_path_buffer.as_deref() } @@ -71,7 +72,7 @@ impl<'a> IconSource<'a> { let handle = if let Some(path) = icon { svg::Handle::from_path(path) } else { - eprintln!("svg icon '{self:?}' size {size} not found"); + error!("svg icon '{self:?}' size {size} not found"); svg::Handle::from_memory(Vec::new()) }; @@ -79,7 +80,7 @@ impl<'a> IconSource<'a> { } else if let Some(icon) = icon { Handle::Image(icon.into()) } else { - eprintln!("icon '{self:?}' size {size} not found"); + error!("icon '{self:?}' size {size} not found"); Handle::Image(image::Handle::from_memory(Vec::new())) } } @@ -189,6 +190,7 @@ pub struct Icon<'a> { #[setters(strip_option)] height: Option, force_svg: bool, + default_fallbacks: bool, } // XXX Hopefully this will be enough precision @@ -230,6 +232,7 @@ pub fn icon<'a>(source: impl Into>, size: u16) -> Icon<'a> { theme: None, width: None, force_svg: false, + default_fallbacks: true, } } @@ -266,7 +269,12 @@ impl<'a> Icon<'a> { std::mem::swap(&mut source, &mut self.source); iced::widget::lazy(hash, move |_| -> Element { - match source.load(self.size, self.theme.as_deref(), self.force_svg) { + match source.load( + self.size, + self.theme.as_deref(), + self.force_svg, + self.default_fallbacks, + ) { Handle::Svg(handle) => self.svg_element(handle), Handle::Image(handle) => self.raster_element(handle), } @@ -280,3 +288,24 @@ impl<'a, Message: 'static> From> for Element<'a, Message> { icon.into_element::() } } + +#[must_use] +pub fn load_icon(name: &str, size: u16, theme: Option<&str>) -> Option { + let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { + let default_theme = default_theme.borrow(); + freedesktop_icons::lookup(name) + .with_size(size) + .with_theme(theme.unwrap_or(&default_theme)) + .with_cache() + .find() + }); + + if icon.is_none() { + freedesktop_icons::lookup(name) + .with_size(size) + .with_cache() + .find() + } else { + icon + } +} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index cee18b34..d7114fa4 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -542,7 +542,7 @@ where bounds.x += offset; bounds.width -= offset; - match icon.load(self.icon_size, None, false) { + match icon.load(self.icon_size, None, false, true) { icon::Handle::Image(_handle) => { unimplemented!() } @@ -583,7 +583,7 @@ where let width = f32::from(self.icon_size); let icon_bounds = close_bounds(original_bounds, width, self.button_padding); - match self.close_icon.load(self.icon_size, None, false) { + match self.close_icon.load(self.icon_size, None, false, true) { icon::Handle::Image(_handle) => { unimplemented!() } From 56d24b2372ed699115fee777b4811c60419e2f66 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 10 Jul 2023 15:05:23 -0400 Subject: [PATCH 0023/1276] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 67120473..9bd267b1 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 671204737990a5d780b95e0ec261f9cd884a7dd1 +Subproject commit 9bd267b1a6a9cb5737e25403d55ac1b8c6be5075 From f17d52f37f06f4d35c2feb65a5da03516d374acf Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 17 Jul 2023 11:44:57 -0400 Subject: [PATCH 0024/1276] card widget --- src/theme/mod.rs | 84 +++++++++++++++++++++++++++++++++++++++- src/widget/card/mod.rs | 1 + src/widget/card/style.rs | 23 +++++++++++ src/widget/mod.rs | 3 ++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/widget/card/mod.rs create mode 100644 src/widget/card/style.rs diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 5ab9cde1..be997b97 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -193,6 +193,7 @@ pub enum Button { Link, LinkActive, Transparent, + Card, Custom { active: Box button::Appearance>, hover: Box button::Appearance>, @@ -220,6 +221,7 @@ impl Button { Button::LinkActive => &cosmic.accent, Button::Transparent => &TRANSPARENT_COMPONENT, Button::Deactivated => &theme.current_container().component, + Button::Card => &theme.current_container().component, Button::Custom { .. } => &TRANSPARENT_COMPONENT, } } @@ -237,6 +239,7 @@ impl button::StyleSheet for Theme { button::Appearance { border_radius: match style { Button::Link => 0.0.into(), + Button::Card => 8.0.into(), _ => 24.0.into(), }, background: match style { @@ -286,6 +289,30 @@ impl button::StyleSheet for Theme { ..active } } + + fn disabled(&self, style: &Self::Style) -> button::Appearance { + let active = self.active(style); + + if matches!(style, Button::Card) { + return active; + } + + button::Appearance { + shadow_offset: iced_core::Vector::default(), + background: active.background.map(|background| match background { + Background::Color(color) => Background::Color(Color { + a: color.a * 0.5, + ..color + }), + Background::Gradient(gradient) => Background::Gradient(gradient.mul_alpha(0.5)), + }), + text_color: Color { + a: active.text_color.a * 0.5, + ..active.text_color + }, + ..active + } + } } /* @@ -494,6 +521,7 @@ pub enum Container { Transparent, HeaderBar, Custom(Box container::Appearance>), + Card, } impl Container { @@ -560,6 +588,39 @@ impl container::StyleSheet for Theme { border_color: Color::TRANSPARENT, } } + Container::Card => { + let palette = self.cosmic(); + + match self.layer { + cosmic_theme::Layer::Background => container::Appearance { + text_color: Some(Color::from(palette.background.component.on)), + background: Some(iced::Background::Color( + palette.background.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + cosmic_theme::Layer::Primary => container::Appearance { + text_color: Some(Color::from(palette.primary.component.on)), + background: Some(iced::Background::Color( + palette.primary.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + cosmic_theme::Layer::Secondary => container::Appearance { + text_color: Some(Color::from(palette.secondary.component.on)), + background: Some(iced::Background::Color( + palette.secondary.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } } } } @@ -784,7 +845,7 @@ impl pane_grid::StyleSheet for Theme { }) } - fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { + fn hovered_region(&self, _style: &Self::Style) -> pane_grid::Appearance { let theme = self.cosmic(); pane_grid::Appearance { background: Background::Color(theme.bg_color().into()), @@ -1176,3 +1237,24 @@ pub fn theme_subscription(id: u64) -> Subscription { crate::theme::Theme::custom(Arc::new(theme)) }) } + +impl crate::widget::card::style::StyleSheet for Theme { + fn default(&self) -> crate::widget::card::style::Appearance { + let cosmic = self.cosmic(); + + match self.layer { + cosmic_theme::Layer::Background => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.background.component.hover.into()), + card_2: Background::Color(cosmic.background.component.pressed.into()), + }, + cosmic_theme::Layer::Primary => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.primary.component.hover.into()), + card_2: Background::Color(cosmic.primary.component.pressed.into()), + }, + cosmic_theme::Layer::Secondary => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.secondary.component.hover.into()), + card_2: Background::Color(cosmic.secondary.component.pressed.into()), + }, + } + } +} diff --git a/src/widget/card/mod.rs b/src/widget/card/mod.rs new file mode 100644 index 00000000..b878bb2c --- /dev/null +++ b/src/widget/card/mod.rs @@ -0,0 +1 @@ +pub mod style; diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs new file mode 100644 index 00000000..43528a6e --- /dev/null +++ b/src/widget/card/style.rs @@ -0,0 +1,23 @@ +use iced_core::{Background, Color}; + +/// Appearance of the cards. +#[derive(Clone, Copy)] +pub struct Appearance { + pub card_1: Background, + pub card_2: Background, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + card_1: Background::Color(Color::WHITE), + card_2: Background::Color(Color::WHITE), + } + } +} + +/// Defines the [`Appearance`] of a cards. +pub trait StyleSheet { + /// The default [`Appearance`] of the cards. + fn default(&self) -> Appearance; +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 1a6b3c17..461e7251 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -8,6 +8,9 @@ pub mod aspect_ratio; mod button; pub use button::*; +pub mod card; +pub use card::*; + pub mod flex_row; pub use flex_row::{flex_row, FlexRow}; From b9bd41483d047841d9e327649c87cec1471ae6a8 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 20 Jul 2023 14:05:23 -0700 Subject: [PATCH 0025/1276] Update iced (#131) --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 9bd267b1..09e0ef40 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 9bd267b1a6a9cb5737e25403d55ac1b8c6be5075 +Subproject commit 09e0ef402a2306cf81c57a95be8cbe5317e37c1b From a3ab6e93f3e308d75092484fbcf4fca1965ea269 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 21 Jul 2023 15:33:55 -0400 Subject: [PATCH 0026/1276] chore: update theme colors --- cosmic-theme/src/model/cosmic_palette.rs | 3 ++ cosmic-theme/src/model/dark.ron | 27 ++++++++------- cosmic-theme/src/model/derivation.rs | 42 ++++++++++++------------ cosmic-theme/src/model/light.ron | 27 ++++++++------- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 45a95561..6ad225eb 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -105,6 +105,8 @@ pub struct CosmicPaletteInner { /// System Neutrals /// A wider spread of dark colors for more general use. + pub neutral_0: C, + /// 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, @@ -166,6 +168,7 @@ impl From> for CosmicPaletteInner { gray_1: p.gray_1.into(), gray_2: p.gray_2.into(), gray_3: p.gray_3.into(), + neutral_0: p.neutral_0.into(), neutral_1: p.neutral_1.into(), neutral_2: p.neutral_2.into(), neutral_3: p.neutral_3.into(), diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index d8d0ba8f..9f283121 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -14,40 +14,43 @@ Dark ( c: "#FFF19E", ), gray_1: ( - c: "#1E1E1E", + c: "#1B1B1B", ), gray_2: ( - c: "#292929", + c: "#262626", ), gray_3: ( - c: "#2E2E2E", + c: "#303030", ), - neutral_1: ( + neutral_0: ( c: "#000000", ), + neutral_1: ( + c: "#1B1B1B", + ), neutral_2: ( - c: "#272727", + c: "#303030", ), neutral_3: ( - c: "#424242", + c: "#474747", ), neutral_4: ( - c: "#5D5D5D", + c: "#5E5E5E", ), neutral_5: ( - c: "#787878", + c: "#777777", ), neutral_6: ( - c: "#939393", + c: "#919191", ), neutral_7: ( - c: "#AEAEAE", + c: "#ABABAB", ), neutral_8: ( - c: "#C9C9C9", + c: "#C6C6C6", ), neutral_9: ( - c: "#E4E4E4", + c: "#E2E2E2", ), neutral_10: ( c: "#FFFFFF", diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index c030dd34..9c186c9f 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -334,7 +334,7 @@ where match (p, t) { (CosmicPalette::Dark(p), ComponentType::Background) => Self::component( p.gray_1, - p.neutral_1, + p.neutral_0, p.neutral_10, 0.08, p.blue, @@ -344,7 +344,7 @@ where (CosmicPalette::Dark(p), ComponentType::Primary) => Self::component( p.gray_2, - p.neutral_1, + p.neutral_0, p.neutral_10, 0.08, p.blue, @@ -354,7 +354,7 @@ where (CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component( p.gray_3, - p.neutral_1, + p.neutral_0, p.neutral_10, 0.08, p.blue, @@ -363,7 +363,7 @@ where ), (CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component( p.gray_1, - p.neutral_1, + p.neutral_0, p.neutral_10, 0.08, p.blue, @@ -372,7 +372,7 @@ where ), (CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component( p.gray_2, - p.neutral_1, + p.neutral_0, p.neutral_10, 0.08, p.blue, @@ -381,7 +381,7 @@ where ), (CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component( p.gray_3, - p.neutral_1, + p.neutral_0, p.neutral_10.clone(), 0.08, p.blue, @@ -391,8 +391,8 @@ where (CosmicPalette::Light(p), ComponentType::Background) => Component::component( p.gray_1.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 0.75, p.blue.clone(), p.neutral_8, @@ -400,8 +400,8 @@ where ), (CosmicPalette::Light(p), ComponentType::Primary) => Component::component( p.gray_2.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 0.9, p.blue.clone(), p.neutral_8, @@ -409,8 +409,8 @@ where ), (CosmicPalette::Light(p), ComponentType::Secondary) => Component::component( p.gray_3.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 1.0, p.blue.clone(), p.neutral_8, @@ -419,8 +419,8 @@ where (CosmicPalette::HighContrastLight(p), ComponentType::Background) => { Component::component( p.gray_1.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 0.75, p.blue.clone(), p.neutral_9, @@ -429,8 +429,8 @@ where } (CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component( p.gray_2.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 0.9, p.blue.clone(), p.neutral_9, @@ -439,8 +439,8 @@ where (CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => { Component::component( p.gray_3.clone(), - p.neutral_1.clone(), - p.neutral_1, + p.neutral_0.clone(), + p.neutral_0, 1.0, p.blue.clone(), p.neutral_9, @@ -459,21 +459,21 @@ where | (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()) + Component::colored_component(p.yellow.clone(), p.neutral_0, 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()) + Component::colored_component(p.green.clone(), p.neutral_0, 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()) + Component::colored_component(p.blue.clone(), p.neutral_0, p.blue.clone()) } } } diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 92951bb7..cd9f0f93 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -14,40 +14,43 @@ Light ( c: "#966800", ), gray_1: ( - c: "#DEDEDE", + c: "#DDDDDD", ), gray_2: ( - c: "#E9E9E9", + c: "#E8E8E8", ), gray_3: ( - c: "#F4F4F4", + c: "#F3F3F3", ), - neutral_1: ( + neutral_0: ( c: "#FFFFFF", ), + neutral_1: ( + c: "#E2E2E2", + ), neutral_2: ( - c: "#E4E4E4", + c: "#C6C6C6", ), neutral_3: ( - c: "#C9C9C9", + c: "#ABABAB", ), neutral_4: ( - c: "#AEAEAE", + c: "#919191", ), neutral_5: ( - c: "#939393", + c: "#777777", ), neutral_6: ( - c: "#787878", + c: "#5E5E5E", ), neutral_7: ( - c: "#5D5D5D", + c: "#474747", ), neutral_8: ( - c: "#424242", + c: "#303030", ), neutral_9: ( - c: "#272727", + c: "#1B1B1B", ), neutral_10: ( c: "#000000", From f77bd443d7f4c074040b3b8cc09ed35bd1ca3983 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 25 Jul 2023 09:31:07 -0700 Subject: [PATCH 0027/1276] Use same sctk commit as iced --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 247744b6..5d1140e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.7" tokio = { version = "1.24.2", optional = true } -sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", optional = true, tag = "themed-pointer"} +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", optional = true, rev = "c9940f4"} slotmap = "1.0.6" fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } From da9b6345d8af4326ba8fb4c268c8670dbba6ff9d Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 25 Jul 2023 18:56:01 -0700 Subject: [PATCH 0028/1276] Add 'disabled' style for text-input, instead of panicking with `todo!` --- src/theme/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index be997b97..69b945aa 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1191,11 +1191,14 @@ impl text_input::StyleSheet for Theme { } fn disabled_color(&self, _style: &Self::Style) -> Color { - todo!() + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.5; + neutral_9.into() } - fn disabled(&self, _style: &Self::Style) -> text_input::Appearance { - todo!() + fn disabled(&self, style: &Self::Style) -> text_input::Appearance { + self.active(style) } } From dd3f421c72a4a86d6e5abd0897e8067f0c30802b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 26 Jul 2023 16:28:47 -0400 Subject: [PATCH 0029/1276] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 09e0ef40..17a10240 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 09e0ef402a2306cf81c57a95be8cbe5317e37c1b +Subproject commit 17a10240bee93add36c33f451bd028579f89718e From ab88a5b59f2b5b5635abe58bb2ba4d009d512989 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 26 Jul 2023 16:32:23 -0400 Subject: [PATCH 0030/1276] feat: animated image widget --- Cargo.toml | 4 + src/widget/frames.rs | 407 +++++++++++++++++++++++++++++++++++++++++++ src/widget/mod.rs | 3 + 3 files changed, 414 insertions(+) create mode 100644 src/widget/frames.rs diff --git a/Cargo.toml b/Cargo.toml index 5d1140e4..68649e05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ winit = ["iced/winit", "iced_winit"] winit_tokio = ["iced/winit", "iced_winit", "tokio"] winit_debug = ["iced/winit", "iced_winit", "debug"] winit_wgpu = ["winit", "wgpu"] +animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] [dependencies] apply = "0.3.0" @@ -30,6 +31,9 @@ slotmap = "1.0.6" fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } tracing = "0.1" +image = { version = "0.24.6", optional = true } +thiserror = "1.0.44" +async-fs = { version = "1.6", optional = true } [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.2" diff --git a/src/widget/frames.rs b/src/widget/frames.rs new file mode 100644 index 00000000..7be04d5d --- /dev/null +++ b/src/widget/frames.rs @@ -0,0 +1,407 @@ +//! Display an animated image in your user interface +//! Based on + +use std::ffi::OsStr; +use std::fmt; +use std::io; +use std::path::Path; +use std::time::{Duration, Instant}; + +use ::image as image_rs; +use iced_core::image::Renderer as ImageRenderer; +use iced_core::mouse::Cursor; +use iced_core::widget::{tree, Tree}; +use iced_core::{ + event, layout, renderer, window, Clipboard, ContentFit, Element, Event, Layout, Length, + Rectangle, Shell, Size, Vector, Widget, +}; +use iced_runtime::Command; +use iced_widget::image::{self, Handle}; +use image_rs::codecs::gif::GifDecoder; +use image_rs::codecs::png::PngDecoder; +use image_rs::codecs::webp::WebPDecoder; +use image_rs::AnimationDecoder; + +#[cfg(not(feature = "tokio"))] +use iced_futures::futures::{AsyncRead, AsyncReadExt}; +#[cfg(feature = "tokio")] +use tokio::io::{AsyncRead, AsyncReadExt}; + +use super::icon::load_icon; + +#[must_use] +/// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] +pub fn animated_image(frames: &Frames) -> AnimatedImage { + AnimatedImage::new(frames) +} + +/// Error loading or decoding a animated_image +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Decode error + #[error(transparent)] + Image(#[from] image_rs::ImageError), + /// Load error + #[error(transparent)] + Io(#[from] std::io::Error), + /// Missing image + #[error("The image with the requested name is missing")] + Missing, + /// Unsupported Extension + #[error("The extension is unsupported")] + Extension, +} + +#[derive(Clone)] +/// The frames of a decoded gif +pub struct Frames { + first: Frame, + frames: Vec, + total_bytes: u64, +} + +impl fmt::Debug for Frames { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Frames").finish() + } +} + +impl Frames { + /// Load [`Frames`] from the supplied name + pub fn load_from_name( + name: &str, + size: u16, + theme: Option<&str>, + default_fallbacks: bool, + ) -> Command> { + let mut name_path_buffer = None; + if let Some(path) = load_icon(name, size, theme) { + name_path_buffer = Some(path); + } else if default_fallbacks { + for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { + if let Some(path) = load_icon(name, size, theme) { + name_path_buffer = Some(path); + break; + } + } + }; + + if let Some(name_path_buffer) = name_path_buffer { + Self::load_from_path(name_path_buffer) + } else { + Command::perform(async { Err(Error::Missing) }, std::convert::identity) + } + } + + /// Load [`Frames`] from the supplied path + pub fn load_from_path(path: impl AsRef) -> Command> { + #[cfg(feature = "tokio")] + use tokio::fs::File; + #[cfg(feature = "tokio")] + use tokio::io::BufReader; + + #[cfg(not(feature = "tokio"))] + use async_fs::File; + #[cfg(not(feature = "tokio"))] + use iced_futures::futures::io::BufReader; + + let path = path.as_ref().to_path_buf(); + + let f = async move { + let image_type = match &path.extension() { + Some(ext) if ext == &OsStr::new("gif") => ImageType::Gif, + Some(ext) if ext == &OsStr::new("apng") => ImageType::Apng, + Some(ext) if ext == &OsStr::new("webp") => ImageType::WebP, + _ => return Err(Error::Extension), + }; + let reader = BufReader::new(File::open(path).await?); + + Self::from_reader(reader, image_type).await + }; + + Command::perform(f, std::convert::identity) + } + + /// Decode [`Frames`] from the supplied async reader + /// # Errors + /// If the type of image is not supported this function will error. IO errors may also occur. + pub async fn from_reader( + reader: R, + image_type: ImageType, + ) -> Result { + use iced_futures::futures::pin_mut; + + pin_mut!(reader); + + let mut bytes = vec![]; + + reader.read_to_end(&mut bytes).await?; + + match image_type { + ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), + ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), + } + } + + /// Decode [`Frames`] from the supplied bytes + /// # Errors + /// + /// IO errors may occur. + /// + /// # Panics + /// + /// If there are no frames in the image, this panics. + pub fn from_decoder<'a, T: AnimationDecoder<'a>>(decoder: T) -> Result { + let frames = decoder + .into_frames() + .map(|result| result.map(Frame::from)) + .collect::, _>>()?; + + let first = frames.first().cloned().unwrap(); + let total_bytes = frames + .iter() + .map(|f| match f.handle.data() { + iced_core::image::Data::Path(_) => 0, + iced_core::image::Data::Bytes(b) => b.len(), + iced_core::image::Data::Rgba { pixels, .. } => pixels.len(), + }) + .sum::() + .try_into() + .unwrap_or_default(); + Ok(Frames { + first, + frames, + total_bytes, + }) + } +} + +#[derive(Clone)] +struct Frame { + delay: Duration, + handle: image::Handle, +} + +impl From for Frame { + fn from(frame: image_rs::Frame) -> Self { + let (width, height) = frame.buffer().dimensions(); + + let delay = frame.delay().into(); + + let handle = image::Handle::from_pixels(width, height, frame.into_buffer().into_vec()); + + Self { delay, handle } + } +} + +struct State { + index: usize, + current: Current, + total_bytes: u64, +} + +struct Current { + frame: Frame, + started: Instant, +} + +impl From for Current { + fn from(frame: Frame) -> Self { + Self { + started: Instant::now(), + frame, + } + } +} + +/// A frame that displays an animated image while keeping aspect ratio +#[derive(Debug)] +pub struct AnimatedImage<'a> { + frames: &'a Frames, + width: Length, + height: Length, + content_fit: ContentFit, +} + +pub enum ImageType { + Gif, + Apng, + WebP, +} + +impl<'a> AnimatedImage<'a> { + #[must_use] + /// Creates a new [`AnimatedImage`] with the given [`Frames`] + pub fn new(frames: &'a Frames) -> Self { + AnimatedImage { + frames, + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + } + } + + #[must_use] + /// Sets the width of the [`AnimatedImage`] boundaries. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + #[must_use] + /// Sets the height of the [`AnimatedImage`] boundaries. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + #[must_use] + /// Sets the [`ContentFit`] of the [`AnimatedImage`]. + /// + /// Defaults to [`ContentFit::Contain`] + pub fn content_fit(self, content_fit: ContentFit) -> Self { + Self { + content_fit, + ..self + } + } +} + +impl<'a, Message, Renderer> Widget for AnimatedImage<'a> +where + Renderer: ImageRenderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + index: 0, + current: self.frames.first.clone().into(), + total_bytes: self.frames.total_bytes, + }) + } + + fn diff(&mut self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + + // Reset state if new gif Frames is used w/ + // same state tree. + // + // Total bytes of the gif should be a good enough + // proxy for it changing. + if state.total_bytes != self.frames.total_bytes { + *state = State { + index: 0, + current: self.frames.first.clone().into(), + total_bytes: self.frames.total_bytes, + }; + } + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + iced_widget::image::layout( + renderer, + limits, + &self.frames.first.handle, + self.width, + self.height, + self.content_fit, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor_position: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + let elapsed = now.duration_since(state.current.started); + + if elapsed > state.current.frame.delay { + state.index = (state.index + 1) % self.frames.frames.len(); + + state.current = self.frames.frames[state.index].clone().into(); + + shell.request_redraw(window::RedrawRequest::At(now + state.current.frame.delay)); + } else { + let remaining = state.current.frame.delay - elapsed; + + shell.request_redraw(window::RedrawRequest::At(now + remaining)); + } + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Cursor, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + + // Pulled from iced_native::widget::::draw + // + // TODO: export iced_native::widget::image::draw as standalone function + { + let Size { width, height } = renderer.dimensions(&state.current.frame.handle); + let image_size = Size::new(width as f32, height as f32); + + let bounds = layout.bounds(); + let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + + let render = |renderer: &mut Renderer| { + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + + let drawing_bounds = Rectangle { + width: adjusted_fit.width, + height: adjusted_fit.height, + ..bounds + }; + + renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); + }; + + if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } + } + } +} + +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> +where + Renderer: ImageRenderer + 'a, +{ + fn from(gif: AnimatedImage<'a>) -> Element<'a, Message, Renderer> { + Element::new(gif) + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 461e7251..5e96cdf9 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -20,6 +20,9 @@ pub use header_bar::{header_bar, HeaderBar}; pub mod icon; pub use icon::{icon, Icon, IconSource}; +#[cfg(feature = "animated-image")] +pub mod frames; + pub mod list; pub use list::*; From 4895b0c9bda9e46fc7db173e239d155dac957186 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 28 Jul 2023 12:44:57 -0700 Subject: [PATCH 0031/1276] config: Implement `fmt::Display` and `std::error::Error` for `Error` (#136) --- cosmic-config/src/lib.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 20e03a55..e3d82138 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -9,7 +9,7 @@ use notify::{ use serde::{de::DeserializeOwned, Serialize}; use std::{ borrow::Cow, - fs, + fmt, fs, hash::Hash, io::Write, path::{Path, PathBuf}, @@ -33,6 +33,22 @@ pub enum Error { RonSpanned(ron::error::SpannedError), } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::AtomicWrites(err) => err.fmt(f), + Self::InvalidName(name) => write!(f, "invalid config name '{}'", name), + Self::Io(err) => err.fmt(f), + Self::NoConfigDirectory => write!(f, "cosmic config directory not found"), + Self::Notify(err) => err.fmt(f), + Self::Ron(err) => err.fmt(f), + Self::RonSpanned(err) => err.fmt(f), + } + } +} + +impl std::error::Error for Error {} + impl From> for Error { fn from(f: atomicwrites::Error) -> Self { Self::AtomicWrites(f) From 5745ed3ffea7ae8f3f624a891812b519d0dd3f78 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 17 Jul 2023 11:52:07 -0400 Subject: [PATCH 0032/1276] chore: examples of animated card usage --- examples/cosmic-sctk/Cargo.toml | 3 +- examples/cosmic-sctk/src/window.rs | 49 ++++++++++++++++++++++--- examples/cosmic/Cargo.toml | 1 + examples/cosmic/src/window.rs | 11 ++++++ examples/cosmic/src/window/demo.rs | 58 +++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index 10490282..b32527d2 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,4 +7,5 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="39c96ac", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="8ab038b", default-features = false, features = ["libcosmic", "once_cell"] } +# cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 3ab239e8..70ac3bc9 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -10,9 +10,10 @@ use cosmic::{ }, iced_futures::Subscription, iced_style::application, + iced_widget::text, theme::{self, Theme}, widget::{ - button, header_bar, nav_bar, nav_bar_toggle, + button, cosmic_container, header_bar, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, scrollable, segmented_button, segmented_selection, settings, toggler, IconSource, }, @@ -27,6 +28,7 @@ use theme::Button as ButtonTheme; static DEBUG_TOGGLER: Lazy = Lazy::new(id::Toggler::unique); static TOGGLER: Lazy = Lazy::new(id::Toggler::unique); +static CARDS: Lazy = Lazy::new(id::Cards::unique); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Page { @@ -114,6 +116,7 @@ pub struct Window { slider_value: f32, checkbox_value: bool, toggler_value: bool, + cards_value: bool, pick_list_selected: Option<&'static str>, nav_bar_pages: segmented_button::SingleSelectModel, nav_bar_toggled_condensed: bool, @@ -171,6 +174,7 @@ pub enum Message { SliderChanged(f32), CheckboxToggled(bool), TogglerToggled(bool), + CardsToggled(bool), PickListSelected(&'static str), RowSelected(usize), Close, @@ -207,6 +211,17 @@ impl Window { timeline.start(); } + + fn update_cards(&mut self) { + let timeline = &mut self.timeline; + let chain = if self.cards_value { + chain::Cards::on(CARDS.clone(), 1.) + } else { + chain::Cards::off(CARDS.clone(), 1.) + }; + timeline.set_chain(chain); + timeline.start(); + } } impl Application for Window { @@ -276,6 +291,10 @@ impl Application for Window { self.toggler_value = value; self.update_togglers(); } + Message::CardsToggled(value) => { + self.cards_value = value; + self.update_cards(); + } Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::Close => self.exit = true, Message::ToggleNavBar => self.nav_bar_toggled = !self.nav_bar_toggled, @@ -288,9 +307,7 @@ impl Application for Window { Message::RowSelected(row) => println!("Selected row {row}"), Message::InputChanged => {} Message::Rectangle(r) => match r { - RectangleUpdate::Rectangle(r) => { - dbg!(r); - } + RectangleUpdate::Rectangle(_) => {} RectangleUpdate::Init(t) => { self.rectangle_tracker.replace(t); } @@ -436,6 +453,30 @@ impl Application for Window { segmented_selection::horizontal(&self.selection) .on_activate(Message::Selection), )) + .add(settings::item( + "Cards", + cosmic_container::container(anim!( + //cards + CARDS, + &self.timeline, + vec![ + text("Card 1").size(24).width(Length::Fill).into(), + text("Card 2").size(24).width(Length::Fill).into(), + text("Card 3").size(24).width(Length::Fill).into(), + text("Card 4").size(24).width(Length::Fill).into(), + ], + Message::Ignore, + |_, e| Message::CardsToggled(e), + "Show More", + "Show Less", + "Clear All", + None, + self.cards_value, + )) + .layer(cosmic::cosmic_theme::Layer::Secondary) + .padding(16) + .style(cosmic::theme::Container::Secondary), + )) .into(), ]) .into(); diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 894b1291..221d901c 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -13,3 +13,4 @@ once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="8ab038b", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index d25e053e..3352bb81 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -17,9 +17,12 @@ use cosmic::{ }, Element, ElementExt, }; +use cosmic_time::{Instant, Timeline}; use log::error; use std::{ borrow::Cow, + cell::RefCell, + rc::Rc, sync::{ atomic::{AtomicU32, Ordering}, Arc, @@ -161,6 +164,7 @@ pub struct Window { scale_factor: f64, scale_factor_string: String, system_theme: Arc, + timeline: Rc>, } impl Window { @@ -206,6 +210,7 @@ pub enum Message { ToggleWarning, FontsLoaded, SystemTheme(CosmicTheme), + Tick(Instant), } impl From for Message { @@ -352,6 +357,7 @@ impl Application for Window { window.insert_page(Page::TimeAndLanguage(None)); window.insert_page(Page::Accessibility); window.insert_page(Page::Applications); + window.demo.timeline = window.timeline.clone(); (window, load_fonts().map(|_| Message::FontsLoaded)) } @@ -393,6 +399,10 @@ impl Application for Window { Message::SystemTheme(t.into_srgba()) } }), + self.timeline + .borrow() + .as_subscription() + .map(|(_, instant)| Self::Message::Tick(instant)), ]) } @@ -452,6 +462,7 @@ impl Application for Window { Message::SystemTheme(t) => { self.system_theme = Arc::new(t); } + Message::Tick(instant) => self.timeline.borrow_mut().now(instant), } ret } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 095f2671..4c8b2cb9 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use apply::Apply; use cosmic::{ cosmic_theme, @@ -10,11 +12,14 @@ use cosmic::{ }, Element, }; +use cosmic_time::{anim, chain, Timeline}; use fraction::{Decimal, ToPrimitive}; use once_cell::sync::Lazy; use super::{Page, Window}; +static CARDS: Lazy = Lazy::new(cosmic_time::id::Cards::unique); + #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] pub enum ThemeVariant { Light, @@ -76,6 +81,8 @@ pub enum Message { TogglerToggled(bool), ViewSwitcher(segmented_button::Entity), InputChanged(String), + ClearAll, + CardsToggled(bool), } pub enum Output { @@ -98,6 +105,9 @@ pub struct State { pub toggler_value: bool, pub view_switcher: segmented_button::SingleSelectModel, pub entry_value: String, + pub cards_value: bool, + cards: Vec, + pub timeline: Rc>, } impl Default for State { @@ -135,7 +145,15 @@ impl Default for State { .insert(|b| b.text("Segmented Button").data(DemoView::TabB)) .insert(|b| b.text("Tab C").data(DemoView::TabC)) .build(), + cards_value: false, entry_value: String::new(), + cards: vec![ + "card 1".to_string(), + "card 2".to_string(), + "card 3".to_string(), + "card 4".to_string(), + ], + timeline: Rc::new(RefCell::new(Default::default())), } } } @@ -171,6 +189,13 @@ impl State { Message::InputChanged(s) => { self.entry_value = s; } + Message::ClearAll => { + self.cards.clear(); + } + Message::CardsToggled(v) => { + self.cards_value = v; + self.update_cards(); + } } None @@ -203,7 +228,7 @@ impl State { let choose_icon_theme = segmented_selection::horizontal(&self.icon_themes).on_activate(Message::IconTheme); - + let timeline = self.timeline.borrow(); settings::view_column(vec![ window.page_title(Page::Demo), view_switcher::horizontal(&self.view_switcher) @@ -435,6 +460,26 @@ impl State { .padding(8) .width(Length::Fill) .into(), + container(anim!( + //cards + CARDS, + &timeline, + self.cards + .iter() + .map(|c| text(c).size(24).width(Length::Fill).into()) + .collect(), + Message::ClearAll, + |_, e| Message::CardsToggled(e), + "Show More", + "Show Less", + "Clear All", + None, + self.cards_value, + )) + .layer(cosmic::cosmic_theme::Layer::Secondary) + .padding(16) + .style(cosmic::theme::Container::Secondary) + .into(), text_input( "Type to search apps or type “?” for more options...", &self.entry_value, @@ -448,4 +493,15 @@ impl State { ]) .into() } + + fn update_cards(&mut self) { + let mut timeline = self.timeline.borrow_mut(); + let chain = if self.cards_value { + chain::Cards::on(CARDS.clone(), 1.) + } else { + chain::Cards::off(CARDS.clone(), 1.) + }; + timeline.set_chain(chain); + timeline.start(); + } } From 785861a630ba186bc070d4d4b870db6eb0c49c00 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 18 Jul 2023 10:53:45 -0400 Subject: [PATCH 0033/1276] example: update cosmic-time and add extra row to each card --- examples/cosmic-sctk/Cargo.toml | 2 +- examples/cosmic/Cargo.toml | 2 +- examples/cosmic/src/window/demo.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index b32527d2..4f290f4a 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,5 +7,5 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="8ab038b", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="178637c", default-features = false, features = ["libcosmic", "once_cell"] } # cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 221d901c..1c2100a9 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -13,4 +13,4 @@ once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="8ab038b", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="178637c", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 4c8b2cb9..ccf18f0e 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -466,7 +466,7 @@ impl State { &timeline, self.cards .iter() - .map(|c| text(c).size(24).width(Length::Fill).into()) + .map(|c| column![text("test"), text(c).size(24).width(Length::Fill)].into()) .collect(), Message::ClearAll, |_, e| Message::CardsToggled(e), From e24465ba37a6d5970238edcd90e79bb4d9fc9ae7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 21 Jul 2023 12:37:17 -0400 Subject: [PATCH 0034/1276] update(example): add button to cards and update cosmic-time --- examples/cosmic-sctk/Cargo.toml | 2 +- examples/cosmic/Cargo.toml | 2 +- examples/cosmic/src/window/demo.rs | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index 4f290f4a..cb9332bd 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,5 +7,5 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="178637c", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c39e737", default-features = false, features = ["libcosmic", "once_cell"] } # cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 1c2100a9..0eed3000 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -13,4 +13,4 @@ once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="178637c", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c39e737", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index ccf18f0e..bd0da06c 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -81,6 +81,7 @@ pub enum Message { TogglerToggled(bool), ViewSwitcher(segmented_button::Entity), InputChanged(String), + DeleteCard(usize), ClearAll, CardsToggled(bool), } @@ -196,6 +197,9 @@ impl State { self.cards_value = v; self.update_cards(); } + Message::DeleteCard(i) => { + self.cards.remove(i); + } } None @@ -466,7 +470,14 @@ impl State { &timeline, self.cards .iter() - .map(|c| column![text("test"), text(c).size(24).width(Length::Fill)].into()) + .enumerate() + .map(|(i, c)| column![ + button(cosmic::theme::Button::Text) + .text("Delete me") + .on_press(Message::DeleteCard(i)), + text(c).size(24).width(Length::Fill) + ] + .into()) .collect(), Message::ClearAll, |_, e| Message::CardsToggled(e), From a223b60a0c60b982d4abf97a9c11ae52d9a392a5 Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Wed, 2 Aug 2023 11:54:07 +0200 Subject: [PATCH 0035/1276] feat!: implement Application API --- .vscode/settings.json | 5 +- Cargo.toml | 2 +- examples/application/Cargo.toml | 13 ++ examples/application/src/main.rs | 131 ++++++++++++ examples/cosmic-sctk/src/main.rs | 9 +- examples/cosmic-sctk/src/window.rs | 11 +- examples/cosmic/src/main.rs | 6 +- examples/cosmic/src/window.rs | 5 +- examples/cosmic/src/window/demo.rs | 2 +- justfile | 14 ++ src/app/command.rs | 63 ++++++ src/app/core.rs | 144 +++++++++++++ src/app/cosmic.rs | 290 +++++++++++++++++++++++++ src/app/mod.rs | 331 +++++++++++++++++++++++++++++ src/app/settings.rs | 80 +++++++ src/command.rs | 116 ++++++++++ src/icon_theme.rs | 20 ++ src/keyboard_nav.rs | 82 +++---- src/lib.rs | 34 +-- src/settings.rs | 34 --- src/theme/mod.rs | 2 +- src/track.rs | 40 ++++ src/widget/header_bar.rs | 103 +++++++-- src/widget/icon.rs | 4 +- src/widget/nav_bar.rs | 3 + src/widget/nav_bar_toggle.rs | 14 +- 26 files changed, 1420 insertions(+), 138 deletions(-) create mode 100644 examples/application/Cargo.toml create mode 100644 examples/application/src/main.rs create mode 100644 justfile create mode 100644 src/app/command.rs create mode 100644 src/app/core.rs create mode 100644 src/app/cosmic.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/settings.rs create mode 100644 src/command.rs create mode 100644 src/icon_theme.rs delete mode 100644 src/settings.rs create mode 100644 src/track.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index b04a057b..ee258746 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "rust-analyzer.check.overrideCommand": [ - "cargo", "clippy", "--no-deps", "--message-format=json", "--", "-W", "clippy::pedantic" - ] + "rust-analyzer.check.overrideCommand": ["just", "check-json"], + "git-blame.gitWebUrl": "" } diff --git a/Cargo.toml b/Cargo.toml index 68649e05..dea5d0cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" name = "cosmic" [features] -default = ["winit", "tokio", "a11y"] +default = ["wayland", "tokio", "a11y"] debug = ["iced/debug"] a11y = ["iced/a11y", "iced_accessibility"] wayland = ["iced/wayland", "iced_sctk", "sctk"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml new file mode 100644 index 00000000..b05d86a8 --- /dev/null +++ b/examples/application/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "application" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.37" +tracing-subscriber = "0.3.17" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "wayland", "tokio"] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs new file mode 100644 index 00000000..78332018 --- /dev/null +++ b/examples/application/src/main.rs @@ -0,0 +1,131 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Testing ground for improving COSMIC application API ergonomics. + +use cosmic::app::{Command, Core, Settings}; +use cosmic::widget::nav_bar; +use cosmic::{executor, iced, ApplicationExt, Element}; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + let input = vec![ + ("Page 1".into(), "🖖 Hello from libcosmic.".into()), + ("Page 2".into(), "🌟 This is an example application.".into()), + ("Page 3".into(), "🚧 The libcosmic API is not stable yet.".into()), + ("Page 4".into(), "🚀 Copy the source code and experiment today!".into()), + ]; + + let settings = Settings::default() + .antialiasing(true) + .client_decorations(true) + .debug(false) + .default_icon_theme("Pop") + .default_text_size(16.0) + .scale_factor(1.0) + .size((1024, 768)) + .theme(cosmic::Theme::dark()); + + cosmic::app::run::(settings, input)?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message {} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + nav_model: nav_bar::Model, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = Vec<(String, String)>; + + /// Message type specific to our [`App`]. + type Message = Message; + + const APP_ID: &'static str = "org.cosmic.AppDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, input: Self::Flags) -> (Self, Command) { + let mut nav_model = nav_bar::Model::default(); + + for (title, content) in input { + nav_model.insert().text(title).data(content); + } + + nav_model.activate_position(0); + + let mut app = App { core, nav_model }; + + let command = app.update_title(); + + (app, command) + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav_model) + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + self.nav_model.activate(id); + self.update_title() + } + + fn update(&mut self, _message: Self::Message) -> Command { + Command::none() + } + + fn view(&self) -> Element { + let page_content = self + .nav_model + .active_data::() + .map(String::as_str) + .unwrap_or("No page selected"); + + let text = cosmic::widget::text(page_content); + + let centered = iced::widget::container(text) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); + + Element::from(centered) + } +} + +impl App +where + Self: cosmic::Application, +{ + fn active_page_title(&mut self) -> &str { + self.nav_model + .text(self.nav_model.active()) + .unwrap_or("Unknown Page") + } + + fn update_title(&mut self) -> Command { + let title = self.active_page_title().to_owned(); + self.set_title(title) + } +} diff --git a/examples/cosmic-sctk/src/main.rs b/examples/cosmic-sctk/src/main.rs index 16c9af71..7ce3ddcb 100644 --- a/examples/cosmic-sctk/src/main.rs +++ b/examples/cosmic-sctk/src/main.rs @@ -1,14 +1,11 @@ -use cosmic::{ - iced::{wayland::InitialSurface, Application}, - settings, -}; +use cosmic::iced::{wayland::InitialSurface, Application, Settings}; mod window; pub use window::Window; pub fn main() -> cosmic::iced::Result { - settings::set_default_icon_theme("Pop"); - let mut settings = settings(); + cosmic::icon_theme::set_default("Pop"); + let mut settings = Settings::default(); settings.initial_surface = InitialSurface::XdgWindow(Default::default()); Window::run(settings) } diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 70ac3bc9..76a857d0 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -6,7 +6,7 @@ use cosmic::{ iced::{ wayland::window::{start_drag_window, toggle_maximize}, widget::{column, container, horizontal_space, pick_list, progress_bar, row, slider}, - window, Color, Event, + window, Color, }, iced_futures::Subscription, iced_style::application, @@ -15,7 +15,7 @@ use cosmic::{ widget::{ button, cosmic_container, header_bar, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, - scrollable, segmented_button, segmented_selection, settings, toggler, IconSource, + scrollable, segmented_button, segmented_selection, settings, IconSource, }, Element, ElementExt, }; @@ -336,9 +336,8 @@ impl Application for Window { .on_drag(Message::Drag) .start( nav_bar_toggle() - .on_nav_bar_toggled(nav_bar_message) - .nav_bar_active(nav_bar_toggled) - .into(), + .on_toggle(nav_bar_message) + .active(nav_bar_toggled), ); if self.show_maximize { @@ -509,7 +508,7 @@ impl Application for Window { self.theme.clone() } - fn close_requested(&self, id: window::Id) -> Self::Message { + fn close_requested(&self, _id: window::Id) -> Self::Message { Message::Close } fn subscription(&self) -> iced::Subscription { diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index 5700a590..f180b100 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use cosmic::{iced::Application, settings}; +use cosmic::iced::{Application, Settings}; mod window; use env_logger::Env; @@ -13,8 +13,8 @@ pub fn main() -> cosmic::iced::Result { .write_style_or("MY_LOG_STYLE", "always"); env_logger::init_from_env(env); - settings::set_default_icon_theme("Pop"); - let mut settings = settings(); + cosmic::icon_theme::set_default("Pop"); + let mut settings = Settings::default(); settings.window.min_size = Some((600, 300)); Window::run(settings) } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 3352bb81..cccb49d0 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -483,9 +483,8 @@ impl Application for Window { .on_drag(Message::Drag) .start( nav_bar_toggle() - .on_nav_bar_toggled(nav_bar_message) - .nav_bar_active(nav_bar_toggled) - .into(), + .on_toggle(nav_bar_message) + .active(nav_bar_toggled), ); if self.show_maximize { diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index bd0da06c..d5402a48 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -184,7 +184,7 @@ impl State { Message::IconTheme(key) => { self.icon_themes.activate(key); if let Some(theme) = self.icon_themes.text(key) { - cosmic::settings::set_default_icon_theme(theme); + cosmic::icon_theme::set_default(theme); } } Message::InputChanged(s) => { diff --git a/justfile b/justfile new file mode 100644 index 00000000..8b01b8ab --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +# Check for errors and linter warnings +check *args: + cargo clippy --no-deps {{args}} -- -W clippy::pedantic + cargo clippy --no-deps --no-default-features --features="winit,tokio" {{args}} -- -W clippy::pedantic + cargo check -p application {{args}} + cargo check -p cosmic {{args}} + cargo check -p cosmic_sctk {{args}} + +# Runs a check with JSON message format for IDE integration +check-json: (check '--message-format=json') + +# Runs an example of the given {{name}} +example name: + cargo run --release -p {{name}} \ No newline at end of file diff --git a/src/app/command.rs b/src/app/command.rs new file mode 100644 index 00000000..1997b002 --- /dev/null +++ b/src/app/command.rs @@ -0,0 +1,63 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::future::Future; + +use super::{Command, Message}; +use iced_runtime::command::Action; + +/// Yields a command which contains a batch of commands. +pub fn batch(commands: impl IntoIterator>) -> Command { + Command::batch(commands) +} + +/// Yields a command which will run the future on the runtime executor. +pub fn future( + future: impl Future> + Send + 'static, +) -> Command { + Command::single(Action::Future(Box::pin(future))) +} + +/// Creates a command which yields a [`crate::app::Message`]. +pub fn message(message: Message) -> Command { + crate::command::message(message) +} + +/// Convenience methods for building message-based commands. +pub mod message { + /// Creates a command which yields an application message. + pub fn app(message: M) -> crate::app::Command { + super::message(super::Message::App(message)) + } + + /// Creates a command which yields a cosmic message. + pub fn cosmic( + message: crate::app::cosmic::Message, + ) -> crate::app::Command { + super::message(super::Message::Cosmic(message)) + } +} + +pub fn drag() -> iced::Command> { + crate::command::drag().map(Message::Cosmic) +} + +pub fn fullscreen() -> iced::Command> { + crate::command::fullscreen().map(Message::Cosmic) +} + +pub fn minimize() -> iced::Command> { + crate::command::minimize().map(Message::Cosmic) +} + +pub fn set_title(title: String) -> iced::Command> { + crate::command::set_title(title).map(Message::Cosmic) +} + +pub fn set_windowed() -> iced::Command> { + crate::command::set_windowed().map(Message::Cosmic) +} + +pub fn toggle_fullscreen() -> iced::Command> { + crate::command::toggle_fullscreen().map(Message::Cosmic) +} diff --git a/src/app/core.rs b/src/app/core.rs new file mode 100644 index 00000000..73d05342 --- /dev/null +++ b/src/app/core.rs @@ -0,0 +1,144 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, Theme}; + +/// Status of the nav bar and its panels. +#[derive(Clone)] +pub struct NavBar { + active: bool, + toggled: bool, + toggled_condensed: bool, +} + +/// COSMIC-specific settings for windows. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone)] +pub struct Window { + pub can_fullscreen: bool, + pub sharp_corners: bool, + pub show_headerbar: bool, + pub show_window_menu: bool, + pub show_maximize: bool, + pub show_minimize: bool, + height: u32, + width: u32, +} + +/// COSMIC-specific application settings +#[derive(Clone)] +pub struct Core { + /// Enables debug features in cosmic/iced. + pub debug: bool, + + /// Whether the window is too small for the nav bar + main content. + is_condensed: bool, + + /// Current status of the nav bar panel. + nav_bar: NavBar, + + /// Scaling factor used by the application + scale_factor: f32, + + pub theme: Theme, + pub(crate) title: String, + pub window: Window, +} + +impl Default for Core { + fn default() -> Self { + Self { + debug: false, + is_condensed: false, + nav_bar: NavBar { + active: true, + toggled: true, + toggled_condensed: true, + }, + scale_factor: 1.0, + theme: theme::theme(), + title: String::new(), + window: Window { + can_fullscreen: false, + sharp_corners: false, + show_headerbar: true, + show_maximize: true, + show_minimize: true, + show_window_menu: false, + height: 0, + width: 0, + }, + } + } +} + +impl Core { + /// Whether the window is too small for the nav bar + main content. + #[must_use] + pub fn is_condensed(&self) -> bool { + self.is_condensed + } + + /// The scaling factor used by the application. + #[must_use] + pub fn scale_factor(&self) -> f32 { + self.scale_factor + } + + /// Changes the scaling factor used by the application. + pub(crate) fn set_scale_factor(&mut self, factor: f32) { + self.scale_factor = factor; + self.is_condensed_update(); + } + + /// Whether to show or hide the main window's content. + pub(crate) fn show_content(&self) -> bool { + !self.is_condensed || !self.nav_bar.toggled_condensed + } + + /// Call this whenever the scaling factor or window width has changed. + #[allow(clippy::cast_precision_loss)] + fn is_condensed_update(&mut self) { + self.is_condensed = (600.0 * self.scale_factor) > self.window.width as f32; + self.nav_bar_update(); + } + + /// Whether the nav panel is visible or not + #[must_use] + pub fn nav_bar_active(&self) -> bool { + self.nav_bar.active + } + + pub fn nav_bar_toggle(&mut self) { + self.nav_bar.toggled = !self.nav_bar.toggled; + self.nav_bar_set_toggled_condensed(self.nav_bar.toggled); + } + + pub fn nav_bar_toggle_condensed(&mut self) { + self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed); + } + + pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) { + self.nav_bar.toggled_condensed = toggled; + self.nav_bar_update(); + } + + pub(crate) fn nav_bar_update(&mut self) { + self.nav_bar.active = if self.is_condensed { + self.nav_bar.toggled_condensed + } else { + self.nav_bar.toggled + }; + } + + /// Set the height of the main window. + pub(crate) fn set_window_height(&mut self, new_height: u32) { + self.window.height = new_height; + } + + /// Set the width of the main window. + pub(crate) fn set_window_width(&mut self, new_width: u32) { + self.window.width = new_width; + self.is_condensed_update(); + } +} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs new file mode 100644 index 00000000..6069d3c9 --- /dev/null +++ b/src/app/cosmic.rs @@ -0,0 +1,290 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{command, Application, ApplicationExt, Core, Subscription}; +use crate::theme::{self, Theme}; +use crate::widget::nav_bar; +use crate::{keyboard_nav, Element}; +#[cfg(feature = "wayland")] +use iced::event::wayland::{self, WindowEvent}; +#[cfg(feature = "wayland")] +use iced::event::PlatformSpecific; +use iced::window; +#[cfg(not(feature = "wayland"))] +use iced_runtime::command::Action; +#[cfg(not(feature = "wayland"))] +use iced_runtime::window::Action as WindowAction; +#[cfg(feature = "wayland")] +use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; + +/// A message managed internally by COSMIC. +#[derive(Clone, Debug)] +pub enum Message { + /// Requests to close the window. + Close, + /// Requests to drag the window. + Drag, + /// Keyboard shortcuts managed by libcosmic. + KeyboardNav(keyboard_nav::Message), + /// Requests to maximize the window. + Maximize, + /// Requests to minimize the window. + Minimize, + /// Activates a navigation element from the nav bar. + NavBar(nav_bar::Id), + /// Set scaling factor + ScaleFactor(f32), + /// Requests theme changes. + ThemeChange(Theme), + /// Toggles visibility of the nav bar. + ToggleNavBar, + /// Toggles the condensed status of the nav bar. + ToggleNavBarCondensed, + /// Updates the tracked window geometry. + WindowResize(window::Id, u32, u32), + /// Tracks updates to window state. + #[cfg(feature = "wayland")] + WindowState(window::Id, WindowState), + /// Capabilities the window manager supports + #[cfg(feature = "wayland")] + WmCapabilities(window::Id, WindowManagerCapabilities), +} + +#[derive(Default)] +pub(crate) struct Cosmic { + pub(crate) app: App, + #[cfg(feature = "wayland")] + pub(crate) should_exit: bool, +} + +impl iced::Application for Cosmic +where + T::Message: Send + 'static, +{ + type Executor = T::Executor; + type Flags = (Core, T::Flags); + type Message = super::Message; + type Theme = Theme; + + fn new((core, flags): Self::Flags) -> (Self, iced::Command) { + let (model, command) = T::init(core, flags); + + (Cosmic::new(model), command) + } + + #[cfg(feature = "wayland")] + fn close_requested(&self, id: window::Id) -> Self::Message { + self.app + .on_close_requested(id) + .map_or(super::Message::None, super::Message::App) + } + + fn title(&self) -> String { + self.app.title().to_string() + } + + fn update(&mut self, message: Self::Message) -> iced::Command { + match message { + super::Message::App(message) => self.app.update(message), + super::Message::Cosmic(message) => self.cosmic_update(message), + super::Message::None => iced::Command::none(), + } + } + + fn scale_factor(&self) -> f64 { + f64::from(self.app.core().scale_factor()) + } + + #[cfg(feature = "wayland")] + fn should_exit(&self) -> bool { + self.should_exit + } + + fn style(&self) -> ::Style { + if self.app.core().window.sharp_corners { + theme::Application::default() + } else { + theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { + background_color: iced_core::Color::TRANSPARENT, + text_color: theme.cosmic().on_bg_color().into(), + })) + } + } + + fn subscription(&self) -> Subscription { + let window_events = iced::subscription::events_with(|event, _| { + match event { + iced::Event::Window(id, window::Event::Resized { width, height }) => { + return Some(Message::WindowResize(id, width, height)); + } + + #[cfg(feature = "wayland")] + iced::Event::PlatformSpecific(PlatformSpecific::Wayland(event)) => match event { + wayland::Event::Window(WindowEvent::State(state), _surface, id) => { + return Some(Message::WindowState(id, state)); + } + + wayland::Event::Window( + WindowEvent::WmCapabilities(capabilities), + _surface, + id, + ) => { + return Some(Message::WmCapabilities(id, capabilities)); + } + + _ => (), + }, + _ => (), + } + + None + }); + + Subscription::batch(vec![ + self.app.subscription().map(super::Message::App), + keyboard_nav::subscription() + .map(Message::KeyboardNav) + .map(super::Message::Cosmic), + theme::subscription(0) + .map(Message::ThemeChange) + .map(super::Message::Cosmic), + window_events.map(super::Message::Cosmic), + ]) + } + + fn theme(&self) -> Self::Theme { + self.app.core().theme.clone() + } + + #[cfg(feature = "wayland")] + fn view(&self, id: window::Id) -> Element { + if id != window::Id(0) { + return self.app.view_window(id).map(super::Message::App); + } + + self.app.view_main() + } + + #[cfg(not(feature = "wayland"))] + fn view(&self) -> Element { + self.app.view_main() + } +} + +impl Cosmic { + #[cfg(feature = "wayland")] + pub fn close(&mut self) -> iced::Command> { + self.should_exit = true; + iced::Command::none() + } + + #[cfg(not(feature = "wayland"))] + #[allow(clippy::unused_self)] + pub fn close(&mut self) -> iced::Command> { + iced::Command::single(Action::Window(WindowAction::Close)) + } + + fn cosmic_update(&mut self, message: Message) -> iced::Command> { + match message { + Message::WindowResize(id, width, height) => { + if window::Id(0) == id { + self.app.core_mut().set_window_width(width); + self.app.core_mut().set_window_height(height); + } + + self.app.on_window_resize(id, width, height); + } + + #[cfg(feature = "wayland")] + Message::WindowState(id, state) => { + if window::Id(0) == id { + self.app.core_mut().window.sharp_corners = + matches!(state, WindowState::ACTIVATED) + || state.contains(WindowState::TILED); + } + } + + #[cfg(feature = "wayland")] + Message::WmCapabilities(id, capabilities) => { + if window::Id(0) == id { + self.app.core_mut().window.can_fullscreen = + capabilities.contains(WindowManagerCapabilities::FULLSCREEN); + self.app.core_mut().window.show_maximize = + capabilities.contains(WindowManagerCapabilities::MAXIMIZE); + self.app.core_mut().window.show_minimize = + capabilities.contains(WindowManagerCapabilities::MINIMIZE); + self.app.core_mut().window.show_window_menu = + capabilities.contains(WindowManagerCapabilities::WINDOW_MENU); + } + } + + Message::KeyboardNav(message) => match message { + keyboard_nav::Message::Unfocus => { + return keyboard_nav::unfocus().map(super::Message::Cosmic) + } + keyboard_nav::Message::FocusNext => { + return iced::widget::focus_next().map(super::Message::Cosmic) + } + keyboard_nav::Message::FocusPrevious => { + return iced::widget::focus_previous().map(super::Message::Cosmic) + } + keyboard_nav::Message::Escape => return self.app.on_escape(), + keyboard_nav::Message::Search => return self.app.on_search(), + + keyboard_nav::Message::Fullscreen => return command::toggle_fullscreen(), + }, + + Message::Drag => return command::drag(), + + Message::Close => { + self.app.on_app_exit(); + return self.close(); + } + + Message::Minimize => return command::minimize(), + + Message::Maximize => { + if self.app.core().window.sharp_corners { + self.app.core_mut().window.sharp_corners = false; + return command::set_windowed(); + } + + self.app.core_mut().window.sharp_corners = true; + return command::fullscreen(); + } + + Message::NavBar(key) => { + self.app.core_mut().nav_bar_set_toggled_condensed(false); + return self.app.on_nav_select(key); + } + + Message::ToggleNavBar => { + self.app.core_mut().nav_bar_toggle(); + } + + Message::ToggleNavBarCondensed => { + self.app.core_mut().nav_bar_toggle_condensed(); + } + + Message::ThemeChange(theme) => { + self.app.core_mut().theme = theme; + } + + Message::ScaleFactor(factor) => { + self.app.core_mut().set_scale_factor(factor); + } + } + + iced::Command::none() + } +} + +impl Cosmic { + pub fn new(app: App) -> Self { + Self { + app, + #[cfg(feature = "wayland")] + should_exit: false, + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 00000000..6cc6e637 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,331 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod command; +mod core; +pub mod cosmic; +pub mod settings; + +pub mod message { + #[derive(Clone, Debug)] + #[must_use] + pub enum Message { + /// Messages from the application, for the application. + App(M), + /// Internal messages to be handled by libcosmic. + Cosmic(super::cosmic::Message), + /// Do nothing + None, + } + + pub fn app(message: M) -> Message { + Message::App(message) + } + + pub fn cosmic(message: super::cosmic::Message) -> Message { + Message::Cosmic(message) + } + + pub fn none() -> Message { + Message::None + } +} + +pub use self::core::Core; +pub use self::settings::Settings; +use crate::widget::nav_bar; +use crate::{Element, ElementExt}; +use apply::Apply; +use iced::Subscription; +use iced::{window, Application as IcedApplication}; +pub use message::Message; + +/// Commands for COSMIC applications. +pub type Command = iced::Command>; + +/// Launch the application with the given settings. +/// +/// # Errors +/// +/// Returns error on application failure. +pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { + if let Some(icon_theme) = settings.default_icon_theme { + crate::icon_theme::set_default(icon_theme); + } + + let mut core = Core::default(); + core.debug = settings.debug; + core.set_scale_factor(settings.scale_factor); + core.set_window_width(settings.size.0); + core.set_window_height(settings.size.1); + core.theme = settings.theme; + + let mut iced = iced::Settings::with_flags((core, flags)); + + iced.antialiasing = settings.antialiasing; + iced.default_font = settings.default_font; + iced.default_text_size = settings.default_text_size; + iced.id = Some(App::APP_ID.to_owned()); + + #[cfg(feature = "wayland")] + { + use iced::wayland::actions::window::SctkWindowSettings; + use iced_sctk::settings::InitialSurface; + iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { + app_id: Some(App::APP_ID.to_owned()), + autosize: settings.autosize, + client_decorations: settings.client_decorations, + resizable: settings.resizable, + size: settings.size, + size_limits: settings.size_limits, + title: None, + transparent: settings.transparent, + ..SctkWindowSettings::default() + }); + } + + #[cfg(not(feature = "wayland"))] + { + if let Some(_border_size) = settings.resizable { + // iced.window.border_size = border_size as u32; + iced.window.resizable = true; + } + iced.window.decorations = !settings.client_decorations; + iced.window.size = settings.size; + iced.window.transparent = settings.transparent; + } + + cosmic::Cosmic::::run(iced) +} + +#[allow(unused_variables)] +pub trait Application +where + Self: Sized + 'static, +{ + /// Default async executor to use with the app. + type Executor: iced_futures::Executor; + + /// Argument received [`Application::new`]. + type Flags: Clone; + + /// Message type specific to our app. + type Message: Clone + std::fmt::Debug + Send + 'static; + + /// An ID that uniquely identifies the application. + /// The standard is to pick an ID based on a reverse-domain name notation. + /// IE: `com.system76.Settings` + const APP_ID: &'static str; + + /// Grants access to the COSMIC Core. + fn core(&self) -> &Core; + + /// Grants access to the COSMIC Core. + fn core_mut(&mut self) -> &mut Core; + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, flags: Self::Flags) -> (Self, iced::Command>); + + /// Attaches elements to the start section of the header. + fn header_start(&self) -> Vec> { + Vec::new() + } + + /// Attaches elements to the center of the header. + fn header_center(&self) -> Vec> { + Vec::new() + } + + /// Attaches elements to the end section of the header. + fn header_end(&self) -> Vec> { + Vec::new() + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + None + } + + /// Called before closing the application. + fn on_app_exit(&mut self) {} + + /// Called when a window requests to be closed. + fn on_close_requested(&self, id: window::Id) -> Option { + None + } + + /// Called when the escape key is pressed. + fn on_escape(&mut self) -> iced::Command> { + iced::Command::none() + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> iced::Command> { + iced::Command::none() + } + + /// Called when the search function is requested. + fn on_search(&mut self) -> iced::Command> { + iced::Command::none() + } + + /// Called when a window is resized. + fn on_window_resize(&mut self, id: window::Id, width: u32, height: u32) {} + + /// Event sources that are to be listened to. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Respond to an application-specific message. + fn update(&mut self, message: Self::Message) -> iced::Command> { + iced::Command::none() + } + + /// Constructs the view for the main window. + fn view(&self) -> Element; + + /// Constructs views for other windows. + fn view_window(&self, id: window::Id) -> Element { + panic!("no view for window {}", id.0); + } +} + +/// Methods automatically derived for all types implementing [`Application`]. +pub trait ApplicationExt: Application { + /// Initiates a window drag. + fn drag(&mut self) -> iced::Command>; + + /// Fullscreens the window. + fn fullscreen(&mut self) -> iced::Command>; + + /// Minimizes the window. + fn minimize(&mut self) -> iced::Command>; + + /// Get the title of the main window. + fn title(&self) -> &str; + + /// Set the title of the main window. + fn set_title(&mut self, title: String) -> iced::Command>; + + /// View template for the main window. + fn view_main(&self) -> Element>; +} + +impl ApplicationExt for App { + fn drag(&mut self) -> iced::Command> { + command::drag() + } + + fn fullscreen(&mut self) -> iced::Command> { + command::fullscreen() + } + + fn minimize(&mut self) -> iced::Command> { + command::minimize() + } + + fn title(&self) -> &str { + &self.core().title + } + + #[cfg(feature = "wayland")] + fn set_title(&mut self, title: String) -> iced::Command> { + self.core_mut().title = title.clone(); + command::set_title(title) + } + + #[cfg(not(feature = "wayland"))] + fn set_title(&mut self, title: String) -> iced::Command> { + self.core_mut().title = title.clone(); + iced::Command::none() + } + + fn view_main<'a>(&'a self) -> Element<'a, Message> { + let core = self.core(); + let is_condensed = core.is_condensed(); + let mut main: Vec>> = Vec::with_capacity(2); + + if core.window.show_headerbar { + main.push({ + let mut header = crate::widget::header_bar() + .title(self.title()) + .on_drag(Message::Cosmic(cosmic::Message::Drag)) + .on_close(Message::Cosmic(cosmic::Message::Close)); + + if self.nav_model().is_some() { + let toggle = crate::widget::nav_bar_toggle() + .active(core.nav_bar_active()) + .on_toggle(if is_condensed { + Message::Cosmic(cosmic::Message::ToggleNavBarCondensed) + } else { + Message::Cosmic(cosmic::Message::ToggleNavBar) + }); + + header = header.start(toggle); + } + + if core.window.show_maximize { + header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); + } + + if core.window.show_minimize { + header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); + } + + for element in self.header_start() { + header = header.start(element.map(Message::App)); + } + + for element in self.header_center() { + header = header.center(element.map(Message::App)); + } + + for element in self.header_end() { + header = header.end(element.map(Message::App)); + } + + Element::from(header).debug(core.debug) + }); + } + + // The content element contains every element beneath the header. + main.push( + iced::widget::row({ + let mut widgets = Vec::with_capacity(2); + + // Insert nav bar onto the left side of the window. + if core.nav_bar_active() { + if let Some(nav_model) = self.nav_model() { + let mut nav = crate::widget::nav_bar(nav_model, |entity| { + Message::Cosmic(cosmic::Message::NavBar(entity)) + }); + + if !is_condensed { + nav = nav.max_width(300); + } + + widgets.push(nav.apply(Element::from).debug(core.debug)); + } + } + + if core.show_content() { + let main_content = self.view().debug(core.debug).map(Message::App); + + widgets.push(main_content); + } + + widgets + }) + .spacing(8) + .apply(iced::widget::container) + .padding([0, 8, 8, 8]) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .style(crate::theme::Container::Background) + .into(), + ); + + iced::widget::column(main).into() + } +} diff --git a/src/app/settings.rs b/src/app/settings.rs new file mode 100644 index 00000000..42b42a1e --- /dev/null +++ b/src/app/settings.rs @@ -0,0 +1,80 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{font, Theme}; +#[cfg(feature = "wayland")] +use iced::Limits; +use iced_core::Font; + +#[allow(clippy::struct_excessive_bools)] +#[derive(derive_setters::Setters)] +pub struct Settings { + /// Produces a smoother result in some widgets, at a performance cost. + pub(crate) antialiasing: bool, + + /// Autosize the window to fit its contents + #[cfg(feature = "wayland")] + pub(crate) autosize: bool, + + /// Whether the window should have a border, a title bar, etc. or not. + pub(crate) client_decorations: bool, + + /// Enables debug features in cosmic/iced. + pub(crate) debug: bool, + + /// The default [`Font`] to be used. + pub(crate) default_font: Font, + + /// Name of the icon theme to search by default. + #[setters(strip_option, into)] + pub(crate) default_icon_theme: Option, + + /// Default size of fonts. + pub(crate) default_text_size: f32, + + /// Whether the window should be resizable or not. + /// and the size of the window border which can be dragged for a resize + #[setters(strip_option)] + pub(crate) resizable: Option, + + /// Scale factor to use by default. + pub(crate) scale_factor: f32, + + /// Initial size of the window. + pub(crate) size: (u32, u32), + + /// Limitations of the window size + #[cfg(feature = "wayland")] + pub(crate) size_limits: Limits, + + /// The theme to apply to the application. + pub(crate) theme: Theme, + + /// Whether the window should be transparent. + pub(crate) transparent: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + antialiasing: true, + #[cfg(feature = "wayland")] + autosize: false, + client_decorations: true, + debug: false, + default_font: font::FONT, + default_icon_theme: Some(String::from("Pop")), + default_text_size: 14.0, + resizable: Some(8.0), + scale_factor: std::env::var("COSMIC_SCALE") + .ok() + .and_then(|scale| scale.parse::().ok()) + .unwrap_or(1.0), + size: (1024, 768), + #[cfg(feature = "wayland")] + size_limits: Limits::NONE.min_height(1.0).min_width(1.0), + theme: crate::theme::theme(), + transparent: false, + } + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 00000000..a4e416cd --- /dev/null +++ b/src/command.rs @@ -0,0 +1,116 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +#[cfg(feature = "wayland")] +use iced::window; +use iced::Command; +use iced_core::window::Mode; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::wayland::window::Action as WindowAction; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::wayland::Action as WaylandAction; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::Action as PlatformAction; +use iced_runtime::command::Action; +#[cfg(not(feature = "wayland"))] +use iced_runtime::window::Action as WindowAction; +use std::future::Future; + +/// Yields a command which contains a batch of commands. +pub fn batch(commands: impl IntoIterator>) -> Command { + Command::batch(commands) +} + +/// Yields a command which will run the future on the runtime executor. +pub fn future(future: impl Future + Send + 'static) -> Command { + Command::single(Action::Future(Box::pin(future))) +} + +/// Yields a command which will return a message. +pub fn message(message: M) -> Command { + future(async move { message }) +} + +/// Initiates a window drag. +#[cfg(feature = "wayland")] +pub fn drag() -> Command { + iced_sctk::commands::window::start_drag_window(window::Id(0)) +} + +/// Initiates a window drag. +#[cfg(not(feature = "wayland"))] +pub fn drag() -> Command { + iced::Command::none() +} + +/// Fullscreens the window. +#[cfg(feature = "wayland")] +pub fn fullscreen() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Fullscreen) +} + +/// Fullscreens the window. +#[cfg(not(feature = "wayland"))] +pub fn fullscreen() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Fullscreen))) +} + +/// Minimizes the window. +#[cfg(feature = "wayland")] +pub fn minimize() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Hidden) +} + +/// Minimizes the window. +#[cfg(not(feature = "wayland"))] +pub fn minimize() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Hidden))) +} + +/// Sets the title of a window. +#[cfg(feature = "wayland")] +pub fn set_title(title: String) -> Command { + window_action(WindowAction::Title { + id: window::Id(0), + title, + }) +} + +/// Sets the title of a window. +#[cfg(not(feature = "wayland"))] +#[allow(unused_variables, clippy::needless_pass_by_value)] +pub fn set_title(title: String) -> Command { + Command::none() +} + +/// Sets the window mode to windowed. +#[cfg(feature = "wayland")] +pub fn set_windowed() -> Command { + iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Windowed) +} + +/// Sets the window mode to windowed. +#[cfg(not(feature = "wayland"))] +pub fn set_windowed() -> Command { + iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Windowed))) +} + +/// Toggles the windows' maximization state. +#[cfg(feature = "wayland")] +pub fn toggle_fullscreen() -> Command { + window_action(WindowAction::ToggleFullscreen { id: window::Id(0) }) +} + +/// Toggles the windows' maximization state. +#[cfg(not(feature = "wayland"))] +pub fn toggle_fullscreen() -> Command { + iced::Command::single(Action::Window(WindowAction::ToggleMaximize)) +} + +/// Creates a command to apply an action to a window. +#[cfg(feature = "wayland")] +pub fn window_action(action: WindowAction) -> Command { + Command::single(Action::PlatformSpecific(PlatformAction::Wayland( + WaylandAction::Window(action), + ))) +} diff --git a/src/icon_theme.rs b/src/icon_theme.rs new file mode 100644 index 00000000..58c05643 --- /dev/null +++ b/src/icon_theme.rs @@ -0,0 +1,20 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::cell::RefCell; + +thread_local! { + /// The fallback icon theme to search if no icon theme was specified. + pub(crate) static DEFAULT: RefCell = RefCell::new(String::from("Pop")); +} + +/// The fallback icon theme to search if no icon theme was specified. +#[must_use] +pub fn default() -> String { + DEFAULT.with(|f| f.borrow().clone()) +} + +/// Set the fallback icon theme to search when loading system icons. +pub fn set_default(name: impl Into) { + DEFAULT.with(|f| *f.borrow_mut() = name.into()); +} diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index af4703e4..7610328e 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -1,3 +1,6 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + use iced::{ event, keyboard::{self, KeyCode}, @@ -10,52 +13,57 @@ pub enum Message { Escape, FocusNext, FocusPrevious, + Fullscreen, Unfocus, Search, } pub fn subscription() -> Subscription { - subscription::events_with(|event, status| match (event, status) { - // Focus - ( + subscription::events_with(|event, status| { + if event::Status::Ignored != status { + return None; + } + + match event { Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::Tab, + key_code, modifiers, - .. - }), - event::Status::Ignored, - ) => Some(if modifiers.shift() { - Message::FocusPrevious - } else { - Message::FocusNext - }), - // Escape - ( - Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::Escape, - .. - }), - _, - ) => Some(Message::Escape), - // Search - ( - Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::F, - modifiers, - }), - event::Status::Ignored, - ) => { - if modifiers.control() { - Some(Message::Search) - } else { - None + }) => match key_code { + KeyCode::Tab => { + return Some(if modifiers.shift() { + Message::FocusPrevious + } else { + Message::FocusNext + }); + } + + KeyCode::Escape => { + return Some(Message::Escape); + } + + KeyCode::F11 => { + return Some(Message::Fullscreen); + } + + KeyCode::F => { + return if modifiers.control() { + Some(Message::Search) + } else { + None + }; + } + + _ => (), + }, + + Event::Mouse(mouse::Event::ButtonPressed { .. }) => { + return Some(Message::Unfocus); } + + _ => (), } - // Unfocus - (Event::Mouse(mouse::Event::ButtonPressed { .. }), event::Status::Ignored) => { - Some(Message::Unfocus) - } - _ => None, + + None }) } diff --git a/src/lib.rs b/src/lib.rs index 4dc92191..04b94059 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,22 @@ #![allow(clippy::module_name_repetitions)] +pub mod app; +pub use app::{Application, ApplicationExt}; + +pub mod command; pub use cosmic_config; pub use cosmic_theme; + +pub mod executor; +#[cfg(feature = "tokio")] +pub use executor::single::Executor as SingleThreadExecutor; + +mod ext; +pub use ext::ElementExt; + +pub mod font; + pub use iced; pub use iced_core; pub use iced_futures; @@ -16,23 +30,17 @@ pub use iced_style; pub use iced_widget; #[cfg(feature = "winit")] pub use iced_winit; + +pub mod icon_theme; +pub mod keyboard_nav; + #[cfg(feature = "wayland")] pub use sctk; -pub mod executor; -pub mod font; -pub mod keyboard_nav; + pub mod theme; +pub use theme::Theme; + pub mod widget; -#[cfg(feature = "tokio")] -pub use executor::single::Executor as SingleThreadExecutor; - -pub mod settings; -pub use settings::{settings, settings_with_flags}; - -mod ext; -pub use ext::ElementExt; - -pub use theme::Theme; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 9f9b20fd..00000000 --- a/src/settings.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::font; -use std::cell::RefCell; - -thread_local! { - /// The fallback icon theme to search if no icon theme was specified. - pub(crate) static DEFAULT_ICON_THEME: RefCell = RefCell::new(String::from("Pop")); -} - -/// The fallback icon theme to search if no icon theme was specified. -#[must_use] -pub fn default_icon_theme() -> String { - DEFAULT_ICON_THEME.with(|f| f.borrow().clone()) -} - -/// Set the fallback icon theme to search when loading system icons. -pub fn set_default_icon_theme(name: impl Into) { - DEFAULT_ICON_THEME.with(|f| *f.borrow_mut() = name.into()); -} - -/// Default iced settings for COSMIC applications. -#[must_use] -pub fn settings() -> iced::Settings { - settings_with_flags(Flags::default()) -} - -/// Default iced settings for COSMIC applications. -#[must_use] -pub fn settings_with_flags(flags: Flags) -> iced::Settings { - iced::Settings { - 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 69b945aa..8a23a039 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1221,7 +1221,7 @@ pub fn theme() -> Theme { crate::theme::Theme::custom(Arc::new(t)) } -pub fn theme_subscription(id: u64) -> Subscription { +pub fn subscription(id: u64) -> Subscription { config_subscription::>( id, crate::cosmic_theme::NAME.into(), diff --git a/src/track.rs b/src/track.rs new file mode 100644 index 00000000..104da86d --- /dev/null +++ b/src/track.rs @@ -0,0 +1,40 @@ +/// Records if a change has occurred to its inner value +pub struct Track { + value: T, + changed: bool, +} + +impl Track { + /// Create a new value where changes are tracked. + pub const fn new(value: T) -> Self { + Self { + value, + changed: true, + } + } + + /// Gets the inner value. + pub fn get(&self) -> &T { + &self.value + } + + /// Set a new value, and mark that it has changed. + pub fn set(&mut self, value: T) { + self.value = value; + self.changed = true; + } + + /// Check if value has changed. + pub fn changed(&self) -> bool { + self.changed + } +} + +impl Default for Track +where + T: Default, +{ + fn default() -> Self { + Self::new(T::default()) + } +} diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 51fec134..e7c8ca51 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -10,35 +10,80 @@ use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { HeaderBar { - title: "".into(), + title: Cow::Borrowed(""), on_close: None, on_drag: None, on_maximize: None, on_minimize: None, - start: None, - center: None, - end: None, + start: Vec::new(), + center: Vec::new(), + end: Vec::new(), } } #[derive(Setters)] pub struct HeaderBar<'a, Message> { - #[setters(into)] + /// Defines the title of the window + #[setters(skip)] title: Cow<'a, str>, + + /// A message emitted when the close button is pressed. #[setters(strip_option)] on_close: Option, + + /// A message emitted when dragged. #[setters(strip_option)] on_drag: Option, + + /// A message emitted when the maximize button is pressed. #[setters(strip_option)] on_maximize: Option, + + /// A message emitted when the minimize button is pressed. #[setters(strip_option)] on_minimize: Option, - #[setters(strip_option)] - start: Option>, - #[setters(strip_option)] - center: Option>, - #[setters(strip_option)] - end: Option>, + + /// Elements packed at the start of the headerbar. + #[setters(skip)] + start: Vec>, + + /// Elements packed in the center of the headerbar. + #[setters(skip)] + center: Vec>, + + /// Elements packed at the end of the headerbar. + #[setters(skip)] + end: Vec>, +} + +impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { + /// Defines the title of the window + #[must_use] + pub fn title(mut self, title: impl Into> + 'a) -> Self { + self.title = title.into(); + self + } + + /// Pushes an element to the start region. + #[must_use] + pub fn start(mut self, widget: impl Into> + 'a) -> Self { + self.start.push(widget.into()); + self + } + + /// Pushes an element to the center region. + #[must_use] + pub fn center(mut self, widget: impl Into> + 'a) -> Self { + self.center.push(widget.into()); + self + } + + /// Pushes an element to the end region. + #[must_use] + pub fn end(mut self, widget: impl Into> + 'a) -> Self { + self.end.push(widget.into()); + self + } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -46,16 +91,28 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { pub fn into_element(mut self) -> Element<'a, Message> { let mut packed: Vec> = Vec::with_capacity(4); - if let Some(start) = self.start.take() { + // Take ownership of the regions to be packed. + let start = std::mem::take(&mut self.start); + let center = std::mem::take(&mut self.center); + let mut end = std::mem::take(&mut self.end); + + // If elements exist in the start region, append them here. + if !start.is_empty() { packed.push( - widget::container(start) + iced::widget::row(start) + .align_items(iced::Alignment::Center) + .apply(iced::widget::container) .align_x(iced::alignment::Horizontal::Left) .into(), ); } - packed.push(if let Some(center) = self.center.take() { - widget::container(center) + // If elements exist in the center region, use them here. + // This will otherwise use the title as a widget if a title was defined. + packed.push(if !center.is_empty() { + iced::widget::row(center) + .align_items(iced::Alignment::Center) + .apply(iced::widget::container) .align_x(iced::alignment::Horizontal::Center) .into() } else if self.title.is_empty() { @@ -64,15 +121,17 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.title_widget() }); - packed.push(if let Some(end) = self.end.take() { - widget::row(vec![end, self.window_controls()]) + // Also packs the window controls at the very end. + end.push(self.window_controls()); + packed.push( + iced::widget::row(end) + .align_items(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Right) - .into() - } else { - self.window_controls() - }); + .into(), + ); + // Creates the headerbar widget. let mut widget = widget::row(packed) .height(Length::Fixed(50.0)) .padding(8) @@ -82,10 +141,12 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .center_y() .apply(widget::mouse_area); + // Assigns a message to emit when the headerbar is dragged. if let Some(message) = self.on_drag.clone() { widget = widget.on_press(message); } + // Assigns a message to emit when the headerbar is double-clicked. if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 9e31eea8..cc91decf 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -260,7 +260,7 @@ impl<'a> Icon<'a> { self.hash(&mut hasher); if self.theme.is_none() { - crate::settings::DEFAULT_ICON_THEME.with(|f| f.borrow().hash(&mut hasher)); + crate::icon_theme::DEFAULT.with(|f| f.borrow().hash(&mut hasher)); } let hash = hasher.finish(); @@ -291,7 +291,7 @@ impl<'a, Message: 'static> From> for Element<'a, Message> { #[must_use] pub fn load_icon(name: &str, size: u16, theme: Option<&str>) -> Option { - let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { + let icon = crate::icon_theme::DEFAULT.with(|default_theme| { let default_theme = default_theme.borrow(); freedesktop_icons::lookup(name) .with_size(size) diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 2a236468..3b3baf87 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -14,6 +14,9 @@ use iced_core::Color; use crate::{theme, widget::segmented_button, Theme}; +pub type Id = segmented_button::Entity; +pub type Model = segmented_button::SingleSelectModel; + /// Navigation side panel for switching between views. /// /// For details on the model, see the [`segmented_button`] module for more details. diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 2f6012e1..ee9d9fe0 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -12,23 +12,23 @@ use super::IconSource; #[derive(Setters)] pub struct NavBarToggle { - nav_bar_active: bool, + active: bool, #[setters(strip_option)] - on_nav_bar_toggled: Option, + on_toggle: Option, } #[must_use] pub fn nav_bar_toggle() -> NavBarToggle { NavBarToggle { - nav_bar_active: false, - on_nav_bar_toggled: None, + active: false, + on_toggle: None, } } -impl From> for Element<'static, Message> { +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { let mut widget = super::icon( - if nav_bar_toggle.nav_bar_active { + if nav_bar_toggle.active { IconSource::svg_from_memory(&include_bytes!("../../res/sidebar-active.svg")[..]) } else { IconSource::from("open-menu-symbolic") @@ -41,7 +41,7 @@ impl From> for Element<'static, .padding([8, 16, 8, 16]) .style(theme::Button::Text); - if let Some(message) = nav_bar_toggle.on_nav_bar_toggled { + if let Some(message) = nav_bar_toggle.on_toggle { widget = widget.on_press(message); } From 54d47a1b38e0cbb4306afad74ce4980957ff042f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 3 Aug 2023 13:12:32 -0700 Subject: [PATCH 0036/1276] app/settings: Don't use `strip_option` (#137) Since these don't default to `None`, and the fields aren't public, using `strip_option` makes it impossible to change them to `None`. --- src/app/settings.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index 42b42a1e..19606638 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -26,7 +26,7 @@ pub struct Settings { pub(crate) default_font: Font, /// Name of the icon theme to search by default. - #[setters(strip_option, into)] + #[setters(into)] pub(crate) default_icon_theme: Option, /// Default size of fonts. @@ -34,7 +34,6 @@ pub struct Settings { /// Whether the window should be resizable or not. /// and the size of the window border which can be dragged for a resize - #[setters(strip_option)] pub(crate) resizable: Option, /// Scale factor to use by default. From 620c1adb74a8eae98e943307ca81abc3fe35a8a1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 3 Aug 2023 15:03:23 -0400 Subject: [PATCH 0037/1276] wip: theme update & some cleanup --- Cargo.toml | 3 + cosmic-theme/Cargo.toml | 10 +- cosmic-theme/src/color_picker/exact.rs | 170 --------------- cosmic-theme/src/color_picker/mod.rs | 280 ------------------------- cosmic-theme/src/composite.rs | 27 +++ cosmic-theme/src/config/mod.rs | 196 ----------------- cosmic-theme/src/hex_color.rs | 35 ---- cosmic-theme/src/image.rs | 1 + cosmic-theme/src/lib.rs | 62 +----- cosmic-theme/src/model/constraint.rs | 26 --- cosmic-theme/src/model/derivation.rs | 2 +- cosmic-theme/src/model/mod.rs | 9 +- cosmic-theme/src/model/selection.rs | 99 --------- cosmic-theme/src/output/gtk4_output.rs | 2 +- cosmic-theme/src/steps.rs | 137 ++++++++++++ cosmic-theme/src/theme_provider/mod.rs | 1 - cosmic-theme/src/util.rs | 26 --- 17 files changed, 181 insertions(+), 905 deletions(-) delete mode 100644 cosmic-theme/src/color_picker/exact.rs delete mode 100644 cosmic-theme/src/color_picker/mod.rs create mode 100644 cosmic-theme/src/composite.rs delete mode 100644 cosmic-theme/src/config/mod.rs delete mode 100644 cosmic-theme/src/hex_color.rs create mode 100644 cosmic-theme/src/image.rs delete mode 100644 cosmic-theme/src/model/constraint.rs delete mode 100644 cosmic-theme/src/model/selection.rs create mode 100644 cosmic-theme/src/steps.rs delete mode 100644 cosmic-theme/src/theme_provider/mod.rs diff --git a/Cargo.toml b/Cargo.toml index dea5d0cd..b0ad0d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,3 +97,6 @@ exclude = [ [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} + +[patch.crates-io] +palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index b53e3387..ecf4415c 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -12,17 +12,15 @@ 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"] +theme-from-image = ["kmeans_colors", "image"] [dependencies] -palette = {version = "0.7", features = ["serializing"] } +# palette = {version = "0.7", features = ["serializing"] } +almost = "0.2" +palette = {git = "https://github.com/Ogeon/palette", 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" diff --git a/cosmic-theme/src/color_picker/exact.rs b/cosmic-theme/src/color_picker/exact.rs deleted file mode 100644 index 2e29c265..00000000 --- a/cosmic-theme/src/color_picker/exact.rs +++ /dev/null @@ -1,170 +0,0 @@ -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 deleted file mode 100644 index b5bf4ee7..00000000 --- a/cosmic-theme/src/color_picker/mod.rs +++ /dev/null @@ -1,280 +0,0 @@ -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/composite.rs b/cosmic-theme/src/composite.rs new file mode 100644 index 00000000..c30469b2 --- /dev/null +++ b/cosmic-theme/src/composite.rs @@ -0,0 +1,27 @@ +use palette::Srgba; + +/// 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/cosmic-theme/src/config/mod.rs b/cosmic-theme/src/config/mod.rs deleted file mode 100644 index a558fcf5..00000000 --- a/cosmic-theme/src/config/mod.rs +++ /dev/null @@ -1,196 +0,0 @@ -// 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 deleted file mode 100644 index bf04f216..00000000 --- a/cosmic-theme/src/hex_color.rs +++ /dev/null @@ -1,35 +0,0 @@ -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/image.rs b/cosmic-theme/src/image.rs new file mode 100644 index 00000000..52a319ea --- /dev/null +++ b/cosmic-theme/src/image.rs @@ -0,0 +1 @@ +// TODO theme from image diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs index efa4025b..28e6e8a3 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -6,22 +6,16 @@ //! 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; + +/// composite colors in srgb +pub mod composite; +/// get color steps +pub mod steps; /// utilities pub mod util; @@ -33,47 +27,3 @@ pub const THEME_DIR: &str = "themes"; 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 deleted file mode 100644 index 45132494..00000000 --- a/cosmic-theme/src/model/constraint.rs +++ /dev/null @@ -1,26 +0,0 @@ -/// 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/derivation.rs b/cosmic-theme/src/model/derivation.rs index 9c186c9f..e6265002 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -2,7 +2,7 @@ use palette::Srgba; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fmt; -use crate::{util::over, CosmicPalette}; +use crate::{composite::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, PartialEq, Eq)] diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index 684df0b8..5e82c762 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,14 +1,7 @@ -#[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 deleted file mode 100644 index a4120c48..00000000 --- a/cosmic-theme/src/model/selection.rs +++ /dev/null @@ -1,99 +0,0 @@ -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/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 43fb498c..4472f6eb 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -10,7 +10,7 @@ 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 +/// Trait for outputting the Theme variables as Gtk4CSS pub trait Gtk4Output { /// turn the theme into css fn as_css(&self) -> String; diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs new file mode 100644 index 00000000..5e2f635c --- /dev/null +++ b/cosmic-theme/src/steps.rs @@ -0,0 +1,137 @@ +use almost::equal; +use palette::{convert::FromColorUnclamped, ClampAssign, Oklch, Srgb}; + +/// Get an array of 100 colors with a specific hue and chroma +/// over the full range of lightness. +/// Colors which are not valid Srgb will fallback to a color with the nearest valid chroma. +pub fn steps(mut c: Oklch) -> [Srgb; 100] { + let mut steps = [Srgb::new(0.0, 0.0, 0.0); 100]; + + for i in 0..steps.len() { + let lightness = i as f32 / 100.0; + c.l = lightness; + steps[i] = oklch_to_srgba_nearest_chroma(c) + } + + steps +} + +/// find the nearest chroma which makes our color a valid color in Srgb +pub fn oklch_to_srgba_nearest_chroma(mut c: Oklch) -> Srgb { + let mut r_chroma = c.chroma; + let mut l_chroma = 0.0; + // exit early if we found it right away + let mut new_c = Srgb::from_color_unclamped(c); + + if is_valid_srgb(new_c) { + new_c.clamp_assign(); + return new_c; + } + + // is this an excessive depth to search? + for _ in 0..64 { + let new_c = Srgb::from_color_unclamped(c); + if is_valid_srgb(new_c) { + l_chroma = c.chroma; + c.chroma = (c.chroma + r_chroma) / 2.0; + } else { + r_chroma = c.chroma; + c.chroma = (c.chroma + l_chroma) / 2.0; + } + } + Srgb::from_color_unclamped(c) +} + +/// checks that the color is valid srgb +pub fn is_valid_srgb(c: Srgb) -> bool { + (equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red())) + && (equal(c.blue, Srgb::max_blue()) + || (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue())) + && (equal(c.green, Srgb::max_green()) + || (c.green >= Srgb::min_green() && c.green <= Srgb::max_green())) +} + +#[cfg(test)] +mod tests { + use almost::equal; + use palette::{OklabHue, Srgb}; + + use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; + + #[test] + fn test_valid_check() { + assert!(is_valid_srgb(Srgb::new(1.0, 1.0, 1.0))); + assert!(is_valid_srgb(Srgb::new(0.0, 0.0, 0.0))); + assert!(is_valid_srgb(Srgb::new(0.5, 0.5, 0.5))); + assert!(!is_valid_srgb(Srgb::new(-0.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, -0.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -0.1))); + assert!(!is_valid_srgb(Srgb::new(-100.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, -100.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -100.1))); + assert!(!is_valid_srgb(Srgb::new(1.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, 1.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 1.1))); + assert!(!is_valid_srgb(Srgb::new(100.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, 100.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 100.1))); + } + + #[test] + fn test_conversion_boundaries() { + let c1 = palette::Oklch::new(0.0, 0.288, OklabHue::from_degrees(0.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1); + equal(srgb.red, 0.0); + equal(srgb.blue, 0.0); + equal(srgb.green, 0.0); + + let c1 = palette::Oklch::new(1.0, 0.288, OklabHue::from_degrees(0.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1); + + equal(srgb.red, 1.0); + equal(srgb.blue, 1.0); + equal(srgb.green, 1.0); + } + + #[test] + fn test_conversion_colors() { + let c1 = palette::Oklch::new(0.4608, 0.11111, OklabHue::new(57.31)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 133); + assert!(srgb.green == 69); + assert!(srgb.blue == 0); + + let c1 = palette::Oklch::new(0.30, 0.08, OklabHue::new(35.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 78); + assert!(srgb.green == 27); + assert!(srgb.blue == 15); + + let c1 = palette::Oklch::new(0.757, 0.146, OklabHue::new(301.2)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 192); + assert!(srgb.green == 153); + assert!(srgb.blue == 253); + } + + #[test] + fn test_conversion_fallback_colors() { + let c1 = palette::Oklch::new(0.70, 0.284, OklabHue::new(35.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 255); + assert!(srgb.green == 103); + assert!(srgb.blue == 65); + + let c1 = palette::Oklch::new(0.757, 0.239, OklabHue::new(301.2)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 193); + assert!(srgb.green == 152); + assert!(srgb.blue == 255); + + let c1 = palette::Oklch::new(0.163, 0.333, OklabHue::new(141.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 1); + assert!(srgb.green == 19); + assert!(srgb.blue == 0); + } +} diff --git a/cosmic-theme/src/theme_provider/mod.rs b/cosmic-theme/src/theme_provider/mod.rs deleted file mode 100644 index 8b137891..00000000 --- a/cosmic-theme/src/theme_provider/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs index bb264c8e..762017d6 100644 --- a/cosmic-theme/src/util.rs +++ b/cosmic-theme/src/util.rs @@ -31,29 +31,3 @@ impl Into for CssColor { ) } } - -/// 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 -} From 607883e4ada96cf0cd2b9ae13f0959437882212a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 3 Aug 2023 16:23:24 -0400 Subject: [PATCH 0038/1276] feat: add ThemeBuilder --- cosmic-theme/src/model/corner.rs | 31 ++++ cosmic-theme/src/model/cosmic_palette.rs | 38 ++++ cosmic-theme/src/model/dark.ron | 3 + cosmic-theme/src/model/derivation.rs | 24 +-- cosmic-theme/src/model/light.ron | 3 + cosmic-theme/src/model/mod.rs | 4 + cosmic-theme/src/model/spacing.rs | 43 +++++ cosmic-theme/src/model/theme.rs | 223 ++++++++++++++++++++++- 8 files changed, 354 insertions(+), 15 deletions(-) create mode 100644 cosmic-theme/src/model/corner.rs create mode 100644 cosmic-theme/src/model/spacing.rs diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs new file mode 100644 index 00000000..e466959d --- /dev/null +++ b/cosmic-theme/src/model/corner.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +/// Corner radii variables for the Cosmic theme +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct CornerRadii { + /// corner radii of 0 + pub radius_0: [u16; 4], + /// smallest size of corner radii that can be non-zero + pub radius_xs: [u16; 4], + /// small corner radii + pub radius_s: [u16; 4], + /// medium corner radii + pub radius_m: [u16; 4], + /// large corner radii + pub radius_l: [u16; 4], + /// extra large corner radii + pub radius_xl: [u16; 4], +} + +impl Default for CornerRadii { + fn default() -> Self { + Self { + radius_0: [0; 4], + radius_xs: [4; 4], + radius_s: [8; 4], + radius_m: [16; 4], + radius_l: [32; 4], + radius_xl: [160; 4], + } + } +} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 6ad225eb..7419e102 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -35,6 +35,29 @@ pub enum CosmicPalette { HighContrastDark(CosmicPaletteInner), } +impl CosmicPalette { + /// extract the inner palette + pub fn inner(self) -> CosmicPaletteInner { + match self { + CosmicPalette::Dark(p) => p, + CosmicPalette::Light(p) => p, + CosmicPalette::HighContrastLight(p) => p, + CosmicPalette::HighContrastDark(p) => p, + } + } +} + +impl AsMut> for CosmicPalette { + fn as_mut(&mut self) -> &mut CosmicPaletteInner { + match self { + CosmicPalette::Dark(p) => p, + CosmicPalette::Light(p) => p, + CosmicPalette::HighContrastLight(p) => p, + CosmicPalette::HighContrastDark(p) => p, + } + } +} + impl AsRef> for CosmicPalette where C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, @@ -85,6 +108,9 @@ pub struct CosmicPaletteInner { /// name of the palette pub name: String, + /// the selected accent color + pub accent: C, + /// basic palette /// blue: colors used for various points of emphasis in the UI pub blue: C, @@ -161,6 +187,7 @@ impl From> for CosmicPaletteInner { fn from(p: CosmicPaletteInner) -> Self { CosmicPaletteInner { name: p.name, + accent: p.accent.into(), blue: p.blue.into(), red: p.red.into(), green: p.green.into(), @@ -253,3 +280,14 @@ where Ok(ron::de::from_reader(f)?) } } + +impl Into> for CosmicPalette { + fn into(self) -> CosmicPalette { + match self { + CosmicPalette::Dark(p) => CosmicPalette::Dark(p.into()), + CosmicPalette::Light(p) => CosmicPalette::Light(p.into()), + CosmicPalette::HighContrastLight(p) => CosmicPalette::HighContrastLight(p.into()), + CosmicPalette::HighContrastDark(p) => CosmicPalette::HighContrastDark(p.into()), + } + } +} diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 9f283121..a7d77282 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1,6 +1,9 @@ Dark ( ( name: "cosmic-dark", + accent: ( + c: "#94EBEB", + ), blue: ( c: "#94EBEB", ), diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index e6265002..1868cdba 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -337,7 +337,7 @@ where p.neutral_0, p.neutral_10, 0.08, - p.blue, + p.accent, p.neutral_8, false, ), @@ -347,7 +347,7 @@ where p.neutral_0, p.neutral_10, 0.08, - p.blue, + p.accent, p.neutral_8, false, ), @@ -357,7 +357,7 @@ where p.neutral_0, p.neutral_10, 0.08, - p.blue, + p.accent, p.neutral_9, false, ), @@ -366,7 +366,7 @@ where p.neutral_0, p.neutral_10, 0.08, - p.blue, + p.accent, p.neutral_9, true, ), @@ -375,7 +375,7 @@ where p.neutral_0, p.neutral_10, 0.08, - p.blue, + p.accent, p.neutral_9, true, ), @@ -384,7 +384,7 @@ where p.neutral_0, p.neutral_10.clone(), 0.08, - p.blue, + p.accent, p.neutral_10, true, ), @@ -394,7 +394,7 @@ where p.neutral_0.clone(), p.neutral_0, 0.75, - p.blue.clone(), + p.accent.clone(), p.neutral_8, false, ), @@ -403,7 +403,7 @@ where p.neutral_0.clone(), p.neutral_0, 0.9, - p.blue.clone(), + p.accent.clone(), p.neutral_8, false, ), @@ -412,7 +412,7 @@ where p.neutral_0.clone(), p.neutral_0, 1.0, - p.blue.clone(), + p.accent.clone(), p.neutral_8, false, ), @@ -422,7 +422,7 @@ where p.neutral_0.clone(), p.neutral_0, 0.75, - p.blue.clone(), + p.accent.clone(), p.neutral_9, true, ) @@ -432,7 +432,7 @@ where p.neutral_0.clone(), p.neutral_0, 0.9, - p.blue.clone(), + p.accent.clone(), p.neutral_9, true, ), @@ -442,7 +442,7 @@ where p.neutral_0.clone(), p.neutral_0, 1.0, - p.blue.clone(), + p.accent.clone(), p.neutral_9, true, ) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index cd9f0f93..8ae7847b 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1,6 +1,9 @@ Light ( ( name: "cosmic-light", + accent: ( + c: "#00496D", + ), blue: ( c: "#00496D", ), diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index 5e82c762..5751e231 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,7 +1,11 @@ +pub use corner::*; pub use cosmic_palette::*; pub use derivation::*; +pub use spacing::*; pub use theme::*; +mod corner; mod cosmic_palette; mod derivation; +mod spacing; mod theme; diff --git a/cosmic-theme/src/model/spacing.rs b/cosmic-theme/src/model/spacing.rs new file mode 100644 index 00000000..93b1bf43 --- /dev/null +++ b/cosmic-theme/src/model/spacing.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +/// Spacing variables for the Cosmic theme +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Spacing { + /// No spacing + pub space_none: u16, + /// smallest spacing that can be non-zero + pub space_xxxs: u16, + /// extra extra small spacing + pub space_xxs: u16, + /// extra small spacing + pub space_xs: u16, + /// small spacing + pub space_s: u16, + /// medium spacing + pub space_m: u16, + /// large spacing + pub space_l: u16, + /// extra large spacing + pub space_xl: u16, + /// extra extra large spacing + pub space_xxl: u16, + /// largest possible spacing + pub space_xxxl: u16, +} + +impl Default for Spacing { + fn default() -> Self { + Self { + space_none: 0, + space_xxxs: 4, + space_xxs: 8, + space_xs: 12, + space_s: 16, + space_m: 24, + space_l: 32, + space_xl: 48, + space_xxl: 64, + space_xxxl: 128, + } + } +} diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 284dd6c6..503b12d5 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,11 +1,11 @@ use crate::{ - util::CssColor, Component, ComponentType, Container, ContainerType, CosmicPalette, - CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, + util::CssColor, Component, ComponentType, Container, ContainerType, CornerRadii, CosmicPalette, + CosmicPaletteInner, Spacing, 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 palette::{Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ fmt, @@ -47,6 +47,10 @@ pub struct Theme { pub warning: Component, /// palette pub palette: CosmicPaletteInner, + /// spacing + pub spacing: Spacing, + /// corner radii + pub corner_radii: CornerRadii, /// is dark pub is_dark: bool, /// is high contrast @@ -122,6 +126,14 @@ impl CosmicConfigEntry for Theme { Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast, Err(e) => errors.push(e), } + match config.get::("spacing") { + Ok(spacing) => default.spacing = spacing, + Err(e) => errors.push(e), + } + match config.get::("corner_radii") { + Ok(corner_radii) => default.corner_radii = corner_radii, + Err(e) => errors.push(e), + } if errors.is_empty() { Ok(default) @@ -346,6 +358,72 @@ where pub fn window_header_bg(&self) -> Srgba { self.background.base.clone().into() } + + /// get @space_none + pub fn space_none(&self) -> u16 { + self.spacing.space_none + } + /// get @space_xxxs + pub fn space_xxxs(&self) -> u16 { + self.spacing.space_xxxs + } + /// get @space_xxs + pub fn space_xxs(&self) -> u16 { + self.spacing.space_xxs + } + /// get @space_xs + pub fn space_xs(&self) -> u16 { + self.spacing.space_xs + } + /// get @space_s + pub fn space_s(&self) -> u16 { + self.spacing.space_s + } + /// get @space_m + pub fn space_m(&self) -> u16 { + self.spacing.space_m + } + /// get @space_l + pub fn space_l(&self) -> u16 { + self.spacing.space_l + } + /// get @space_xl + pub fn space_xl(&self) -> u16 { + self.spacing.space_xl + } + /// get @space_xxl + pub fn space_xxl(&self) -> u16 { + self.spacing.space_xxl + } + /// get @space_xxxl + pub fn space_xxxl(&self) -> u16 { + self.spacing.space_xxxl + } + + /// get @radius_0 + pub fn radius_0(&self) -> [u16; 4] { + self.corner_radii.radius_0 + } + /// get @radius_xs + pub fn radius_xs(&self) -> [u16; 4] { + self.corner_radii.radius_xs + } + /// get @radius_s + pub fn radius_s(&self) -> [u16; 4] { + self.corner_radii.radius_s + } + /// get @radius_m + pub fn radius_m(&self) -> [u16; 4] { + self.corner_radii.radius_m + } + /// get @radius_l + pub fn radius_l(&self) -> [u16; 4] { + self.corner_radii.radius_l + } + /// get @radius_xl + pub fn radius_xl(&self) -> [u16; 4] { + self.corner_radii.radius_xl + } } impl Theme { @@ -383,6 +461,8 @@ impl Theme { palette: self.palette.into(), is_dark: self.is_dark, is_high_contrast: self.is_high_contrast, + corner_radii: self.corner_radii, + spacing: self.spacing, } } } @@ -411,6 +491,143 @@ where }, is_dark, is_high_contrast, + spacing: Spacing::default(), + corner_radii: CornerRadii::default(), } } } + +/// Helper for building customized themes +#[derive(Debug, Serialize, Deserialize)] +pub struct ThemeBuilder { + palette: CosmicPalette, + spacing: Spacing, + corner_radii: CornerRadii, + neutral_tint: Option, + bg_color: Option, + primary_container_bg: Option, + text_tint: Option, + accent: Option, +} + +impl Default for ThemeBuilder { + fn default() -> Self { + Self { + palette: DARK_PALETTE.to_owned().into(), + spacing: Spacing::default(), + corner_radii: CornerRadii::default(), + neutral_tint: Default::default(), + text_tint: Default::default(), + bg_color: Default::default(), + primary_container_bg: Default::default(), + accent: Default::default(), + } + } +} + +impl ThemeBuilder { + /// Get a builder that is initialized with the default dark theme + pub fn dark() -> Self { + Self { + palette: DARK_PALETTE.to_owned().into(), + ..Default::default() + } + } + + /// Get a builder that is initialized with the default light theme + pub fn light() -> Self { + Self { + palette: LIGHT_PALETTE.to_owned().into(), + ..Default::default() + } + } + + /// Get a builder that is initialized with the default dark high contrast theme + pub fn dark_high_contrast() -> Self { + let palette: CosmicPalette = DARK_PALETTE.to_owned().into(); + Self { + palette: CosmicPalette::HighContrastLight(palette.inner()), + ..Default::default() + } + } + + /// Get a builder that is initialized with the default light high contrast theme + pub fn light_high_contrast() -> Self { + let palette: CosmicPalette = LIGHT_PALETTE.to_owned().into(); + Self { + palette: CosmicPalette::HighContrastLight(palette.inner()), + ..Default::default() + } + } + + /// set the spacing of the builder + pub fn spacing(mut self, spacing: Spacing) -> Self { + self.spacing = spacing; + self + } + + /// set the corner_radii of the builder + pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self { + self.corner_radii = corner_radii; + self + } + + /// apply a neutral tint to the palette + pub fn neutral_tint(mut self, tint: Srgb) -> Self { + self.neutral_tint = Some(tint); + self + } + + /// apply a text tint to the palette + pub fn text_tint(mut self, tint: Srgb) -> Self { + self.text_tint = Some(tint); + self + } + + /// apply a background color to the palette + pub fn bg_color(mut self, c: Srgba) -> Self { + self.bg_color = Some(c); + self + } + + /// apply a primary container background color to the palette + pub fn primary_container_bg(mut self, c: Srgba) -> Self { + self.primary_container_bg = Some(c); + self + } + + /// apply a accent color to the palette + pub fn accent(mut self, c: Srgb) -> Self { + self.accent = Some(c); + self + } + + /// build the theme + pub fn build(self) -> Theme { + let Self { + mut palette, + spacing, + corner_radii, + neutral_tint, + text_tint, + bg_color, + primary_container_bg, + accent, + } = self; + + if let Some(accent) = accent { + palette.as_mut().accent = accent.into(); + } + + // TODO apply the customizations + + if let Some(accent) = accent { + palette.as_mut().accent = accent.into(); + } + + let mut theme: Theme = palette.into(); + theme.spacing = spacing; + theme.corner_radii = corner_radii; + theme + } +} From a618c1b94add2fa16a74c927ce56712732831106 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 3 Aug 2023 19:30:08 -0400 Subject: [PATCH 0039/1276] wip: calculate theme using new method --- Cargo.toml | 1 + cosmic-theme/src/model/derivation.rs | 297 +-------------------------- cosmic-theme/src/model/layout.rs | 5 + cosmic-theme/src/model/theme.rs | 253 +++++++++++++++++------ cosmic-theme/src/steps.rs | 80 ++++---- src/theme/mod.rs | 44 ++-- 6 files changed, 257 insertions(+), 423 deletions(-) create mode 100644 cosmic-theme/src/model/layout.rs diff --git a/Cargo.toml b/Cargo.toml index b0ad0d80..c8db8479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,5 +98,6 @@ exclude = [ [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} +# TODO Remove me when the palette crate gets an update & before merging [patch.crates-io] palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 1868cdba..26b82ca9 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -2,7 +2,7 @@ use palette::Srgba; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fmt; -use crate::{composite::over, CosmicPalette}; +use crate::composite::over; /// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] @@ -31,134 +31,20 @@ where } } - pub(crate) fn new( - palette: CosmicPalette, - container_type: ComponentType, - bg: C, - on_bg: C, - ) -> Self { + pub(crate) fn new(component: Component, 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(), + component, 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, Eq)] pub struct Component { @@ -263,8 +149,6 @@ where 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, @@ -276,9 +160,6 @@ where 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; @@ -306,175 +187,3 @@ where } } } - -/// 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_0, - p.neutral_10, - 0.08, - p.accent, - p.neutral_8, - false, - ), - - (CosmicPalette::Dark(p), ComponentType::Primary) => Self::component( - p.gray_2, - p.neutral_0, - p.neutral_10, - 0.08, - p.accent, - p.neutral_8, - false, - ), - - (CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component( - p.gray_3, - p.neutral_0, - p.neutral_10, - 0.08, - p.accent, - p.neutral_9, - false, - ), - (CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component( - p.gray_1, - p.neutral_0, - p.neutral_10, - 0.08, - p.accent, - p.neutral_9, - true, - ), - (CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component( - p.gray_2, - p.neutral_0, - p.neutral_10, - 0.08, - p.accent, - p.neutral_9, - true, - ), - (CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component( - p.gray_3, - p.neutral_0, - p.neutral_10.clone(), - 0.08, - p.accent, - p.neutral_10, - true, - ), - - (CosmicPalette::Light(p), ComponentType::Background) => Component::component( - p.gray_1.clone(), - p.neutral_0.clone(), - p.neutral_0, - 0.75, - p.accent.clone(), - p.neutral_8, - false, - ), - (CosmicPalette::Light(p), ComponentType::Primary) => Component::component( - p.gray_2.clone(), - p.neutral_0.clone(), - p.neutral_0, - 0.9, - p.accent.clone(), - p.neutral_8, - false, - ), - (CosmicPalette::Light(p), ComponentType::Secondary) => Component::component( - p.gray_3.clone(), - p.neutral_0.clone(), - p.neutral_0, - 1.0, - p.accent.clone(), - p.neutral_8, - false, - ), - (CosmicPalette::HighContrastLight(p), ComponentType::Background) => { - Component::component( - p.gray_1.clone(), - p.neutral_0.clone(), - p.neutral_0, - 0.75, - p.accent.clone(), - p.neutral_9, - true, - ) - } - (CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component( - p.gray_2.clone(), - p.neutral_0.clone(), - p.neutral_0, - 0.9, - p.accent.clone(), - p.neutral_9, - true, - ), - (CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => { - Component::component( - p.gray_3.clone(), - p.neutral_0.clone(), - p.neutral_0, - 1.0, - p.accent.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_0, 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_0, 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_0, p.blue.clone()) - } - } - } -} diff --git a/cosmic-theme/src/model/layout.rs b/cosmic-theme/src/model/layout.rs new file mode 100644 index 00000000..79456dc6 --- /dev/null +++ b/cosmic-theme/src/model/layout.rs @@ -0,0 +1,5 @@ +#[derive(Default)] +pub struct Layout { + corner_radii: [u32;4], + +} \ No newline at end of file diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 503b12d5..6c2bbcf8 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,11 +1,11 @@ use crate::{ - util::CssColor, Component, ComponentType, Container, ContainerType, CornerRadii, CosmicPalette, - CosmicPaletteInner, Spacing, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, + steps::steps, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, + DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, }; use anyhow::Context; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use directories::{BaseDirsExt, ProjectDirsExt}; -use palette::{Srgb, Srgba}; +use palette::{FromColor, Oklcha, Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ fmt, @@ -57,7 +57,7 @@ pub struct Theme { pub is_high_contrast: bool, } -impl CosmicConfigEntry for Theme { +impl CosmicConfigEntry for Theme { fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { let self_ = self.clone(); // TODO do as transaction @@ -86,35 +86,35 @@ impl CosmicConfigEntry for Theme { Ok(name) => default.name = name, Err(e) => errors.push(e), } - match config.get::>("background") { + match config.get::>("background") { Ok(background) => default.background = background, Err(e) => errors.push(e), } - match config.get::>("primary") { + match config.get::>("primary") { Ok(primary) => default.primary = primary, Err(e) => errors.push(e), } - match config.get::>("secondary") { + match config.get::>("secondary") { Ok(secondary) => default.secondary = secondary, Err(e) => errors.push(e), } - match config.get::>("accent") { + match config.get::>("accent") { Ok(accent) => default.accent = accent, Err(e) => errors.push(e), } - match config.get::>("success") { + match config.get::>("success") { Ok(success) => default.success = success, Err(e) => errors.push(e), } - match config.get::>("destructive") { + match config.get::>("destructive") { Ok(destructive) => default.destructive = destructive, Err(e) => errors.push(e), } - match config.get::>("warning") { + match config.get::>("warning") { Ok(warning) => default.warning = warning, Err(e) => errors.push(e), } - match config.get::>("palette") { + match config.get::>("palette") { Ok(palette) => default.palette = palette, Err(e) => errors.push(e), } @@ -144,12 +144,6 @@ impl CosmicConfigEntry for Theme { } impl Default for Theme { - fn default() -> Self { - Theme::::dark_default().into_srgba() - } -} - -impl Default for Theme { fn default() -> Self { Self::dark_default() } @@ -426,7 +420,7 @@ where } } -impl Theme { +impl Theme { /// get the built in light theme pub fn light_default() -> Self { LIGHT_PALETTE.clone().into() @@ -446,54 +440,14 @@ impl 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, - corner_radii: self.corner_radii, - spacing: self.spacing, - } - } } -impl From> for Theme +impl From> for Theme where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, + CosmicPalette: Into>, { 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, - spacing: Spacing::default(), - corner_radii: CornerRadii::default(), - } + ThemeBuilder::palette(p.into()).build() } } @@ -506,6 +460,7 @@ pub struct ThemeBuilder { neutral_tint: Option, bg_color: Option, primary_container_bg: Option, + secondary_container_bg: Option, text_tint: Option, accent: Option, } @@ -520,6 +475,7 @@ impl Default for ThemeBuilder { text_tint: Default::default(), bg_color: Default::default(), primary_container_bg: Default::default(), + secondary_container_bg: Default::default(), accent: Default::default(), } } @@ -560,6 +516,14 @@ impl ThemeBuilder { } } + /// Get a builder that is initialized with the provided palette + pub fn palette(palette: CosmicPalette) -> Self { + Self { + palette, + ..Default::default() + } + } + /// set the spacing of the builder pub fn spacing(mut self, spacing: Spacing) -> Self { self.spacing = spacing; @@ -612,6 +576,7 @@ impl ThemeBuilder { text_tint, bg_color, primary_container_bg, + secondary_container_bg, accent, } = self; @@ -619,15 +584,175 @@ impl ThemeBuilder { palette.as_mut().accent = accent.into(); } - // TODO apply the customizations + // TODO apply the tint customizations + + let is_dark = palette.is_dark(); + let is_high_contrast = palette.is_high_contrast(); if let Some(accent) = accent { palette.as_mut().accent = accent.into(); } + let p_ref = palette.as_ref(); - let mut theme: Theme = palette.into(); + let bg = if let Some(bg_color) = bg_color { + bg_color + } else { + p_ref.neutral_0.clone() + }; + let ok_bg = Oklcha::from_color(bg); + let step_array = steps(ok_bg); + + let bg_index = color_index(bg, step_array.len()); + let primary_container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + primary_container_bg_color + } else { + get_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) + }; + + let secondary_container_bg = if let Some(secondary_container_bg) = secondary_container_bg { + secondary_container_bg + } else { + get_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) + }; + + let bg_component = get_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); + let on_bg_component = get_text( + color_index(bg_component, step_array.len()), + &step_array, + is_dark, + &p_ref.neutral_8, + ); + let bg_component = Component::component( + bg_component, + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + on_bg_component, + is_high_contrast, + ); + + let primary_index = color_index(primary_container_bg, step_array.len()); + let primary_component = get_color(primary_index, 6, &step_array, is_dark, &p_ref.neutral_3); + let on_primary_component = get_text( + color_index(primary_component, step_array.len()), + &step_array, + is_dark, + &p_ref.neutral_8, + ); + let primary_component = Component::component( + primary_component, + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + on_primary_component, + is_high_contrast, + ); + + let secondary_index = color_index(secondary_container_bg, step_array.len()); + let secondary_component = + get_color(secondary_index, 3, &step_array, is_dark, &p_ref.neutral_4); + let on_secondary_component = get_text( + color_index(secondary_component, step_array.len()), + &step_array, + is_dark, + &p_ref.neutral_10, + ); + let secondary_component = Component::component( + secondary_component, + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + on_secondary_component, + is_high_contrast, + ); + + let mut theme: Theme = Theme { + name: palette.name().to_string(), + background: Container::new( + bg_component, + bg, + get_text(bg_index, &step_array, is_dark, &p_ref.neutral_8), + ), + primary: Container::new( + primary_component, + primary_container_bg, + get_text(primary_index, &step_array, is_dark, &p_ref.neutral_8), + ), + secondary: Container::new( + secondary_component, + secondary_container_bg, + get_text(secondary_index, &step_array, is_dark, &p_ref.neutral_8), + ), + accent: Component::colored_component( + p_ref.accent.to_owned(), + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + ), + success: Component::colored_component( + p_ref.green.to_owned(), + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + ), + destructive: Component::colored_component( + p_ref.red.to_owned(), + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + ), + warning: Component::colored_component( + p_ref.yellow.to_owned(), + p_ref.neutral_0.to_owned(), + p_ref.accent.to_owned(), + ), + palette: palette.inner(), + spacing, + corner_radii, + is_dark, + is_high_contrast, + }; theme.spacing = spacing; theme.corner_radii = corner_radii; theme } } + +fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option { + if is_dark { + base_index.checked_add(steps) + } else { + base_index.checked_sub(steps) + } + .filter(|i| *i < step_len) +} + +fn get_color( + base_index: usize, + steps: usize, + step_array: &[Srgba; 100], + is_dark: bool, + fallback: &Srgba, +) -> Srgba { + get_index(base_index, steps, step_array.len(), is_dark) + .and_then(|i| step_array.get(i).cloned()) + .unwrap_or_else(|| fallback.to_owned()) +} + +fn get_text( + base_index: usize, + step_array: &[Srgba; 100], + is_dark: bool, + fallback: &Srgba, +) -> Srgba { + let Some(index) = get_index(base_index, 70, step_array.len(), is_dark).or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) else { + return fallback.to_owned(); + }; + + step_array + .get(index) + .cloned() + .unwrap_or_else(|| fallback.to_owned()) +} + +fn color_index(c: C, array_len: usize) -> usize +where + Oklcha: FromColor, +{ + let c = Oklcha::from_color(c); + ((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1) +} diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 5e2f635c..5407a584 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -1,11 +1,11 @@ use almost::equal; -use palette::{convert::FromColorUnclamped, ClampAssign, Oklch, Srgb}; +use palette::{convert::FromColorUnclamped, ClampAssign, Oklcha, Srgb, Srgba}; /// Get an array of 100 colors with a specific hue and chroma /// over the full range of lightness. -/// Colors which are not valid Srgb will fallback to a color with the nearest valid chroma. -pub fn steps(mut c: Oklch) -> [Srgb; 100] { - let mut steps = [Srgb::new(0.0, 0.0, 0.0); 100]; +/// Colors which are not valid Srgba will fallback to a color with the nearest valid chroma. +pub fn steps(mut c: Oklcha) -> [Srgba; 100] { + let mut steps = [Srgba::new(0.0, 0.0, 0.0, 1.0); 100]; for i in 0..steps.len() { let lightness = i as f32 / 100.0; @@ -16,12 +16,12 @@ pub fn steps(mut c: Oklch) -> [Srgb; 100] { steps } -/// find the nearest chroma which makes our color a valid color in Srgb -pub fn oklch_to_srgba_nearest_chroma(mut c: Oklch) -> Srgb { +/// find the nearest chroma which makes our color a valid color in Srgba +pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba { let mut r_chroma = c.chroma; let mut l_chroma = 0.0; // exit early if we found it right away - let mut new_c = Srgb::from_color_unclamped(c); + let mut new_c = Srgba::from_color_unclamped(c); if is_valid_srgb(new_c) { new_c.clamp_assign(); @@ -30,7 +30,7 @@ pub fn oklch_to_srgba_nearest_chroma(mut c: Oklch) -> Srgb { // is this an excessive depth to search? for _ in 0..64 { - let new_c = Srgb::from_color_unclamped(c); + let new_c = Srgba::from_color_unclamped(c); if is_valid_srgb(new_c) { l_chroma = c.chroma; c.chroma = (c.chroma + r_chroma) / 2.0; @@ -39,11 +39,11 @@ pub fn oklch_to_srgba_nearest_chroma(mut c: Oklch) -> Srgb { c.chroma = (c.chroma + l_chroma) / 2.0; } } - Srgb::from_color_unclamped(c) + Srgba::from_color_unclamped(c) } /// checks that the color is valid srgb -pub fn is_valid_srgb(c: Srgb) -> bool { +pub fn is_valid_srgb(c: Srgba) -> bool { (equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red())) && (equal(c.blue, Srgb::max_blue()) || (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue())) @@ -54,38 +54,38 @@ pub fn is_valid_srgb(c: Srgb) -> bool { #[cfg(test)] mod tests { use almost::equal; - use palette::{OklabHue, Srgb}; + use palette::{OklabHue, Srgba}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; #[test] fn test_valid_check() { - assert!(is_valid_srgb(Srgb::new(1.0, 1.0, 1.0))); - assert!(is_valid_srgb(Srgb::new(0.0, 0.0, 0.0))); - assert!(is_valid_srgb(Srgb::new(0.5, 0.5, 0.5))); - assert!(!is_valid_srgb(Srgb::new(-0.1, 0.0, 0.0))); - assert!(!is_valid_srgb(Srgb::new(0.0, -0.1, 0.0))); - assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -0.1))); - assert!(!is_valid_srgb(Srgb::new(-100.1, 0.0, 0.0))); - assert!(!is_valid_srgb(Srgb::new(0.0, -100.1, 0.0))); - assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -100.1))); - assert!(!is_valid_srgb(Srgb::new(1.1, 0.0, 0.0))); - assert!(!is_valid_srgb(Srgb::new(0.0, 1.1, 0.0))); - assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 1.1))); - assert!(!is_valid_srgb(Srgb::new(100.1, 0.0, 0.0))); - assert!(!is_valid_srgb(Srgb::new(0.0, 100.1, 0.0))); - assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 100.1))); + assert!(is_valid_srgb(Srgba::new(1.0, 1.0, 1.0, 1.0))); + assert!(is_valid_srgb(Srgba::new(0.0, 0.0, 0.0, 1.0))); + assert!(is_valid_srgb(Srgba::new(0.5, 0.5, 0.5, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-0.1, 0.0, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(0.0, -0.1, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -0.1, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-100.1, 0.0, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(0.0, -100.1, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -100.1, 1.0))); + assert!(!is_valid_srgb(Srgba::new(1.1, 0.0, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(0.0, 1.1, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 1.1, 1.0))); + assert!(!is_valid_srgb(Srgba::new(100.1, 0.0, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(0.0, 100.1, 0.0, 1.0))); + assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 100.1, 1.0))); } #[test] fn test_conversion_boundaries() { - let c1 = palette::Oklch::new(0.0, 0.288, OklabHue::from_degrees(0.0)); + let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); equal(srgb.red, 0.0); equal(srgb.blue, 0.0); equal(srgb.green, 0.0); - let c1 = palette::Oklch::new(1.0, 0.288, OklabHue::from_degrees(0.0)); + let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); equal(srgb.red, 1.0); @@ -95,20 +95,20 @@ mod tests { #[test] fn test_conversion_colors() { - let c1 = palette::Oklch::new(0.4608, 0.11111, OklabHue::new(57.31)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 133); assert!(srgb.green == 69); assert!(srgb.blue == 0); - let c1 = palette::Oklch::new(0.30, 0.08, OklabHue::new(35.0)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 78); assert!(srgb.green == 27); assert!(srgb.blue == 15); - let c1 = palette::Oklch::new(0.757, 0.146, OklabHue::new(301.2)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 192); assert!(srgb.green == 153); assert!(srgb.blue == 253); @@ -116,20 +116,20 @@ mod tests { #[test] fn test_conversion_fallback_colors() { - let c1 = palette::Oklch::new(0.70, 0.284, OklabHue::new(35.0)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 255); assert!(srgb.green == 103); assert!(srgb.blue == 65); - let c1 = palette::Oklch::new(0.757, 0.239, OklabHue::new(301.2)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 193); assert!(srgb.green == 152); assert!(srgb.blue == 255); - let c1 = palette::Oklch::new(0.163, 0.333, OklabHue::new(141.0)); - let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert!(srgb.red == 1); assert!(srgb.green == 19); assert!(srgb.blue == 0); diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 8a23a039..2fb5e8c1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -44,13 +44,12 @@ use palette::Srgba; 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(); - pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicThemeCss::high_contrast_dark_default().into_srgba(); - pub static ref COSMIC_LIGHT: CosmicTheme = CosmicThemeCss::light_default().into_srgba(); - pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicThemeCss::high_contrast_light_default().into_srgba(); + pub static ref COSMIC_DARK: CosmicTheme = CosmicTheme::dark_default(); + pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicTheme::high_contrast_dark_default(); + pub static ref COSMIC_LIGHT: CosmicTheme = CosmicTheme::light_default(); + pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicTheme::high_contrast_light_default(); pub static ref TRANSPARENT_COMPONENT: Component = Component { base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), @@ -1202,6 +1201,7 @@ impl text_input::StyleSheet for Theme { } } +#[must_use] pub fn theme() -> Theme { let Ok(helper) = crate::cosmic_config::Config::new( crate::cosmic_theme::NAME, @@ -1209,34 +1209,28 @@ pub fn theme() -> Theme { ) else { return crate::theme::Theme::dark(); }; - let t = crate::cosmic_theme::Theme::get_entry(&helper).map_or_else( - |(errors, theme)| { - for err in errors { - tracing::error!("{:?}", err); - } - theme.into_srgba() - }, - crate::cosmic_theme::Theme::into_srgba, - ); + let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme + }); crate::theme::Theme::custom(Arc::new(t)) } pub fn subscription(id: u64) -> Subscription { - config_subscription::>( + config_subscription::>( id, crate::cosmic_theme::NAME.into(), - crate::cosmic_theme::Theme::::version(), + crate::cosmic_theme::Theme::::version(), ) .map(|(_, res)| { - let theme = res.map_or_else( - |(errors, theme)| { - for err in errors { - tracing::error!("{:?}", err); - } - theme.into_srgba() - }, - crate::cosmic_theme::Theme::into_srgba, - ); + let theme = res.unwrap_or_else(|(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme + }); crate::theme::Theme::custom(Arc::new(theme)) }) } From 85f816f35b10250b76df136b477f9017ba50fdf7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 3 Aug 2023 19:31:16 -0400 Subject: [PATCH 0040/1276] fix: example --- examples/cosmic/src/window.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index cccb49d0..213af78e 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -10,7 +10,7 @@ use cosmic::{ window::{self, close, drag, minimize, toggle_maximize}, }, keyboard_nav, - theme::{self, CosmicTheme, CosmicThemeCss, Theme}, + theme::{self, CosmicTheme, Theme}, widget::{ header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings, warning, IconSource, @@ -389,16 +389,17 @@ 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()), + config_subscription::<_, CosmicTheme>(0, Cow::from("com.system76.CosmicTheme"), 1).map( + |(_, update)| match update { + Ok(t) => Message::SystemTheme(t), Err((errors, t)) => { for error in errors { error!("{:?}", error); } - Message::SystemTheme(t.into_srgba()) + Message::SystemTheme(t) } - }), + }, + ), self.timeline .borrow() .as_subscription() From 4c6912d351bfc9aa67ee23f1c4bddcab6e60d3f0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 3 Aug 2023 19:56:16 -0400 Subject: [PATCH 0041/1276] fix: typo --- cosmic-theme/src/model/theme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 6c2bbcf8..4e761768 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -597,7 +597,7 @@ impl ThemeBuilder { let bg = if let Some(bg_color) = bg_color { bg_color } else { - p_ref.neutral_0.clone() + p_ref.gray_1.clone() }; let ok_bg = Oklcha::from_color(bg); let step_array = steps(ok_bg); From c819f94e745b2bdf13f768680aaa6b2990300b4c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 11:56:31 -0400 Subject: [PATCH 0042/1276] feat: apply tints --- cosmic-theme/src/model/theme.rs | 106 +++++++++++++++----------------- cosmic-theme/src/steps.rs | 80 +++++++++++++++++++++--- 2 files changed, 123 insertions(+), 63 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 4e761768..b85e2fa7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,16 +1,17 @@ use crate::{ - steps::steps, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, + steps::*, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, }; use anyhow::Context; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use directories::{BaseDirsExt, ProjectDirsExt}; -use palette::{FromColor, Oklcha, Srgb, Srgba}; +use palette::{Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ fmt, fs::File, io::Write, + num::NonZeroUsize, path::{Path, PathBuf}, }; @@ -580,14 +581,34 @@ impl ThemeBuilder { accent, } = self; + let is_dark = palette.is_dark(); + let is_high_contrast = palette.is_high_contrast(); + if let Some(accent) = accent { palette.as_mut().accent = accent.into(); } - // TODO apply the tint customizations + let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); - let is_dark = palette.is_dark(); - let is_high_contrast = palette.is_high_contrast(); + if let Some(neutral_tint) = neutral_tint { + let mut neutral_steps_arr = steps(neutral_tint, NonZeroUsize::new(11).unwrap()); + if !is_dark { + neutral_steps_arr.reverse(); + } + + let p = palette.as_mut(); + p.neutral_0 = neutral_steps_arr[0]; + p.neutral_1 = neutral_steps_arr[1]; + p.neutral_2 = neutral_steps_arr[2]; + p.neutral_3 = neutral_steps_arr[3]; + p.neutral_4 = neutral_steps_arr[4]; + p.neutral_5 = neutral_steps_arr[5]; + p.neutral_6 = neutral_steps_arr[6]; + p.neutral_7 = neutral_steps_arr[7]; + p.neutral_8 = neutral_steps_arr[8]; + p.neutral_9 = neutral_steps_arr[9]; + p.neutral_10 = neutral_steps_arr[10]; + } if let Some(accent) = accent { palette.as_mut().accent = accent.into(); @@ -599,8 +620,7 @@ impl ThemeBuilder { } else { p_ref.gray_1.clone() }; - let ok_bg = Oklcha::from_color(bg); - let step_array = steps(ok_bg); + let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); let primary_container_bg = if let Some(primary_container_bg_color) = primary_container_bg { @@ -621,6 +641,7 @@ impl ThemeBuilder { &step_array, is_dark, &p_ref.neutral_8, + text_steps_array.as_ref(), ); let bg_component = Component::component( bg_component, @@ -637,6 +658,7 @@ impl ThemeBuilder { &step_array, is_dark, &p_ref.neutral_8, + text_steps_array.as_ref(), ); let primary_component = Component::component( primary_component, @@ -654,6 +676,7 @@ impl ThemeBuilder { &step_array, is_dark, &p_ref.neutral_10, + text_steps_array.as_ref(), ); let secondary_component = Component::component( secondary_component, @@ -668,17 +691,35 @@ impl ThemeBuilder { background: Container::new( bg_component, bg, - get_text(bg_index, &step_array, is_dark, &p_ref.neutral_8), + get_text( + bg_index, + &step_array, + is_dark, + &p_ref.neutral_8, + text_steps_array.as_ref(), + ), ), primary: Container::new( primary_component, primary_container_bg, - get_text(primary_index, &step_array, is_dark, &p_ref.neutral_8), + get_text( + primary_index, + &step_array, + is_dark, + &p_ref.neutral_8, + text_steps_array.as_ref(), + ), ), secondary: Container::new( secondary_component, secondary_container_bg, - get_text(secondary_index, &step_array, is_dark, &p_ref.neutral_8), + get_text( + secondary_index, + &step_array, + is_dark, + &p_ref.neutral_8, + text_steps_array.as_ref(), + ), ), accent: Component::colored_component( p_ref.accent.to_owned(), @@ -711,48 +752,3 @@ impl ThemeBuilder { theme } } - -fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option { - if is_dark { - base_index.checked_add(steps) - } else { - base_index.checked_sub(steps) - } - .filter(|i| *i < step_len) -} - -fn get_color( - base_index: usize, - steps: usize, - step_array: &[Srgba; 100], - is_dark: bool, - fallback: &Srgba, -) -> Srgba { - get_index(base_index, steps, step_array.len(), is_dark) - .and_then(|i| step_array.get(i).cloned()) - .unwrap_or_else(|| fallback.to_owned()) -} - -fn get_text( - base_index: usize, - step_array: &[Srgba; 100], - is_dark: bool, - fallback: &Srgba, -) -> Srgba { - let Some(index) = get_index(base_index, 70, step_array.len(), is_dark).or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) else { - return fallback.to_owned(); - }; - - step_array - .get(index) - .cloned() - .unwrap_or_else(|| fallback.to_owned()) -} - -fn color_index(c: C, array_len: usize) -> usize -where - Oklcha: FromColor, -{ - let c = Oklcha::from_color(c); - ((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1) -} diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 5407a584..58117e5d 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -1,21 +1,85 @@ +use std::num::NonZeroUsize; + use almost::equal; -use palette::{convert::FromColorUnclamped, ClampAssign, Oklcha, Srgb, Srgba}; +use palette::{convert::FromColorUnclamped, ClampAssign, FromColor, Oklcha, Srgb, Srgba}; /// Get an array of 100 colors with a specific hue and chroma /// over the full range of lightness. /// Colors which are not valid Srgba will fallback to a color with the nearest valid chroma. -pub fn steps(mut c: Oklcha) -> [Srgba; 100] { - let mut steps = [Srgba::new(0.0, 0.0, 0.0, 1.0); 100]; +pub fn steps(c: C, len: NonZeroUsize) -> Vec +where + Oklcha: FromColor, +{ + let mut c = Oklcha::from_color(c); + let mut steps = Vec::with_capacity(len.get()); - for i in 0..steps.len() { - let lightness = i as f32 / 100.0; + for i in 0..len.get() { + let lightness = i as f32 / (len.get() - 1) as f32; c.l = lightness; - steps[i] = oklch_to_srgba_nearest_chroma(c) + steps.push(oklch_to_srgba_nearest_chroma(c)) } - steps } +/// get the index for a new color some steps away from a base color +pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option { + if is_dark { + base_index.checked_add(steps) + } else { + base_index.checked_sub(steps) + } + .filter(|i| *i < step_len) +} + +/// get color given a base and some steps +pub fn get_color( + base_index: usize, + steps: usize, + step_array: &Vec, + is_dark: bool, + fallback: &Srgba, +) -> Srgba { + assert!(step_array.len() == 100); + get_index(base_index, steps, step_array.len(), is_dark) + .and_then(|i| step_array.get(i).cloned()) + .unwrap_or_else(|| fallback.to_owned()) +} + +/// get text color given a base background color +pub fn get_text( + base_index: usize, + step_array: &Vec, + is_dark: bool, + fallback: &Srgba, + tint_array: Option<&Vec>, +) -> Srgba { + assert!(step_array.len() == 100); + let step_array = if let Some(tint_array) = tint_array { + assert!(tint_array.len() == 100); + tint_array + } else { + step_array + }; + let Some(index) = get_index(base_index, 70, step_array.len(), is_dark).or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) else { + return fallback.to_owned(); + }; + + step_array + .get(index) + .cloned() + .unwrap_or_else(|| fallback.to_owned()) +} + +/// get the index into the steps array for a given color +/// the index is the lightness value of the color converted to Oklcha, scaled to the range [0, 100] +pub fn color_index(c: C, array_len: usize) -> usize +where + Oklcha: FromColor, +{ + let c = Oklcha::from_color(c); + ((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1) +} + /// find the nearest chroma which makes our color a valid color in Srgba pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba { let mut r_chroma = c.chroma; @@ -39,7 +103,7 @@ pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba { c.chroma = (c.chroma + l_chroma) / 2.0; } } - Srgba::from_color_unclamped(c) + Srgba::from_color(c) } /// checks that the color is valid srgb From ea09abb8929bbddd201a6c01b4c51acde01055d6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 12:42:09 -0400 Subject: [PATCH 0043/1276] cleanup: remove methods that aren't used anymore --- cosmic-theme/Cargo.toml | 3 -- cosmic-theme/src/lib.rs | 4 -- cosmic-theme/src/model/cosmic_palette.rs | 54 +---------------------- cosmic-theme/src/model/theme.rs | 56 +----------------------- 4 files changed, 4 insertions(+), 113 deletions(-) diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index ecf4415c..4a02dbe1 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -18,13 +18,10 @@ theme-from-image = ["kmeans_colors", "image"] # palette = {version = "0.7", features = ["serializing"] } almost = "0.2" palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } -anyhow = "1.0" kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } image = {version = "0.24.1", 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/src/lib.rs b/cosmic-theme/src/lib.rs index 28e6e8a3..782d15f1 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -21,9 +21,5 @@ 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; diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 7419e102..cd3f5491 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,17 +1,10 @@ -use std::{ - fmt, - fs::File, - io::Write, - path::{Path, PathBuf}, -}; +use std::fmt; -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}; +use crate::util::CssColor; lazy_static! { /// built in light palette @@ -236,49 +229,6 @@ where 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)?) - } } impl Into> for CosmicPalette { diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index b85e2fa7..25cf5c20 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,19 +1,11 @@ use crate::{ steps::*, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, - DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR, + DARK_PALETTE, LIGHT_PALETTE, NAME, }; -use anyhow::Context; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; -use directories::{BaseDirsExt, ProjectDirsExt}; use palette::{Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{ - fmt, - fs::File, - io::Write, - num::NonZeroUsize, - path::{Path, PathBuf}, -}; +use std::{fmt, num::NonZeroUsize}; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] /// Theme layer type @@ -177,50 +169,6 @@ where 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 { From 2dfa9dab5ab925348e95055ffb05f07c2336c9fb Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 12:59:35 -0400 Subject: [PATCH 0044/1276] feat: add customization for status colors --- cosmic-theme/src/model/cosmic_palette.rs | 4 -- cosmic-theme/src/model/dark.ron | 3 - cosmic-theme/src/model/light.ron | 3 - cosmic-theme/src/model/theme.rs | 80 ++++++++++++++++++------ 4 files changed, 62 insertions(+), 28 deletions(-) diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index cd3f5491..073031ba 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -101,9 +101,6 @@ pub struct CosmicPaletteInner { /// name of the palette pub name: String, - /// the selected accent color - pub accent: C, - /// basic palette /// blue: colors used for various points of emphasis in the UI pub blue: C, @@ -180,7 +177,6 @@ impl From> for CosmicPaletteInner { fn from(p: CosmicPaletteInner) -> Self { CosmicPaletteInner { name: p.name, - accent: p.accent.into(), blue: p.blue.into(), red: p.red.into(), green: p.green.into(), diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index a7d77282..9f283121 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1,9 +1,6 @@ Dark ( ( name: "cosmic-dark", - accent: ( - c: "#94EBEB", - ), blue: ( c: "#94EBEB", ), diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 8ae7847b..cd9f0f93 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1,9 +1,6 @@ Light ( ( name: "cosmic-light", - accent: ( - c: "#00496D", - ), blue: ( c: "#00496D", ), diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 25cf5c20..0cc54e6c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -3,7 +3,7 @@ use crate::{ DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; -use palette::{Srgb, Srgba}; +use palette::{IntoColor, Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{fmt, num::NonZeroUsize}; @@ -412,6 +412,9 @@ pub struct ThemeBuilder { secondary_container_bg: Option, text_tint: Option, accent: Option, + success: Option, + warning: Option, + destructive: Option, } impl Default for ThemeBuilder { @@ -426,6 +429,9 @@ impl Default for ThemeBuilder { primary_container_bg: Default::default(), secondary_container_bg: Default::default(), accent: Default::default(), + success: Default::default(), + warning: Default::default(), + destructive: Default::default(), } } } @@ -515,6 +521,24 @@ impl ThemeBuilder { self } + /// apply a success color to the palette + pub fn success(mut self, c: Srgb) -> Self { + self.success = Some(c); + self + } + + /// apply a warning color to the palette + pub fn warning(mut self, c: Srgb) -> Self { + self.warning = Some(c); + self + } + + /// apply a destructive color to the palette + pub fn destructive(mut self, c: Srgb) -> Self { + self.destructive = Some(c); + self + } + /// build the theme pub fn build(self) -> Theme { let Self { @@ -527,14 +551,37 @@ impl ThemeBuilder { primary_container_bg, secondary_container_bg, accent, + success, + warning, + destructive, } = self; let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); - if let Some(accent) = accent { - palette.as_mut().accent = accent.into(); - } + let accent = if let Some(accent) = accent { + accent.into_color() + } else { + palette.as_ref().blue.to_owned() + }; + + let success = if let Some(success) = success { + success.into_color() + } else { + palette.as_ref().green.to_owned() + }; + + let warning = if let Some(warning) = warning { + warning.into_color() + } else { + palette.as_ref().yellow.to_owned() + }; + + let destructive = if let Some(destructive) = destructive { + destructive.into_color() + } else { + palette.as_ref().red.to_owned() + }; let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); @@ -558,9 +605,6 @@ impl ThemeBuilder { p.neutral_10 = neutral_steps_arr[10]; } - if let Some(accent) = accent { - palette.as_mut().accent = accent.into(); - } let p_ref = palette.as_ref(); let bg = if let Some(bg_color) = bg_color { @@ -594,7 +638,7 @@ impl ThemeBuilder { let bg_component = Component::component( bg_component, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), on_bg_component, is_high_contrast, ); @@ -611,7 +655,7 @@ impl ThemeBuilder { let primary_component = Component::component( primary_component, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), on_primary_component, is_high_contrast, ); @@ -629,7 +673,7 @@ impl ThemeBuilder { let secondary_component = Component::component( secondary_component, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), on_secondary_component, is_high_contrast, ); @@ -670,24 +714,24 @@ impl ThemeBuilder { ), ), accent: Component::colored_component( - p_ref.accent.to_owned(), + accent.clone(), p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), ), success: Component::colored_component( - p_ref.green.to_owned(), + success, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), ), destructive: Component::colored_component( - p_ref.red.to_owned(), + destructive, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), ), warning: Component::colored_component( - p_ref.yellow.to_owned(), + warning, p_ref.neutral_0.to_owned(), - p_ref.accent.to_owned(), + accent.clone(), ), palette: palette.inner(), spacing, From 68225c78cd5792d72b86ed4024d1ad054aea9eee Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 15:44:18 -0400 Subject: [PATCH 0045/1276] fix: write spacing and corner_radii when writing the theme --- cosmic-theme/src/model/theme.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 0cc54e6c..ec7bb64f 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -67,6 +67,8 @@ impl CosmicConfigEntry for Theme { tx.set("palette", self_.palette)?; tx.set("is_dark", self_.is_dark)?; tx.set("is_high_contrast", self_.is_high_contrast)?; + tx.set("spacing", self_.spacing)?; + tx.set("corner_radii", self_.corner_radii)?; tx.commit() } From 40efcbbe3120abe8e350fa7c36a5d6fa7e621b55 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 15:45:31 -0400 Subject: [PATCH 0046/1276] refactor: use channel subscription for config subscriptions --- cosmic-config/src/lib.rs | 99 +++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index e3d82138..27f3a13e 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,7 +1,6 @@ +use iced_futures::futures::SinkExt; #[cfg(feature = "subscription")] -use iced_futures::futures::channel::mpsc; -#[cfg(feature = "subscription")] -use iced_futures::subscription; +use iced_futures::{futures::channel::mpsc, subscription}; use notify::{ event::{EventKind, ModifyKind}, RecommendedWatcher, Watcher, @@ -319,24 +318,27 @@ pub fn config_subscription< 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), - ) + subscription::channel(id, 100, move |mut output| { + let config_id = config_id.clone(); + async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version); + + loop { + state = start_listening(state, &mut output, id).await; + } + } + }) } -#[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, -) { + output: &mut mpsc::Sender<(I, Result, T)>)>, + id: I, +) -> ConfigState { use iced_futures::futures::{future::pending, StreamExt}; match state { @@ -344,66 +346,49 @@ async fn start_listening< let (tx, rx) = mpsc::channel(100); let config = match Config::new(&config_id, version) { Ok(c) => c, - Err(_) => return (None, ConfigState::Failed), + Err(_) => return 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), + Err(_) => return ConfigState::Failed, }; + let msg = T::get_entry(&config); + _ = output.send((id, msg)).await; 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), - ), + Ok(t) => { + _ = output.send((id, Ok(t.clone()))).await; + ConfigState::Waiting(t, watcher, rx, config) + } + Err((errors, t)) => { + _ = output.send((id, Err((errors, t.clone())))).await; + ConfigState::Waiting(t, watcher, rx, config) + } } } - ConfigState::Waiting(old, watcher, mut rx, config) => match rx.next().await { + ConfigState::Waiting(mut old, watcher, mut rx, config) => match rx.next().await { Some(_) => match T::get_entry(&config) { - Ok(t) => ( + Ok(t) => { if t != old { - Some((id, Ok(t.clone()))) - } else { - None - }, - ConfigState::Waiting(t, watcher, rx, config), - ), - Err((errors, t)) => ( + old = t; + _ = output.send((id, Ok(old.clone()))).await; + } + ConfigState::Waiting(old, watcher, rx, config) + } + Err((errors, t)) => { if t != old { - Some((id, Err((errors, t.clone())))) - } else { - None - }, - ConfigState::Waiting(t, watcher, rx, config), - ), + old = t; + _ = output.send((id, Err((errors, old.clone())))).await; + } + ConfigState::Waiting(old, watcher, rx, config) + } }, - None => (None, ConfigState::Failed), + 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); - } - } -} From 6c57e04e36e47ef4668c736f20d73ca87d5b5bae Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 15:48:34 -0400 Subject: [PATCH 0047/1276] refactor: introduce thread local THEME variable and distinguish between custom and system theme settings --- examples/cosmic/src/window.rs | 38 ++++++++++++--------------- examples/cosmic/src/window/demo.rs | 5 +++- examples/cosmic/src/window/desktop.rs | 2 +- examples/cosmic/src/window/editor.rs | 5 ++-- src/app/core.rs | 4 +-- src/app/cosmic.rs | 30 ++++++++++++++++++--- src/app/mod.rs | 6 ++++- src/theme/mod.rs | 26 +++++++++++++++--- 8 files changed, 80 insertions(+), 36 deletions(-) diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 213af78e..964d2f94 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -1,7 +1,10 @@ /// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 use cosmic::{ - cosmic_config::config_subscription, + cosmic_theme::{ + palette::{rgb::Rgb, Srgba}, + ThemeBuilder, + }, font::load_fonts, iced::{self, Application, Command, Length, Subscription}, iced::{ @@ -10,7 +13,7 @@ use cosmic::{ window::{self, close, drag, minimize, toggle_maximize}, }, keyboard_nav, - theme::{self, CosmicTheme, Theme}, + theme::{self, Theme}, widget::{ header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings, warning, IconSource, @@ -18,9 +21,7 @@ use cosmic::{ Element, ElementExt, }; use cosmic_time::{Instant, Timeline}; -use log::error; use std::{ - borrow::Cow, cell::RefCell, rc::Rc, sync::{ @@ -163,7 +164,6 @@ pub struct Window { warning_message: String, scale_factor: f64, scale_factor_string: String, - system_theme: Arc, timeline: Rc>, } @@ -209,7 +209,6 @@ pub enum Message { ToggleNavBarCondensed, ToggleWarning, FontsLoaded, - SystemTheme(CosmicTheme), Tick(Instant), } @@ -389,17 +388,6 @@ impl Application for Window { Subscription::batch(vec![ window_break.map(|_| Message::CondensedViewToggle), keyboard_nav::subscription().map(Message::KeyboardNav), - config_subscription::<_, CosmicTheme>(0, Cow::from("com.system76.CosmicTheme"), 1).map( - |(_, update)| match update { - Ok(t) => Message::SystemTheme(t), - Err((errors, t)) => { - for error in errors { - error!("{:?}", error); - } - Message::SystemTheme(t) - } - }, - ), self.timeline .borrow() .as_subscription() @@ -429,7 +417,18 @@ impl Application for Window { 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()), + demo::ThemeVariant::Custom => Theme::custom(Arc::new( + ThemeBuilder::light() + .bg_color(Srgba::new(1.0, 0.9, 0.9, 1.0)) + .text_tint(Rgb::new(0.0, 1.0, 0.0)) + .neutral_tint(Rgb::new(0.0, 0.5, 1.0)) + .accent(Rgb::new(0.5, 0.1, 0.5)) + .success(Rgb::new(0.0, 0.5, 0.3)) + .warning(Rgb::new(0.894, 0.816, 0.039)) + .destructive(Rgb::new(0.890, 0.145, 0.420)) + .build(), + )), + demo::ThemeVariant::System => cosmic::theme::theme(), }; } Some(demo::Output::ToggleWarning) => self.toggle_warning(), @@ -460,9 +459,6 @@ impl Application for Window { }, Message::ToggleWarning => self.toggle_warning(), Message::FontsLoaded => {} - Message::SystemTheme(t) => { - self.system_theme = Arc::new(t); - } Message::Tick(instant) => self.timeline.borrow_mut().now(instant), } ret diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index d5402a48..85e396d3 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -27,6 +27,7 @@ pub enum ThemeVariant { HighContrastDark, HighContrastLight, Custom, + System, } impl From<&ThemeType> for ThemeVariant { @@ -37,6 +38,7 @@ impl From<&ThemeType> for ThemeVariant { ThemeType::HighContrastDark => ThemeVariant::HighContrastDark, ThemeType::HighContrastLight => ThemeVariant::HighContrastLight, ThemeType::Custom(_) => ThemeVariant::Custom, + ThemeType::System(_) => ThemeVariant::System, } } } @@ -210,8 +212,9 @@ impl State { ThemeVariant::Light, ThemeVariant::Dark, ThemeVariant::HighContrastLight, - ThemeVariant::HighContrastLight, + ThemeVariant::HighContrastDark, ThemeVariant::Custom, + ThemeVariant::System, ] .into_iter() .fold( diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index f20722fb..3b3858da 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -193,7 +193,7 @@ impl State { } fn view_desktop_wallpaper<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { - let mut image_paths: Vec = Vec::new(); + let image_paths: Vec = Vec::new(); /* //TODO: load image paths, do this asynchronously somehow if let Ok(entries) = std::fs::read_dir("/usr/share/backgrounds") { diff --git a/examples/cosmic/src/window/editor.rs b/examples/cosmic/src/window/editor.rs index e272050d..6c027c09 100644 --- a/examples/cosmic/src/window/editor.rs +++ b/examples/cosmic/src/window/editor.rs @@ -1,5 +1,4 @@ -use apply::Apply; -use cosmic::iced::widget::{horizontal_space, row, scrollable}; +use cosmic::iced::widget::{horizontal_space, row}; use cosmic::iced::{Alignment, Length}; use cosmic::widget::{button, segmented_button, view_switcher}; use cosmic::{theme, Element}; @@ -60,7 +59,7 @@ impl State { self.pages.remove(id); } - pub(super) fn view<'a>(&'a self, window: &'a super::Window) -> Element<'a, Message> { + pub(super) fn view<'a>(&'a self, _window: &'a super::Window) -> Element<'a, Message> { let tabs = view_switcher::horizontal(&self.pages) .show_close_icon_on_hover(true) .on_activate(Message::Activate) diff --git a/src/app/core.rs b/src/app/core.rs index 73d05342..79f09a42 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -40,7 +40,7 @@ pub struct Core { /// Scaling factor used by the application scale_factor: f32, - pub theme: Theme, + pub system_theme: Theme, pub(crate) title: String, pub window: Window, } @@ -56,7 +56,7 @@ impl Default for Core { toggled_condensed: true, }, scale_factor: 1.0, - theme: theme::theme(), + system_theme: theme::theme(), title: String::new(), window: Window { can_fullscreen: false, diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6069d3c9..9b6bab96 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::{command, Application, ApplicationExt, Core, Subscription}; -use crate::theme::{self, Theme}; +use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; use crate::{keyboard_nav, Element}; #[cfg(feature = "wayland")] @@ -36,6 +36,8 @@ pub enum Message { ScaleFactor(f32), /// Requests theme changes. ThemeChange(Theme), + /// Notification of system theme changes. + SystemThemeChange(Theme), /// Toggles visibility of the nav bar. ToggleNavBar, /// Toggles the condensed status of the nav bar. @@ -146,14 +148,14 @@ where .map(Message::KeyboardNav) .map(super::Message::Cosmic), theme::subscription(0) - .map(Message::ThemeChange) + .map(Message::SystemThemeChange) .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), ]) } fn theme(&self) -> Self::Theme { - self.app.core().theme.clone() + THEME.with(|t| t.borrow().clone()) } #[cfg(feature = "wayland")] @@ -267,12 +269,32 @@ impl Cosmic { } Message::ThemeChange(theme) => { - self.app.core_mut().theme = theme; + // our system theme is always receiving updates so we should use it instead + let theme = if matches!(theme.theme_type, ThemeType::System(_)) { + self.app.core().system_theme.clone() + } else { + theme + }; + + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + cosmic_theme.set_theme(theme.theme_type); + }); } Message::ScaleFactor(factor) => { self.app.core_mut().set_scale_factor(factor); } + Message::SystemThemeChange(theme) => { + self.app.core_mut().system_theme = theme.clone(); + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + // only apply update if the theme is set to load a system theme + if matches!(cosmic_theme.theme_type, ThemeType::System(_)) { + cosmic_theme.set_theme(theme.theme_type); + } + }); + } } iced::Command::none() diff --git a/src/app/mod.rs b/src/app/mod.rs index 6cc6e637..f37571fe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -33,6 +33,7 @@ pub mod message { pub use self::core::Core; pub use self::settings::Settings; +use crate::theme::THEME; use crate::widget::nav_bar; use crate::{Element, ElementExt}; use apply::Apply; @@ -58,7 +59,10 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res core.set_scale_factor(settings.scale_factor); core.set_window_width(settings.size.0); core.set_window_height(settings.size.1); - core.theme = settings.theme; + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + cosmic_theme.set_theme(settings.theme.theme_type); + }); let mut iced = iced::Settings::with_flags((core, flags)); diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 2fb5e8c1..eddcc773 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -4,6 +4,7 @@ pub mod expander; mod segmented_button; +use std::cell::RefCell; use std::f32::consts::PI; use std::hash::Hash; use std::hash::Hasher; @@ -64,6 +65,10 @@ lazy_static::lazy_static! { }; } +thread_local! { + pub(crate) static THEME: RefCell = RefCell::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background }); +} + #[derive(Debug, Clone, PartialEq, Default)] pub enum ThemeType { #[default] @@ -72,6 +77,7 @@ pub enum ThemeType { HighContrastDark, HighContrastLight, Custom(Arc), + System(Arc), } #[derive(Debug, Clone, PartialEq, Default)] @@ -88,7 +94,7 @@ impl Theme { ThemeType::Light => &COSMIC_LIGHT, ThemeType::HighContrastDark => &COSMIC_HC_DARK, ThemeType::HighContrastLight => &COSMIC_HC_LIGHT, - ThemeType::Custom(ref t) => t.as_ref(), + ThemeType::Custom(ref t) | ThemeType::System(ref t) => t.as_ref(), } } @@ -132,6 +138,14 @@ impl Theme { } } + #[must_use] + pub fn system(theme: Arc) -> Self { + Self { + theme_type: ThemeType::System(theme), + ..Default::default() + } + } + /// get current container /// can be used in a component that is intended to be a child of a `CosmicContainer` #[must_use] @@ -142,6 +156,11 @@ impl Theme { cosmic_theme::Layer::Secondary => &self.cosmic().secondary, } } + + /// set the theme + pub fn set_theme(&mut self, theme: ThemeType) { + self.theme_type = theme; + } } impl LayeredTheme for Theme { @@ -1215,7 +1234,7 @@ pub fn theme() -> Theme { } theme }); - crate::theme::Theme::custom(Arc::new(t)) + crate::theme::Theme::system(Arc::new(t)) } pub fn subscription(id: u64) -> Subscription { @@ -1231,7 +1250,8 @@ pub fn subscription(id: u64) -> Subscription { } theme }); - crate::theme::Theme::custom(Arc::new(theme)) + + crate::theme::Theme::system(Arc::new(theme)) }) } From 3507e9f4cffa09c79b16d5e33f637ee392d9c606 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 16:06:35 -0400 Subject: [PATCH 0048/1276] refactor: make corner radius f32 so that it's easier to use with BorderRadius --- cosmic-theme/src/model/corner.rs | 26 +++++++++++++------------- cosmic-theme/src/model/theme.rs | 14 +++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs index e466959d..ecd18c0b 100644 --- a/cosmic-theme/src/model/corner.rs +++ b/cosmic-theme/src/model/corner.rs @@ -1,31 +1,31 @@ use serde::{Deserialize, Serialize}; /// Corner radii variables for the Cosmic theme -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] pub struct CornerRadii { /// corner radii of 0 - pub radius_0: [u16; 4], + pub radius_0: [f32; 4], /// smallest size of corner radii that can be non-zero - pub radius_xs: [u16; 4], + pub radius_xs: [f32; 4], /// small corner radii - pub radius_s: [u16; 4], + pub radius_s: [f32; 4], /// medium corner radii - pub radius_m: [u16; 4], + pub radius_m: [f32; 4], /// large corner radii - pub radius_l: [u16; 4], + pub radius_l: [f32; 4], /// extra large corner radii - pub radius_xl: [u16; 4], + pub radius_xl: [f32; 4], } impl Default for CornerRadii { fn default() -> Self { Self { - radius_0: [0; 4], - radius_xs: [4; 4], - radius_s: [8; 4], - radius_m: [16; 4], - radius_l: [32; 4], - radius_xl: [160; 4], + radius_0: [0.0; 4], + radius_xs: [4.0; 4], + radius_s: [8.0; 4], + radius_m: [16.0; 4], + radius_l: [32.0; 4], + radius_xl: [160.0; 4], } } } diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index ec7bb64f..07addca1 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -20,7 +20,7 @@ pub enum Layer { } /// Cosmic Theme data structure with all colors and its name -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct Theme { /// name of the theme pub name: String, @@ -346,27 +346,27 @@ where } /// get @radius_0 - pub fn radius_0(&self) -> [u16; 4] { + pub fn radius_0(&self) -> [f32; 4] { self.corner_radii.radius_0 } /// get @radius_xs - pub fn radius_xs(&self) -> [u16; 4] { + pub fn radius_xs(&self) -> [f32; 4] { self.corner_radii.radius_xs } /// get @radius_s - pub fn radius_s(&self) -> [u16; 4] { + pub fn radius_s(&self) -> [f32; 4] { self.corner_radii.radius_s } /// get @radius_m - pub fn radius_m(&self) -> [u16; 4] { + pub fn radius_m(&self) -> [f32; 4] { self.corner_radii.radius_m } /// get @radius_l - pub fn radius_l(&self) -> [u16; 4] { + pub fn radius_l(&self) -> [f32; 4] { self.corner_radii.radius_l } /// get @radius_xl - pub fn radius_xl(&self) -> [u16; 4] { + pub fn radius_xl(&self) -> [f32; 4] { self.corner_radii.radius_xl } } From f8c25096fdeb6cb1a5f8bf67980c111e424090e8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 17:19:30 -0400 Subject: [PATCH 0049/1276] refactor: update existing buttons --- src/theme/mod.rs | 7 ++++--- src/widget/button.rs | 9 +++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index eddcc773..6acb13e0 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -253,12 +253,13 @@ impl button::StyleSheet for Theme { return active(self); } + let corner_radii = &self.cosmic().corner_radii; let component = style.cosmic(self); button::Appearance { border_radius: match style { - Button::Link => 0.0.into(), - Button::Card => 8.0.into(), - _ => 24.0.into(), + Button::Link => corner_radii.radius_0.into(), + Button::Card => corner_radii.radius_xs.into(), + _ => corner_radii.radius_xl.into(), }, background: match style { Button::Link | Button::Text => None, diff --git a/src/widget/button.rs b/src/widget/button.rs index d94aa10e..786c1099 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -1,7 +1,10 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::{theme, Element, Renderer}; +use crate::{ + theme::{self, THEME}, + Element, Renderer, +}; use iced::widget; /// A button widget with COSMIC styling @@ -44,9 +47,11 @@ impl Button { /// A custom button that has the desired default spacing and padding. pub fn custom(self, children: Vec>) -> widget::Button { + let theme = THEME.with(|t| t.borrow().clone()); + let theme = theme.cosmic(); let button = widget::button(widget::row(children).spacing(8)) .style(self.style) - .padding([8, 16]); + .padding([theme.space_xs(), theme.space_s()]); if let Some(message) = self.message { button.on_press(message) From dae262f4662c63e6f34ddffac9344246ca385555 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 4 Aug 2023 17:30:32 -0400 Subject: [PATCH 0050/1276] fix: make surfaces lighter if possible in light mode --- cosmic-theme/src/model/theme.rs | 11 ++++++----- cosmic-theme/src/steps.rs | 9 ++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 07addca1..61a13742 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -620,16 +620,16 @@ impl ThemeBuilder { let primary_container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { - get_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) + get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) }; let secondary_container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { - get_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) + get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) }; - let bg_component = get_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); + let bg_component = get_surface_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); let on_bg_component = get_text( color_index(bg_component, step_array.len()), &step_array, @@ -646,7 +646,8 @@ impl ThemeBuilder { ); let primary_index = color_index(primary_container_bg, step_array.len()); - let primary_component = get_color(primary_index, 6, &step_array, is_dark, &p_ref.neutral_3); + let primary_component = + get_surface_color(primary_index, 6, &step_array, is_dark, &p_ref.neutral_3); let on_primary_component = get_text( color_index(primary_component, step_array.len()), &step_array, @@ -664,7 +665,7 @@ impl ThemeBuilder { let secondary_index = color_index(secondary_container_bg, step_array.len()); let secondary_component = - get_color(secondary_index, 3, &step_array, is_dark, &p_ref.neutral_4); + get_surface_color(secondary_index, 3, &step_array, is_dark, &p_ref.neutral_4); let on_secondary_component = get_text( color_index(secondary_component, step_array.len()), &step_array, diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 58117e5d..de23e52c 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -31,15 +31,18 @@ pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool .filter(|i| *i < step_len) } -/// get color given a base and some steps -pub fn get_color( +/// get surface color given a base and some steps +pub fn get_surface_color( base_index: usize, steps: usize, step_array: &Vec, - is_dark: bool, + mut is_dark: bool, fallback: &Srgba, ) -> Srgba { assert!(step_array.len() == 100); + if !is_dark && base_index >= 88 { + is_dark = true; + } get_index(base_index, steps, step_array.len(), is_dark) .and_then(|i| step_array.get(i).cloned()) .unwrap_or_else(|| fallback.to_owned()) From ff83f893ef19531a9cefbdaaa3cd2c692137d7a3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 7 Aug 2023 13:48:01 -0400 Subject: [PATCH 0051/1276] refactor: updates for buttons and checkboxes --- cosmic-theme/src/model/derivation.rs | 19 +++- cosmic-theme/src/model/theme.rs | 152 ++++++++++++++++----------- examples/cosmic/src/window/demo.rs | 2 +- src/theme/mod.rs | 100 +++++++++--------- 4 files changed, 159 insertions(+), 114 deletions(-) diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 26b82ca9..e70aad9d 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -70,6 +70,10 @@ pub struct Component { pub disabled: C, /// the color of text in the widget when it is disabled pub on_disabled: C, + /// the color of the border for the widget + pub border: C, + /// the color of the border for the widget when it is disabled + pub disabled_border: C, } impl Component @@ -109,6 +113,8 @@ where on: self.on.into(), disabled: self.disabled.into(), on_disabled: self.on_disabled.into(), + border: self.border.into(), + disabled_border: self.disabled_border.into(), } } @@ -124,7 +130,7 @@ where let base: Srgba = base.into(); let mut base_50 = base.clone(); - base_50.alpha = 0.5; + base_50.alpha *= 0.5; let on_20 = neutral.clone(); let mut on_50 = on_20.clone(); @@ -142,6 +148,8 @@ where disabled: base_50.into(), on_disabled: on_50.into(), focus: accent, + border: base.into(), + disabled_border: base_50.into(), } } @@ -152,6 +160,7 @@ where accent: C, on_component: C, is_high_contrast: bool, + border: C, ) -> Self { let component_state_overlay = component_state_overlay.clone().into(); let mut component_state_overlay_10 = component_state_overlay.clone(); @@ -161,7 +170,7 @@ where let base = base.into(); let mut base_50 = base.clone(); - base_50.alpha = 0.5; + base_50.alpha *= 0.5; let mut on_20 = on_component.clone().into(); let mut on_50 = on_20.clone(); @@ -169,6 +178,10 @@ where on_20.alpha = 0.2; on_50.alpha = 0.5; + let border = border.into(); + let mut disabled_border = border; + disabled_border.alpha *= 0.5; + Component { base: base.clone().into(), hover: over(component_state_overlay_10, base).into(), @@ -184,6 +197,8 @@ where on: on_component.clone(), disabled: base_50.into(), on_disabled: on_50.into(), + border: border.into(), + disabled_border: disabled_border.into(), } } } diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 61a13742..f6e80b2d 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -4,8 +4,8 @@ use crate::{ }; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use palette::{IntoColor, Srgb, Srgba}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{fmt, num::NonZeroUsize}; +use serde::{Deserialize, Serialize}; +use std::num::NonZeroUsize; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] /// Theme layer type @@ -38,6 +38,8 @@ pub struct Theme { pub destructive: Component, /// warning element colors pub warning: Component, + /// button component styling + pub button: Component, /// palette pub palette: CosmicPaletteInner, /// spacing @@ -162,10 +164,27 @@ impl Theme { } } -impl Theme -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +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 the theme to a high-contrast variant pub fn to_high_contrast(&self) -> Self { todo!(); @@ -174,134 +193,142 @@ where // TODO convenient getter functions for each named color variable /// get @accent_color pub fn accent_color(&self) -> Srgba { - self.accent.base.clone().into() + self.accent.base.clone() } /// get @success_color pub fn success_color(&self) -> Srgba { - self.success.base.clone().into() + self.success.base.clone() } /// get @destructive_color pub fn destructive_color(&self) -> Srgba { - self.destructive.base.clone().into() + self.destructive.base.clone() } /// get @warning_color pub fn warning_color(&self) -> Srgba { - self.warning.base.clone().into() + self.warning.base.clone() } // Containers /// get @bg_color pub fn bg_color(&self) -> Srgba { - self.background.base.clone().into() + self.background.base.clone() } /// get @bg_component_color pub fn bg_component_color(&self) -> Srgba { - self.background.component.base.clone().into() + self.background.component.base.clone() } /// get @primary_container_color pub fn primary_container_color(&self) -> Srgba { - self.primary.base.clone().into() + self.primary.base.clone() } /// get @primary_component_color pub fn primary_component_color(&self) -> Srgba { - self.primary.component.base.clone().into() + self.primary.component.base.clone() } /// get @secondary_container_color pub fn secondary_container_color(&self) -> Srgba { - self.secondary.base.clone().into() + self.secondary.base.clone() } /// get @secondary_component_color pub fn secondary_component_color(&self) -> Srgba { - self.secondary.component.base.clone().into() + self.secondary.component.base.clone() + } + /// get @button_bg_color + pub fn button_bg_color(&self) -> Srgba { + self.button.base.clone() } // Text /// get @on_bg_color pub fn on_bg_color(&self) -> Srgba { - self.background.on.clone().into() + self.background.on.clone() } /// get @on_bg_component_color pub fn on_bg_component_color(&self) -> Srgba { - self.background.component.on.clone().into() + self.background.component.on.clone() } /// get @on_primary_color pub fn on_primary_container_color(&self) -> Srgba { - self.primary.on.clone().into() + self.primary.on.clone() } /// get @on_primary_component_color pub fn on_primary_component_color(&self) -> Srgba { - self.primary.component.on.clone().into() + self.primary.component.on.clone() } /// get @on_secondary_color pub fn on_secondary_container_color(&self) -> Srgba { - self.secondary.on.clone().into() + self.secondary.on.clone() } /// get @on_secondary_component_color pub fn on_secondary_component_color(&self) -> Srgba { - self.secondary.component.on.clone().into() + self.secondary.component.on.clone() } /// get @accent_text_color pub fn accent_text_color(&self) -> Srgba { - self.accent.base.clone().into() + self.accent.base.clone() } /// get @success_text_color pub fn success_text_color(&self) -> Srgba { - self.success.base.clone().into() + self.success.base.clone() } /// get @warning_text_color pub fn warning_text_color(&self) -> Srgba { - self.warning.base.clone().into() + self.warning.base.clone() } /// get @destructive_text_color pub fn destructive_text_color(&self) -> Srgba { - self.destructive.base.clone().into() + self.destructive.base.clone() } /// get @on_accent_color pub fn on_accent_color(&self) -> Srgba { - self.accent.on.clone().into() + self.accent.on.clone() } /// get @on_success_color pub fn on_success_color(&self) -> Srgba { - self.success.on.clone().into() + self.success.on.clone() } /// get @oon_warning_color pub fn on_warning_color(&self) -> Srgba { - self.warning.on.clone().into() + self.warning.on.clone() } /// get @on_destructive_color pub fn on_destructive_color(&self) -> Srgba { - self.destructive.on.clone().into() + self.destructive.on.clone() + } + /// get @button_color + pub fn button_color(&self) -> Srgba { + self.button.on.clone() } // Borders and Dividers /// get @bg_divider pub fn bg_divider(&self) -> Srgba { - self.background.divider.clone().into() + self.background.divider.clone() } /// get @bg_component_divider pub fn bg_component_divider(&self) -> Srgba { - self.background.component.divider.clone().into() + self.background.component.divider.clone() } /// get @primary_container_divider pub fn primary_container_divider(&self) -> Srgba { - self.primary.divider.clone().into() + self.primary.divider.clone() } /// get @primary_component_divider pub fn primary_component_divider(&self) -> Srgba { - self.primary.component.divider.clone().into() + self.primary.component.divider.clone() } /// get @secondary_container_divider pub fn secondary_container_divider(&self) -> Srgba { - self.secondary.divider.clone().into() + self.secondary.divider.clone() } - /// get @secondary_component_divider - pub fn secondary_component_divider(&self) -> Srgba { - self.secondary.component.divider.clone().into() + /// get @button_divider + pub fn button_divider(&self) -> Srgba { + self.button.divider.clone() } /// get @window_header_bg pub fn window_header_bg(&self) -> Srgba { - self.background.base.clone().into() + self.background.base.clone() } /// get @space_none @@ -371,28 +398,6 @@ where } } -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() - } -} - impl From> for Theme where CosmicPalette: Into>, @@ -643,6 +648,7 @@ impl ThemeBuilder { accent.clone(), on_bg_component, is_high_contrast, + p_ref.neutral_8, ); let primary_index = color_index(primary_container_bg, step_array.len()); @@ -661,6 +667,7 @@ impl ThemeBuilder { accent.clone(), on_primary_component, is_high_contrast, + p_ref.neutral_8, ); let secondary_index = color_index(secondary_container_bg, step_array.len()); @@ -679,7 +686,22 @@ impl ThemeBuilder { accent.clone(), on_secondary_component, is_high_contrast, + p_ref.neutral_8, ); + let neutral_7 = p_ref.neutral_7; + let mut button_bg = neutral_7; + button_bg.alpha = 0.25; + let neutral_10 = p_ref.neutral_10; + let mut button_hover_overlay = neutral_10; + button_hover_overlay.alpha = 0.10; + let mut button_press_overlay = neutral_10; + button_press_overlay.alpha = 0.20; + + let mut button_disabled_bg = button_bg; + button_disabled_bg.alpha *= 0.5; + let button_border = p_ref.neutral_8.clone(); + let mut button_disabled_border = button_border; + button_disabled_border.alpha *= 0.5; let mut theme: Theme = Theme { name: palette.name().to_string(), @@ -736,6 +758,14 @@ impl ThemeBuilder { p_ref.neutral_0.to_owned(), accent.clone(), ), + button: Component::component( + button_bg, + p_ref.neutral_10, + accent, + p_ref.neutral_9, + is_high_contrast, + p_ref.neutral_8, + ), palette: palette.inner(), spacing, corner_radii, diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 85e396d3..c1856eb8 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -492,7 +492,7 @@ impl State { )) .layer(cosmic::cosmic_theme::Layer::Secondary) .padding(16) - .style(cosmic::theme::Container::Secondary) + .style(cosmic::theme::Container::Background) .into(), text_input( "Type to search apps or type “?” for more options...", diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 6acb13e0..b335b942 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -62,6 +62,8 @@ lazy_static::lazy_static! { on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), }; } @@ -355,69 +357,69 @@ impl checkbox::StyleSheet for Theme { type Style = Checkbox; fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { - let palette = self.cosmic(); - let neutral_7 = palette.palette.neutral_10; + let cosmic = self.cosmic(); + let corners = &cosmic.corner_radii; match style { Checkbox::Primary => checkbox::Appearance { background: Background::Color(if is_checked { - palette.accent.base.into() + cosmic.accent.base.into() } else { - palette.background.base.into() + cosmic.button.base.into() }), - icon_color: palette.accent.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.accent.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.accent.base + cosmic.accent.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, }, Checkbox::Secondary => checkbox::Appearance { background: Background::Color(if is_checked { - palette.background.component.base.into() + cosmic.background.component.base.into() } else { - palette.background.base.into() + cosmic.background.base.into() }), - icon_color: palette.background.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.background.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: neutral_7.into(), + border_color: cosmic.button.border.into(), text_color: None, }, Checkbox::Success => checkbox::Appearance { background: Background::Color(if is_checked { - palette.success.base.into() + cosmic.success.base.into() } else { - palette.background.base.into() + cosmic.button.base.into() }), - icon_color: palette.success.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.success.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.success.base + cosmic.success.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, }, Checkbox::Danger => checkbox::Appearance { background: Background::Color(if is_checked { - palette.destructive.base.into() + cosmic.destructive.base.into() } else { - palette.background.base.into() + cosmic.button.base.into() }), - icon_color: palette.destructive.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.destructive.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.destructive.base + cosmic.destructive.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, @@ -426,25 +428,23 @@ impl checkbox::StyleSheet for Theme { } fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { - let palette = self.cosmic(); - let mut neutral_10 = palette.palette.neutral_10; - let neutral_7 = palette.palette.neutral_10; + let cosmic = self.cosmic(); + let corners = &cosmic.corner_radii; - neutral_10.alpha = 0.1; match style { Checkbox::Primary => checkbox::Appearance { background: Background::Color(if is_checked { - palette.accent.base.into() + cosmic.accent.base.into() } else { - neutral_10.into() + cosmic.button.base.into() }), - icon_color: palette.accent.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.accent.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.accent.base + cosmic.accent.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, @@ -453,49 +453,49 @@ impl checkbox::StyleSheet for Theme { background: Background::Color(if is_checked { self.current_container().base.into() } else { - neutral_10.into() + cosmic.button.base.into() }), icon_color: self.current_container().on.into(), - border_radius: 4.0.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { self.current_container().base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, }, Checkbox::Success => checkbox::Appearance { background: Background::Color(if is_checked { - palette.success.base.into() + cosmic.success.base.into() } else { - neutral_10.into() + cosmic.button.base.into() }), - icon_color: palette.success.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.success.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.success.base + cosmic.success.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, }, Checkbox::Danger => checkbox::Appearance { background: Background::Color(if is_checked { - palette.destructive.base.into() + cosmic.destructive.base.into() } else { - neutral_10.into() + cosmic.button.base.into() }), - icon_color: palette.destructive.on.into(), - border_radius: 4.0.into(), + icon_color: cosmic.destructive.on.into(), + border_radius: corners.radius_xs.into(), border_width: if is_checked { 0.0 } else { 1.0 }, border_color: if is_checked { - palette.destructive.base + cosmic.destructive.base } else { - neutral_7 + cosmic.button.border } .into(), text_color: None, From 1c5a233a98facdb7bed133bc0cdf9c94a5b9dd4e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 7 Aug 2023 16:03:44 -0400 Subject: [PATCH 0052/1276] chore: update toggler colors --- src/theme/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b335b942..8f3e457d 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -15,6 +15,7 @@ pub use self::segmented_button::SegmentedButton; use cosmic_config::config_subscription; use cosmic_config::CosmicConfigEntry; +use cosmic_theme::composite::over; use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; @@ -808,16 +809,13 @@ impl toggler::StyleSheet for Theme { fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance { let theme = self.cosmic(); - toggler::Appearance { background: if is_active { theme.accent.base.into() } else { - //TODO: Grab neutral from palette theme.palette.neutral_5.into() }, background_border: None, - //TODO: Grab neutral from palette foreground: theme.palette.neutral_2.into(), foreground_border: None, } @@ -828,11 +826,12 @@ impl toggler::StyleSheet for Theme { //TODO: grab colors from palette let mut neutral_10 = cosmic.palette.neutral_10; neutral_10.alpha = 0.1; + toggler::Appearance { background: if is_active { - cosmic.accent.hover + over(neutral_10, cosmic.accent_color()) } else { - neutral_10 + over(neutral_10, cosmic.palette.neutral_5) } .into(), ..self.active(style, is_active) From 20a5227eca5d566427ecdfdb132ec9bd4becc2f1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 7 Aug 2023 16:57:25 -0400 Subject: [PATCH 0053/1276] refactor: add button components to theme because they have different overlays than others when they are hovered or pressed --- cosmic-theme/src/model/derivation.rs | 10 +++++++ cosmic-theme/src/model/theme.rs | 42 ++++++++++++++++++++++++++++ src/theme/mod.rs | 12 ++++---- src/widget/spin_button/mod.rs | 9 +++--- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index e70aad9d..c35e8c1b 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -153,6 +153,16 @@ where } } + /// helper for producing a button component + pub fn colored_button(base: C, overlay: C, on_button: C, accent: C) -> Self { + let mut component = Component::colored_component(base, overlay, accent); + component.on = on_button.clone(); + let mut on_disabled = on_button.into(); + on_disabled.alpha = 0.5; + component.on_disabled = on_disabled.into(); + component + } + /// helper for producing a component color theme pub fn component( base: C, diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index f6e80b2d..76c73055 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -38,6 +38,16 @@ pub struct Theme { pub destructive: Component, /// warning element colors pub warning: Component, + /// accent button element colors + pub accent_button: Component, + /// suggested button element colors + pub success_button: Component, + /// destructive button element colors + pub destructive_button: Component, + /// warning button element colors + pub warning_button: Component, + /// text button element colors + pub text_button: Component, /// button component styling pub button: Component, /// palette @@ -766,6 +776,38 @@ impl ThemeBuilder { is_high_contrast, p_ref.neutral_8, ), + accent_button: Component::colored_button( + accent.clone(), + p_ref.neutral_10.to_owned(), + p_ref.neutral_0.to_owned(), + accent.clone(), + ), + success_button: Component::colored_button( + success, + p_ref.neutral_10.to_owned(), + p_ref.neutral_0.to_owned(), + accent.clone(), + ), + destructive_button: Component::colored_button( + destructive, + p_ref.neutral_10.to_owned(), + p_ref.neutral_0.to_owned(), + accent.clone(), + ), + warning_button: Component::colored_button( + warning, + p_ref.neutral_10.to_owned(), + p_ref.neutral_0.to_owned(), + accent.clone(), + ), + text_button: Component::component( + Srgba::new(0.0, 0.0, 0.0, 0.0), + p_ref.neutral_10, + accent, + p_ref.neutral_9, + is_high_contrast, + p_ref.neutral_8, + ), palette: palette.inner(), spacing, corner_radii, diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 8f3e457d..dd2f8e43 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -233,13 +233,13 @@ impl Button { fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { let cosmic = theme.cosmic(); match self { - Button::Primary => &cosmic.accent, + Button::Primary => &cosmic.accent_button, Button::Secondary => &theme.current_container().component, - Button::Positive => &cosmic.success, - Button::Destructive => &cosmic.destructive, - Button::Text => &theme.current_container().component, - Button::Link => &cosmic.accent, - Button::LinkActive => &cosmic.accent, + Button::Positive => &cosmic.success_button, + Button::Destructive => &cosmic.destructive_button, + Button::Text => &cosmic.text_button, + Button::Link => &cosmic.accent_button, + Button::LinkActive => &cosmic.accent_button, Button::Transparent => &TRANSPARENT_COMPONENT, Button::Deactivated => &theme.current_container().component, Button::Card => &theme.current_container().component, diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs index c069422e..27bdb32e 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -12,7 +12,7 @@ use apply::Apply; use iced::{ alignment::{Horizontal, Vertical}, widget::{button, container, row}, - Alignment, Background, Length, + Alignment, Length, }; pub struct SpinButton<'a, Message> { @@ -98,11 +98,12 @@ fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance { let basic = &theme.cosmic(); let mut neutral_10 = basic.palette.neutral_10; neutral_10.alpha = 0.1; - let accent = &theme.cosmic().accent; + let accent = &basic.accent; + let corners = &basic.corner_radii; iced_style::container::Appearance { text_color: Some(basic.palette.neutral_10.into()), - background: Some(Background::Color(neutral_10.into())), - border_radius: 24.0.into(), + background: None, + border_radius: corners.radius_s.into(), border_width: 0.0, border_color: accent.base.into(), } From fb2fb65af04a43901b850a67558c6eab647c3955 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 10 Aug 2023 10:53:54 -0400 Subject: [PATCH 0054/1276] chore: use palette 0.7.3 --- Cargo.toml | 6 +----- cosmic-theme/Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8db8479..f311ede8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] apply = "0.3.0" derive_setters = "0.1.5" lazy_static = "1.4.0" -palette = "0.7" +palette = "0.7.3" tokio = { version = "1.24.2", optional = true } sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", optional = true, rev = "c9940f4"} slotmap = "1.0.6" @@ -97,7 +97,3 @@ exclude = [ [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} - -# TODO Remove me when the palette crate gets an update & before merging -[patch.crates-io] -palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 4a02dbe1..a6613217 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -15,9 +15,8 @@ no-default = [] theme-from-image = ["kmeans_colors", "image"] [dependencies] -# palette = {version = "0.7", features = ["serializing"] } +palette = {version = "0.7.3", features = ["serializing"] } almost = "0.2" -palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } image = {version = "0.24.1", optional = true } serde = { version = "1.0.129", features = ["derive"] } From 0c57ec7446f02529474551a40cbfb8e63c879f31 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 11:02:00 +0200 Subject: [PATCH 0055/1276] chore: remove unused module --- src/track.rs | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 src/track.rs diff --git a/src/track.rs b/src/track.rs deleted file mode 100644 index 104da86d..00000000 --- a/src/track.rs +++ /dev/null @@ -1,40 +0,0 @@ -/// Records if a change has occurred to its inner value -pub struct Track { - value: T, - changed: bool, -} - -impl Track { - /// Create a new value where changes are tracked. - pub const fn new(value: T) -> Self { - Self { - value, - changed: true, - } - } - - /// Gets the inner value. - pub fn get(&self) -> &T { - &self.value - } - - /// Set a new value, and mark that it has changed. - pub fn set(&mut self, value: T) { - self.value = value; - self.changed = true; - } - - /// Check if value has changed. - pub fn changed(&self) -> bool { - self.changed - } -} - -impl Default for Track -where - T: Default, -{ - fn default() -> Self { - Self::new(T::default()) - } -} From a387adcb1bfb300089cfbbddb8fa8ce91999ac3f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 10:51:59 +0200 Subject: [PATCH 0056/1276] chore: improve documentation --- src/app/command.rs | 19 ++++--------------- src/app/mod.rs | 23 ++++++++++++++--------- src/app/settings.rs | 3 +++ src/executor/mod.rs | 4 ++++ src/executor/multi.rs | 2 ++ src/executor/single.rs | 2 ++ src/font.rs | 2 ++ src/icon_theme.rs | 2 ++ src/keyboard_nav.rs | 2 ++ src/theme/mod.rs | 2 ++ 10 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/app/command.rs b/src/app/command.rs index 1997b002..8aa4f9d3 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -1,22 +1,11 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use std::future::Future; +/// Asynchronous actions for COSMIC applications. +use super::Message; -use super::{Command, Message}; -use iced_runtime::command::Action; - -/// Yields a command which contains a batch of commands. -pub fn batch(commands: impl IntoIterator>) -> Command { - Command::batch(commands) -} - -/// Yields a command which will run the future on the runtime executor. -pub fn future( - future: impl Future> + Send + 'static, -) -> Command { - Command::single(Action::Future(Box::pin(future))) -} +/// Commands for COSMIC applications. +pub type Command = iced::Command>; /// Creates a command which yields a [`crate::app::Message`]. pub fn message(message: Message) -> Command { diff --git a/src/app/mod.rs b/src/app/mod.rs index f37571fe..5d98cbd1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,11 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Build interactive cross-platform COSMIC applications. +//! +//! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) +//! example in our repository. + pub mod command; mod core; pub mod cosmic; @@ -9,7 +14,7 @@ pub mod settings; pub mod message { #[derive(Clone, Debug)] #[must_use] - pub enum Message { + pub enum Message { /// Messages from the application, for the application. App(M), /// Internal messages to be handled by libcosmic. @@ -18,19 +23,20 @@ pub mod message { None, } - pub fn app(message: M) -> Message { + pub const fn app(message: M) -> Message { Message::App(message) } - pub fn cosmic(message: super::cosmic::Message) -> Message { + pub const fn cosmic(message: super::cosmic::Message) -> Message { Message::Cosmic(message) } - pub fn none() -> Message { + pub const fn none() -> Message { Message::None } } +pub use self::command::Command; pub use self::core::Core; pub use self::settings::Settings; use crate::theme::THEME; @@ -41,10 +47,7 @@ use iced::Subscription; use iced::{window, Application as IcedApplication}; pub use message::Message; -/// Commands for COSMIC applications. -pub type Command = iced::Command>; - -/// Launch the application with the given settings. +/// Launch a COSMIC application with the given [`Settings`]. /// /// # Errors /// @@ -102,6 +105,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res cosmic::Cosmic::::run(iced) } +/// An interactive cross-platform COSMIC application. #[allow(unused_variables)] pub trait Application where @@ -245,6 +249,7 @@ impl ApplicationExt for App { iced::Command::none() } + /// Creates the view for the main window. fn view_main<'a>(&'a self) -> Element<'a, Message> { let core = self.core(); let is_condensed = core.is_condensed(); @@ -313,7 +318,7 @@ impl ApplicationExt for App { } } - if core.show_content() { + if self.nav_model().is_none() || core.show_content() { let main_content = self.view().debug(core.debug).map(Message::App); widgets.push(main_content); diff --git a/src/app/settings.rs b/src/app/settings.rs index 19606638..0745fc95 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -1,11 +1,14 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Configure a new COSMIC application. + use crate::{font, Theme}; #[cfg(feature = "wayland")] use iced::Limits; use iced_core::Font; +/// Configure a new COSMIC application. #[allow(clippy::struct_excessive_bools)] #[derive(derive_setters::Setters)] pub struct Settings { diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 1dd67e84..2f98b14e 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,14 +1,18 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Select the preferred async executor for an application. + #[cfg(feature = "tokio")] pub mod multi; #[cfg(feature = "tokio")] pub mod single; +/// Uses the single thread executor by default. #[cfg(not(feature = "tokio"))] pub type Default = iced::executor::Default; +/// Uses the single thread executor by default. #[cfg(feature = "tokio")] pub type Default = single::Executor; diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 18cb8234..50aa111e 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! An async executor that schedules tasks across a pol ofbackground thread. + use std::future::Future; #[cfg(feature = "tokio")] diff --git a/src/executor/single.rs b/src/executor/single.rs index 1ffa0529..aaa4f9f5 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! An async executor that schedules tasks on the same background thread. + use std::future::Future; #[cfg(feature = "tokio")] diff --git a/src/font.rs b/src/font.rs index e971a3b4..5484e1f5 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,6 +1,8 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! Select preferred fonts. + pub use iced::Font; use iced::{ font::{load, Error}, diff --git a/src/icon_theme.rs b/src/icon_theme.rs index 58c05643..f426ae7f 100644 --- a/src/icon_theme.rs +++ b/src/icon_theme.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Select the preferred icon theme. + use std::cell::RefCell; thread_local! { diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 7610328e..801c5947 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Subscribe to common application keyboard shortcuts. + use iced::{ event, keyboard::{self, KeyCode}, diff --git a/src/theme/mod.rs b/src/theme/mod.rs index dd2f8e43..16b14706 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! Use COSMIC's themes and styles. + pub mod expander; mod segmented_button; From e3f3dc2e82f7f64fefd255f901b534ec94f3a7b0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 10:53:34 +0200 Subject: [PATCH 0057/1276] fix(widget/button: padding causing misaligned text in button --- src/widget/button.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/button.rs b/src/widget/button.rs index 786c1099..1bd5f106 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -51,7 +51,7 @@ impl Button { let theme = theme.cosmic(); let button = widget::button(widget::row(children).spacing(8)) .style(self.style) - .padding([theme.space_xs(), theme.space_s()]); + .padding([theme.space_xxs(), theme.space_s()]); if let Some(message) = self.message { button.on_press(message) From a5d3814fffa805e06ef3a4208d16f5d3f70957cd Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 10:54:45 +0200 Subject: [PATCH 0058/1276] fix(widget/header_bar): add padding between end elements and window controls --- src/widget/header_bar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index e7c8ca51..f2b48f5a 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -122,6 +122,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }); // Also packs the window controls at the very end. + end.push(iced::widget::horizontal_space(Length::Fixed(12.0)).into()); end.push(self.window_controls()); packed.push( iced::widget::row(end) From 1705b6fe27d9f674ea28034f0178efabd9ccf89f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 10:58:46 +0200 Subject: [PATCH 0059/1276] feat(dialog): XDG portal integrations for open and save dialogs --- Cargo.toml | 32 +++- examples/application/src/main.rs | 37 +++- examples/open-dialog/Cargo.toml | 16 ++ examples/open-dialog/src/main.rs | 259 ++++++++++++++++++++++++++++ justfile | 8 +- src/{command.rs => command/mod.rs} | 4 +- src/dialog/mod.rs | 9 + src/dialog/open_file.rs | 252 +++++++++++++++++++++++++++ src/dialog/save_file.rs | 262 +++++++++++++++++++++++++++++ src/lib.rs | 3 + 10 files changed, 861 insertions(+), 21 deletions(-) create mode 100644 examples/open-dialog/Cargo.toml create mode 100644 examples/open-dialog/src/main.rs rename src/{command.rs => command/mod.rs} (96%) create mode 100644 src/dialog/mod.rs create mode 100644 src/dialog/open_file.rs create mode 100644 src/dialog/save_file.rs diff --git a/Cargo.toml b/Cargo.toml index f311ede8..9e9dcbd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,29 @@ name = "cosmic" [features] default = ["wayland", "tokio", "a11y"] -debug = ["iced/debug"] +# Accessibility support a11y = ["iced/a11y", "iced_accessibility"] -wayland = ["iced/wayland", "iced_sctk", "sctk"] -wgpu = ["iced/wgpu", "iced_wgpu"] -tokio = ["dep:tokio", "iced/tokio"] -smol = ["iced/smol"] -winit = ["iced/winit", "iced_winit"] -winit_tokio = ["iced/winit", "iced_winit", "tokio"] -winit_debug = ["iced/winit", "iced_winit", "debug"] -winit_wgpu = ["winit", "wgpu"] +# Builds support for animated images animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] +# Debug features +debug = ["iced/debug"] +# Enables pipewire support in ashpd, if ashpd is enabled +pipewire = ["ashpd?/pipewire"] +# smol async runtime +smol = ["iced/smol"] +# Tokio async runtime +tokio = ["dep:tokio", "ashpd/tokio", "iced/tokio"] +# Wayland window support +wayland = ["ashpd?/wayland", "iced/wayland", "iced_sctk", "sctk"] +# Render with wgpu +wgpu = ["iced/wgpu", "iced_wgpu"] +# X11 window support via winit +winit = ["iced/winit", "iced_winit"] +winit_debug = ["iced/winit", "iced_winit", "debug"] +winit_tokio = ["iced/winit", "iced_winit", "tokio"] +winit_wgpu = ["winit", "wgpu"] +# Enables XDG portal integrations +xdg-portal = ["ashpd"] [dependencies] apply = "0.3.0" @@ -34,6 +46,8 @@ tracing = "0.1" image = { version = "0.24.6", optional = true } thiserror = "1.0.44" async-fs = { version = "1.6", optional = true } +ashpd = { version = "0.5.0", default-features = false, optional = true } +url = "2.4.0" [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.2" diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 78332018..591556c7 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -1,27 +1,46 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -//! Testing ground for improving COSMIC application API ergonomics. +//! Application API example use cosmic::app::{Command, Core, Settings}; use cosmic::widget::nav_bar; use cosmic::{executor, iced, ApplicationExt, Element}; +#[derive(Clone, Copy)] +pub enum Page { + Page1, + Page2, + Page3, + Page4, +} + +impl Page { + const fn as_str(self) -> &'static str { + match self { + Page::Page1 => "Page 1", + Page::Page2 => "Page 2", + Page::Page3 => "Page 3", + Page::Page4 => "Page 4", + } + } +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { let input = vec![ - ("Page 1".into(), "🖖 Hello from libcosmic.".into()), - ("Page 2".into(), "🌟 This is an example application.".into()), - ("Page 3".into(), "🚧 The libcosmic API is not stable yet.".into()), - ("Page 4".into(), "🚀 Copy the source code and experiment today!".into()), + (Page::Page1, "🖖 Hello from libcosmic.".into()), + (Page::Page2, "🌟 This is an example application.".into()), + (Page::Page3, "🚧 The libcosmic API is not stable yet.".into()), + (Page::Page4, "🚀 Copy the source code and experiment today!".into()), ]; let settings = Settings::default() .antialiasing(true) .client_decorations(true) .debug(false) - .default_icon_theme("Pop") + .default_icon_theme(Some("Pop".into())) .default_text_size(16.0) .scale_factor(1.0) .size((1024, 768)) @@ -48,7 +67,7 @@ impl cosmic::Application for App { type Executor = executor::Default; /// Argument received [`cosmic::Application::new`]. - type Flags = Vec<(String, String)>; + type Flags = Vec<(Page, String)>; /// Message type specific to our [`App`]. type Message = Message; @@ -68,7 +87,7 @@ impl cosmic::Application for App { let mut nav_model = nav_bar::Model::default(); for (title, content) in input { - nav_model.insert().text(title).data(content); + nav_model.insert().text(title.as_str()).data(content); } nav_model.activate_position(0); @@ -91,10 +110,12 @@ impl cosmic::Application for App { self.update_title() } + /// Handle application events here. fn update(&mut self, _message: Self::Message) -> Command { Command::none() } + /// Creates a view after each update. fn view(&self) -> Element { let page_content = self .nav_model diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml new file mode 100644 index 00000000..71e32ac6 --- /dev/null +++ b/examples/open-dialog/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "open_dialog" +version = "0.1.0" +edition = "2021" + +[dependencies] +apply = "0.3.0" +tokio = { version = "1.31", features = ["full"] } +tracing = "0.1.37" +tracing-subscriber = "0.3.17" +url = "2.4.0" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "wayland", "tokio", "xdg-portal"] diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs new file mode 100644 index 00000000..7c1cdebd --- /dev/null +++ b/examples/open-dialog/src/main.rs @@ -0,0 +1,259 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! An application which provides an open dialog + +use apply::Apply; +use cosmic::app::{Command, Core, Settings}; +use cosmic::dialog::{open_file, FileFilter}; +use cosmic::iced_core::Length; +use cosmic::{executor, iced, ApplicationExt, Element}; +use tokio::io::AsyncReadExt; +use url::Url; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + let settings = Settings::default() + .size((1024, 768)); + + cosmic::app::run::(settings, ())?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message { + CloseError, + DialogClosed, + DialogInit(open_file::Sender), + DialogOpened, + Error(String), + FileRead(Url, String), + OpenFile, + Selected(Url), +} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + open_sender: Option, + file_contents: String, + selected_file: Option, + error_status: Option, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + const APP_ID: &'static str = "org.cosmic.OpenDialogDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + let mut app = App { + core, + open_sender: None, + file_contents: String::new(), + selected_file: None, + error_status: None, + }; + + let command = app.set_title("Open a file".into()); + + (app, command) + } + + fn header_end(&self) -> Vec> { + // Places a button the header to create open dialogs. + vec![cosmic::widget::button(cosmic::theme::Button::Primary) + .text("Open") + .on_press(Message::OpenFile) + .into()] + } + + fn subscription(&self) -> cosmic::iced_futures::Subscription { + // Creates a subscription for handling open dialogs. + open_file::subscription(|response| match response { + open_file::Message::Closed => Message::DialogClosed, + open_file::Message::Opened => Message::DialogOpened, + open_file::Message::Selected(files) => match files.uris().first() { + Some(file) => Message::Selected(file.to_owned()), + None => Message::DialogClosed, + }, + open_file::Message::Init(sender) => Message::DialogInit(sender), + open_file::Message::Err(why) => { + let mut source: &dyn std::error::Error = &why; + let mut string = format!("open dialog subscription errored\n cause: {source}"); + + while let Some(new_source) = source.source() { + string.push_str(&format!("\n cause: {new_source}")); + source = new_source; + } + + Message::Error(string) + } + }) + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::DialogClosed => { + eprintln!("dialog closed"); + } + + Message::DialogOpened => { + if let Some(sender) = self.open_sender.as_mut() { + eprintln!("requesting selection"); + return sender.response().map(|_| cosmic::app::Message::None); + } + } + + Message::FileRead(url, contents) => { + eprintln!("read file"); + self.selected_file = Some(url); + self.file_contents = contents; + } + + Message::Selected(url) => { + eprintln!("selected file"); + + // Take existing file contents buffer to reuse its allocation. + let mut contents = String::new(); + std::mem::swap(&mut contents, &mut self.file_contents); + + return Command::batch(vec![ + // Set the file's URL as the application title. + self.set_title(url.to_string()), + // Reads the selected file into memory. + cosmic::command::future(async move { + // Check if its a valid local file path. + let path = match url.scheme() { + "file" => url.path(), + other => { + return Message::Error(format!( + "{url} has unknown scheme: {other}" + )); + } + }; + + // Open the file by its path. + let mut file = match tokio::fs::File::open(path).await { + Ok(file) => file, + Err(why) => { + return Message::Error(format!("failed to open {path}: {why}")); + } + }; + + // Read the file into our contents buffer. + contents.clear(); + + if let Err(why) = file.read_to_string(&mut contents).await { + return Message::Error(format!("failed to read {path}: {why}")); + } + + contents.shrink_to_fit(); + + // Send this back to the application. + Message::FileRead(url, contents) + }) + .map(cosmic::app::message::app), + ]); + } + + // Creates a new open dialog. + Message::OpenFile => { + if let Some(sender) = self.open_sender.as_mut() { + if let Some(dialog) = open_file::builder() { + eprintln!("opening new dialog"); + + return dialog + // Sets title of the dialog window. + .title("Choose a file".into()) + // Sets the label of the accept button. + .accept_label("_Open".into()) + // Exclude directories from file selection. + .include_directories(false) + // Defines whether to block the main window while requesting input. + .modal(false) + // Only accept one file as input. + .multiple_files(false) + // Accept only plain text files + .filter(FileFilter::new("Text files").mimetype("text/plain")) + // Emits the dialog to our sender + .create(sender) + // Ignores the output because it's empty. + .map(|_| cosmic::app::message::none()); + } + } + } + + // Displays an error in the application's warning bar. + Message::Error(why) => { + self.error_status = Some(why); + } + + // Closes the warning bar, if it was shown. + Message::CloseError => { + self.error_status = None; + } + + // The open dialog. subscription provides this on register. + Message::DialogInit(sender) => { + eprintln!("dialog subscription enabled"); + self.open_sender = Some(sender); + } + } + + Command::none() + } + + fn view(&self) -> Element { + let mut content = Vec::new(); + + if let Some(error) = self.error_status.as_deref() { + content.push( + cosmic::widget::warning(error) + .on_close(Message::CloseError) + .into(), + ); + content.push(iced::widget::vertical_space(Length::Fixed(12.0)).into()) + } + + content.push(if self.selected_file.is_none() { + center(iced::widget::text("Choose a text file")) + } else { + cosmic::widget::text(&self.file_contents) + .apply(iced::widget::scrollable) + .width(iced::Length::Fill) + .into() + }); + + iced::widget::column(content).into() + } +} + +fn center<'a>(input: impl Into> + 'a) -> Element<'a, Message> { + iced::widget::container(input.into()) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center) + .into() +} diff --git a/justfile b/justfile index 8b01b8ab..163ebceb 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,12 @@ +projects := 'application cosmic cosmic_sctk open_dialog' + # Check for errors and linter warnings check *args: cargo clippy --no-deps {{args}} -- -W clippy::pedantic cargo clippy --no-deps --no-default-features --features="winit,tokio" {{args}} -- -W clippy::pedantic - cargo check -p application {{args}} - cargo check -p cosmic {{args}} - cargo check -p cosmic_sctk {{args}} + for project in {{projects}}; do \ + cargo check -p ${project}; \ + done # Runs a check with JSON message format for IDE integration check-json: (check '--message-format=json') diff --git a/src/command.rs b/src/command/mod.rs similarity index 96% rename from src/command.rs rename to src/command/mod.rs index a4e416cd..48300e2b 100644 --- a/src/command.rs +++ b/src/command/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +//! Create asynchronous actions to be performed in the background. + #[cfg(feature = "wayland")] use iced::window; use iced::Command; @@ -21,7 +23,7 @@ pub fn batch(commands: impl IntoIterator>) -> Command { Command::batch(commands) } -/// Yields a command which will run the future on the runtime executor. +/// Yields a command which will run the future on thet runtime executor. pub fn future(future: impl Future + Send + 'static) -> Command { Command::single(Action::Future(Box::pin(future))) } diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs new file mode 100644 index 00000000..f4d85537 --- /dev/null +++ b/src/dialog/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Create dialogs for retrieving user input. + +pub use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; +pub use ashpd::WindowIdentifier; + +pub mod open_file; diff --git a/src/dialog/open_file.rs b/src/dialog/open_file.rs new file mode 100644 index 00000000..cbb92cf1 --- /dev/null +++ b/src/dialog/open_file.rs @@ -0,0 +1,252 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Request to open files and/or directories. +//! +//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) +//! example in our repository. + +use derive_setters::Setters; +use iced::futures::{channel, SinkExt, StreamExt}; +use iced::{Command, Subscription}; +use std::cell::Cell; +use thiserror::Error; + +thread_local! { + /// Prevents duplicate dialog open requests. + static OPENED: Cell = Cell::new(false); +} + +fn dialog_is_open() -> bool { + OPENED.with(Cell::get) +} + +/// Creates a [`Builder`] if no other open file dialog exists. +pub fn builder() -> Option { + if dialog_is_open() { + None + } else { + Some(Builder::new()) + } +} + +/// Creates a subscription for open file dialog events. +pub fn subscription(handle: fn(Message) -> M) -> Subscription { + let type_id = std::any::TypeId::of::>(); + + iced::subscription::channel(type_id, 1, move |output| async move { + let mut state = State { + active: None, + handle, + output, + }; + + loop { + let (sender, mut receiver) = channel::mpsc::channel(1); + + state.emit(Message::Init(Sender(sender))).await; + + while let Some(request) = receiver.next().await { + match request { + Request::Close => state.close().await, + + Request::Open(dialog) => { + state.open(dialog).await; + OPENED.with(|last| last.set(false)); + } + + Request::Response => state.response().await, + } + } + } + }) +} + +/// Errors that my occur when interacting with an open file dialog subscription +#[derive(Debug, Error)] +pub enum Error { + #[error("dialog close failed")] + Close(#[source] ashpd::Error), + #[error("dialog open failed")] + Open(#[source] ashpd::Error), + #[error("dialog response failed")] + Response(#[source] ashpd::Error), +} + +/// Requests for an open file dialog subscription +enum Request { + Close, + Open(Builder), + Response, +} + +/// Messages from an open file dialog subscription. +pub enum Message { + Closed, + Err(Error), + Init(Sender), + Opened, + Selected(super::SelectedFiles), +} + +/// Sends requests to an open file dialog subscription. +#[derive(Clone, Debug)] +pub struct Sender(channel::mpsc::Sender); + +impl Sender { + /// Creates a [`Command`] that closes an active open file dialog. + pub fn close(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Close).await; + () + }) + } + + /// Creates a [`Command`] that opens a new open file dialog. + pub fn open(&mut self, dialog: Builder) -> Command<()> { + OPENED.with(|opened| opened.set(true)); + + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Open(dialog)).await; + () + }) + } + + /// Creates a [`Command`] that requests the response from an active open file dialog. + pub fn response(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Response).await; + () + }) + } +} + +/// A builder for an open file dialog, passed as a request by a [`Sender`] +#[derive(Setters)] +#[must_use] +pub struct Builder { + /// The lab for the dialog's window title. + title: String, + + /// The label for the accept button. Mnemonic underlines are allowed. + #[setters(strip_option)] + accept_label: Option, + + /// Whether to select for folders instead of files. Default is to select files. + include_directories: bool, + + /// Modal dialogs require user input before continuing the program. + modal: bool, + + /// Whether to allow selection of multiple files. Default is no. + multiple_files: bool, + + /// Adds a list of choices. + choices: Vec, + + /// Specifies the default file filter. + #[setters(into)] + current_filter: Option, + + /// A collection of file filters. + filters: Vec, +} + +impl Builder { + const fn new() -> Self { + Self { + title: String::new(), + accept_label: None, + include_directories: false, + modal: true, + multiple_files: false, + current_filter: None, + choices: Vec::new(), + filters: Vec::new(), + } + } + + /// Creates a [`Command`] which opens the dialog. + pub fn create(self, sender: &mut Sender) -> Command<()> { + sender.open(self) + } + + /// Adds a choice. + pub fn choice(mut self, choice: impl Into) -> Self { + self.choices.push(choice.into()); + self + } + + /// Adds a files filter. + pub fn filter(mut self, filter: impl Into) -> Self { + self.filters.push(filter.into()); + self + } +} + +struct State { + active: Option>, + handle: fn(Message) -> M, + output: channel::mpsc::Sender, +} + +impl State { + /// Emits close request if there is an active dialog request. + async fn close(&mut self) { + if let Some(request) = self.active.take() { + if let Err(why) = request.close().await { + self.emit(Message::Err(Error::Close(why))).await; + } + } + } + + async fn emit(&mut self, response: Message) { + let _res = self.output.send((self.handle)(response)).await; + } + + /// Creates a new dialog, and closes any prior active dialogs. + async fn open(&mut self, dialog: Builder) { + let response = match create(dialog).await { + Ok(request) => { + self.active = Some(request); + Message::Opened + } + Err(why) => Message::Err(Error::Open(why)), + }; + + self.emit(response).await; + } + + /// Collects selected files from the active dialog. + async fn response(&mut self) { + if let Some(request) = self.active.as_ref() { + let response = match request.response() { + Ok(selected) => Message::Selected(selected), + Err(why) => Message::Err(Error::Response(why)), + }; + + self.emit(response).await; + } + } +} + +/// Creates a new file dialog, and begins to await its responses. +async fn create(dialog: Builder) -> ashpd::Result> { + ashpd::desktop::file_chooser::OpenFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .directory(dialog.include_directories) + .modal(dialog.modal) + .multiple(dialog.multiple_files) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .send() + .await +} diff --git a/src/dialog/save_file.rs b/src/dialog/save_file.rs new file mode 100644 index 00000000..9da9c7d9 --- /dev/null +++ b/src/dialog/save_file.rs @@ -0,0 +1,262 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Choose a location to save a file to. +//! +//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) +//! example in our repository. + +use derive_setters::Setters; +use iced::{Command, Subscription}; +use iced::futures::{channel, SinkExt, StreamExt}; +use std::cell::Cell; +use std::path::PathBuf; +use std::time::Instant; +use thiserror::Error; + +thread_local! { + /// Prevents duplicate dialog open requests. + static OPENED: Cell = Cell::new(false); +} + +fn dialog_is_open() -> bool { + OPENED.with(Cell::get) +} + +/// Creates a [`Builder`] if no other save file dialog exists. +pub fn builder() -> Option { + if dialog_is_open() { + None + } else { + Some(Builder::new()) + } +} + +/// Creates a subscription for save file dialog events. +pub fn subscription(handle: fn(Message) -> M) -> Subscription { + let type_id = std::any::TypeId::of::>(); + + iced::subscription::channel(type_id, 1, move |output| async move { + let mut state = State { + active: None, + handle, + output, + }; + + loop { + let (sender, mut receiver) = channel::mpsc::channel(1); + + state.emit(Message::Init(Sender(sender))).await; + + while let Some(request) = receiver.next().await { + match request { + Request::Close => state.close().await, + + Request::Open(dialog) => { + state.open(dialog).await; + OPENED.with(|last| last.set(false)); + }, + + Request::Response => state.response().await, + } + } + } + }) +} + +/// Errors that my occur when interacting with an save file dialog subscription +#[derive(Debug, Error)] +pub enum Error { + #[error("dialog close failed")] + Close(#[source] ashpd::Error), + #[error("dialog open failed")] + Open(#[source] ashpd::Error), + #[error("dialog response failed")] + Response(#[source] ashpd::Error), +} + +/// Requests for an save file dialog subscription +enum Request { + Close, + Open(Builder), + Response, +} + +/// Messages from an save file dialog subscription. +pub enum Message { + Closed, + Err(Error), + Init(Sender), + Opened, + Selected(super::SelectedFiles), +} + +/// Sends requests to an save file dialog subscription. +#[derive(Clone, Debug)] +pub struct Sender(channel::mpsc::Sender); + +impl Sender { + /// Creates a [`Command`] that closes an active save file dialog. + pub fn close(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Close).await; + () + }) + } + + /// Creates a [`Command`] that opens a new save file dialog. + pub fn open(&mut self, dialog: Builder) -> Command<()> { + OPENED.with(|opened| opened.set(true)); + + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Open(dialog)).await; + () + }) + } + + /// Creates a [`Command`] that requests the response from an active save file dialog. + pub fn response(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Response).await; + () + }) + } +} + +/// A builder for an save file dialog, passed as a request by a [`Sender`] +#[derive(Setters)] +#[must_use] +pub struct Builder { + /// The lab for the dialog's window title. + title: String, + + /// The label for the accept button. Mnemonic underlines are allowed. + #[setters(strip_option)] + accept_label: Option, + + /// Modal dialogs require user input before continuing the program. + modal: bool, + + /// Sets the current file name. + #[setters(strip_option)] + current_name: Option, + + /// Sets the current folder. + #[setters(strip_option)] + current_folder: Option, + + /// Sets the absolute path of the file + #[setters(strip_option)] + current_file: Option, + + /// Adds a list of choices. + choices: Vec, + + /// Specifies the default file filter. + #[setters(into)] + current_filter: Option, + + /// A collection of file filters. + filters: Vec, +} + +impl Builder { + const fn new() -> Self { + Self { + title: String::new(), + accept_label: None, + modal: true, + current_name: None, + current_folder: None, + current_file: None, + current_filter: None, + choices: Vec::new(), + filters: Vec::new(), + } + } + + /// Creates a [`Command`] which opens the dialog. + pub fn create(self, sender: &mut Sender) -> Command<()> { + sender.open(self) + } + + /// Adds a choice. + pub fn choice(mut self, choice: impl Into) -> Self { + self.choices.push(choice.into()); + self + } + + /// Adds a files filter. + pub fn filter(mut self, filter: impl Into) -> Self { + self.filters.push(filter.into()); + self + } +} + +struct State { + active: Option>, + handle: fn(Message) -> M, + output: channel::mpsc::Sender, +} + +impl State { + /// Emits close request if there is an active dialog request. + async fn close(&mut self) { + if let Some(request) = self.active.take() { + if let Err(why) = request.close().await { + self.emit(Message::Err(Error::Close(why))).await; + } + } + } + + async fn emit(&mut self, response: Message) { + let _res = self.output.send((self.handle)(response)).await; + } + + /// Creates a new dialog, and closes any prior active dialogs. + async fn open(&mut self, dialog: Builder) { + let response = match create(dialog).await { + Ok(request) => { + self.active = Some(request); + Message::Opened + } + Err(why) => Message::Err(Error::Open(why)), + }; + + self.emit(response).await; + } + + /// Collects selected files from the active dialog. + async fn response(&mut self) { + if let Some(request) = self.active.as_ref() { + let response = match request.response() { + Ok(selected) => Message::Selected(selected), + Err(why) => Message::Err(Error::Message(why)), + }; + + self.emit(response).await; + } + } +} + +/// Creates a new file dialog, and begins to await its responses. +async fn create(dialog: Builder) -> ashpd::Result> { + ashpd::desktop::file_chooser::SaveFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .modal(dialog.modal) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .current_name(dialog.current_name) + .current_folder(dialog.current_folder)? + .current_file(dialog.current_file)? + .send() + .await +} diff --git a/src/lib.rs b/src/lib.rs index 04b94059..576332e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,9 @@ pub mod command; pub use cosmic_config; pub use cosmic_theme; +#[cfg(feature = "xdg-portal")] +pub mod dialog; + pub mod executor; #[cfg(feature = "tokio")] pub use executor::single::Executor as SingleThreadExecutor; From 2602e28d22a0e70da8465265127109041877f7cf Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 11:02:35 +0200 Subject: [PATCH 0060/1276] chore(readme): improve descriptions and examples --- README.md | 95 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 64fd8355..381f3a79 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,90 @@ # LIBCOSMIC -Building blocks for COSMIC applications. +A platform toolkit based on iced which provides the building blocks for developing the +future COSMIC desktop environment. Applications and applets alike are equally supported +targets of Libcosmic. Applets integrate directly with COSMIC's interface as shell +components, which was made possible by the Layer Shell protocol of Wayland. ## Building -Libcosmic is written in pure Rust, so `cargo` is all you need. -```shell -cargo build +Libcosmic is written entirely in Rust, with minimal dependence on system libraries. On +Pop!_OS, the following dependencies are all that's necessary compile the cosmic library: + +```sh +sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev pkg-config ``` -## Usage -There's examples in the `examples` directory. +Some examples are included in the [examples](./examples) directory to to kickstart your +COSMIC adventure. To run them, you need to clone the repository with the following commands: -### Widget library -```shell -cargo run --release --example cosmic -``` - -On Pop!_OS -```shell -sudo apt install cargo libexpat1-dev libfontconfig-dev libfreetype-dev pkg-config cmake +```sh git clone https://github.com/pop-os/libcosmic cd libcosmic -git submodule update --init -cargo run --release -p cosmic +git submodule update --init --recursive ``` -If already cloned -```shell -cd libcosmic -git pull origin master -cargo run --release -p cosmic +If you have already cloned the repository, run these to sync with the latest updates: + +```sh +git fetch origin +git checkout master +git reset --hard origin/master +git submodule update --init --recursive ``` -### Text rendering -```shell -cargo run --release --example text +To create a new COSMIC project, use `cargo new {{name_of_project}}` to create a new +project workspace, edit the `Cargo.toml` contained within, and add this to begin. + +```toml +[workspace.dependencies.libcosmic] +git = "https://github.com/pop-os/libcosmic" +default-features = false +features = ["wayland", "tokio"] ``` +### Cargo Features + +Available cargo features to choose from: + +- `a11y`: Experimental accessibility support. +- `animated-image`: Enables animated images from the image crate. +- `debug`: Enables addtional debugging features. +- `smol`: Uses smol as the preferred async runtime. + - Conflicts with `tokio` +- `tokio`: Uses tokio as the preferred async runtime. + - If unset, the default executor defined by iced will be used. + - Conflicts with `smol` +- `wayland`: Wayland-compatible client windows. + - Conflicts with `winit` +- `winit`: Cross-platform and X11 client window support + - Conflicts with `wayland` +- `wgpu`: GPU accelerated rendering with WGPU. + - By default, softbuffer is used for software rendering. +- `xdg-portal`: Enables XDG portal dialog integrations. + +### Project Showcase + +- [COSMIC App Library](https://github.com/pop-os/cosmic-applibrary) +- [COSMIC Applets](https://github.com/pop-os/cosmic-applets) +- [COSMIC Launcher](https://github.com/pop-os/cosmic-launcher) +- [COSMIC Notifications](https://github.com/pop-os/cosmic-notifications) +- [COSMIC Panel](https://github.com/pop-os/cosmic-panel) +- [COSMIC Text Editor](https://github.com/pop-os/cosmic-text-editor) +- [COSMIC Settings](https://github.com/pop-os/cosmic-settings) + ## Documentation -The documentation can be found [here](https://pop-os.github.io/docs/). + +Documentation can be found [here](https://pop-os.github.io/docs/). ## Licence -Libcosmic is licenced under the MPL-2.0 + +Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0). ## Contact + - [Mattermost](https://chat.pop-os.org/) -- [Discord](https://chat.pop-os.org/) +- [Lemmy](https://lemmy.world/c/pop_os) +- [Mastodon](https://fosstodon.org/@pop_os_official) +- [Reddit](https://www.reddit.com/r/pop_os/) - [Twitter](https://twitter.com/pop_os_official) -- [Instagram](https://www.instagram.com/pop_os_official/) +- [Instagram](https://www.instagram.com/pop_os_official) From a8ce524baa58f4fb2db2e26a5fc0899b63d688b5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 15 Aug 2023 17:20:19 +0200 Subject: [PATCH 0061/1276] refactor: combine open and save dialogs --- examples/open-dialog/src/main.rs | 20 +- src/command/mod.rs | 2 +- src/dialog/file_chooser/mod.rs | 216 +++++++++++++++++++++ src/dialog/file_chooser/open.rs | 90 +++++++++ src/dialog/file_chooser/save.rs | 99 ++++++++++ src/dialog/mod.rs | 3 +- src/dialog/open_file.rs | 252 ------------------------- src/dialog/save_file.rs | 262 -------------------------- src/widget/aspect_ratio.rs | 2 +- src/widget/cosmic_container.rs | 2 +- src/widget/rectangle_tracker/mod.rs | 2 +- src/widget/segmented_button/widget.rs | 2 +- 12 files changed, 421 insertions(+), 531 deletions(-) create mode 100644 src/dialog/file_chooser/mod.rs create mode 100644 src/dialog/file_chooser/open.rs create mode 100644 src/dialog/file_chooser/save.rs delete mode 100644 src/dialog/open_file.rs delete mode 100644 src/dialog/save_file.rs diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 7c1cdebd..6533394a 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -5,7 +5,7 @@ use apply::Apply; use cosmic::app::{Command, Core, Settings}; -use cosmic::dialog::{open_file, FileFilter}; +use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::iced_core::Length; use cosmic::{executor, iced, ApplicationExt, Element}; use tokio::io::AsyncReadExt; @@ -27,7 +27,7 @@ fn main() -> Result<(), Box> { pub enum Message { CloseError, DialogClosed, - DialogInit(open_file::Sender), + DialogInit(file_chooser::Sender), DialogOpened, Error(String), FileRead(Url, String), @@ -38,7 +38,7 @@ pub enum Message { /// The [`App`] stores application-specific state. pub struct App { core: Core, - open_sender: Option, + open_sender: Option, file_contents: String, selected_file: Option, error_status: Option, @@ -90,15 +90,15 @@ impl cosmic::Application for App { fn subscription(&self) -> cosmic::iced_futures::Subscription { // Creates a subscription for handling open dialogs. - open_file::subscription(|response| match response { - open_file::Message::Closed => Message::DialogClosed, - open_file::Message::Opened => Message::DialogOpened, - open_file::Message::Selected(files) => match files.uris().first() { + file_chooser::subscription(|response| match response { + file_chooser::Message::Closed => Message::DialogClosed, + file_chooser::Message::Opened => Message::DialogOpened, + file_chooser::Message::Selected(files) => match files.uris().first() { Some(file) => Message::Selected(file.to_owned()), None => Message::DialogClosed, }, - open_file::Message::Init(sender) => Message::DialogInit(sender), - open_file::Message::Err(why) => { + file_chooser::Message::Init(sender) => Message::DialogInit(sender), + file_chooser::Message::Err(why) => { let mut source: &dyn std::error::Error = &why; let mut string = format!("open dialog subscription errored\n cause: {source}"); @@ -180,7 +180,7 @@ impl cosmic::Application for App { // Creates a new open dialog. Message::OpenFile => { if let Some(sender) = self.open_sender.as_mut() { - if let Some(dialog) = open_file::builder() { + if let Some(dialog) = file_chooser::open_file() { eprintln!("opening new dialog"); return dialog diff --git a/src/command/mod.rs b/src/command/mod.rs index 48300e2b..39975d37 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -23,7 +23,7 @@ pub fn batch(commands: impl IntoIterator>) -> Command { Command::batch(commands) } -/// Yields a command which will run the future on thet runtime executor. +/// Yields a command which will run the future on the runtime executor. pub fn future(future: impl Future + Send + 'static) -> Command { Command::single(Action::Future(Box::pin(future))) } diff --git a/src/dialog/file_chooser/mod.rs b/src/dialog/file_chooser/mod.rs new file mode 100644 index 00000000..1305ea6b --- /dev/null +++ b/src/dialog/file_chooser/mod.rs @@ -0,0 +1,216 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Dialogs for opening and save files. + +pub mod open; +pub mod save; + +pub use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; +use iced::futures::{channel, SinkExt, StreamExt}; +use iced::{Command, Subscription}; +use std::sync::atomic::{AtomicBool, Ordering}; +use thiserror::Error; + +/// Prevents duplicate file chooser dialog requests. +static OPENED: AtomicBool = AtomicBool::new(false); + +/// Whether a file chooser dialog is currently active. +fn dialog_active() -> bool { + OPENED.load(Ordering::Relaxed) +} + +/// Sets the existence of a file chooser dialog. +fn dialog_active_set(value: bool) { + OPENED.store(value, Ordering::SeqCst); +} + +/// Creates an [`open::Dialog`] if no other file chooser exists. +pub fn open_file() -> Option { + if dialog_active() { + None + } else { + Some(open::Dialog::new()) + } +} + +/// Creates a [`save::Dialog`] if no other file chooser exists. +pub fn save_file() -> Option { + if dialog_active() { + None + } else { + Some(save::Dialog::new()) + } +} + +/// Creates a subscription for file chooser events. +pub fn subscription(handle: fn(Message) -> M) -> Subscription { + let type_id = std::any::TypeId::of::>(); + + iced::subscription::channel(type_id, 1, move |output| async move { + let mut state = Handler { + active: None, + handle, + output, + }; + + loop { + let (sender, mut receiver) = channel::mpsc::channel(1); + + state.emit(Message::Init(Sender(sender))).await; + + while let Some(request) = receiver.next().await { + match request { + Request::Close => state.close().await, + + Request::Open(dialog) => { + state.open(dialog).await; + dialog_active_set(false); + } + + Request::Save(dialog) => { + state.save(dialog).await; + dialog_active_set(false); + } + + Request::Response => state.response().await, + } + } + } + }) +} + +/// Errors that my occur when interacting with the file chooser subscription +#[derive(Debug, Error)] +pub enum Error { + #[error("dialog close failed")] + Close(#[source] ashpd::Error), + #[error("dialog open failed")] + Open(#[source] ashpd::Error), + #[error("dialog response failed")] + Response(#[source] ashpd::Error), +} + +/// Requests for the file chooser subscription +enum Request { + Close, + Open(open::Dialog), + Save(save::Dialog), + Response, +} + +/// Messages from the file chooser subscription. +pub enum Message { + Closed, + Err(Error), + Init(Sender), + Opened, + Selected(SelectedFiles), +} + +/// Sends requests to the file chooser subscription. +#[derive(Clone, Debug)] +pub struct Sender(channel::mpsc::Sender); + +impl Sender { + /// Creates a [`Command`] that closes a file chooser dialog. + pub fn close(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Close).await; + () + }) + } + + /// Creates a [`Command`] that opens the file chooser. + pub fn open(&mut self, dialog: open::Dialog) -> Command<()> { + dialog_active_set(true); + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Open(dialog)).await; + () + }) + } + + /// Creates a [`Command`] that requests the response from a file chooser dialog. + pub fn response(&mut self) -> Command<()> { + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Response).await; + () + }) + } + + /// Creates a [`Command`] that opens a new save file dialog. + pub fn save(&mut self, dialog: save::Dialog) -> Command<()> { + dialog_active_set(true); + let mut sender = self.0.clone(); + + crate::command::future(async move { + let _res = sender.send(Request::Save(dialog)).await; + () + }) + } +} + +struct Handler { + active: Option>, + handle: fn(Message) -> M, + output: channel::mpsc::Sender, +} + +impl Handler { + /// Emits close request if there is an active dialog request. + async fn close(&mut self) { + if let Some(request) = self.active.take() { + if let Err(why) = request.close().await { + self.emit(Message::Err(Error::Close(why))).await; + } + } + } + + async fn emit(&mut self, response: Message) { + let _res = self.output.send((self.handle)(response)).await; + } + + /// Creates a new dialog, and closes any prior active dialogs. + async fn open(&mut self, dialog: open::Dialog) { + let response = match open::create(dialog).await { + Ok(request) => { + self.active = Some(request); + Message::Opened + } + Err(why) => Message::Err(Error::Open(why)), + }; + + self.emit(response).await; + } + + /// Collects selected files from the active dialog. + async fn response(&mut self) { + if let Some(request) = self.active.as_ref() { + let response = match request.response() { + Ok(selected) => Message::Selected(selected), + Err(why) => Message::Err(Error::Response(why)), + }; + + self.emit(response).await; + } + } + + /// Creates a new dialog, and closes any prior active dialogs. + async fn save(&mut self, dialog: save::Dialog) { + let response = match save::create(dialog).await { + Ok(request) => { + self.active = Some(request); + Message::Opened + } + Err(why) => Message::Err(Error::Open(why)), + }; + + self.emit(response).await; + } +} diff --git a/src/dialog/file_chooser/open.rs b/src/dialog/file_chooser/open.rs new file mode 100644 index 00000000..20d4176b --- /dev/null +++ b/src/dialog/file_chooser/open.rs @@ -0,0 +1,90 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Request to open files and/or directories. +//! +//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) +//! example in our repository. + +use derive_setters::Setters; +use iced::Command; + +/// A builder for an open file dialog, passed as a request by a [`Sender`] +#[derive(Setters)] +#[must_use] +pub struct Dialog { + /// The label for the dialog's window title. + title: String, + + /// The label for the accept button. Mnemonic underlines are allowed. + #[setters(strip_option)] + accept_label: Option, + + /// Whether to select for folders instead of files. Default is to select files. + include_directories: bool, + + /// Modal dialogs require user input before continuing the program. + modal: bool, + + /// Whether to allow selection of multiple files. Default is no. + multiple_files: bool, + + /// Adds a list of choices. + choices: Vec, + + /// Specifies the default file filter. + #[setters(into)] + current_filter: Option, + + /// A collection of file filters. + filters: Vec, +} + +impl Dialog { + pub(super) const fn new() -> Self { + Self { + title: String::new(), + accept_label: None, + include_directories: false, + modal: true, + multiple_files: false, + current_filter: None, + choices: Vec::new(), + filters: Vec::new(), + } + } + + /// Creates a [`Command`] which opens the dialog. + pub fn create(self, sender: &mut super::Sender) -> Command<()> { + sender.open(self) + } + + /// Adds a choice. + pub fn choice(mut self, choice: impl Into) -> Self { + self.choices.push(choice.into()); + self + } + + /// Adds a files filter. + pub fn filter(mut self, filter: impl Into) -> Self { + self.filters.push(filter.into()); + self + } +} + +/// Creates a new file dialog, and begins to await its responses. +pub(super) async fn create( + dialog: Dialog, +) -> ashpd::Result> { + ashpd::desktop::file_chooser::OpenFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .directory(dialog.include_directories) + .modal(dialog.modal) + .multiple(dialog.multiple_files) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .send() + .await +} diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs new file mode 100644 index 00000000..0729b0f7 --- /dev/null +++ b/src/dialog/file_chooser/save.rs @@ -0,0 +1,99 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Choose a location to save a file to. +//! +//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) +//! example in our repository. + +use derive_setters::Setters; +use iced::Command; +use std::path::{Path, PathBuf}; + +/// A builder for an save file dialog, passed as a request by a [`Sender`] +#[derive(Setters)] +#[must_use] +pub struct Dialog { + /// The label for the dialog's window title. + title: String, + + /// The label for the accept button. Mnemonic underlines are allowed. + #[setters(strip_option)] + accept_label: Option, + + /// Modal dialogs require user input before continuing the program. + modal: bool, + + /// Sets the current file name. + #[setters(strip_option)] + current_name: Option, + + /// Sets the current folder. + #[setters(strip_option)] + current_folder: Option, + + /// Sets the absolute path of the file + #[setters(strip_option)] + current_file: Option, + + /// Adds a list of choices. + choices: Vec, + + /// Specifies the default file filter. + #[setters(into)] + current_filter: Option, + + /// A collection of file filters. + filters: Vec, +} + +impl Dialog { + pub(super) const fn new() -> Self { + Self { + title: String::new(), + accept_label: None, + modal: true, + current_name: None, + current_folder: None, + current_file: None, + current_filter: None, + choices: Vec::new(), + filters: Vec::new(), + } + } + + /// Creates a [`Command`] which opens the dialog. + pub fn create(self, sender: &mut super::Sender) -> Command<()> { + sender.save(self) + } + + /// Adds a choice. + pub fn choice(mut self, choice: impl Into) -> Self { + self.choices.push(choice.into()); + self + } + + /// Adds a files filter. + pub fn filter(mut self, filter: impl Into) -> Self { + self.filters.push(filter.into()); + self + } +} + +/// Creates a new file dialog, and begins to await its responses. +pub(super) async fn create( + dialog: Dialog, +) -> ashpd::Result> { + ashpd::desktop::file_chooser::SaveFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .modal(dialog.modal) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .current_name(dialog.current_name.as_deref()) + .current_folder::<&Path>(dialog.current_folder.as_deref())? + .current_file::<&Path>(dialog.current_file.as_deref())? + .send() + .await +} diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index f4d85537..dc753096 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -3,7 +3,6 @@ //! Create dialogs for retrieving user input. -pub use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; pub use ashpd::WindowIdentifier; -pub mod open_file; +pub mod file_chooser; diff --git a/src/dialog/open_file.rs b/src/dialog/open_file.rs deleted file mode 100644 index cbb92cf1..00000000 --- a/src/dialog/open_file.rs +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Request to open files and/or directories. -//! -//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) -//! example in our repository. - -use derive_setters::Setters; -use iced::futures::{channel, SinkExt, StreamExt}; -use iced::{Command, Subscription}; -use std::cell::Cell; -use thiserror::Error; - -thread_local! { - /// Prevents duplicate dialog open requests. - static OPENED: Cell = Cell::new(false); -} - -fn dialog_is_open() -> bool { - OPENED.with(Cell::get) -} - -/// Creates a [`Builder`] if no other open file dialog exists. -pub fn builder() -> Option { - if dialog_is_open() { - None - } else { - Some(Builder::new()) - } -} - -/// Creates a subscription for open file dialog events. -pub fn subscription(handle: fn(Message) -> M) -> Subscription { - let type_id = std::any::TypeId::of::>(); - - iced::subscription::channel(type_id, 1, move |output| async move { - let mut state = State { - active: None, - handle, - output, - }; - - loop { - let (sender, mut receiver) = channel::mpsc::channel(1); - - state.emit(Message::Init(Sender(sender))).await; - - while let Some(request) = receiver.next().await { - match request { - Request::Close => state.close().await, - - Request::Open(dialog) => { - state.open(dialog).await; - OPENED.with(|last| last.set(false)); - } - - Request::Response => state.response().await, - } - } - } - }) -} - -/// Errors that my occur when interacting with an open file dialog subscription -#[derive(Debug, Error)] -pub enum Error { - #[error("dialog close failed")] - Close(#[source] ashpd::Error), - #[error("dialog open failed")] - Open(#[source] ashpd::Error), - #[error("dialog response failed")] - Response(#[source] ashpd::Error), -} - -/// Requests for an open file dialog subscription -enum Request { - Close, - Open(Builder), - Response, -} - -/// Messages from an open file dialog subscription. -pub enum Message { - Closed, - Err(Error), - Init(Sender), - Opened, - Selected(super::SelectedFiles), -} - -/// Sends requests to an open file dialog subscription. -#[derive(Clone, Debug)] -pub struct Sender(channel::mpsc::Sender); - -impl Sender { - /// Creates a [`Command`] that closes an active open file dialog. - pub fn close(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Close).await; - () - }) - } - - /// Creates a [`Command`] that opens a new open file dialog. - pub fn open(&mut self, dialog: Builder) -> Command<()> { - OPENED.with(|opened| opened.set(true)); - - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Open(dialog)).await; - () - }) - } - - /// Creates a [`Command`] that requests the response from an active open file dialog. - pub fn response(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Response).await; - () - }) - } -} - -/// A builder for an open file dialog, passed as a request by a [`Sender`] -#[derive(Setters)] -#[must_use] -pub struct Builder { - /// The lab for the dialog's window title. - title: String, - - /// The label for the accept button. Mnemonic underlines are allowed. - #[setters(strip_option)] - accept_label: Option, - - /// Whether to select for folders instead of files. Default is to select files. - include_directories: bool, - - /// Modal dialogs require user input before continuing the program. - modal: bool, - - /// Whether to allow selection of multiple files. Default is no. - multiple_files: bool, - - /// Adds a list of choices. - choices: Vec, - - /// Specifies the default file filter. - #[setters(into)] - current_filter: Option, - - /// A collection of file filters. - filters: Vec, -} - -impl Builder { - const fn new() -> Self { - Self { - title: String::new(), - accept_label: None, - include_directories: false, - modal: true, - multiple_files: false, - current_filter: None, - choices: Vec::new(), - filters: Vec::new(), - } - } - - /// Creates a [`Command`] which opens the dialog. - pub fn create(self, sender: &mut Sender) -> Command<()> { - sender.open(self) - } - - /// Adds a choice. - pub fn choice(mut self, choice: impl Into) -> Self { - self.choices.push(choice.into()); - self - } - - /// Adds a files filter. - pub fn filter(mut self, filter: impl Into) -> Self { - self.filters.push(filter.into()); - self - } -} - -struct State { - active: Option>, - handle: fn(Message) -> M, - output: channel::mpsc::Sender, -} - -impl State { - /// Emits close request if there is an active dialog request. - async fn close(&mut self) { - if let Some(request) = self.active.take() { - if let Err(why) = request.close().await { - self.emit(Message::Err(Error::Close(why))).await; - } - } - } - - async fn emit(&mut self, response: Message) { - let _res = self.output.send((self.handle)(response)).await; - } - - /// Creates a new dialog, and closes any prior active dialogs. - async fn open(&mut self, dialog: Builder) { - let response = match create(dialog).await { - Ok(request) => { - self.active = Some(request); - Message::Opened - } - Err(why) => Message::Err(Error::Open(why)), - }; - - self.emit(response).await; - } - - /// Collects selected files from the active dialog. - async fn response(&mut self) { - if let Some(request) = self.active.as_ref() { - let response = match request.response() { - Ok(selected) => Message::Selected(selected), - Err(why) => Message::Err(Error::Response(why)), - }; - - self.emit(response).await; - } - } -} - -/// Creates a new file dialog, and begins to await its responses. -async fn create(dialog: Builder) -> ashpd::Result> { - ashpd::desktop::file_chooser::OpenFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .directory(dialog.include_directories) - .modal(dialog.modal) - .multiple(dialog.multiple_files) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .send() - .await -} diff --git a/src/dialog/save_file.rs b/src/dialog/save_file.rs deleted file mode 100644 index 9da9c7d9..00000000 --- a/src/dialog/save_file.rs +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Choose a location to save a file to. -//! -//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) -//! example in our repository. - -use derive_setters::Setters; -use iced::{Command, Subscription}; -use iced::futures::{channel, SinkExt, StreamExt}; -use std::cell::Cell; -use std::path::PathBuf; -use std::time::Instant; -use thiserror::Error; - -thread_local! { - /// Prevents duplicate dialog open requests. - static OPENED: Cell = Cell::new(false); -} - -fn dialog_is_open() -> bool { - OPENED.with(Cell::get) -} - -/// Creates a [`Builder`] if no other save file dialog exists. -pub fn builder() -> Option { - if dialog_is_open() { - None - } else { - Some(Builder::new()) - } -} - -/// Creates a subscription for save file dialog events. -pub fn subscription(handle: fn(Message) -> M) -> Subscription { - let type_id = std::any::TypeId::of::>(); - - iced::subscription::channel(type_id, 1, move |output| async move { - let mut state = State { - active: None, - handle, - output, - }; - - loop { - let (sender, mut receiver) = channel::mpsc::channel(1); - - state.emit(Message::Init(Sender(sender))).await; - - while let Some(request) = receiver.next().await { - match request { - Request::Close => state.close().await, - - Request::Open(dialog) => { - state.open(dialog).await; - OPENED.with(|last| last.set(false)); - }, - - Request::Response => state.response().await, - } - } - } - }) -} - -/// Errors that my occur when interacting with an save file dialog subscription -#[derive(Debug, Error)] -pub enum Error { - #[error("dialog close failed")] - Close(#[source] ashpd::Error), - #[error("dialog open failed")] - Open(#[source] ashpd::Error), - #[error("dialog response failed")] - Response(#[source] ashpd::Error), -} - -/// Requests for an save file dialog subscription -enum Request { - Close, - Open(Builder), - Response, -} - -/// Messages from an save file dialog subscription. -pub enum Message { - Closed, - Err(Error), - Init(Sender), - Opened, - Selected(super::SelectedFiles), -} - -/// Sends requests to an save file dialog subscription. -#[derive(Clone, Debug)] -pub struct Sender(channel::mpsc::Sender); - -impl Sender { - /// Creates a [`Command`] that closes an active save file dialog. - pub fn close(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Close).await; - () - }) - } - - /// Creates a [`Command`] that opens a new save file dialog. - pub fn open(&mut self, dialog: Builder) -> Command<()> { - OPENED.with(|opened| opened.set(true)); - - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Open(dialog)).await; - () - }) - } - - /// Creates a [`Command`] that requests the response from an active save file dialog. - pub fn response(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Response).await; - () - }) - } -} - -/// A builder for an save file dialog, passed as a request by a [`Sender`] -#[derive(Setters)] -#[must_use] -pub struct Builder { - /// The lab for the dialog's window title. - title: String, - - /// The label for the accept button. Mnemonic underlines are allowed. - #[setters(strip_option)] - accept_label: Option, - - /// Modal dialogs require user input before continuing the program. - modal: bool, - - /// Sets the current file name. - #[setters(strip_option)] - current_name: Option, - - /// Sets the current folder. - #[setters(strip_option)] - current_folder: Option, - - /// Sets the absolute path of the file - #[setters(strip_option)] - current_file: Option, - - /// Adds a list of choices. - choices: Vec, - - /// Specifies the default file filter. - #[setters(into)] - current_filter: Option, - - /// A collection of file filters. - filters: Vec, -} - -impl Builder { - const fn new() -> Self { - Self { - title: String::new(), - accept_label: None, - modal: true, - current_name: None, - current_folder: None, - current_file: None, - current_filter: None, - choices: Vec::new(), - filters: Vec::new(), - } - } - - /// Creates a [`Command`] which opens the dialog. - pub fn create(self, sender: &mut Sender) -> Command<()> { - sender.open(self) - } - - /// Adds a choice. - pub fn choice(mut self, choice: impl Into) -> Self { - self.choices.push(choice.into()); - self - } - - /// Adds a files filter. - pub fn filter(mut self, filter: impl Into) -> Self { - self.filters.push(filter.into()); - self - } -} - -struct State { - active: Option>, - handle: fn(Message) -> M, - output: channel::mpsc::Sender, -} - -impl State { - /// Emits close request if there is an active dialog request. - async fn close(&mut self) { - if let Some(request) = self.active.take() { - if let Err(why) = request.close().await { - self.emit(Message::Err(Error::Close(why))).await; - } - } - } - - async fn emit(&mut self, response: Message) { - let _res = self.output.send((self.handle)(response)).await; - } - - /// Creates a new dialog, and closes any prior active dialogs. - async fn open(&mut self, dialog: Builder) { - let response = match create(dialog).await { - Ok(request) => { - self.active = Some(request); - Message::Opened - } - Err(why) => Message::Err(Error::Open(why)), - }; - - self.emit(response).await; - } - - /// Collects selected files from the active dialog. - async fn response(&mut self) { - if let Some(request) = self.active.as_ref() { - let response = match request.response() { - Ok(selected) => Message::Selected(selected), - Err(why) => Message::Err(Error::Message(why)), - }; - - self.emit(response).await; - } - } -} - -/// Creates a new file dialog, and begins to await its responses. -async fn create(dialog: Builder) -> ashpd::Result> { - ashpd::desktop::file_chooser::SaveFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .modal(dialog.modal) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .current_name(dialog.current_name) - .current_folder(dialog.current_folder)? - .current_file(dialog.current_file)? - .send() - .await -} diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index bf000006..eae0f89d 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -7,7 +7,7 @@ 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 iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; pub use iced_style::container::{Appearance, StyleSheet}; diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 3214b7fa..8972a01f 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -7,7 +7,7 @@ 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 iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; pub use iced_style::container::{Appearance, StyleSheet}; pub fn container<'a, Message: 'static, T>( diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 6b84e5d2..305634cb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -11,7 +11,7 @@ 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 iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; use std::{fmt::Debug, hash::Hash}; pub use iced_style::container::{Appearance, StyleSheet}; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index d7114fa4..f91654d0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -8,7 +8,7 @@ use crate::widget::{icon, IconSource}; use derive_setters::Setters; use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length, - Point, Rectangle, Size, + Rectangle, Size, }; use iced_core::text::{LineHeight, Shaping}; use iced_core::widget::{self, operation, tree}; From c474b3e9554f1c0ea990632544cfeb5ddba24ca9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 8 Aug 2023 18:09:57 -0400 Subject: [PATCH 0062/1276] wip: add applet module --- Cargo.toml | 10 ++ src/app/applet/mod.rs | 292 ++++++++++++++++++++++++++++++++++++++++++ src/app/core.rs | 6 + src/app/cosmic.rs | 11 +- src/app/mod.rs | 7 + 5 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 src/app/applet/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 9e9dcbd5..adbbbfbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ winit_tokio = ["iced/winit", "iced_winit", "tokio"] winit_wgpu = ["winit", "wgpu"] # Enables XDG portal integrations xdg-portal = ["ashpd"] +applet = ["wayland", "tokio", "a11y", "cosmic-panel-config", "ron"] [dependencies] apply = "0.3.0" @@ -98,6 +99,15 @@ optional = true path = "iced/wgpu" optional = true +[dependencies.cosmic-panel-config] +git = "https://github.com/pop-os/cosmic-panel" +optional = true + +[dependencies.ron] +version = "0.8" +optional = true + + [workspace] members = [ "cosmic-config", diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs new file mode 100644 index 00000000..b3cdd1d9 --- /dev/null +++ b/src/app/applet/mod.rs @@ -0,0 +1,292 @@ +use std::sync::Arc; + +use crate::{ + app::Core, + cosmic_config::CosmicConfigEntry, + cosmic_theme::util::CssColor, + iced::{ + self, + alignment::{Horizontal, Vertical}, + widget::Container, + window, Color, Length, Limits, Rectangle, + }, + iced_style, iced_widget, sctk, + theme::{self, Button, THEME}, + Application, Element, Renderer, +}; +pub use cosmic_panel_config; +use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; +use iced_style::{button::StyleSheet, container::Appearance}; +use iced_widget::runtime::command::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, +}; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use tracing::error; + +use super::cosmic; + +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: 0.0.into(), + ..t.active(&Button::Text) + }), + hover: Box::new(|t| iced_style::button::Appearance { + border_radius: 0.0.into(), + ..t.hovered(&Button::Text) + }), + } +} + +#[derive(Debug, Clone)] +pub struct CosmicAppletHelper { + pub size: Size, + pub anchor: PanelAnchor, + pub background: CosmicPanelBackground, + pub output_name: String, +} + +#[derive(Clone, Debug)] +pub enum Size { + PanelSize(PanelSize), + // (width, height) + Hardcoded((u16, u16)), +} + +impl Default for CosmicAppletHelper { + fn default() -> Self { + Self { + size: Size::PanelSize( + std::env::var("COSMIC_PANEL_SIZE") + .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| 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 { + PanelSize::XL => (64, 64), + PanelSize::L => (36, 36), + PanelSize::M => (24, 24), + PanelSize::S => (16, 16), + PanelSize::XS => (12, 12), + }, + Size::Hardcoded((width, height)) => (*width, *height), + } + } + + // Set the default window size. Helper for application init with hardcoded size. + pub fn window_size(&mut self, width: u16, height: u16) { + self.size = Size::Hardcoded((width, height)); + } + + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub fn window_settings(&self) -> super::Settings { + let (width, height) = self.suggested_size(); + let width = u32::from(width); + let height = u32::from(height); + super::Settings::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_text_size(18.0) + .default_font(crate::font::FONT) + .transparent(true) + .theme(self.theme()) + } + + #[must_use] + pub fn icon_button<'a, Message: 'static>( + &self, + icon_name: &'a str, + ) -> iced::widget::Button<'a, Message, Renderer> { + crate::widget::button(theme::Button::Text) + .icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0) + .padding(8) + } + + // TODO popup container which tracks the size of itself and requests the popup to resize to match + pub fn popup_container<'a, Message: 'static>( + &self, + content: impl Into>, + ) -> Container<'a, Message, Renderer> { + 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), + PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), + }; + + Container::::new(Container::::new(content).style( + 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.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }), + )) + .width(Length::Shrink) + .height(Length::Shrink) + .align_x(horizontal_align) + .align_y(vertical_align) + } + + #[must_use] + #[allow(clippy::cast_possible_wrap)] + pub fn get_popup_settings( + &self, + parent: window::Id, + id: window::Id, + size: Option<(u32, u32)>, + width_padding: Option, + height_padding: Option, + ) -> SctkPopupSettings { + let (width, height) = self.suggested_size(); + let pixel_offset = 8; + let (offset, anchor, gravity) = match self.anchor { + PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), + PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left), + PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom), + PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top), + }; + SctkPopupSettings { + parent, + id, + positioner: SctkPositioner { + anchor, + gravity, + offset, + size, + anchor_rect: Rectangle { + x: 0, + y: 0, + width: width_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(width), + height: height_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(height), + }, + reactive: true, + constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y + ..Default::default() + }, + parent_size: None, + grab: true, + } + } + + #[must_use] + pub fn theme(&self) -> theme::Theme { + match self.background { + CosmicPanelBackground::ThemeDefault | CosmicPanelBackground::Color(_) => { + let Ok(helper) = cosmic_config::Config::new( + cosmic_theme::NAME, + cosmic_theme::Theme::::version(), + ) else { + return theme::Theme::dark(); + }; + let t = + cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { + for err in errors { + error!("{:?}", err); + } + theme + }); + theme::Theme::custom(Arc::new(t)) + } + CosmicPanelBackground::Dark => theme::Theme::dark(), + CosmicPanelBackground::Light => theme::Theme::light(), + } + } +} + +/// Launch the application with the given settings. +/// +/// # Errors +/// +/// Returns error on application failure. +pub fn run(autosize: bool, flags: App::Flags) -> iced::Result { + let helper = CosmicAppletHelper::default(); + let mut settings = helper.window_settings(); + settings.autosize = autosize; + + if let Some(icon_theme) = settings.default_icon_theme { + crate::icon_theme::set_default(icon_theme); + } + + let mut core = Core::default(); + core.window.show_window_menu = false; + core.window.show_headerbar = false; + core.window.sharp_corners = true; + core.window.show_maximize = false; + core.window.show_minimize = false; + core.window.use_template = false; + + core.debug = settings.debug; + core.set_scale_factor(settings.scale_factor); + core.set_window_width(settings.size.0); + core.set_window_height(settings.size.1); + + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + cosmic_theme.set_theme(settings.theme.theme_type); + }); + + let mut iced = iced::Settings::with_flags((core, flags)); + + iced.antialiasing = settings.antialiasing; + iced.default_font = settings.default_font; + iced.default_text_size = settings.default_text_size; + iced.id = Some(App::APP_ID.to_owned()); + + { + use iced::wayland::actions::window::SctkWindowSettings; + use iced_sctk::settings::InitialSurface; + iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { + app_id: Some(App::APP_ID.to_owned()), + autosize: settings.autosize, + client_decorations: settings.client_decorations, + resizable: settings.resizable, + size: settings.size, + size_limits: settings.size_limits, + title: None, + transparent: settings.transparent, + ..SctkWindowSettings::default() + }); + } + + as iced::Application>::run(iced) +} + +#[must_use] +pub fn style() -> ::Style { + ::Style::Custom(Box::new(|theme| { + iced_style::application::Appearance { + background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), + text_color: theme.cosmic().on_bg_color().into(), + } + })) +} diff --git a/src/app/core.rs b/src/app/core.rs index 79f09a42..d5a979a3 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -15,6 +15,7 @@ pub struct NavBar { #[allow(clippy::struct_excessive_bools)] #[derive(Clone)] pub struct Window { + pub use_template: bool, pub can_fullscreen: bool, pub sharp_corners: bool, pub show_headerbar: bool, @@ -43,6 +44,8 @@ pub struct Core { pub system_theme: Theme, pub(crate) title: String, pub window: Window, + #[cfg(feature = "applet")] + pub applet_helper: super::applet::CosmicAppletHelper, } impl Default for Core { @@ -59,6 +62,7 @@ impl Default for Core { system_theme: theme::theme(), title: String::new(), window: Window { + use_template: true, can_fullscreen: false, sharp_corners: false, show_headerbar: true, @@ -68,6 +72,8 @@ impl Default for Core { height: 0, width: 0, }, + #[cfg(feature = "applet")] + applet_helper: super::applet::CosmicAppletHelper::default(), } } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9b6bab96..6758e4d0 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -103,7 +103,9 @@ where } fn style(&self) -> ::Style { - if self.app.core().window.sharp_corners { + if let Some(style) = self.app.style() { + style + } else if self.app.core().window.sharp_corners { theme::Application::default() } else { theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { @@ -163,8 +165,11 @@ where if id != window::Id(0) { return self.app.view_window(id).map(super::Message::App); } - - self.app.view_main() + if self.app.core().window.use_template { + self.app.view_main() + } else { + self.app.view().map(super::Message::App) + } } #[cfg(not(feature = "wayland"))] diff --git a/src/app/mod.rs b/src/app/mod.rs index 5d98cbd1..6182f76b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,6 +6,8 @@ //! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) //! example in our repository. +#[cfg(feature = "applet")] +pub mod applet; pub mod command; mod core; pub mod cosmic; @@ -197,6 +199,11 @@ where fn view_window(&self, id: window::Id) -> Element { panic!("no view for window {}", id.0); } + + /// Overrides the default style for applications + fn style(&self) -> Option<::Style> { + None + } } /// Methods automatically derived for all types implementing [`Application`]. From c1495d07e5a44c16ec55ea517202991829257b3c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 10 Aug 2023 11:07:52 -0400 Subject: [PATCH 0063/1276] cleanup (applet): settings already has the system theme by default --- src/app/applet/mod.rs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index b3cdd1d9..9eb9a712 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -104,7 +104,7 @@ impl CosmicAppletHelper { let (width, height) = self.suggested_size(); let width = u32::from(width); let height = u32::from(height); - super::Settings::default() + let settings = super::Settings::default() .size((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)) .size_limits( Limits::NONE @@ -116,8 +116,11 @@ impl CosmicAppletHelper { .resizable(None) .default_text_size(18.0) .default_font(crate::font::FONT) - .transparent(true) - .theme(self.theme()) + .transparent(true); + if let Some(theme) = self.theme() { + settings.theme(theme); + } + settings } #[must_use] @@ -199,26 +202,11 @@ impl CosmicAppletHelper { } #[must_use] - pub fn theme(&self) -> theme::Theme { + pub fn theme(&self) -> Option { match self.background { - CosmicPanelBackground::ThemeDefault | CosmicPanelBackground::Color(_) => { - let Ok(helper) = cosmic_config::Config::new( - cosmic_theme::NAME, - cosmic_theme::Theme::::version(), - ) else { - return theme::Theme::dark(); - }; - let t = - cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { - for err in errors { - error!("{:?}", err); - } - theme - }); - theme::Theme::custom(Arc::new(t)) - } CosmicPanelBackground::Dark => theme::Theme::dark(), CosmicPanelBackground::Light => theme::Theme::light(), + _ => None, } } } From 9f36d33e345492044de6c7739171d86d9881cbc8 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 15 Aug 2023 13:44:06 -0700 Subject: [PATCH 0064/1276] Fix compilation errors with `applet` feature --- src/app/applet/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index 9eb9a712..20bf4216 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -104,7 +104,7 @@ impl CosmicAppletHelper { let (width, height) = self.suggested_size(); let width = u32::from(width); let height = u32::from(height); - let settings = super::Settings::default() + let mut settings = super::Settings::default() .size((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)) .size_limits( Limits::NONE @@ -118,7 +118,7 @@ impl CosmicAppletHelper { .default_font(crate::font::FONT) .transparent(true); if let Some(theme) = self.theme() { - settings.theme(theme); + settings = settings.theme(theme); } settings } @@ -204,8 +204,8 @@ impl CosmicAppletHelper { #[must_use] pub fn theme(&self) -> Option { match self.background { - CosmicPanelBackground::Dark => theme::Theme::dark(), - CosmicPanelBackground::Light => theme::Theme::light(), + CosmicPanelBackground::Dark => Some(theme::Theme::dark()), + CosmicPanelBackground::Light => Some(theme::Theme::light()), _ => None, } } From be49bb2a25d436204eb143f9e561134649a15cc0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 17 Aug 2023 16:19:45 -0400 Subject: [PATCH 0065/1276] refactor: switch applet back to secondary button --- src/app/applet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index 20bf4216..7671cdc9 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -128,7 +128,7 @@ impl CosmicAppletHelper { &self, icon_name: &'a str, ) -> iced::widget::Button<'a, Message, Renderer> { - crate::widget::button(theme::Button::Text) + crate::widget::button(theme::Button::Secondary) .icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0) .padding(8) } From 4f964a4bc8a97809adb1cc38b177e799c6073bf6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 18 Aug 2023 13:36:03 -0400 Subject: [PATCH 0066/1276] fix(theme): use overlay colors directly for the text button --- cosmic-theme/src/model/theme.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 76c73055..f5a5ec36 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -713,6 +713,24 @@ impl ThemeBuilder { let mut button_disabled_border = button_border; button_disabled_border.alpha *= 0.5; + let mut text_button = Component::component( + Srgba::new(0.0, 0.0, 0.0, 0.0), + p_ref.neutral_10, + accent, + p_ref.neutral_9, + is_high_contrast, + p_ref.neutral_8, + ); + + let overlay = p_ref.neutral_10.clone(); + let mut hover = overlay.clone(); + hover.alpha = 0.1; + text_button.hover = hover; + let mut press = overlay.clone(); + press.alpha = 0.2; + text_button.pressed = press; + text_button.focus = text_button.base.clone(); + let mut theme: Theme = Theme { name: palette.name().to_string(), background: Container::new( @@ -800,14 +818,7 @@ impl ThemeBuilder { p_ref.neutral_0.to_owned(), accent.clone(), ), - text_button: Component::component( - Srgba::new(0.0, 0.0, 0.0, 0.0), - p_ref.neutral_10, - accent, - p_ref.neutral_9, - is_high_contrast, - p_ref.neutral_8, - ), + text_button, palette: palette.inner(), spacing, corner_radii, From 4d63d06a7e2bf38d579ff7d2b0d625d4647d7f0e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 18 Aug 2023 16:30:32 -0400 Subject: [PATCH 0067/1276] fix: make buttons text buttons again --- src/app/applet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index 7671cdc9..20bf4216 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -128,7 +128,7 @@ impl CosmicAppletHelper { &self, icon_name: &'a str, ) -> iced::widget::Button<'a, Message, Renderer> { - crate::widget::button(theme::Button::Secondary) + crate::widget::button(theme::Button::Text) .icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0) .padding(8) } From 2086a0ee0ece7502fc56f14cd31d0df6a897fba4 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 18 Aug 2023 13:36:22 -0700 Subject: [PATCH 0068/1276] Don't require "a18y" for applet feature Causing panic currently. --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index adbbbfbc..0f73d2df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,8 @@ winit_tokio = ["iced/winit", "iced_winit", "tokio"] winit_wgpu = ["winit", "wgpu"] # Enables XDG portal integrations xdg-portal = ["ashpd"] -applet = ["wayland", "tokio", "a11y", "cosmic-panel-config", "ron"] +# XXX Use "a11y"; which is causing a panic currently +applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] [dependencies] apply = "0.3.0" From 69da283aebe18762a4e80db64547c95fb54df455 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 21 Aug 2023 11:52:19 -0400 Subject: [PATCH 0069/1276] update iced --- Cargo.toml | 6 ++++-- examples/cosmic-sctk/Cargo.toml | 5 ++--- examples/cosmic/Cargo.toml | 4 ++-- iced | 2 +- src/keyboard_nav.rs | 6 +++++- src/widget/aspect_ratio.rs | 2 ++ src/widget/cosmic_container.rs | 2 ++ src/widget/popover.rs | 3 +++ src/widget/rectangle_tracker/mod.rs | 2 ++ src/widget/segmented_button/widget.rs | 7 ++++--- 10 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f73d2df..28c771d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" name = "cosmic" [features] -default = ["wayland", "tokio", "a11y"] +default = ["wayland", "tokio"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Builds support for animated images @@ -21,7 +21,7 @@ smol = ["iced/smol"] # Tokio async runtime tokio = ["dep:tokio", "ashpd/tokio", "iced/tokio"] # Wayland window support -wayland = ["ashpd?/wayland", "iced/wayland", "iced_sctk", "sctk"] +wayland = ["ashpd?/wayland", "iced_runtime/wayland", "iced/wayland", "iced_sctk", "sctk"] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit @@ -50,6 +50,7 @@ thiserror = "1.0.44" async-fs = { version = "1.6", optional = true } ashpd = { version = "0.5.0", default-features = false, optional = true } url = "2.4.0" +unicode-segmentation = "1.6" [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.2" @@ -120,5 +121,6 @@ exclude = [ "iced", ] + [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index cb9332bd..9af75610 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -6,6 +6,5 @@ edition = "2021" publish = false [dependencies] -libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio", "a11y"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c39e737", default-features = false, features = ["libcosmic", "once_cell"] } -# cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"] } +libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="6f3ff1aa", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 0eed3000..a71a7b62 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,9 +8,9 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.13.0" -libcosmic = { path = "../..", default-features = false, features = ["debug", "winit", "a11y"] } +libcosmic = { path = "../..", default-features = false, features = ["debug", "winit"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c39e737", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="6f3ff1aa", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/iced b/iced index 17a10240..2ead0da0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 17a10240bee93add36c33f451bd028579f89718e +Subproject commit 2ead0da06f6da58b01e107104808b45d6fb61e85 diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 801c5947..40294a07 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -8,7 +8,10 @@ use iced::{ keyboard::{self, KeyCode}, mouse, subscription, Command, Event, Subscription, }; -use iced_core::widget::{operation, Id, Operation}; +use iced_core::{ + widget::{operation, Id, Operation}, + Rectangle, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Message { @@ -88,6 +91,7 @@ fn unfocus_operation() -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self); diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index eae0f89d..076abdb6 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -190,6 +190,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + viewport: &Rectangle, ) -> event::Status { self.container.on_event( tree, @@ -199,6 +200,7 @@ where renderer, clipboard, shell, + viewport, ) } diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 8972a01f..7d170dfd 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -177,6 +177,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + viewport: &Rectangle, ) -> event::Status { self.container.on_event( tree, @@ -186,6 +187,7 @@ where renderer, clipboard, shell, + viewport, ) } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 2135f7f6..754c177e 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -87,6 +87,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + viewport: &Rectangle, ) -> event::Status { self.content.as_widget_mut().on_event( &mut tree.children[0], @@ -96,6 +97,7 @@ where renderer, clipboard, shell, + viewport, ) } @@ -221,6 +223,7 @@ where renderer, clipboard, shell, + &layout.bounds(), ) } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 305634cb..a927decb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -189,6 +189,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + viewport: &iced_core::Rectangle, ) -> event::Status { self.container.on_event( tree, @@ -198,6 +199,7 @@ where renderer, clipboard, shell, + viewport, ) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f91654d0..42389a4e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -224,7 +224,7 @@ where // Add text to measurement if text was given. if let Some(text) = self.model.text(key) { - let (w, h) = renderer.measure( + let Size { width, height } = renderer.measure( text, self.font_size, self.line_height, @@ -233,8 +233,8 @@ where Shaping::Advanced, ); - button_width = w; - button_height = h; + button_width = width; + button_height = height; } // Add icon to measurement if icon was given. @@ -307,6 +307,7 @@ where _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + _viewport: &iced::Rectangle, ) -> event::Status { let bounds = layout.bounds(); let state = tree.state.downcast_mut::(); From 12da20d1849b9c705881e8205954384570a7d58b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 21 Aug 2023 14:47:52 -0400 Subject: [PATCH 0070/1276] chore: update cosmic-time --- examples/cosmic-sctk/Cargo.toml | 2 +- examples/cosmic/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index 9af75610..c90d7f49 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="6f3ff1aa", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index a71a7b62..0ca25e1b 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -13,4 +13,4 @@ once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="6f3ff1aa", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] } From 78c1facd5b9c0e75fef7ac26adf7e2fb78553a88 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Mon, 21 Aug 2023 13:09:36 -0700 Subject: [PATCH 0071/1276] chore: allow &str as input for the icon theme --- src/app/settings.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index 0745fc95..d6f9cdbd 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -29,7 +29,7 @@ pub struct Settings { pub(crate) default_font: Font, /// Name of the icon theme to search by default. - #[setters(into)] + #[setters(skip)] pub(crate) default_icon_theme: Option, /// Default size of fonts. @@ -56,6 +56,15 @@ pub struct Settings { pub(crate) transparent: bool, } +impl Settings { + /// Sets the default icon theme, passing an empty string will unset the theme. + pub fn default_icon_theme(mut self, value: impl Into) -> Self { + let value: String = value.into(); + self.default_icon_theme = if value.is_empty() { None } else { Some(value) }; + self + } +} + impl Default for Settings { fn default() -> Self { Self { From fcdefcd8fbca53705ba761b97e25453bc35e40c0 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 22 Aug 2023 15:20:45 -0700 Subject: [PATCH 0072/1276] fix(app): set `size_limits` to `None` for `autosize` With the default size limit, autosize applets don't work as expected. Setting this to `None` seems to work fairly well, though maybe we'll need to tune some of these settings more later. --- src/app/applet/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index 20bf4216..92aca686 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -220,6 +220,9 @@ pub fn run(autosize: bool, flags: App::Flags) -> iced::Result let helper = CosmicAppletHelper::default(); let mut settings = helper.window_settings(); settings.autosize = autosize; + if autosize { + settings.size_limits = Limits::NONE; + } if let Some(icon_theme) = settings.default_icon_theme { crate::icon_theme::set_default(icon_theme); From db8e791b879f0b2ee49b15bee64d8648e8206212 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:59:26 -0400 Subject: [PATCH 0073/1276] Text input (#143) * update: iced 0.10.0 * wip: text input * wip: text inputs with icons and buttons * wip: improve text input * refactor: text input styling * chore: add scale factor * chore(text_input): add winit example and do some cleanup --- examples/cosmic-sctk/src/window.rs | 52 +- examples/cosmic/src/window/demo.rs | 4 + src/widget/mod.rs | 4 + src/widget/text_input/cursor.rs | 173 +++ src/widget/text_input/editor.rs | 65 + src/widget/text_input/input.rs | 2319 ++++++++++++++++++++++++++++ src/widget/text_input/mod.rs | 10 + src/widget/text_input/style.rs | 263 ++++ src/widget/text_input/value.rs | 131 ++ 9 files changed, 3017 insertions(+), 4 deletions(-) create mode 100644 src/widget/text_input/cursor.rs create mode 100644 src/widget/text_input/editor.rs create mode 100644 src/widget/text_input/input.rs create mode 100644 src/widget/text_input/mod.rs create mode 100644 src/widget/text_input/style.rs create mode 100644 src/widget/text_input/value.rs diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 76a857d0..54d27757 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -13,9 +13,10 @@ use cosmic::{ iced_widget::text, theme::{self, Theme}, widget::{ - button, cosmic_container, header_bar, nav_bar, nav_bar_toggle, + button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, - scrollable, segmented_button, segmented_selection, settings, IconSource, + scrollable, search_input, secure_input, segmented_button, segmented_selection, settings, + text_input, IconSource, }, Element, ElementExt, }; @@ -127,6 +128,8 @@ pub struct Window { rectangle_tracker: Option>, pub selection: segmented_button::SingleSelectModel, timeline: Timeline, + input_value: String, + secure_input_visible: bool, } impl Window { @@ -183,12 +186,13 @@ pub enum Message { Drag, Minimize, Maximize, - InputChanged, Rectangle(RectangleUpdate), NavBar(segmented_button::Entity), Ignore, Selection(segmented_button::Entity), Tick(Instant), + InputChanged(String), + ToggleVisible, } impl Window { @@ -305,7 +309,6 @@ impl Application for Window { 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 { RectangleUpdate::Rectangle(_) => {} RectangleUpdate::Init(t) => { @@ -315,6 +318,12 @@ impl Application for Window { Message::Ignore => {} Message::Selection(key) => self.selection.activate(key), Message::Tick(now) => self.timeline.now(now), + Message::InputChanged(v) => { + self.input_value = v; + } + Message::ToggleVisible => { + self.secure_input_visible = !self.secure_input_visible; + } } Command::none() @@ -476,6 +485,41 @@ impl Application for Window { .padding(16) .style(cosmic::theme::Container::Secondary), )) + .add(settings::item( + "Text Input", + text_input("test", &self.input_value) + .width(Length::Fill) + .on_input(Message::InputChanged), + )) + .add(settings::item( + "Text Input", + secure_input( + "test", + &self.input_value, + Some(Message::ToggleVisible), + !self.secure_input_visible, + ) + .label("Test Secure Input Label") + .helper_text("password") + .width(Length::Fill) + .on_input(Message::InputChanged), + )) + .add(settings::item( + "Text Input", + search_input( + "search for stuff", + &self.input_value, + Some(Message::InputChanged("".to_string())), + ) + .width(Length::Fill) + .on_input(Message::InputChanged), + )) + .add(settings::item( + "Text Input", + inline_input(&self.input_value) + .width(Length::Fill) + .on_input(Message::InputChanged), + )) .into(), ]) .into(); diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index c1856eb8..375b7044 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -504,6 +504,10 @@ impl State { .size(20) .id(INPUT_ID.clone()) .into(), + cosmic::widget::text_input("test", &self.entry_value) + .width(Length::Fill) + .on_input(Message::InputChanged) + .into(), ]) .into() } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 5e96cdf9..272824b8 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -70,6 +70,10 @@ pub use warning::*; pub mod cosmic_container; pub use cosmic_container::*; +// #[cfg(feature = "wayland")] +pub mod text_input; +// #[cfg(feature = "wayland")] +pub use text_input::*; /// An element to distinguish a boundary between two elements. pub mod divider { diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs new file mode 100644 index 00000000..ba59ca04 --- /dev/null +++ b/src/widget/text_input/cursor.rs @@ -0,0 +1,173 @@ +//! Track the cursor of a text input. +use super::value::Value; + +/// The cursor of a text input. +#[derive(Debug, Copy, Clone)] +pub struct Cursor { + state: State, +} + +/// The state of a [`Cursor`]. +#[derive(Debug, Copy, Clone)] +pub enum State { + /// Cursor without a selection + Index(usize), + + /// Cursor selecting a range of text + Selection { + /// The start of the selection + start: usize, + /// The end of the selection + end: usize, + }, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + state: State::Index(0), + } + } +} + +impl Cursor { + /// Returns the [`State`] of the [`Cursor`]. + #[must_use] + pub fn state(&self, value: &Value) -> State { + match self.state { + State::Index(index) => State::Index(index.min(value.len())), + State::Selection { start, end } => { + let start = start.min(value.len()); + let end = end.min(value.len()); + + if start == end { + State::Index(start) + } else { + State::Selection { start, end } + } + } + } + } + + /// Returns the current selection of the [`Cursor`] for the given [`Value`]. + /// + /// `start` is guaranteed to be <= than `end`. + #[must_use] + pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { + match self.state(value) { + State::Selection { start, end } => Some((start.min(end), start.max(end))), + State::Index(_) => None, + } + } + + pub(crate) fn move_to(&mut self, position: usize) { + self.state = State::Index(position); + } + + pub(crate) fn move_right(&mut self, value: &Value) { + self.move_right_by_amount(value, 1); + } + + pub(crate) fn move_right_by_words(&mut self, value: &Value) { + self.move_to(value.next_end_of_word(self.right(value))); + } + + pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) { + match self.state(value) { + State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())), + State::Selection { start, end } => self.move_to(end.max(start)), + } + } + + pub(crate) fn move_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => self.move_to(index - 1), + State::Selection { start, end } => self.move_to(start.min(end)), + State::Index(_) => self.move_to(0), + } + } + + pub(crate) fn move_left_by_words(&mut self, value: &Value) { + self.move_to(value.previous_start_of_word(self.left(value))); + } + + pub(crate) fn select_range(&mut self, start: usize, end: usize) { + if start == end { + self.state = State::Index(start); + } else { + self.state = State::Selection { start, end }; + } + } + + pub(crate) fn select_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => self.select_range(index, index - 1), + State::Selection { start, end } if end > 0 => self.select_range(start, end - 1), + _ => {} + } + } + + pub(crate) fn select_right(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index < value.len() => self.select_range(index, index + 1), + State::Selection { start, end } if end < value.len() => { + self.select_range(start, end + 1); + } + _ => {} + } + } + + pub(crate) fn select_left_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => self.select_range(index, value.previous_start_of_word(index)), + State::Selection { start, end } => { + self.select_range(start, value.previous_start_of_word(end)); + } + } + } + + pub(crate) fn select_right_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => self.select_range(index, value.next_end_of_word(index)), + State::Selection { start, end } => { + self.select_range(start, value.next_end_of_word(end)); + } + } + } + + pub(crate) fn select_all(&mut self, value: &Value) { + self.select_range(0, value.len()); + } + + pub(crate) fn start(&self, value: &Value) -> usize { + let start = match self.state { + State::Index(index) => index, + State::Selection { start, .. } => start, + }; + + start.min(value.len()) + } + + pub(crate) fn end(&self, value: &Value) -> usize { + let end = match self.state { + State::Index(index) => index, + State::Selection { end, .. } => end, + }; + + end.min(value.len()) + } + + fn left(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.min(end), + } + } + + fn right(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.max(end), + } + } +} diff --git a/src/widget/text_input/editor.rs b/src/widget/text_input/editor.rs new file mode 100644 index 00000000..07648b71 --- /dev/null +++ b/src/widget/text_input/editor.rs @@ -0,0 +1,65 @@ +use super::{cursor::Cursor, value::Value}; + +pub struct Editor<'a> { + value: &'a mut Value, + cursor: &'a mut Cursor, +} + +impl<'a> Editor<'a> { + pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { + Editor { value, cursor } + } + + #[must_use] + pub fn contents(&self) -> String { + self.value.to_string() + } + + pub fn insert(&mut self, character: char) { + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + + self.value.insert(self.cursor.end(self.value), character); + self.cursor.move_right(self.value); + } + + pub fn paste(&mut self, content: Value) { + let length = content.len(); + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + + self.value.insert_many(self.cursor.end(self.value), content); + + self.cursor.move_right_by_amount(self.value, length); + } + + pub fn backspace(&mut self) { + if let Some((start, end)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(start, end); + } else { + let start = self.cursor.start(self.value); + + if start > 0 { + self.cursor.move_left(self.value); + self.value.remove(start - 1); + } + } + } + + pub fn delete(&mut self) { + if self.cursor.selection(self.value).is_some() { + self.backspace(); + } else { + let end = self.cursor.end(self.value); + + if end < self.value.len() { + self.value.remove(end); + } + } + } +} diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs new file mode 100644 index 00000000..abb4290d --- /dev/null +++ b/src/widget/text_input/input.rs @@ -0,0 +1,2319 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +use crate::theme::THEME; + +use super::cursor; +pub use super::cursor::Cursor; +use super::editor::Editor; +use super::style::StyleSheet; +pub use super::value::Value; + +use iced::Limits; +use iced_core::event::{self, Event}; +use iced_core::keyboard; +use iced_core::layout; +use iced_core::mouse::{self, click}; +use iced_core::renderer::{self, Renderer as CoreRenderer}; +use iced_core::text::{self, Renderer, Text}; +use iced_core::time::{Duration, Instant}; +use iced_core::touch; +use iced_core::widget::operation::{self, Operation}; +use iced_core::widget::tree::{self, Tree}; +use iced_core::widget::Id; +use iced_core::window; +use iced_core::{alignment, Background}; +use iced_core::{ + Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Vector, Widget, +}; +#[cfg(feature = "wayland")] +use iced_renderer::core::event::{wayland, PlatformSpecific}; +use iced_renderer::core::widget::OperationOutputWrapper; +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific; +use iced_runtime::Command; + +#[cfg(feature = "wayland")] +use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; +#[cfg(feature = "wayland")] +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +/// Creates a new [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn text_input<'a, Message>(placeholder: &str, value: &str) -> TextInput<'a, Message> +where + Message: Clone, +{ + TextInput::new(placeholder, value) +} + +/// Creates a new search [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn search_input<'a, Message>( + placeholder: &str, + value: &str, + on_clear: Option, +) -> TextInput<'a, Message> +where + Message: Clone + 'static, +{ + let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + let input = TextInput::new(placeholder, value) + .padding([0, spacing, 0, spacing]) + .style(super::style::TextInput::Search) + .start_icon( + iced_widget::container( + crate::widget::icon("system-search-symbolic", 16) + .style(crate::theme::Svg::Symbolic), + ) + .padding([spacing, spacing, spacing, spacing]) + .into(), + ); + + if let Some(msg) = on_clear { + input.end_icon( + crate::widget::button::button(crate::theme::Button::Text) + .icon(crate::theme::Svg::Symbolic, "edit-clear-symbolic", 16) + .on_press(msg) + .padding([spacing, spacing, spacing, spacing]) + .into(), + ) + } else { + input + } +} +/// Creates a new search [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn secure_input<'a, Message>( + placeholder: &str, + value: &str, + on_visible_toggle: Option, + hidden: bool, +) -> TextInput<'a, Message> +where + Message: Clone + 'static, +{ + let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + let mut input = TextInput::new(placeholder, value) + .padding([0, spacing, 0, spacing]) + .style(super::style::TextInput::Default) + .start_icon( + iced_widget::container( + crate::widget::icon("system-lock-screen-symbolic", 16) + .style(crate::theme::Svg::Symbolic), + ) + .padding([spacing, spacing, spacing, spacing]) + .into(), + ); + if hidden { + input = input.password(); + } + if let Some(msg) = on_visible_toggle { + input.end_icon( + crate::widget::button::button(crate::theme::Button::Text) + .icon( + crate::theme::Svg::Symbolic, + "document-properties-symbolic", + 16, + ) + .on_press(msg) + .padding([spacing, spacing, spacing, spacing]) + .into(), + ) + } else { + input + } +} + +/// Creates a new inline [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn inline_input<'a, Message>(value: &str) -> TextInput<'a, Message> +where + Message: Clone, +{ + let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + + TextInput::new("", value) + .style(super::style::TextInput::Inline) + .padding([spacing, spacing, spacing, spacing]) +} + +#[cfg(feature = "wayland")] +const SUPPORTED_MIME_TYPES: &[&str; 6] = &[ + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + "UTF8_STRING", + "STRING", + "text/plain", + "TEXT", +]; +#[cfg(feature = "wayland")] +pub type DnDCommand = + Box platform_specific::wayland::data_device::ActionInner>; +#[cfg(not(feature = "wayland"))] +pub type DnDCommand = (); + +/// A field that can be filled with text. +/// +/// # Example +/// ```no_run +/// # pub type TextInput<'a, Message> = +/// # iced_widget::TextInput<'a, Message, iced_widget::renderer::Renderer>; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// TextInputChanged(String), +/// } +/// +/// let value = "Some text"; +/// +/// let input = TextInput::new( +/// "This is the placeholder...", +/// value, +/// ) +/// .on_input(Message::TextInputChanged) +/// .padding(10); +/// ``` +/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true) +#[allow(missing_debug_implementations)] +#[must_use] +pub struct TextInput<'a, Message> { + id: Option, + placeholder: String, + value: Value, + is_secure: bool, + font: Option<::Font>, + width: Length, + padding: Padding, + size: Option, + helper_size: f32, + label: Option<&'a str>, + helper_text: Option<&'a str>, + error: Option<&'a str>, + on_input: Option Message + 'a>>, + on_paste: Option Message + 'a>>, + on_submit: Option, + start_icon: Option>, + end_element: Option>, + style: <::Theme as StyleSheet>::Style, + // (text_input::State, mime_type, dnd_action) -> Message + on_create_dnd_source: Option Message + 'a>>, + on_dnd_command_produced: Option Message + 'a>>, + surface_ids: Option<(window::Id, window::Id)>, + dnd_icon: bool, + line_height: text::LineHeight, + helper_line_height: text::LineHeight, +} + +impl<'a, Message> TextInput<'a, Message> +where + Message: Clone, +{ + /// Creates a new [`TextInput`]. + /// + /// It expects: + /// - a placeholder, + /// - the current value + pub fn new(placeholder: &str, value: &str) -> Self { + let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + + TextInput { + id: None, + placeholder: String::from(placeholder), + value: Value::new(value), + is_secure: false, + font: None, + width: Length::Fill, + padding: [spacing, spacing, spacing, spacing].into(), + size: None, + helper_size: 10.0, + helper_line_height: text::LineHeight::from(14.0), + on_input: None, + on_paste: None, + on_submit: None, + start_icon: None, + end_element: None, + error: None, + style: super::style::TextInput::default(), + on_dnd_command_produced: None, + on_create_dnd_source: None, + surface_ids: None, + dnd_icon: false, + line_height: text::LineHeight::default(), + label: None, + helper_text: None, + } + } + + /// Sets the text of the [`TextInput`]. + pub fn label(mut self, label: &'a str) -> Self { + self.label = Some(label); + self + } + + /// Sets the helper text of the [`TextInput`]. + pub fn helper_text(mut self, helper_text: &'a str) -> Self { + self.helper_text = Some(helper_text); + self + } + + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the error message of the [`TextInput`]. + pub fn error(mut self, error: &'a str) -> Self { + self.error = Some(error); + self + } + + /// Sets the [`LineHeight`] of the [`TextInput`]. + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.line_height = line_height.into(); + self + } + + /// Converts the [`TextInput`] into a secure password input. + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`]. + /// + /// If this method is not called, the [`TextInput`] will be disabled. + pub fn on_input(mut self, callback: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + self.on_input = Some(Box::new(callback)); + self + } + + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed. + pub fn on_submit(mut self, message: Message) -> Self { + self.on_submit = Some(message); + self + } + + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`]. + pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self { + self.on_paste = Some(Box::new(on_paste)); + self + } + + /// Sets the [`Font`] of the [`TextInput`]. + /// + /// [`Font`]: text::Renderer::Font + pub fn font(mut self, font: ::Font) -> Self { + self.font = Some(font); + self + } + + /// Sets the start [`Icon`] of the [`TextInput`]. + pub fn start_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self { + self.start_icon = Some(icon); + self + } + + /// Sets the end [`Icon`] of the [`TextInput`]. + pub fn end_icon(mut self, icon: Element<'a, Message, crate::Renderer>) -> Self { + self.end_element = Some(icon); + self + } + + /// Sets the width of the [`TextInput`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`TextInput`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = Some(size.into().0); + self + } + + /// Sets the style of the [`TextInput`]. + pub fn style( + mut self, + style: impl Into<<::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + /// + /// [`Renderer`]: text::Renderer + #[allow(clippy::too_many_arguments)] + pub fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &::Theme, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + value: Option<&Value>, + style: &renderer::Style, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree, + value.unwrap_or(&self.value), + &self.placeholder, + self.size, + self.font, + self.on_input.is_none(), + self.is_secure, + self.start_icon.as_ref(), + self.end_element.as_ref(), + &self.style, + self.dnd_icon, + self.line_height, + self.error, + self.label, + self.helper_text, + self.helper_size, + self.helper_line_height, + &layout.bounds(), + style, + ); + } + + /// Sets the start dnd handler of the [`TextInput`]. + #[cfg(feature = "wayland")] + pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self { + self.on_create_dnd_source = Some(Box::new(on_start_dnd)); + self + } + + /// Sets the dnd command produced handler of the [`TextInput`]. + /// Commands should be returned in the update function of the application. + #[cfg(feature = "wayland")] + pub fn on_dnd_command_produced( + mut self, + on_dnd_command_produced: impl Fn( + Box platform_specific::wayland::data_device::ActionInner>, + ) -> Message + + 'a, + ) -> Self { + self.on_dnd_command_produced = Some(Box::new(on_dnd_command_produced)); + self + } + + /// Sets the window id of the [`TextInput`] and the window id of the drag icon. + /// Both ids are required to be unique. + /// This is required for the dnd to work. + pub fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self { + self.surface_ids = Some(window_id); + self + } + + /// Sets the mode of this [`TextInput`] to be a drag and drop icon. + pub fn dnd_icon(mut self, dnd_icon: bool) -> Self { + self.dnd_icon = dnd_icon; + self + } + + /// Get the layout node of the actual text input + fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> { + if self.dnd_icon { + layout + } else if self.label.is_some() { + let mut nodes = layout.children(); + nodes.next(); + nodes.next().unwrap() + } else { + layout.children().next().unwrap() + } + } +} + +impl<'a, Message> Widget for TextInput<'a, Message> +where + Message: Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn diff(&mut self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + + // Unfocus text input if it becomes disabled + if self.on_input.is_none() { + state.last_click = None; + state.is_focused = None; + state.is_pasting = None; + state.dragging_state = None; + } + let mut children: Vec<_> = self + .start_icon + .iter_mut() + .chain(self.end_element.iter_mut()) + .map(iced_core::Element::as_widget_mut) + .collect(); + tree.diff_children(children.as_mut_slice()); + } + + fn children(&self) -> Vec { + self.start_icon + .iter() + .chain(self.end_element.iter()) + .map(|icon| Tree::new(icon)) + .collect() + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + if self.dnd_icon { + let limits = limits.width(Length::Shrink).height(Length::Shrink); + + let size = self.size.unwrap_or_else(|| renderer.default_size()); + + let bounds = limits.max(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); + + let Size { width, height } = renderer.measure( + &self.value.to_string(), + size, + self.line_height, + font, + bounds, + text::Shaping::Advanced, + ); + + let size = limits.resolve(Size::new(width, height)); + layout::Node::with_children(size, vec![layout::Node::new(size)]) + } else { + layout( + renderer, + limits, + self.width, + self.padding, + self.size, + self.start_icon.as_ref(), + self.end_element.as_ref(), + self.line_height, + self.label, + self.helper_text, + self.helper_size, + self.helper_line_height, + ) + } + } + + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + _renderer: &crate::Renderer, + operation: &mut dyn Operation>, + ) { + let state = tree.state.downcast_mut::(); + + operation.focusable(state, self.id.as_ref()); + operation.text_input(state, self.id.as_ref()); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let text_layout = self.text_layout(layout); + let mut child_state = tree.children.iter_mut(); + if let (Some(start_icon), Some(tree)) = (self.start_icon.as_mut(), child_state.next()) { + let mut children = text_layout.children(); + children.next(); + let start_icon_layout = children.next().unwrap(); + + if cursor_position.is_over(start_icon_layout.bounds()) { + return start_icon.as_widget_mut().on_event( + tree, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); + } + } + if let (Some(end_icon), Some(tree)) = (self.end_element.as_mut(), child_state.next()) { + let mut children = text_layout.children(); + children.next(); + children.next(); + let end_icon_layout = children.next().unwrap(); + + if cursor_position.is_over(end_icon_layout.bounds()) { + return end_icon.as_widget_mut().on_event( + tree, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + update( + event, + text_layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + self.font, + self.is_secure, + self.on_input.as_deref(), + self.on_paste.as_deref(), + &self.on_submit, + || tree.state.downcast_mut::(), + self.on_create_dnd_source.as_deref(), + self.dnd_icon, + self.on_dnd_command_produced.as_deref(), + self.surface_ids, + self.line_height, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree, + &self.value, + &self.placeholder, + self.size, + self.font, + self.on_input.is_none(), + self.is_secure, + self.start_icon.as_ref(), + self.end_element.as_ref(), + &self.style, + self.dnd_icon, + self.line_height, + self.error, + self.label, + self.helper_text, + self.helper_size, + self.helper_line_height, + viewport, + style, + ); + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + let layout = self.text_layout(layout); + let mut index = 0; + if let (Some(start_icon), Some(tree)) = + (self.start_icon.as_ref(), state.children.get(index)) + { + let mut children = layout.children(); + children.next(); + let start_icon_layout = children.next().unwrap(); + + if cursor_position.is_over(start_icon_layout.bounds()) { + return start_icon.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); + } + index += 1; + } + + if let (Some(end_icon), Some(tree)) = (self.end_element.as_ref(), state.children.get(index)) + { + let mut children = layout.children(); + children.next(); + children.next(); + let end_icon_layout = children.next().unwrap(); + + if cursor_position.is_over(end_icon_layout.bounds()) { + return end_icon.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); + } + } + + mouse_interaction(layout, cursor_position, self.on_input.is_none()) + } +} + +impl<'a, Message> From> for Element<'a, Message, crate::Renderer> +where + Message: 'a + Clone, +{ + fn from(text_input: TextInput<'a, Message>) -> Element<'a, Message, crate::Renderer> { + Element::new(text_input) + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// end. +pub fn move_cursor_to_end(id: Id) -> Command { + Command::widget(operation::text_input::move_cursor_to_end(id)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// front. +pub fn move_cursor_to_front(id: Id) -> Command { + Command::widget(operation::text_input::move_cursor_to_front(id)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// provided position. +pub fn move_cursor_to(id: Id, position: usize) -> Command { + Command::widget(operation::text_input::move_cursor_to(id, position)) +} + +/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. +pub fn select_all(id: Id) -> Command { + Command::widget(operation::text_input::select_all(id)) +} + +/// Computes the layout of a [`TextInput`]. +#[allow(clippy::cast_precision_loss)] +#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_lines)] +pub fn layout( + renderer: &crate::Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option, + start_icon: Option<&Element<'_, Message, crate::Renderer>>, + end_icon: Option<&Element<'_, Message, crate::Renderer>>, + line_height: text::LineHeight, + label: Option<&str>, + helper_text: Option<&str>, + helper_text_size: f32, + helper_text_line_height: text::LineHeight, +) -> layout::Node { + let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + let mut nodes = Vec::with_capacity(3); + + let text_pos = if let Some(label) = label { + let limits = limits.width(width); + let text_bounds = limits.resolve(Size::ZERO); + + let label_size = renderer.measure( + label, + size.unwrap_or_else(|| renderer.default_size()), + line_height, + renderer.default_font(), + text_bounds, + text::Shaping::Advanced, + ); + + nodes.push(layout::Node::new(label_size)); + Vector::new(0.0, label_size.height + f32::from(spacing)) + } else { + Vector::ZERO + }; + + let text_size = size.unwrap_or_else(|| renderer.default_size()); + let padding = padding.fit(Size::ZERO, limits.max()); + + let helper_pos = if start_icon.is_some() || end_icon.is_some() { + // TODO configurable icon spacing, maybe via appearance + let mut height = text_size * 1.2; + let icon_spacing = 8.0; + let (start_icon_width, mut start_icon) = if let Some(icon) = start_icon.as_ref() { + let icon_node = icon.layout( + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + height = height.max(icon_node.bounds().height); + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; + + let (end_icon_width, mut end_icon) = if let Some(icon) = end_icon.as_ref() { + let icon_node = icon.layout( + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + height = height.max(icon_node.bounds().height); + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; + let text_limits = limits.width(width).pad(padding).height(text_size * 1.2); + + let text_bounds = text_limits.resolve(Size::ZERO); + + let mut text_node = + layout::Node::new(text_bounds - Size::new(start_icon_width + end_icon_width, 0.0)); + + text_node.move_to(Point::new( + padding.left + start_icon_width, + padding.top + ((height - text_size * 1.2) / 2.0).max(0.0), + )); + let mut node_list: Vec<_> = Vec::with_capacity(3); + + let text_node_bounds = text_node.bounds(); + node_list.push(text_node); + + if let Some(mut start_icon) = start_icon.take() { + start_icon.move_to(Point::new( + padding.left, + padding.top + ((text_size * 1.2 - start_icon.bounds().height) / 2.0).max(0.0), + )); + node_list.push(start_icon); + } + if let Some(mut end_icon) = end_icon.take() { + end_icon.move_to(Point::new( + text_node_bounds.x + text_node_bounds.width, + padding.top + ((text_size * 1.2 - end_icon.bounds().height) / 2.0).max(0.0), + )); + node_list.push(end_icon); + } + + let input_limits = limits.width(width).pad(padding).height(height); + let input_bounds = input_limits.resolve(Size::ZERO); + let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos); + let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing); + nodes.push(input_node); + + Vector::new(0.0, y_pos) + } else { + let limits = limits.width(width).pad(padding).height(text_size * 1.2); + let text_bounds = limits.resolve(Size::ZERO); + + let mut text = layout::Node::new(text_bounds); + text.move_to(Point::new(padding.left, padding.top)); + + let node = + layout::Node::with_children(text_bounds.pad(padding), vec![text]).translate(text_pos); + let y_pos = node.bounds().y + node.bounds().height + f32::from(spacing); + nodes.push(node); + + Vector::new(0.0, y_pos) + }; + + if let Some(helper_text) = helper_text { + let limits = limits + .width(width) + .pad(padding) + .height(helper_text_size * 1.2); + let text_bounds = limits.resolve(Size::ZERO); + + let helper_text_size = renderer.measure( + helper_text, + helper_text_size, + helper_text_line_height, + renderer.default_font(), + text_bounds, + text::Shaping::Advanced, + ); + + nodes.push(layout::Node::new(helper_text_size).translate(helper_pos)); + }; + + let mut size = nodes.iter().fold(Size::ZERO, |size, node| { + Size::new( + size.width.max(node.bounds().width), + size.height + node.bounds().height, + ) + }); + size.height += (nodes.len() - 1) as f32 * f32::from(spacing); + let limits = limits.width(width).pad(padding).height(size.height); + + layout::Node::with_children(limits.resolve(size), nodes) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_lines)] +#[allow(clippy::missing_panics_doc)] +#[allow(clippy::cast_lossless)] +#[allow(clippy::cast_possible_truncation)] +pub fn update<'a, Message, Renderer>( + event: Event, + text_layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option, + font: Option, + is_secure: bool, + on_input: Option<&dyn Fn(String) -> Message>, + on_paste: Option<&dyn Fn(String) -> Message>, + on_submit: &Option, + state: impl FnOnce() -> &'a mut State, + on_start_dnd_source: Option<&dyn Fn(State) -> Message>, + _dnd_icon: bool, + on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, + surface_ids: Option<(window::Id, window::Id)>, + line_height: text::LineHeight, +) -> event::Status +where + Message: Clone, + Renderer: text::Renderer, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + let is_clicked = cursor_position.is_over(text_layout.bounds()) && on_input.is_some(); + + state.is_focused = if is_clicked { + state.is_focused.or_else(|| { + let now = Instant::now(); + Some(Focus { + updated_at: now, + now, + }) + }) + } else { + None + }; + + let font: ::Font = + font.unwrap_or_else(|| renderer.default_font()); + if is_clicked { + let Some(pos) = cursor_position.position() else { + return event::Status::Ignored; + }; + let target = pos.x - text_layout.bounds().x; + + let click = mouse::Click::new(pos, state.last_click); + + match ( + &state.dragging_state, + click.kind(), + state.cursor().state(value), + ) { + #[cfg(feature = "wayland")] + (None, click::Kind::Single, cursor::State::Selection { start, end }) => { + // if something is already selected, we can start a drag and drop for a + // single click that is on top of the selected text + // is the click on selected text? + if is_secure { + return event::Status::Ignored; + } + + if let ( + Some(on_start_dnd), + Some(on_dnd_command_produced), + Some((window_id, icon_id)), + Some(on_input), + ) = ( + on_start_dnd_source, + on_dnd_command_produced, + surface_ids, + on_input, + ) { + let actual_size = size.unwrap_or_else(|| renderer.default_size()); + + let left = start.min(end); + let right = end.max(start); + + let (left_position, _left_offset) = measure_cursor_and_scroll_offset( + renderer, + text_layout.bounds(), + value, + actual_size, + left, + font, + ); + + let (right_position, _right_offset) = measure_cursor_and_scroll_offset( + renderer, + text_layout.bounds(), + value, + actual_size, + right, + font, + ); + + let width = right_position - left_position; + let selection_bounds = Rectangle { + x: text_layout.bounds().x + left_position, + y: text_layout.bounds().y, + width, + height: text_layout.bounds().height, + }; + + if cursor_position.is_over(selection_bounds) { + let text = + state.selected_text(&value.to_string()).unwrap_or_default(); + state.dragging_state = + Some(DraggingState::Dnd(DndAction::empty(), text.clone())); + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + shell.publish(on_start_dnd(state.clone())); + let state = state.clone(); + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::StartDnd { + mime_types: SUPPORTED_MIME_TYPES + .iter() + .map(std::string::ToString::to_string) + .collect(), + actions: DndAction::Move, + origin_id: window_id, + icon_id: Some(DndIcon::Widget( + icon_id, + Box::new(state.clone()), + )), + data: Box::new(TextInputString(text.clone())), + } + }))); + } else { + // existing logic for setting the selection + let position = if target > 0.0 { + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + &value, + state, + target, + line_height, + ) + } else { + None + }; + + state.cursor.move_to(position.unwrap_or(0)); + state.dragging_state = Some(DraggingState::Selection); + } + } else { + state.dragging_state = None; + } + } + (None, click::Kind::Single, _) => { + // existing logic for setting the selection + let position = if target > 0.0 { + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + &value, + state, + target, + line_height, + ) + } else { + None + }; + + state.cursor.move_to(position.unwrap_or(0)); + state.dragging_state = Some(DraggingState::Selection); + } + (None | Some(DraggingState::Selection), click::Kind::Double, _) => { + if is_secure { + state.cursor.select_all(value); + } else { + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + value, + state, + target, + line_height, + ) + .unwrap_or(0); + + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); + } + state.dragging_state = Some(DraggingState::Selection); + } + (None | Some(DraggingState::Selection), click::Kind::Triple, _) => { + state.cursor.select_all(value); + state.dragging_state = Some(DraggingState::Selection); + } + _ => { + state.dragging_state = None; + } + } + + state.last_click = Some(click); + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { + let state = state(); + state.dragging_state = None; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state(); + + if matches!(state.dragging_state, Some(DraggingState::Selection)) { + let target = position.x - text_layout.bounds().x; + + let value: Value = if is_secure { + value.secure() + } else { + value.clone() + }; + let font: ::Font = + font.unwrap_or_else(|| renderer.default_font()); + + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + &value, + state, + target, + line_height, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + + if state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() + { + let mut editor = Editor::new(value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + return event::Status::Captured; + } + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + + let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); + + match key_code { + keyboard::KeyCode::Enter | keyboard::KeyCode::NumpadEnter => { + if let Some(on_submit) = on_submit.clone() { + shell.publish(on_submit); + } + } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(value).is_none() + { + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(0, cursor_pos); + } else { + state.cursor.select_left_by_words(value); + } + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.backspace(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(value).is_none() + { + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(cursor_pos, value.len()); + } else { + state.cursor.select_right_by_words(value); + } + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_left_by_words(value); + } else { + state.cursor.move_left_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_left(value); + } else { + state.cursor.move_left(value); + } + } + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) && !is_secure { + if modifiers.shift() { + state.cursor.select_right_by_words(value); + } else { + state.cursor.move_right_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_right(value); + } else { + state.cursor.move_right(value); + } + } + keyboard::KeyCode::Home => { + if modifiers.shift() { + state.cursor.select_range(state.cursor.start(value), 0); + } else { + state.cursor.move_to(0); + } + } + keyboard::KeyCode::End => { + if modifiers.shift() { + state + .cursor + .select_range(state.cursor.start(value), value.len()); + } else { + state.cursor.move_to(value.len()); + } + } + keyboard::KeyCode::C if state.keyboard_modifiers.command() => { + if let Some((start, end)) = state.cursor.selection(value) { + clipboard.write(value.select(start, end).to_string()); + } + } + keyboard::KeyCode::X if state.keyboard_modifiers.command() => { + if let Some((start, end)) = state.cursor.selection(value) { + clipboard.write(value.select(start, end).to_string()); + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::V => { + if state.keyboard_modifiers.command() { + let content = if let Some(content) = state.is_pasting.take() { + content + } else { + let content: String = clipboard + .read() + .unwrap_or_default() + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + }; + + let mut editor = Editor::new(value, &mut state.cursor); + + editor.paste(content.clone()); + + let message = if let Some(paste) = &on_paste { + (paste)(editor.contents()) + } else { + (on_input)(editor.contents()) + }; + shell.publish(message); + + state.is_pasting = Some(content); + } else { + state.is_pasting = None; + } + } + keyboard::KeyCode::A if state.keyboard_modifiers.command() => { + state.cursor.select_all(value); + } + keyboard::KeyCode::Escape => { + state.is_focused = None; + state.dragging_state = None; + state.is_pasting = None; + + state.keyboard_modifiers = keyboard::Modifiers::default(); + } + keyboard::KeyCode::Tab | keyboard::KeyCode::Up | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + let state = state(); + + if state.is_focused.is_some() { + match key_code { + keyboard::KeyCode::V => { + state.is_pasting = None; + } + keyboard::KeyCode::Tab | keyboard::KeyCode::Up | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + } + Event::Window(_, window::Event::RedrawRequested(now)) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + focus.now = now; + + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis(u64::try_from(millis_until_redraw).unwrap()), + )); + } + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndFinished | wayland::DataSourceEvent::Cancelled, + ))) => { + let state = state(); + if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { + state.dragging_state = None; + return event::Status::Captured; + } + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::Cancelled + | wayland::DataSourceEvent::DndFinished + | wayland::DataSourceEvent::Cancelled, + ))) => { + let state = state(); + if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { + state.dragging_state = None; + return event::Status::Captured; + } + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndActionAccepted(action), + ))) => { + let state = state(); + if let Some(DraggingState::Dnd(_, text)) = state.dragging_state.as_ref() { + state.dragging_state = Some(DraggingState::Dnd(action, text.clone())); + return event::Status::Captured; + } + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Enter { x, y, mime_types }, + ))) => { + let Some(on_dnd_command_produced) = on_dnd_command_produced else { + return event::Status::Ignored + }; + + let state = state(); + let is_clicked = text_layout.bounds().contains(Point { + x: x as f32, + y: y as f32, + }); + + if !is_clicked { + state.dnd_offer = DndOfferState::OutsideWidget(mime_types, DndAction::None); + return event::Status::Captured; + } + let mut accepted = false; + for m in &mime_types { + if SUPPORTED_MIME_TYPES.contains(&m.as_str()) { + let clone = m.clone(); + accepted = true; + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::Accept(Some( + clone.clone(), + )) + }))); + } + } + if accepted { + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move.union(DndAction::Copy), + } + }))); + let target = x as f32 - text_layout.bounds().x; + state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None); + // existing logic for setting the selection + let position = if target > 0.0 { + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + let font = font.unwrap_or_else(|| renderer.default_font()); + + find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + &value, + state, + target, + line_height, + ) + } else { + None + }; + + state.cursor.move_to(position.unwrap_or(0)); + return event::Status::Captured; + } + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Motion { x, y }, + ))) => { + let Some(on_dnd_command_produced) = on_dnd_command_produced else { + return event::Status::Ignored; + }; + + let state = state(); + let is_clicked = text_layout.bounds().contains(Point { + x: x as f32, + y: y as f32, + }); + + if !is_clicked { + if let DndOfferState::HandlingOffer(mime_types, action) = state.dnd_offer.clone() { + state.dnd_offer = DndOfferState::OutsideWidget(mime_types, action); + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::None, + accepted: DndAction::None, + } + }))); + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::Accept(None) + }))); + } + return event::Status::Captured; + } else if let DndOfferState::OutsideWidget(mime_types, action) = state.dnd_offer.clone() + { + let mut accepted = false; + for m in &mime_types { + if SUPPORTED_MIME_TYPES.contains(&m.as_str()) { + accepted = true; + let clone = m.clone(); + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::Accept(Some( + clone.clone(), + )) + }))); + } + } + if accepted { + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: DndAction::Move, + accepted: DndAction::Move.union(DndAction::Copy), + } + }))); + state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), action); + } + }; + let target = x as f32 - text_layout.bounds().x; + // existing logic for setting the selection + let position = if target > 0.0 { + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + let font = font.unwrap_or_else(|| renderer.default_font()); + + find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + &value, + state, + target, + line_height, + ) + } else { + None + }; + + state.cursor.move_to(position.unwrap_or(0)); + return event::Status::Captured; + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DropPerformed, + ))) => { + let Some(on_dnd_command_produced) = on_dnd_command_produced else { + return event::Status::Ignored; + }; + + let state = state(); + if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { + let Some(mime_type) = SUPPORTED_MIME_TYPES + .iter() + .find(|m| mime_types.contains(&(**m).to_string())) + else { + state.dnd_offer = DndOfferState::None; + return event::Status::Captured; + }; + state.dnd_offer = DndOfferState::Dropped; + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::RequestDndData( + (*mime_type).to_string(), + ) + }))); + } else if let DndOfferState::OutsideWidget(..) = &state.dnd_offer { + state.dnd_offer = DndOfferState::None; + return event::Status::Captured; + } + return event::Status::Ignored; + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Leave, + ))) => { + let state = state(); + // ASHLEY TODO we should be able to reset but for now we don't if we are handling a + // drop + match state.dnd_offer { + DndOfferState::Dropped => {} + _ => { + state.dnd_offer = DndOfferState::None; + } + }; + return event::Status::Captured; + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DndData { mime_type, data }, + ))) => { + let Some(on_dnd_command_produced) = on_dnd_command_produced else { + return event::Status::Ignored; + }; + + let state = state(); + if let DndOfferState::Dropped = state.dnd_offer.clone() { + state.dnd_offer = DndOfferState::None; + if !SUPPORTED_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() { + return event::Status::Captured; + } + let Ok(content) = String::from_utf8(data) else { + return event::Status::Captured; + }; + + let mut editor = Editor::new(value, &mut state.cursor); + + editor.paste(Value::new(content.as_str())); + if let Some(on_paste) = on_paste.as_ref() { + let message = (on_paste)(editor.contents()); + shell.publish(message); + } + if let Some(on_paste) = on_paste { + let message = (on_paste)(editor.contents()); + shell.publish(message); + } + + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::DndFinished + }))); + return event::Status::Captured; + } + return event::Status::Ignored; + } + #[cfg(feature = "wayland")] + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::SourceActions(actions), + ))) => { + let Some(on_dnd_command_produced) = on_dnd_command_produced else { + return event::Status::Ignored; + }; + + let state = state(); + if let DndOfferState::HandlingOffer(..) = state.dnd_offer.clone() { + shell.publish(on_dnd_command_produced(Box::new(move || { + platform_specific::wayland::data_device::ActionInner::SetActions { + preferred: actions.intersection(DndAction::Move), + accepted: actions, + } + }))); + return event::Status::Captured; + } + return event::Status::Ignored; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +/// +/// [`Renderer`]: text::Renderer +#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_lines)] +#[allow(clippy::missing_panics_doc)] +pub fn draw<'a, Message>( + renderer: &mut crate::Renderer, + theme: &crate::Theme, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + tree: &Tree, + value: &Value, + placeholder: &str, + size: Option, + font: Option<::Font>, + is_disabled: bool, + is_secure: bool, + icon: Option<&Element<'a, Message, crate::Renderer>>, + end_element: Option<&Element<'a, Message, crate::Renderer>>, + style: &::Style, + dnd_icon: bool, + line_height: text::LineHeight, + error: Option<&str>, + label: Option<&str>, + helper_text: Option<&str>, + helper_text_size: f32, + helper_line_height: text::LineHeight, + viewport: &Rectangle, + renderer_style: &renderer::Style, +) { + // all children should be icon images + let children = &tree.children; + + let state = tree.state.downcast_ref::(); + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let mut children_layout = layout.children(); + + let (label_layout, layout, helper_text_layout) = if label.is_some() && helper_text.is_some() { + let label_layout = children_layout.next(); + let layout = children_layout.next().unwrap(); + let helper_text_layout = children_layout.next(); + (label_layout, layout, helper_text_layout) + } else if label.is_some() { + let label_layout = children_layout.next(); + let layout = children_layout.next().unwrap(); + (label_layout, layout, None) + } else if helper_text.is_some() { + let layout = children_layout.next().unwrap(); + let helper_text_layout = children_layout.next(); + (None, layout, helper_text_layout) + } else { + let layout = children_layout.next().unwrap(); + + (None, layout, None) + }; + + let mut children_layout = layout.children(); + let bounds = layout.bounds(); + let text_bounds = children_layout.next().unwrap().bounds(); + + let is_mouse_over = cursor_position.is_over(bounds); + + let appearance = if is_disabled { + theme.disabled(style) + } else if error.is_some() { + theme.error(style) + } else if state.is_focused() { + theme.focused(style) + } else if is_mouse_over { + theme.hovered(style) + } else { + theme.active(style) + }; + + // draw background and its border + if let Some(border_offset) = appearance.border_offset { + let offset_bounds = Rectangle { + x: bounds.x - border_offset, + y: bounds.y - border_offset, + width: bounds.width + border_offset * 2.0, + height: bounds.height + border_offset * 2.0, + }; + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + appearance.background, + ); + renderer.fill_quad( + renderer::Quad { + bounds: offset_bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + Background::Color(Color::TRANSPARENT), + ); + } else { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + } + + // draw the label if it exists + if let (Some(label_layout), Some(label)) = (label_layout, label) { + renderer.fill_text(Text { + content: label, + size: size.unwrap_or_else(|| renderer.default_size()), + font: font.unwrap_or_else(|| renderer.default_font()), + color: appearance.label_color, + bounds: label_layout.bounds(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height, + shaping: text::Shaping::Advanced, + }); + } + let mut child_index = 0; + let start_icon_tree = children.get(child_index); + // draw the start icon in the text input + if let (Some(icon), Some(tree)) = (icon, start_icon_tree) { + let icon_layout = children_layout.next().unwrap(); + + icon.as_widget().draw( + tree, + renderer, + theme, + &renderer::Style { + text_color: appearance.text_color, + scale_factor: renderer_style.scale_factor, + }, + icon_layout, + cursor_position, + viewport, + ); + child_index += 1; + } + + let text = value.to_string(); + let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size()); + + let (cursor, offset) = if let Some(focus) = &state.is_focused { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + position, + font, + ); + + let is_cursor_visible = + ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2 + == 0; + + if is_cursor_visible { + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + appearance.text_color, + )), + offset, + ) + } + } else { + (None, 0.0) + } + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + if dnd_icon { + (None, 0.0) + } else { + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + appearance.selected_fill, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { placeholder } else { &text }, + size, + font, + text::Shaping::Advanced, + ); + + let color = if text.is_empty() { + theme.placeholder_color(style) + } else { + appearance.text_color + }; + + let render = |renderer: &mut crate::Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render); + }); + } else { + render(renderer); + } + + let end_icon_tree = children.get(child_index); + + // draw the end icon in the text input + if let (Some(icon), Some(tree)) = (end_element, end_icon_tree) { + let icon_layout = children_layout.next().unwrap(); + + icon.as_widget().draw( + tree, + renderer, + theme, + &renderer::Style { + text_color: appearance.text_color, + scale_factor: renderer_style.scale_factor, + }, + icon_layout, + cursor_position, + viewport, + ); + } + + // draw the helper text if it exists + if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) { + renderer.fill_text(Text { + content: helper_text, + size: helper_text_size, + font, + color, + bounds: helper_text_layout.bounds(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height: helper_line_height, + shaping: text::Shaping::Advanced, + }); + } +} + +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +#[must_use] +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: mouse::Cursor, + is_disabled: bool, +) -> mouse::Interaction { + if cursor_position.is_over(layout.bounds()) { + if is_disabled { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } +} + +/// A string which can be sent to the clipboard or drag-and-dropped. +#[derive(Debug, Clone)] +pub struct TextInputString(String); + +#[cfg(feature = "wayland")] +impl DataFromMimeType for TextInputString { + fn from_mime_type(&self, mime_type: &str) -> Option> { + if SUPPORTED_MIME_TYPES.contains(&mime_type) { + Some(self.0.as_bytes().to_vec()) + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum DraggingState { + Selection, + #[cfg(feature = "wayland")] + Dnd(DndAction, String), +} + +#[cfg(feature = "wayland")] +#[derive(Debug, Default, Clone)] +pub(crate) enum DndOfferState { + #[default] + None, + OutsideWidget(Vec, DndAction), + HandlingOffer(Vec, DndAction), + Dropped, +} +#[derive(Debug, Default, Clone)] +#[cfg(not(feature = "wayland"))] +pub(crate) struct DndOfferState; + +/// The state of a [`TextInput`]. +#[derive(Debug, Default, Clone)] +#[must_use] +pub struct State { + is_focused: Option, + dragging_state: Option, + dnd_offer: DndOfferState, + is_pasting: Option, + last_click: Option, + cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, + // TODO: Add stateful horizontal scrolling offset +} + +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, +} + +impl State { + /// Creates a new [`State`], representing an unfocused [`TextInput`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the current value of the selected text in the [`TextInput`]. + #[must_use] + pub fn selected_text(&self, text: &str) -> Option { + let value = Value::new(text); + match self.cursor.state(&value) { + cursor::State::Index(_) => None, + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + Some(text[left..right].to_string()) + } + } + } + + #[cfg(feature = "wayland")] + /// Returns the current value of the dragged text in the [`TextInput`]. + #[must_use] + pub fn dragged_text(&self) -> Option { + match self.dragging_state.as_ref() { + Some(DraggingState::Dnd(_, text)) => Some(text.clone()), + _ => None, + } + } + + /// Creates a new [`State`], representing a focused [`TextInput`]. + pub fn focused() -> Self { + Self { + is_focused: None, + dragging_state: None, + dnd_offer: DndOfferState::default(), + is_pasting: None, + last_click: None, + cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + } + } + + /// Returns whether the [`TextInput`] is currently focused or not. + #[must_use] + pub fn is_focused(&self) -> bool { + self.is_focused.is_some() + } + + /// Returns the [`Cursor`] of the [`TextInput`]. + #[must_use] + pub fn cursor(&self) -> Cursor { + self.cursor + } + + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + let now = Instant::now(); + + self.is_focused = Some(Focus { + updated_at: now, + now, + }); + + self.move_cursor_to_end(); + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = None; + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } + + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + self.cursor.select_range(0, usize::MAX); + } +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self); + } + + fn unfocus(&mut self) { + State::unfocus(self); + } +} + +impl operation::TextInput for State { + fn move_cursor_to_front(&mut self) { + State::move_cursor_to_front(self); + } + + fn move_cursor_to_end(&mut self) { + State::move_cursor_to_end(self); + } + + fn move_cursor_to(&mut self, position: usize) { + State::move_cursor_to(self, position); + } + + fn select_all(&mut self) { + State::select_all(self); + } +} + +mod platform { + use iced_core::keyboard; + + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt() + } else { + modifiers.control() + } + } +} + +fn offset( + renderer: &Renderer, + text_bounds: Rectangle, + font: Renderer::Font, + size: f32, + value: &Value, + state: &State, +) -> f32 +where + Renderer: text::Renderer, +{ + if state.is_focused() { + let cursor = state.cursor(); + + let focus_position = match cursor.state(value) { + cursor::State::Index(i) => i, + cursor::State::Selection { end, .. } => end, + }; + + let (_, offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 0.0 + } +} + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &Value, + size: f32, + cursor_index: usize, + font: Renderer::Font, +) -> (f32, f32) +where + Renderer: text::Renderer, +{ + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_width(&text_before_cursor, size, font, text::Shaping::Advanced); + + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} + +/// Computes the position of the text cursor at the given X coordinate of +/// a [`TextInput`]. +#[allow(clippy::too_many_arguments)] +fn find_cursor_position( + renderer: &Renderer, + text_bounds: Rectangle, + font: Renderer::Font, + size: Option, + value: &Value, + state: &State, + x: f32, + line_height: text::LineHeight, +) -> Option +where + Renderer: text::Renderer, +{ + let size = size.unwrap_or_else(|| renderer.default_size()); + + let offset = offset(renderer, text_bounds, font, size, value, state); + let value = value.to_string(); + + let char_offset = renderer + .hit_test( + &value, + size, + line_height, + font, + Size::INFINITY, + text::Shaping::Advanced, + Point::new(x + offset, text_bounds.height / 2.0), + true, + ) + .map(text::Hit::cursor)?; + + Some(unicode_segmentation::UnicodeSegmentation::graphemes(&value[..char_offset], true).count()) +} + +const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; diff --git a/src/widget/text_input/mod.rs b/src/widget/text_input/mod.rs new file mode 100644 index 00000000..dac71503 --- /dev/null +++ b/src/widget/text_input/mod.rs @@ -0,0 +1,10 @@ +//! A text input widget from iced widgets plus some added details. + +pub mod cursor; +pub mod editor; +mod input; +mod style; +pub mod value; + +pub use input::*; +pub use style::{Appearance as TextInputAppearance, StyleSheet as TextInputStyleSheet}; diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs new file mode 100644 index 00000000..fe383cd7 --- /dev/null +++ b/src/widget/text_input/style.rs @@ -0,0 +1,263 @@ +//! Change the appearance of a text input. +use iced_core::{Background, BorderRadius, Color}; + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The border radius of the text input. + pub border_radius: BorderRadius, + /// The border offset + pub border_offset: Option, + /// The border width of the text input. + pub border_width: f32, + /// The border [`Color`] of the text input. + pub border_color: Color, + /// The label [`Color`] of the text input. + pub label_color: Color, + /// The text [`Color`] of the text input. + pub selected_text_color: Color, + /// The text [`Color`] of the text input. + pub text_color: Color, + /// The selected fill [`Color`] of the text input. + pub selected_fill: Color, +} + +/// A set of rules that dictate the style of a text input. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the style of an active text input. + fn active(&self, style: &Self::Style) -> Appearance; + + /// Produces the style of an errored text input. + fn error(&self, style: &Self::Style) -> Appearance; + + /// Produces the style of a focused text input. + fn focused(&self, style: &Self::Style) -> Appearance; + + /// Produces the [`Color`] of the placeholder of a text input. + fn placeholder_color(&self, style: &Self::Style) -> Color; + + /// Produces the style of an hovered text input. + fn hovered(&self, style: &Self::Style) -> Appearance { + self.focused(style) + } + + /// Produces the style of a disabled text input. + fn disabled(&self, style: &Self::Style) -> Appearance; +} + +#[derive(Copy, Clone, Default)] +pub enum TextInput { + #[default] + Default, + ExpandableSearch, + Search, + Inline, +} + +impl StyleSheet for crate::Theme { + type Style = TextInput; + + fn active(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + match style { + TextInput::Default => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: None, + border_color: self.current_container().component.divider.into(), + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::ExpandableSearch => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + } + } + + fn error(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: Some(2.0), + border_color: Color::from(palette.destructive_color()), + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search | TextInput::ExpandableSearch => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + } + } + + fn hovered(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: None, + border_color: palette.accent.base.into(), + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search | TextInput::ExpandableSearch => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_xl.into(), + border_offset: None, + border_width: 0.0, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::from(self.current_container().component.hover).into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + } + } + + fn focused(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: Some(2.0), + border_color: palette.accent.base.into(), + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search | TextInput::ExpandableSearch => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: Some(2.0), + border_color: Color::TRANSPARENT, + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::from(palette.accent.base).into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + // TODO use regular text color here after text rendering handles multiple colors + // in this case, for selected and unselected text + text_color: palette.on_accent_color().into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + } + } + + fn placeholder_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.7; + neutral_9.into() + } + + fn disabled(&self, style: &Self::Style) -> Appearance { + self.active(style) + } +} diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs new file mode 100644 index 00000000..3b7f8198 --- /dev/null +++ b/src/widget/text_input/value.rs @@ -0,0 +1,131 @@ +use unicode_segmentation::UnicodeSegmentation; + +/// The value of a [`TextInput`]. +/// +/// [`TextInput`]: crate::widget::TextInput +// TODO: Reduce allocations, cache results (?) +#[derive(Debug, Clone)] +pub struct Value { + graphemes: Vec, +} + +impl Value { + /// Creates a new [`Value`] from a string slice. + pub fn new(string: &str) -> Self { + let graphemes = UnicodeSegmentation::graphemes(string, true) + .map(String::from) + .collect(); + + Self { graphemes } + } + + /// Returns whether the [`Value`] is empty or not. + /// + /// A [`Value`] is empty when it contains no graphemes. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. + #[must_use] + pub fn len(&self) -> usize { + self.graphemes.len() + } + + /// Returns the position of the previous start of a word from the given + /// grapheme `index`. + #[must_use] + pub fn previous_start_of_word(&self, index: usize) -> usize { + let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); + + UnicodeSegmentation::split_word_bound_indices(previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() + .map_or(0, |(i, previous_word)| { + index + - UnicodeSegmentation::graphemes(previous_word, true).count() + - UnicodeSegmentation::graphemes( + &previous_string[i + previous_word.len()..] as &str, + true, + ) + .count() + }) + } + + /// Returns the position of the next end of a word from the given grapheme + /// `index`. + #[must_use] + pub fn next_end_of_word(&self, index: usize) -> usize { + let next_string = &self.graphemes[index..].concat(); + + UnicodeSegmentation::split_word_bound_indices(next_string as &str) + .find(|(_, word)| !word.trim_start().is_empty()) + .map_or(self.len(), |(i, next_word)| { + index + + UnicodeSegmentation::graphemes(next_word, true).count() + + UnicodeSegmentation::graphemes(&next_string[..i] as &str, true).count() + }) + } + + /// Returns a new [`Value`] containing the graphemes from `start` until the + /// given `end`. + #[must_use] + pub fn select(&self, start: usize, end: usize) -> Self { + let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Returns a new [`Value`] containing the graphemes until the given + /// `index`. + #[must_use] + pub fn until(&self, index: usize) -> Self { + let graphemes = self.graphemes[..index.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Inserts a new `char` at the given grapheme `index`. + pub fn insert(&mut self, index: usize, c: char) { + self.graphemes.insert(index, c.to_string()); + + self.graphemes = UnicodeSegmentation::graphemes(&self.to_string() as &str, true) + .map(String::from) + .collect(); + } + + /// Inserts a bunch of graphemes at the given grapheme `index`. + pub fn insert_many(&mut self, index: usize, mut value: Value) { + let _ = self + .graphemes + .splice(index..index, value.graphemes.drain(..)); + } + + /// Removes the grapheme at the given `index`. + pub fn remove(&mut self, index: usize) { + let _ = self.graphemes.remove(index); + } + + /// Removes the graphemes from `start` to `end`. + pub fn remove_many(&mut self, start: usize, end: usize) { + let _ = self.graphemes.splice(start..end, std::iter::empty()); + } + + /// Returns a new [`Value`] with all its graphemes replaced with the + /// dot ('•') character. + #[must_use] + pub fn secure(&self) -> Self { + Self { + graphemes: std::iter::repeat(String::from("•")) + .take(self.graphemes.len()) + .collect(), + } + } +} + +impl ToString for Value { + fn to_string(&self) -> String { + self.graphemes.concat() + } +} From 55416c8b9d4d4dd9149fba7d83b67096258085c1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 23 Aug 2023 14:51:49 -0400 Subject: [PATCH 0074/1276] feat: allow creating apps with no main window on wayland --- src/app/mod.rs | 26 +++++++++++++++----------- src/app/settings.rs | 6 ++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 6182f76b..f84d7e0e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -80,17 +80,21 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res { use iced::wayland::actions::window::SctkWindowSettings; use iced_sctk::settings::InitialSurface; - iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { - app_id: Some(App::APP_ID.to_owned()), - autosize: settings.autosize, - client_decorations: settings.client_decorations, - resizable: settings.resizable, - size: settings.size, - size_limits: settings.size_limits, - title: None, - transparent: settings.transparent, - ..SctkWindowSettings::default() - }); + iced.initial_surface = if settings.no_main_window { + InitialSurface::None + } else { + InitialSurface::XdgWindow(SctkWindowSettings { + app_id: Some(App::APP_ID.to_owned()), + autosize: settings.autosize, + client_decorations: settings.client_decorations, + resizable: settings.resizable, + size: settings.size, + size_limits: settings.size_limits, + title: None, + transparent: settings.transparent, + ..SctkWindowSettings::default() + }) + }; } #[cfg(not(feature = "wayland"))] diff --git a/src/app/settings.rs b/src/app/settings.rs index d6f9cdbd..77dcb633 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -19,6 +19,10 @@ pub struct Settings { #[cfg(feature = "wayland")] pub(crate) autosize: bool, + /// Set the application to not create a main window + #[cfg(feature = "wayland")] + pub(crate) no_main_window: bool, + /// Whether the window should have a border, a title bar, etc. or not. pub(crate) client_decorations: bool, @@ -71,6 +75,8 @@ impl Default for Settings { antialiasing: true, #[cfg(feature = "wayland")] autosize: false, + #[cfg(feature = "wayland")] + no_main_window: false, client_decorations: true, debug: false, default_font: font::FONT, From d35dfad48609c63ddccf46a7299b1be6dd34615a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 23 Aug 2023 17:42:21 -0400 Subject: [PATCH 0075/1276] fix: search input border style --- examples/cosmic/src/window/demo.rs | 8 ++++++++ src/widget/text_input/style.rs | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 375b7044..0609ea9c 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -508,6 +508,14 @@ impl State { .width(Length::Fill) .on_input(Message::InputChanged) .into(), + cosmic::widget::search_input( + "test", + &self.entry_value, + Some(Message::InputChanged("".to_string())), + ) + .width(Length::Fill) + .on_input(Message::InputChanged) + .into(), ]) .into() } diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs index fe383cd7..1a3f3b8d 100644 --- a/src/widget/text_input/style.rs +++ b/src/widget/text_input/style.rs @@ -94,9 +94,9 @@ impl StyleSheet for crate::Theme { TextInput::Search => Appearance { background: Color::from(bg).into(), border_radius: corner.radius_xl.into(), - border_width: 0.0, + border_width: 1.0, border_offset: None, - border_color: Color::TRANSPARENT, + border_color: self.current_container().component.divider.into(), text_color: self.current_container().on.into(), selected_text_color: palette.on_accent_color().into(), selected_fill: palette.accent_color().into(), @@ -179,7 +179,18 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, - TextInput::Search | TextInput::ExpandableSearch => Appearance { + TextInput::Search => Appearance { + background: Color::from(bg).into(), + border_radius: corner.radius_xl.into(), + border_offset: None, + border_width: 1.0, + border_color: palette.accent.base.into(), + text_color: self.current_container().on.into(), + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::ExpandableSearch => Appearance { background: Color::from(bg).into(), border_radius: corner.radius_xl.into(), border_offset: None, @@ -226,9 +237,9 @@ impl StyleSheet for crate::Theme { TextInput::Search | TextInput::ExpandableSearch => Appearance { background: Color::from(bg).into(), border_radius: corner.radius_xl.into(), - border_width: 0.0, + border_width: 1.0, border_offset: Some(2.0), - border_color: Color::TRANSPARENT, + border_color: palette.accent.base.into(), text_color: self.current_container().on.into(), selected_text_color: palette.on_accent_color().into(), selected_fill: palette.accent_color().into(), From 6457481ae5c1435526a8538bc792ebc55c6dff02 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 24 Aug 2023 13:51:00 -0400 Subject: [PATCH 0076/1276] fix: text input layout & cargo fmt --- cosmic-theme/src/steps.rs | 4 +- examples/cosmic/src/window/demo.rs | 43 +++++++++++++++++- src/widget/segmented_button/model/mod.rs | 6 +-- src/widget/text_input/input.rs | 58 +++++++++++++++++------- 4 files changed, 89 insertions(+), 22 deletions(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index de23e52c..6487ba85 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -63,7 +63,9 @@ pub fn get_text( } else { step_array }; - let Some(index) = get_index(base_index, 70, step_array.len(), is_dark).or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) else { + let Some(index) = get_index(base_index, 70, step_array.len(), is_dark) + .or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) + else { return fallback.to_owned(); }; diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 0609ea9c..69be5ccd 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -500,7 +500,6 @@ impl State { ) .on_input(Message::InputChanged) // .on_submit(Message::Activate(None)) - .padding(8) .size(20) .id(INPUT_ID.clone()) .into(), @@ -508,6 +507,11 @@ impl State { .width(Length::Fill) .on_input(Message::InputChanged) .into(), + cosmic::widget::text_input("test", &self.entry_value) + .width(Length::Fixed(600.0)) + .padding(32) + .on_input(Message::InputChanged) + .into(), cosmic::widget::search_input( "test", &self.entry_value, @@ -516,6 +520,43 @@ impl State { .width(Length::Fill) .on_input(Message::InputChanged) .into(), + cosmic::widget::text_input("test", &self.entry_value) + .width(Length::Fixed(600.0)) + .on_input(Message::InputChanged) + .into(), + cosmic::widget::search_input( + "test", + &self.entry_value, + Some(Message::InputChanged("".to_string())), + ) + .width(Length::Fixed(100.0)) + .on_input(Message::InputChanged) + .into(), + cosmic::widget::search_input( + "test", + &self.entry_value, + Some(Message::InputChanged("".to_string())), + ) + .padding([24, 48]) + .width(Length::Fixed(400.0)) + .on_input(Message::InputChanged) + .into(), + cosmic::widget::search_input( + "test", + &self.entry_value, + Some(Message::InputChanged("".to_string())), + ) + .width(Length::Fixed(400.0)) + .on_input(Message::InputChanged) + .into(), + cosmic::widget::search_input( + "test", + &self.entry_value, + Some(Message::InputChanged("".to_string())), + ) + .width(Length::Fixed(800.0)) + .on_input(Message::InputChanged) + .into(), ]) .into() } diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index 425dcf81..405f2784 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -339,7 +339,7 @@ where /// ``` pub fn position_set(&mut self, id: Entity, position: u16) -> Option { let Some(index) = self.position(id) else { - return None + return None; }; let position = self.order.len().min(position as usize); @@ -360,11 +360,11 @@ where /// ``` pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool { let Some(first_index) = self.position(first) else { - return false + return false; }; let Some(second_index) = self.position(second) else { - return false + return false; }; self.order.swap(first_index as usize, second_index as usize); diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index abb4290d..37b4e467 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -705,7 +705,8 @@ where ); } } - + let mut children = layout.children(); + let layout = children.next().unwrap(); mouse_interaction(layout, cursor_position, self.on_input.is_none()) } } @@ -765,11 +766,11 @@ pub fn layout( helper_text_size: f32, helper_text_line_height: text::LineHeight, ) -> layout::Node { + let limits = limits.width(width); let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); let mut nodes = Vec::with_capacity(3); let text_pos = if let Some(label) = label { - let limits = limits.width(width); let text_bounds = limits.resolve(Size::ZERO); let label_size = renderer.measure( @@ -788,11 +789,14 @@ pub fn layout( }; let text_size = size.unwrap_or_else(|| renderer.default_size()); + let mut text_input_height = text_size * 1.2; let padding = padding.fit(Size::ZERO, limits.max()); let helper_pos = if start_icon.is_some() || end_icon.is_some() { // TODO configurable icon spacing, maybe via appearance - let mut height = text_size * 1.2; + let limits_copy = limits; + + let limits = limits.pad(padding); let icon_spacing = 8.0; let (start_icon_width, mut start_icon) = if let Some(icon) = start_icon.as_ref() { let icon_node = icon.layout( @@ -801,7 +805,7 @@ pub fn layout( .width(icon.as_widget().width()) .height(icon.as_widget().height()), ); - height = height.max(icon_node.bounds().height); + text_input_height = text_input_height.max(icon_node.bounds().height); (icon_node.bounds().width + icon_spacing, Some(icon_node)) } else { (0.0, None) @@ -814,12 +818,12 @@ pub fn layout( .width(icon.as_widget().width()) .height(icon.as_widget().height()), ); - height = height.max(icon_node.bounds().height); + text_input_height = text_input_height.max(icon_node.bounds().height); (icon_node.bounds().width + icon_spacing, Some(icon_node)) } else { (0.0, None) }; - let text_limits = limits.width(width).pad(padding).height(text_size * 1.2); + let text_limits = limits.width(width).height(text_size * 1.2); let text_bounds = text_limits.resolve(Size::ZERO); @@ -828,7 +832,7 @@ pub fn layout( text_node.move_to(Point::new( padding.left + start_icon_width, - padding.top + ((height - text_size * 1.2) / 2.0).max(0.0), + padding.top + ((text_input_height - text_size * 1.2) / 2.0).max(0.0), )); let mut node_list: Vec<_> = Vec::with_capacity(3); @@ -844,21 +848,33 @@ pub fn layout( } if let Some(mut end_icon) = end_icon.take() { end_icon.move_to(Point::new( - text_node_bounds.x + text_node_bounds.width, + text_node_bounds.x + text_node_bounds.width + f32::from(spacing), padding.top + ((text_size * 1.2 - end_icon.bounds().height) / 2.0).max(0.0), )); node_list.push(end_icon); } - let input_limits = limits.width(width).pad(padding).height(height); - let input_bounds = input_limits.resolve(Size::ZERO); + let text_input_size = Size::new( + text_node_bounds.x + text_node_bounds.width + end_icon_width, + text_input_height, + ) + .pad(padding); + + let input_limits = limits_copy + .width(width) + .height(text_input_height.max(text_input_size.height)) + .min_width(text_input_size.width); + let input_bounds = input_limits.resolve(text_input_size); let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos); let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing); nodes.push(input_node); Vector::new(0.0, y_pos) } else { - let limits = limits.width(width).pad(padding).height(text_size * 1.2); + let limits = limits + .width(width) + .height(text_input_height + padding.vertical()) + .pad(padding); let text_bounds = limits.resolve(Size::ZERO); let mut text = layout::Node::new(text_bounds); @@ -898,7 +914,11 @@ pub fn layout( ) }); size.height += (nodes.len() - 1) as f32 * f32::from(spacing); - let limits = limits.width(width).pad(padding).height(size.height); + + let limits = limits + .width(width) + .height(size.height) + .min_width(size.width); layout::Node::with_children(limits.resolve(size), nodes) } @@ -1182,7 +1202,9 @@ where let state = state(); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { return event::Status::Ignored }; + let Some(on_input) = on_input else { + return event::Status::Ignored; + }; if state.is_pasting.is_none() && !state.keyboard_modifiers.command() @@ -1205,7 +1227,9 @@ where let state = state(); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { return event::Status::Ignored }; + let Some(on_input) = on_input else { + return event::Status::Ignored; + }; let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -1433,7 +1457,7 @@ where wayland::DndOfferEvent::Enter { x, y, mime_types }, ))) => { let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored + return event::Status::Ignored; }; let state = state(); @@ -1588,8 +1612,8 @@ where .iter() .find(|m| mime_types.contains(&(**m).to_string())) else { - state.dnd_offer = DndOfferState::None; - return event::Status::Captured; + state.dnd_offer = DndOfferState::None; + return event::Status::Captured; }; state.dnd_offer = DndOfferState::Dropped; shell.publish(on_dnd_command_produced(Box::new(move || { From 6927220325a8757c087cd011806c6c76bd0bb74b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 17 Aug 2023 06:11:43 +0200 Subject: [PATCH 0077/1276] fix(app): draggable windows on X11 systems using winit --- examples/application/Cargo.toml | 2 +- examples/application/src/main.rs | 2 +- src/command/mod.rs | 12 +++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b05d86a8..33ba4505 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -10,4 +10,4 @@ tracing-subscriber = "0.3.17" [dependencies.libcosmic] path = "../../" default-features = false -features = ["debug", "wayland", "tokio"] +features = ["debug", "winit", "tokio"] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 591556c7..e8d5fbe1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -40,7 +40,7 @@ fn main() -> Result<(), Box> { .antialiasing(true) .client_decorations(true) .debug(false) - .default_icon_theme(Some("Pop".into())) + .default_icon_theme("Pop") .default_text_size(16.0) .scale_factor(1.0) .size((1024, 768)) diff --git a/src/command/mod.rs b/src/command/mod.rs index 39975d37..109f7263 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -14,8 +14,6 @@ use iced_runtime::command::platform_specific::wayland::Action as WaylandAction; #[cfg(feature = "wayland")] use iced_runtime::command::platform_specific::Action as PlatformAction; use iced_runtime::command::Action; -#[cfg(not(feature = "wayland"))] -use iced_runtime::window::Action as WindowAction; use std::future::Future; /// Yields a command which contains a batch of commands. @@ -42,7 +40,7 @@ pub fn drag() -> Command { /// Initiates a window drag. #[cfg(not(feature = "wayland"))] pub fn drag() -> Command { - iced::Command::none() + iced_runtime::window::drag() } /// Fullscreens the window. @@ -54,7 +52,7 @@ pub fn fullscreen() -> Command { /// Fullscreens the window. #[cfg(not(feature = "wayland"))] pub fn fullscreen() -> Command { - iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Fullscreen))) + iced_runtime::window::change_mode(Mode::Fullscreen) } /// Minimizes the window. @@ -66,7 +64,7 @@ pub fn minimize() -> Command { /// Minimizes the window. #[cfg(not(feature = "wayland"))] pub fn minimize() -> Command { - iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Hidden))) + iced_runtime::window::minimize(true) } /// Sets the title of a window. @@ -94,7 +92,7 @@ pub fn set_windowed() -> Command { /// Sets the window mode to windowed. #[cfg(not(feature = "wayland"))] pub fn set_windowed() -> Command { - iced::Command::single(Action::Window(WindowAction::ChangeMode(Mode::Windowed))) + iced_runtime::window::change_mode(Mode::Windowed) } /// Toggles the windows' maximization state. @@ -106,7 +104,7 @@ pub fn toggle_fullscreen() -> Command { /// Toggles the windows' maximization state. #[cfg(not(feature = "wayland"))] pub fn toggle_fullscreen() -> Command { - iced::Command::single(Action::Window(WindowAction::ToggleMaximize)) + iced_runtime::window::toggle_maximize() } /// Creates a command to apply an action to a window. From 069163264fc88555a8f6f6542c383bb313082856 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 21 Aug 2023 22:56:39 +0200 Subject: [PATCH 0078/1276] chore(command): `future` does not need to restrict the message type --- src/command/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command/mod.rs b/src/command/mod.rs index 109f7263..032e5323 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -22,7 +22,7 @@ pub fn batch(commands: impl IntoIterator>) -> Command { } /// Yields a command which will run the future on the runtime executor. -pub fn future(future: impl Future + Send + 'static) -> Command { +pub fn future(future: impl Future + Send + 'static) -> Command { Command::single(Action::Future(Box::pin(future))) } From caf07e828883bbf68864cde20864c9233586e268 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 21 Aug 2023 22:57:56 +0200 Subject: [PATCH 0079/1276] chore: re-export `iced::Command` --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 576332e7..dd8d8616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod app; pub use app::{Application, ApplicationExt}; +pub use iced::Command; pub mod command; pub use cosmic_config; pub use cosmic_theme; From 2e3d9af72002afb4b8a6f4cd105d1ca574d0c17e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 21 Aug 2023 22:58:10 +0200 Subject: [PATCH 0080/1276] chore(widget): re-export missing iced widgets --- src/widget/mod.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 272824b8..fdec6106 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -3,6 +3,17 @@ //! Cosmic-themed widget implementations. +// Re-exports from Iced +pub use iced::widget::{checkbox, Checkbox}; +pub use iced::widget::{column, Column}; +pub use iced::widget::{image, Image}; +pub use iced::widget::{pick_list, PickList}; +pub use iced::widget::{radio, Radio}; +pub use iced::widget::{row, Row}; +pub use iced::widget::{slider, Slider}; +pub use iced::widget::{space, Space}; +pub use iced::widget::{text_input, TextInput}; + pub mod aspect_ratio; mod button; @@ -37,21 +48,16 @@ pub use popover::{popover, Popover}; pub mod rectangle_tracker; +mod scrollable; +pub use scrollable::*; + pub mod search; pub mod segmented_button; -pub use segmented_button::horizontal as horizontal_segmented_button; -pub use segmented_button::vertical as vertical_segmented_button; - pub mod segmented_selection; -pub use segmented_selection::horizontal as horizontal_segmented_selection; -pub use segmented_selection::vertical as vertical_segmented_selection; pub mod settings; -mod scrollable; -pub use scrollable::*; - pub mod spin_button; pub use spin_button::{spin_button, SpinButton}; From 6a07e341caa2a56390eb76171e44081281391b6c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 28 Aug 2023 12:05:31 -0400 Subject: [PATCH 0081/1276] chore: custom text input style --- src/widget/text_input/style.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs index 1a3f3b8d..77cdb4ad 100644 --- a/src/widget/text_input/style.rs +++ b/src/widget/text_input/style.rs @@ -50,13 +50,21 @@ pub trait StyleSheet { fn disabled(&self, style: &Self::Style) -> Appearance; } -#[derive(Copy, Clone, Default)] +#[derive(Default)] pub enum TextInput { #[default] Default, ExpandableSearch, Search, Inline, + Custom { + active: Box Appearance>, + error: Box Appearance>, + hovered: Box Appearance>, + focused: Box Appearance>, + disabled: Box Appearance>, + placeholder_color: Box Color>, + }, } impl StyleSheet for crate::Theme { @@ -113,6 +121,7 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::Custom { active, .. } => active(self), } } @@ -157,6 +166,7 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::Custom { error, .. } => error(self), } } @@ -212,6 +222,7 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::Custom { hovered, .. } => hovered(self), } } @@ -258,10 +269,17 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::Custom { focused, .. } => focused(self), } } - fn placeholder_color(&self, _style: &Self::Style) -> Color { + fn placeholder_color(&self, style: &Self::Style) -> Color { + if let TextInput::Custom { + placeholder_color, .. + } = style + { + return placeholder_color(self); + } let palette = self.cosmic(); let mut neutral_9 = palette.palette.neutral_9; neutral_9.alpha = 0.7; @@ -269,6 +287,9 @@ impl StyleSheet for crate::Theme { } fn disabled(&self, style: &Self::Style) -> Appearance { + if let TextInput::Custom { disabled, .. } = style { + return disabled(self); + } self.active(style) } } From 984d545546abf96e385ba366e073d4eac3100f1e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 29 Aug 2023 14:53:42 -0400 Subject: [PATCH 0082/1276] fix: remove conflicting iced text_input from reexports --- src/widget/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index fdec6106..de0660b9 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -12,7 +12,6 @@ pub use iced::widget::{radio, Radio}; pub use iced::widget::{row, Row}; pub use iced::widget::{slider, Slider}; pub use iced::widget::{space, Space}; -pub use iced::widget::{text_input, TextInput}; pub mod aspect_ratio; From 4903d7792ea4e1952d123e8f9b540ca89a834418 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 29 Aug 2023 15:01:56 -0400 Subject: [PATCH 0083/1276] refactor: default the icon theme to Cosmic --- src/app/settings.rs | 2 +- src/icon_theme.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index 77dcb633..99ab8de6 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -80,7 +80,7 @@ impl Default for Settings { client_decorations: true, debug: false, default_font: font::FONT, - default_icon_theme: Some(String::from("Pop")), + default_icon_theme: Some(String::from("Cosmic")), default_text_size: 14.0, resizable: Some(8.0), scale_factor: std::env::var("COSMIC_SCALE") diff --git a/src/icon_theme.rs b/src/icon_theme.rs index f426ae7f..4a25d615 100644 --- a/src/icon_theme.rs +++ b/src/icon_theme.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; thread_local! { /// The fallback icon theme to search if no icon theme was specified. - pub(crate) static DEFAULT: RefCell = RefCell::new(String::from("Pop")); + pub(crate) static DEFAULT: RefCell = RefCell::new(String::from("Cosmic")); } /// The fallback icon theme to search if no icon theme was specified. From 57c2ea7b623d77cbdfa9ea28646b3b77e2ab4e8f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:16:13 +0200 Subject: [PATCH 0084/1276] feat(font): add FONT_MONO_REGULAR from Fira Mono --- src/font.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/font.rs b/src/font.rs index 5484e1f5..052d4642 100644 --- a/src/font.rs +++ b/src/font.rs @@ -10,6 +10,8 @@ use iced::{ }; use iced_core::font::Family; +pub const DEFAULT: Font = FONT; + pub const FONT: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Normal, @@ -37,10 +39,20 @@ pub const FONT_SEMIBOLD: Font = Font { pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); +pub const FONT_MONO_REGULAR: Font = Font { + family: Family::Name("Fira Mono"), + weight: iced_core::font::Weight::Normal, + stretch: iced_core::font::Stretch::Normal, + monospaced: true, +}; + +pub const FONT_MONO_REGULAR_DATA: &[u8] = include_bytes!("../res/Fira/FiraMono-Regular.otf"); + pub fn load_fonts() -> Command> { Command::batch(vec![ load(FONT_DATA), load(FONT_LIGHT_DATA), load(FONT_SEMIBOLD_DATA), + load(FONT_MONO_REGULAR_DATA), ]) } From c45556d8e38f97cc6a083565717a145745bc5eb6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:19:32 +0200 Subject: [PATCH 0085/1276] feat(widget): add typography functions to text module --- src/widget/mod.rs | 2 +- src/widget/text.rs | 81 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index de0660b9..df62e544 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -60,7 +60,7 @@ pub mod settings; pub mod spin_button; pub use spin_button::{spin_button, SpinButton}; -mod text; +pub mod text; pub use text::{text, Text}; mod toggler; diff --git a/src/widget/text.rs b/src/widget/text.rs index d2357081..6712d84c 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,14 +1,81 @@ -use std::borrow::Cow; - +use crate::Renderer; pub use iced::widget::Text; +use std::borrow::Cow; /// Creates a new [`Text`] widget with the provided content. /// /// [`Text`]: widget::Text -pub fn text<'a, Renderer>(text: impl Into>) -> Text<'a, Renderer> -where - Renderer: iced_core::text::Renderer, - Renderer::Theme: iced::widget::text::StyleSheet, -{ +pub fn text<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { Text::new(text) } + +/// Available presets for text typography +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Typography { + Body, + Caption, + CaptionHeading, + Heading, + Monotext, + Title1, + Title2, + Title3, + Title4, +} + +/// [`Text`] widget with the Title 1 typography preset. +pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text) + .size(32.0) + .line_height(44.0) + .font(crate::font::FONT_LIGHT) +} + +/// [`Text`] widget with the Title 2 typography preset. +pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text).size(28.0).line_height(36.0) +} + +/// [`Text`] widget with the Title 3 typography preset. +pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text).size(24.0).line_height(32.0) +} + +/// [`Text`] widget with the Title 4 typography preset. +pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text).size(20.0).line_height(28.0) +} + +/// [`Text`] widget with the Heading typography preset. +pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text) + .size(14.0) + .line_height(20.0) + .font(crate::font::FONT_SEMIBOLD) +} + +/// [`Text`] widget with the Caption Heading typography preset. +pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text) + .size(10.0) + .line_height(14.0) + .font(crate::font::FONT_SEMIBOLD) +} + +/// [`Text`] widget with the Body typography preset. +pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text).size(14.0).line_height(20.0) +} + +/// [`Text`] widget with the Caption typography preset. +pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text).size(10.0).line_height(14.0) +} + +/// [`Text`] widget with the Monotext typography preset. +pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { + Text::new(text) + .size(14.0) + .line_height(20.0) + .font(crate::font::FONT_MONO_REGULAR) +} From 6383ecc0cb5c1bc418cb25780be2098c70acf366 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:21:15 +0200 Subject: [PATCH 0086/1276] chore(widget): text_input does not need to be wayland-only --- src/widget/mod.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index df62e544..3e9362b7 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -21,6 +21,9 @@ pub use button::*; pub mod card; pub use card::*; +pub mod cosmic_container; +pub use cosmic_container::LayerContainer; + pub mod flex_row; pub use flex_row::{flex_row, FlexRow}; @@ -63,6 +66,9 @@ pub use spin_button::{spin_button, SpinButton}; pub mod text; pub use text::{text, Text}; +pub mod text_input; +pub use text_input::*; + mod toggler; pub use toggler::toggler; @@ -73,13 +79,6 @@ pub use view_switcher::vertical as vertical_view_switcher; pub mod warning; pub use warning::*; -pub mod cosmic_container; -pub use cosmic_container::*; -// #[cfg(feature = "wayland")] -pub mod text_input; -// #[cfg(feature = "wayland")] -pub use text_input::*; - /// An element to distinguish a boundary between two elements. pub mod divider { /// Horizontal variant of a divider. From e9035a95826ae223926f439ba15b401c7d393a1a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:22:06 +0200 Subject: [PATCH 0087/1276] chore(widget): add more iced widget re-exports --- src/widget/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 3e9362b7..525074a0 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -5,13 +5,14 @@ // Re-exports from Iced pub use iced::widget::{checkbox, Checkbox}; -pub use iced::widget::{column, Column}; +pub use iced::widget::{container, Container}; +pub use iced::widget::{horizontal_space, space, vertical_space, Space}; pub use iced::widget::{image, Image}; +pub use iced::widget::{mouse_area, MouseArea}; pub use iced::widget::{pick_list, PickList}; pub use iced::widget::{radio, Radio}; -pub use iced::widget::{row, Row}; pub use iced::widget::{slider, Slider}; -pub use iced::widget::{space, Space}; +pub use iced::widget::{svg, Svg}; pub mod aspect_ratio; From 26777464c5b2fe471d5bb7b2c522357d491438ae Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:23:24 +0200 Subject: [PATCH 0088/1276] feat(widget): add functions for columns and rows --- src/widget/mod.rs | 86 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 525074a0..f557dd33 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -22,9 +22,51 @@ pub use button::*; pub mod card; pub use card::*; +pub use column::{column, Column}; +pub mod column { + pub use iced::widget::Column; + + #[must_use] + pub fn column<'a, Message>() -> Column<'a, Message, crate::Renderer> { + Column::new() + } + + #[must_use] + pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message, crate::Renderer> { + Column::with_children(Vec::with_capacity(capacity)) + } + + #[must_use] + pub fn with_children( + children: Vec>, + ) -> Column { + Column::with_children(children) + } +} + pub mod cosmic_container; pub use cosmic_container::LayerContainer; +/// An element to distinguish a boundary between two elements. +pub mod divider { + /// Horizontal variant of a divider. + pub mod horizontal { + use iced::widget::{horizontal_rule, Rule}; + + /// Horizontal divider with light thickness + #[must_use] + pub fn light() -> Rule { + horizontal_rule(4).style(crate::theme::Rule::LightDivider) + } + + /// Horizontal divider with heavy thickness. + #[must_use] + pub fn heavy() -> Rule { + horizontal_rule(10).style(crate::theme::Rule::HeavyDivider) + } + } +} + pub mod flex_row; pub use flex_row::{flex_row, FlexRow}; @@ -32,7 +74,7 @@ mod header_bar; pub use header_bar::{header_bar, HeaderBar}; pub mod icon; -pub use icon::{icon, Icon, IconSource}; +pub use icon::{icon, Icon}; #[cfg(feature = "animated-image")] pub mod frames; @@ -51,6 +93,28 @@ pub use popover::{popover, Popover}; pub mod rectangle_tracker; +pub use row::{row, Row}; +pub mod row { + pub use iced::widget::Row; + + #[must_use] + pub fn row<'a, Message>() -> Row<'a, Message, crate::Renderer> { + Row::new() + } + + #[must_use] + pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message, crate::Renderer> { + Row::with_children(Vec::with_capacity(capacity)) + } + + #[must_use] + pub fn with_children( + children: Vec>, + ) -> Row { + Row::with_children(children) + } +} + mod scrollable; pub use scrollable::*; @@ -79,23 +143,3 @@ pub use view_switcher::vertical as vertical_view_switcher; pub mod warning; pub use warning::*; - -/// An element to distinguish a boundary between two elements. -pub mod divider { - /// Horizontal variant of a divider. - pub mod horizontal { - use iced::widget::{horizontal_rule, Rule}; - - /// Horizontal divider with light thickness - #[must_use] - pub fn light() -> Rule { - horizontal_rule(4).style(crate::theme::Rule::LightDivider) - } - - /// Horizontal divider with heavy thickness. - #[must_use] - pub fn heavy() -> Rule { - horizontal_rule(10).style(crate::theme::Rule::HeavyDivider) - } - } -} From 28c9b001e4cffefd8c38d4404cea62c93ff189e5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:25:00 +0200 Subject: [PATCH 0089/1276] feat(ext): add CollectionWidget extension trait --- src/ext.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/ext.rs b/src/ext.rs index 12e52853..984a0f6b 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use iced::Color; +use iced_core::Widget; pub trait ElementExt { #[must_use] @@ -17,3 +18,82 @@ impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { } } } + +/// Additional methods for the [`Column`] and [`Row`] widgets. +pub trait CollectionWidget<'a, Message>: Widget { + /// Moves all the elements of `other` into `self`, leaving `other` empty. + #[must_use] + fn append(self, other: &mut Vec) -> Self + where + E: Into>; + + /// Appends all elements in an iterator to the widget. + #[must_use] + fn extend(self, iterator: impl Iterator) -> Self + where + E: Into>; + + /// Conditionally pushes an element to the widget. + #[must_use] + fn push_maybe(self, element: Option>>) -> Self; +} + +impl<'a, Message> CollectionWidget<'a, Message> + for crate::widget::Column<'a, Message, crate::Renderer> +{ + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..)) + } + + fn extend(mut self, iterator: impl Iterator) -> Self + where + E: Into>, + { + for item in iterator { + self = self.push(item.into()); + } + + self + } + + fn push_maybe(self, element: Option>>) -> Self { + if let Some(element) = element { + self.push(element.into()) + } else { + self + } + } +} + +impl<'a, Message> CollectionWidget<'a, Message> + for crate::widget::Row<'a, Message, crate::Renderer> +{ + fn append(self, other: &mut Vec) -> Self + where + E: Into>, + { + self.extend(other.drain(..)) + } + + fn extend(mut self, iterator: impl Iterator) -> Self + where + E: Into>, + { + for item in iterator { + self = self.push(item.into()); + } + + self + } + + fn push_maybe(self, element: Option>>) -> Self { + if let Some(element) = element { + self.push(element.into()) + } else { + self + } + } +} From 2ab760e66de3f9ddeb212e140519608168f4e128 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:25:23 +0200 Subject: [PATCH 0090/1276] chore: add prelude module --- src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index dd8d8616..e70f8963 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,14 @@ #![allow(clippy::module_name_repetitions)] +/// Recommended default imports. +pub mod prelude { + pub use crate::ext::*; + pub use crate::{Element, Renderer, Theme}; +} + +pub use apply::{Also, Apply}; + pub mod app; pub use app::{Application, ApplicationExt}; @@ -19,7 +27,6 @@ pub mod executor; pub use executor::single::Executor as SingleThreadExecutor; mod ext; -pub use ext::ElementExt; pub mod font; From 5904d2c0f08d9808363756ef5e42292d7a780891 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:26:00 +0200 Subject: [PATCH 0091/1276] chore(icon_theme): store default theme as Cow --- src/icon_theme.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/icon_theme.rs b/src/icon_theme.rs index 4a25d615..6d8dfc89 100644 --- a/src/icon_theme.rs +++ b/src/icon_theme.rs @@ -3,20 +3,21 @@ //! Select the preferred icon theme. +use std::borrow::Cow; use std::cell::RefCell; thread_local! { /// The fallback icon theme to search if no icon theme was specified. - pub(crate) static DEFAULT: RefCell = RefCell::new(String::from("Cosmic")); + pub(crate) static DEFAULT: RefCell> = RefCell::new("Cosmic".into()); } /// The fallback icon theme to search if no icon theme was specified. #[must_use] pub fn default() -> String { - DEFAULT.with(|f| f.borrow().clone()) + DEFAULT.with(|theme| theme.borrow().to_string()) } /// Set the fallback icon theme to search when loading system icons. -pub fn set_default(name: impl Into) { - DEFAULT.with(|f| *f.borrow_mut() = name.into()); +pub fn set_default(name: impl Into>) { + DEFAULT.with(|theme| *theme.borrow_mut() = name.into()); } From b805fc894c8aaa32ac6d2a63bea1f6dd243207bd Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:31:13 +0200 Subject: [PATCH 0092/1276] feat(examples): add design demo --- examples/design/Cargo.toml | 13 ++ examples/design/src/main.rs | 325 ++++++++++++++++++++++++++++++++++++ justfile | 2 +- 3 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 examples/design/Cargo.toml create mode 100644 examples/design/src/main.rs diff --git a/examples/design/Cargo.toml b/examples/design/Cargo.toml new file mode 100644 index 00000000..eb89855d --- /dev/null +++ b/examples/design/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "design" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.37" +tracing-subscriber = "0.3.17" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = ["debug", "winit", "tokio"] diff --git a/examples/design/src/main.rs b/examples/design/src/main.rs new file mode 100644 index 00000000..81c36efd --- /dev/null +++ b/examples/design/src/main.rs @@ -0,0 +1,325 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Controls: buttons, radio buttons, toggles, etc. + +use cosmic::app::{Command, Core, Settings}; +use cosmic::widget::{button, column, container, icon, nav_bar, row, scrollable, text}; +use cosmic::{executor, iced, ApplicationExt, Apply, Element}; + +#[derive(Clone, Copy)] +pub enum Page { + Buttons, +} + +impl Page { + const fn as_str(self) -> &'static str { + match self { + Page::Buttons => "Buttons", + } + } +} + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + let settings = Settings::default() + .antialiasing(true) + .client_decorations(true) + .debug(false) + .size((1024, 768)) + .theme(cosmic::Theme::dark()); + + cosmic::app::run::(settings, &[Page::Buttons])?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message { + Clicked, +} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + nav_model: nav_bar::Model, + app_icon: icon::Handle, + bt_icon: icon::Handle, + leading_icon: icon::Handle, + trailing_icon: icon::Handle, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = &'static [Page]; + + /// Message type specific to our [`App`]. + type Message = Message; + + const APP_ID: &'static str = "org.cosmic.DesignDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, input: Self::Flags) -> (Self, Command) { + let mut nav_model = nav_bar::Model::default(); + + for &page in input { + nav_model.insert().text(page.as_str()).data(page); + } + + nav_model.activate_position(0); + + let mut app = App { + core, + app_icon: icon::handle::from_name("firefox").size(16).handle(), + bt_icon: icon::handle::from_name("bluetooth-active-symbolic") + .size(16) + .handle(), + leading_icon: icon::handle::from_name("document-save-symbolic") + .size(16) + .handle(), + trailing_icon: button::hyperlink::icon(), + nav_model, + }; + + let command = app.update_title(); + + (app, command) + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav_model) + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + self.nav_model.activate(id); + self.update_title() + } + + /// Handle application events here. + fn update(&mut self, _message: Self::Message) -> Command { + Command::none() + } + + /// Creates a view after each update. + fn view(&self) -> Element { + let page_content = match self.nav_model.active_data::() { + Some(Page::Buttons) => self.view_buttons(), + None => cosmic::widget::text("Unknown page selected").into(), + }; + + container(page_content) + .width(iced::Length::Fill) + .align_x(iced::alignment::Horizontal::Center) + .apply(scrollable) + .into() + } +} + +impl App +where + Self: cosmic::Application, +{ + fn active_page_title(&mut self) -> &str { + self.nav_model + .text(self.nav_model.active()) + .unwrap_or("Unknown Page") + } + + fn update_title(&mut self) -> Command { + let title = self.active_page_title().to_owned(); + self.set_title(title) + } + + fn view_buttons(&self) -> Element { + column() + .max_width(800) + .spacing(24) + .push(text::title1("Label Buttons")) + // Suggested button header + .push( + column() + .spacing(8) + .push(text::title3("Suggested Button")) + .push(text("Highest level of attention, there should only be one primary button used on the page.").size(14.0)) + ) + // Suggested button demo + .push( + row() + .spacing(36) + .push(button::suggested("Label").on_press(Message::Clicked)) + .push(button::suggested("Label").on_press(Message::Clicked).leading_icon(self.leading_icon.clone())) + .push(button::suggested("Label").on_press(Message::Clicked).trailing_icon(self.trailing_icon.clone())) + .push(button::suggested("Label").on_press(Message::Clicked).leading_icon(self.app_icon.clone())) + .push( + button::suggested("Label") + .on_press(Message::Clicked) + .leading_icon(self.app_icon.clone()) + .trailing_icon(self.trailing_icon.clone()) + ) + ) + // Destructive button header + .push( + column() + .spacing(8) + .push(text::title3("Destructive Button")) + .push(text("Highest level of attention, there should only be one primary button used on the page.").size(14.0)) + ) + // Destructive button demo + .push( + row() + .spacing(36) + .push(button::destructive("Label").on_press(Message::Clicked)) + .push(button::destructive("Label").on_press(Message::Clicked).leading_icon(self.leading_icon.clone())) + .push(button::destructive("Label").on_press(Message::Clicked).trailing_icon(self.trailing_icon.clone())) + .push(button::destructive("Label").on_press(Message::Clicked).leading_icon(self.app_icon.clone())) + .push( + button::destructive("Label") + .on_press(Message::Clicked) + .leading_icon(self.app_icon.clone()) + .trailing_icon(self.trailing_icon.clone()) + ) + ) + // Standard button header + .push( + column() + .spacing(8) + .push(text::title3("Standard Button")) + .push( + text( + "Requires less attention from the user. Could be more \ + than one button on the page, if necessary." + ) + .size(14.0) + ) + ) + // Standard button demo + .push( + row() + .spacing(36) + .push(button::standard("Label").on_press(Message::Clicked)) + .push(button::standard("Label").on_press(Message::Clicked).leading_icon(self.leading_icon.clone())) + .push(button::standard("Label").on_press(Message::Clicked).trailing_icon(self.trailing_icon.clone())) + .push(button::standard("Label").on_press(Message::Clicked).leading_icon(self.app_icon.clone())) + .push( + button::standard("Label") + .on_press(Message::Clicked) + .leading_icon(self.app_icon.clone()) + .trailing_icon(self.trailing_icon.clone()) + ) + ) + // Text button header + .push( + column() + .spacing(8) + .push(text::title3("Text Button")) + .push(text( + "Lowest priority actions, especially when presenting multiple options. Because text buttons \ + don’t have a visible container in their default state, they don’t distract from nearby \ + content. But they are also more difficult to recognize because of that." + ).size(14.0)) + ) + // Text button demo + .push( + row() + .spacing(36) + .push(button::text("Label").on_press(Message::Clicked)) + .push(button::text("Label").on_press(Message::Clicked).leading_icon(self.leading_icon.clone())) + .push(button::text("Label").on_press(Message::Clicked).trailing_icon(self.trailing_icon.clone())) + .push(button::text("Label").on_press(Message::Clicked).leading_icon(self.app_icon.clone())) + .push( + button::text("Label") + .on_press(Message::Clicked) + .leading_icon(self.app_icon.clone()) + .trailing_icon(self.trailing_icon.clone()) + ) + ) + // Icon buttons + .push(text::title1("Icon Buttons")) + // Extra small icon buttons + .push( + row() + .spacing(36) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_small()) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_small().selected(true)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_small().label("Label")) + .push( + button::icon(self.bt_icon.clone()) + .on_press(Message::Clicked) + .extra_small() + .label("Label") + .selected(true) + ) + ) + // Small (default) icon buttons + .push( + row() + .spacing(36) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).selected(true)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).label("Label")) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).label("Label").selected(true)) + ) + // Medium icon buttons + .push( + row() + .spacing(36) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).medium()) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).medium().selected(true)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).medium().label("Label")) + .push( + button::icon(self.bt_icon.clone()) + .on_press(Message::Clicked) + .medium() + .label("Label") + .selected(true) + ) + ) + // Large icon buttons + .push( + row() + .spacing(36) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).large()) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).large().selected(true)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).large().label("Label")) + .push( + button::icon(self.bt_icon.clone()) + .on_press(Message::Clicked) + .large() + .label("Label") + .selected(true) + ) + ) + // Extra large icon buttons + .push( + row() + .spacing(36) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_large()) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_large().selected(true)) + .push(button::icon(self.bt_icon.clone()).on_press(Message::Clicked).extra_large().label("Label")) + .push( + button::icon(self.bt_icon.clone()) + .on_press(Message::Clicked) + .extra_large() + .label("Label") + .selected(true) + ) + ) + .into() + } +} diff --git a/justfile b/justfile index 163ebceb..b83e0cbc 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -projects := 'application cosmic cosmic_sctk open_dialog' +projects := 'application cosmic cosmic_sctk design open_dialog' # Check for errors and linter warnings check *args: From 796fe3c1a960364d958bcb42ddb61ce39db92e30 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 10:40:59 +0200 Subject: [PATCH 0093/1276] chore: add Apply to prelude --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e70f8963..7e73e102 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ /// Recommended default imports. pub mod prelude { pub use crate::ext::*; - pub use crate::{Element, Renderer, Theme}; + pub use crate::{Also, ApplicationExt, Apply, Element, Renderer, Theme}; } pub use apply::{Also, Apply}; From 18debe546d55f92c5d118910c6c0053e75727ad5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 09:31:31 +0200 Subject: [PATCH 0094/1276] chore: no default features --- Cargo.toml | 4 +--- justfile | 19 +++++++++++++------ src/lib.rs | 8 ++++++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 28c771d6..9581498d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" name = "cosmic" [features] -default = ["wayland", "tokio"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Builds support for animated images @@ -121,6 +120,5 @@ exclude = [ "iced", ] - [patch."https://github.com/pop-os/libcosmic"] -libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} +libcosmic = { path = "./" } diff --git a/justfile b/justfile index b83e0cbc..08dd2d48 100644 --- a/justfile +++ b/justfile @@ -1,13 +1,20 @@ -projects := 'application cosmic cosmic_sctk design open_dialog' +examples := 'application cosmic cosmic_sctk design open_dialog' # Check for errors and linter warnings -check *args: - cargo clippy --no-deps {{args}} -- -W clippy::pedantic - cargo clippy --no-deps --no-default-features --features="winit,tokio" {{args}} -- -W clippy::pedantic - for project in {{projects}}; do \ - cargo check -p ${project}; \ +check *args: (check-wayland args) (check-winit args) (check-examples args) + +check-examples *args: + #!/bin/bash + for project in {{examples}}; do + cargo check -p ${project} {{args}} done +check-wayland *args: + cargo clippy --no-deps --features="wayland,tokio" {{args}} -- -W clippy::pedantic + +check-winit *args: + cargo clippy --no-deps --features="winit,tokio" {{args}} -- -W clippy::pedantic + # Runs a check with JSON message format for IDE integration check-json: (check '--message-format=json') diff --git a/src/lib.rs b/src/lib.rs index 7e73e102..b2da43e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,12 @@ #![allow(clippy::module_name_repetitions)] +#[cfg(all(not(feature = "wayland"), not(feature = "winit")))] +compile_error!("must define `wayland` or `winit` feature"); + +#[cfg(all(feature = "wayland", feature = "winit"))] +compile_error!("cannot use `wayland` feature with `winit"); + /// Recommended default imports. pub mod prelude { pub use crate::ext::*; @@ -55,3 +61,5 @@ pub mod widget; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; + + From 4e4eeaac129f55bb6e9282e67a90ec5983645701 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 1 Sep 2023 07:29:19 +0200 Subject: [PATCH 0095/1276] feat!(widget): rewrite button & icon widget APIs --- examples/application/src/main.rs | 4 +- examples/cosmic-sctk/Cargo.toml | 5 +- examples/cosmic-sctk/README.md | 10 +- examples/cosmic-sctk/src/window.rs | 47 +- examples/cosmic/Cargo.toml | 4 +- examples/cosmic/README.md | 10 +- examples/cosmic/src/window.rs | 33 +- examples/cosmic/src/window/demo.rs | 62 +- examples/cosmic/src/window/editor.rs | 9 +- .../cosmic/src/window/system_and_accounts.rs | 2 +- examples/open-dialog/src/main.rs | 6 +- iced | 2 +- res/external-link.svg | 6 + src/app/applet/mod.rs | 2 +- src/app/cosmic.rs | 1 + src/app/mod.rs | 151 ++-- src/app/settings.rs | 1 + src/theme/button.rs | 113 +++ src/theme/mod.rs | 144 ++-- src/widget/button.rs | 62 -- src/widget/button/hyperlink.rs | 50 ++ src/widget/button/icon.rs | 179 +++++ src/widget/button/mod.rs | 113 +++ src/widget/button/style.rs | 90 +++ src/widget/button/text.rs | 123 ++++ src/widget/button/widget.rs | 677 ++++++++++++++++++ src/widget/card/mod.rs | 3 + src/widget/card/style.rs | 3 + src/widget/flex_row.rs | 21 +- src/widget/header_bar.rs | 110 ++- src/widget/icon.rs | 311 -------- src/widget/icon/builder.rs | 114 +++ src/widget/icon/handle.rs | 88 +++ src/widget/icon/mod.rs | 104 +++ src/widget/list/column.rs | 9 +- src/widget/mod.rs | 4 +- src/widget/nav_bar.rs | 2 +- src/widget/nav_bar_toggle.rs | 38 +- src/widget/search/field.rs | 58 +- src/widget/search/mod.rs | 26 +- src/widget/segmented_button/horizontal.rs | 30 +- src/widget/segmented_button/mod.rs | 8 - src/widget/segmented_button/model/builder.rs | 12 +- src/widget/segmented_button/model/entity.rs | 12 +- src/widget/segmented_button/model/mod.rs | 37 +- src/widget/segmented_button/vertical.rs | 30 +- src/widget/segmented_button/widget.rs | 183 ++--- src/widget/segmented_selection.rs | 4 +- src/widget/settings/item.rs | 20 +- src/widget/settings/mod.rs | 7 +- src/widget/settings/section.rs | 9 +- src/widget/spin_button/mod.rs | 27 +- src/widget/text_input/cursor.rs | 4 + src/widget/text_input/editor.rs | 4 + src/widget/text_input/input.rs | 58 +- src/widget/text_input/mod.rs | 4 + src/widget/text_input/style.rs | 4 + src/widget/text_input/value.rs | 4 + src/widget/view_switcher.rs | 4 +- src/widget/warning.rs | 46 +- 60 files changed, 2191 insertions(+), 1113 deletions(-) create mode 100644 res/external-link.svg create mode 100644 src/theme/button.rs delete mode 100644 src/widget/button.rs create mode 100644 src/widget/button/hyperlink.rs create mode 100644 src/widget/button/icon.rs create mode 100644 src/widget/button/mod.rs create mode 100644 src/widget/button/style.rs create mode 100644 src/widget/button/text.rs create mode 100644 src/widget/button/widget.rs delete mode 100644 src/widget/icon.rs create mode 100644 src/widget/icon/builder.rs create mode 100644 src/widget/icon/handle.rs create mode 100644 src/widget/icon/mod.rs diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index e8d5fbe1..5f4de877 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -125,9 +125,9 @@ impl cosmic::Application for App { let text = cosmic::widget::text(page_content); - let centered = iced::widget::container(text) + let centered = cosmic::widget::container(text) .width(iced::Length::Fill) - .height(iced::Length::Fill) + .height(iced::Length::Shrink) .align_x(iced::alignment::Horizontal::Center) .align_y(iced::alignment::Vertical::Center); diff --git a/examples/cosmic-sctk/Cargo.toml b/examples/cosmic-sctk/Cargo.toml index c90d7f49..81f82ca9 100644 --- a/examples/cosmic-sctk/Cargo.toml +++ b/examples/cosmic-sctk/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" publish = false [dependencies] -libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] } -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] } +libcosmic = { path = "../..", features = ["wayland", "tokio"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", branch = "icon-color", default-features = false, features = ["libcosmic", "once_cell"] } +# cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"]} \ No newline at end of file diff --git a/examples/cosmic-sctk/README.md b/examples/cosmic-sctk/README.md index c52803b3..04483068 100644 --- a/examples/cosmic-sctk/README.md +++ b/examples/cosmic-sctk/README.md @@ -1,9 +1,3 @@ -# COSMIC -An example of the COSMIC design system. +# Deprecated -All the example code is located in the __[`main`](src/main.rs)__ file. - -You can run it with `cargo run`: -``` -cargo run --package cosmic --release -``` +This example will be removed once its contents are migrated to the design demo. \ No newline at end of file diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index 54d27757..8156ec61 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -10,22 +10,21 @@ use cosmic::{ }, iced_futures::Subscription, iced_style::application, - iced_widget::text, + prelude::*, theme::{self, Theme}, widget::{ button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle, rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate}, scrollable, search_input, secure_input, segmented_button, segmented_selection, settings, - text_input, IconSource, + text, text_input, }, - Element, ElementExt, + Element, }; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use std::{ sync::atomic::{AtomicU32, Ordering}, vec, }; -use theme::Button as ButtonTheme; static DEBUG_TOGGLER: Lazy = Lazy::new(id::Toggler::unique); static TOGGLER: Lazy = Lazy::new(id::Toggler::unique); @@ -138,7 +137,7 @@ impl Window { self.nav_bar_pages .insert() .text(page.title()) - .icon(IconSource::from(page.icon_name())) + .icon(icon::handle::from_name(page.icon_name()).icon()) .data(page) } @@ -373,15 +372,6 @@ impl Application for Window { } if !nav_bar_toggled { - let secondary = button(ButtonTheme::Secondary) - .text("Secondary") - .on_press(Message::ButtonPressed); - - let secondary = if let Some(tracker) = self.rectangle_tracker.as_ref() { - tracker.container(0, secondary).into() - } else { - secondary.into() - }; let content: Element<_> = settings::view_column(vec![ settings::view_section("Debug") .add(settings::item( @@ -396,34 +386,6 @@ impl Application for Window { )), )) .into(), - settings::view_section("Buttons") - .add(settings::item_row(vec![ - button(ButtonTheme::Primary) - .text("Primary") - .on_press(Message::ButtonPressed) - .into(), - secondary, - button(ButtonTheme::Positive) - .text("Positive") - .on_press(Message::ButtonPressed) - .into(), - button(ButtonTheme::Destructive) - .text("Destructive") - .on_press(Message::ButtonPressed) - .into(), - button(ButtonTheme::Text) - .text("Text") - .on_press(Message::ButtonPressed) - .into(), - ])) - .add(settings::item_row(vec![ - button(ButtonTheme::Primary).text("Primary").into(), - button(ButtonTheme::Secondary).text("Secondary").into(), - button(ButtonTheme::Positive).text("Positive").into(), - button(ButtonTheme::Destructive).text("Destructive").into(), - button(ButtonTheme::Text).text("Text").into(), - ])) - .into(), settings::view_section("Controls") .add(settings::item( "Toggler", @@ -567,6 +529,7 @@ impl Application for Window { fn style(&self) -> ::Style { cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance { background_color: Color::TRANSPARENT, + icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), })) } diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 0ca25e1b..74b7e30c 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,9 +8,9 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.13.0" -libcosmic = { path = "../..", default-features = false, features = ["debug", "winit"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" log = "0.4.17" -cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time", branch="icon-color", default-features = false, features = ["libcosmic", "once_cell"] } diff --git a/examples/cosmic/README.md b/examples/cosmic/README.md index c52803b3..04483068 100644 --- a/examples/cosmic/README.md +++ b/examples/cosmic/README.md @@ -1,9 +1,3 @@ -# COSMIC -An example of the COSMIC design system. +# Deprecated -All the example code is located in the __[`main`](src/main.rs)__ file. - -You can run it with `cargo run`: -``` -cargo run --package cosmic --release -``` +This example will be removed once its contents are migrated to the design demo. \ No newline at end of file diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 964d2f94..446b3cb7 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -13,12 +13,13 @@ use cosmic::{ window::{self, close, drag, minimize, toggle_maximize}, }, keyboard_nav, + prelude::*, theme::{self, Theme}, widget::{ - header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings, - warning, IconSource, + button, header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, + settings, warning, }, - Element, ElementExt, + Element, }; use cosmic_time::{Instant, Timeline}; use std::{ @@ -224,7 +225,7 @@ impl Window { self.nav_bar .insert() .text(page.title()) - .icon(IconSource::from(page.icon_name())) + .icon(icon::handle::from_name(page.icon_name()).icon()) .secondary(&mut self.nav_id_to_page, page) } @@ -247,14 +248,10 @@ impl Window { ) -> Element { let page = sub_page.parent_page(); column!( - iced::widget::Button::new(row!( - icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink), - text(page.title()).size(14), - )) - .padding(0) - .style(theme::Button::Link) - // .id(BTN.clone()) - .on_press(Message::from(page)), + button::icon(icon::handle::from_name("go-previous-symbolic").size(16)) + .label(page.title()) + .padding(0) + .on_press(Message::from(page)), row!( text(sub_page.title()).size(28), horizontal_space(Length::Fill), @@ -276,8 +273,9 @@ impl Window { iced::widget::Button::new( container( settings::item_row(vec![ - icon(sub_page.icon_name(), 20) - .style(theme::Svg::Symbolic) + icon::handle::from_name(sub_page.icon_name()) + .size(20) + .icon() .into(), column!( text(sub_page.title()).size(14), @@ -286,8 +284,9 @@ impl Window { .spacing(2) .into(), horizontal_space(iced::Length::Fill).into(), - icon("go-next-symbolic", 20) - .style(theme::Svg::Symbolic) + icon::handle::from_name("go-next-symbolic") + .size(20) + .icon() .into(), ]) .spacing(16), @@ -296,7 +295,7 @@ impl Window { .style(theme::Container::custom(list::column::style)), ) .padding(0) - .style(theme::Button::Transparent) + .style(theme::IcedButton::Transparent) .on_press(Message::from(sub_page.into_page())) // .id(BTN.clone()) .into() diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 69be5ccd..9df671d7 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -7,8 +7,8 @@ use cosmic::{ iced::{id, Alignment, Length}, theme::{self, Button as ButtonTheme, ThemeType}, widget::{ - button, container, icon, segmented_button, segmented_selection, settings, spin_button, - toggler, view_switcher, + button, cosmic_container::container, icon, segmented_button, segmented_selection, settings, + spin_button, toggler, view_switcher, }, Element, }; @@ -186,7 +186,7 @@ impl State { Message::IconTheme(key) => { self.icon_themes.activate(key); if let Some(theme) = self.icon_themes.text(key) { - cosmic::icon_theme::set_default(theme); + cosmic::icon_theme::set_default(theme.to_owned()); } } Message::InputChanged(s) => { @@ -255,45 +255,13 @@ impl State { "Scaling Factor", spin_button(&window.scale_factor_string, Message::ScalingFactor), )) - .add(settings::item_row(vec![button(ButtonTheme::Destructive) - .on_press(Message::ToggleWarning) - .custom(vec![ - icon("dialog-warning-symbolic", 16) - .style(theme::Svg::SymbolicPrimary) - .into(), - text("Do Not Touch").into(), - ]) - .into()])) - .into(), - settings::view_section("Buttons") .add(settings::item_row(vec![ - button(ButtonTheme::Primary) - .text("Primary") - .on_press(Message::ButtonPressed) + cosmic::widget::button::destructive("Do not Touch") + .trailing_icon( + icon::handle::from_name("dialog-warning-symbolic").size(16), + ) + .on_press(Message::ToggleWarning) .into(), - button(ButtonTheme::Secondary) - .text("Secondary") - .on_press(Message::ButtonPressed) - .into(), - button(ButtonTheme::Positive) - .text("Positive") - .on_press(Message::ButtonPressed) - .into(), - button(ButtonTheme::Destructive) - .text("Destructive") - .on_press(Message::ButtonPressed) - .into(), - button(ButtonTheme::Text) - .text("Text") - .on_press(Message::ButtonPressed) - .into(), - ])) - .add(settings::item_row(vec![ - button(ButtonTheme::Primary).text("Primary").into(), - button(ButtonTheme::Secondary).text("Secondary").into(), - button(ButtonTheme::Positive).text("Positive").into(), - button(ButtonTheme::Destructive).text("Destructive").into(), - button(ButtonTheme::Text).text("Text").into(), ])) .into(), settings::view_section("Controls") @@ -454,9 +422,13 @@ impl State { "Primary container with some text and a couple icons testing default fallbacks" ) .size(24), - icon("microphone-sensitivity-high-symbolic-test", 24) - .style(cosmic::theme::Svg::SymbolicActive), - icon("microphone-sensitivity-high-symbolic-test", 16).default_fallbacks(false) + icon::handle::from_name("microphone-sensitivity-high-symbolic-test") + .size(24) + .icon(), + icon::handle::from_name("microphone-sensitivity-high-symbolic-test") + .size(24) + .fallback(false) + .icon(), ]) .layer(cosmic_theme::Layer::Primary) .padding(8) @@ -475,9 +447,7 @@ impl State { .iter() .enumerate() .map(|(i, c)| column![ - button(cosmic::theme::Button::Text) - .text("Delete me") - .on_press(Message::DeleteCard(i)), + button::text("Delete me").on_press(Message::DeleteCard(i)), text(c).size(24).width(Length::Fill) ] .into()) diff --git a/examples/cosmic/src/window/editor.rs b/examples/cosmic/src/window/editor.rs index 6c027c09..3c66a6e2 100644 --- a/examples/cosmic/src/window/editor.rs +++ b/examples/cosmic/src/window/editor.rs @@ -1,7 +1,7 @@ use cosmic::iced::widget::{horizontal_space, row}; use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{button, segmented_button, view_switcher}; -use cosmic::{theme, Element}; +use cosmic::widget::{button, icon, segmented_button, view_switcher}; +use cosmic::{theme, Apply, Element}; use slotmap::Key; #[derive(Clone, Copy, Debug)] @@ -66,8 +66,9 @@ impl State { .on_close(Message::Close) .width(Length::Shrink); - let new_tab_button = button(theme::Button::Text) - .icon(theme::Svg::Symbolic, "tab-new-symbolic", 20) + let new_tab_button = icon::handle::from_name("tab-new-symbolic") + .size(20) + .apply(button::icon) .on_press(Message::AddNew); let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center); diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index 7047b196..f05a44e8 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -62,7 +62,7 @@ impl State { window.parent_page_button(SystemAndAccountsPage::About), row!( horizontal_space(Length::Fill), - icon("distributor-logo", 78), + icon::handle::from_name("distributor-logo").size(78).icon(), horizontal_space(Length::Fill), ) .into(), diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 6533394a..9e6297a1 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -7,6 +7,7 @@ use apply::Apply; use cosmic::app::{Command, Core, Settings}; use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::iced_core::Length; +use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; use tokio::io::AsyncReadExt; use url::Url; @@ -82,10 +83,7 @@ impl cosmic::Application for App { fn header_end(&self) -> Vec> { // Places a button the header to create open dialogs. - vec![cosmic::widget::button(cosmic::theme::Button::Primary) - .text("Open") - .on_press(Message::OpenFile) - .into()] + vec![button::suggested("Open").on_press(Message::OpenFile).into()] } fn subscription(&self) -> cosmic::iced_futures::Subscription { diff --git a/iced b/iced index 2ead0da0..8b2389f1 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2ead0da06f6da58b01e107104808b45d6fb61e85 +Subproject commit 8b2389f144966a5f9b60ab778c1073748fee5e70 diff --git a/res/external-link.svg b/res/external-link.svg new file mode 100644 index 00000000..156f00bc --- /dev/null +++ b/res/external-link.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs index 92aca686..a6a70390 100644 --- a/src/app/applet/mod.rs +++ b/src/app/applet/mod.rs @@ -127,7 +127,7 @@ impl CosmicAppletHelper { pub fn icon_button<'a, Message: 'static>( &self, icon_name: &'a str, - ) -> iced::widget::Button<'a, Message, Renderer> { + ) -> crate::widget::Button<'a, Message, Renderer> { crate::widget::button(theme::Button::Text) .icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0) .padding(8) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6758e4d0..74d15f08 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -110,6 +110,7 @@ where } else { theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { background_color: iced_core::Color::TRANSPARENT, + icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), })) } diff --git a/src/app/mod.rs b/src/app/mod.rs index f84d7e0e..603de483 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -41,9 +41,9 @@ pub mod message { pub use self::command::Command; pub use self::core::Core; pub use self::settings::Settings; +use crate::prelude::*; use crate::theme::THEME; use crate::widget::nav_bar; -use crate::{Element, ElementExt}; use apply::Apply; use iced::Subscription; use iced::{window, Application as IcedApplication}; @@ -261,91 +261,90 @@ impl ApplicationExt for App { } /// Creates the view for the main window. - fn view_main<'a>(&'a self) -> Element<'a, Message> { + fn view_main(&self) -> Element> { let core = self.core(); let is_condensed = core.is_condensed(); - let mut main: Vec>> = Vec::with_capacity(2); - if core.window.show_headerbar { - main.push({ - let mut header = crate::widget::header_bar() - .title(self.title()) - .on_drag(Message::Cosmic(cosmic::Message::Drag)) - .on_close(Message::Cosmic(cosmic::Message::Close)); + crate::widget::column::with_capacity(2) + .push_maybe(if core.window.show_headerbar { + Some({ + let mut header = crate::widget::header_bar() + .title(self.title()) + .on_drag(Message::Cosmic(cosmic::Message::Drag)) + .on_close(Message::Cosmic(cosmic::Message::Close)); - if self.nav_model().is_some() { - let toggle = crate::widget::nav_bar_toggle() - .active(core.nav_bar_active()) - .on_toggle(if is_condensed { - Message::Cosmic(cosmic::Message::ToggleNavBarCondensed) - } else { - Message::Cosmic(cosmic::Message::ToggleNavBar) - }); + if self.nav_model().is_some() { + let toggle = crate::widget::nav_bar_toggle() + .active(core.nav_bar_active()) + .on_toggle(if is_condensed { + Message::Cosmic(cosmic::Message::ToggleNavBarCondensed) + } else { + Message::Cosmic(cosmic::Message::ToggleNavBar) + }); - header = header.start(toggle); - } - - if core.window.show_maximize { - header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); - } - - if core.window.show_minimize { - header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); - } - - for element in self.header_start() { - header = header.start(element.map(Message::App)); - } - - for element in self.header_center() { - header = header.center(element.map(Message::App)); - } - - for element in self.header_end() { - header = header.end(element.map(Message::App)); - } - - Element::from(header).debug(core.debug) - }); - } - - // The content element contains every element beneath the header. - main.push( - iced::widget::row({ - let mut widgets = Vec::with_capacity(2); - - // Insert nav bar onto the left side of the window. - if core.nav_bar_active() { - if let Some(nav_model) = self.nav_model() { - let mut nav = crate::widget::nav_bar(nav_model, |entity| { - Message::Cosmic(cosmic::Message::NavBar(entity)) - }); - - if !is_condensed { - nav = nav.max_width(300); - } - - widgets.push(nav.apply(Element::from).debug(core.debug)); + header = header.start(toggle); } - } - if self.nav_model().is_none() || core.show_content() { - let main_content = self.view().debug(core.debug).map(Message::App); + if core.window.show_maximize { + header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); + } - widgets.push(main_content); - } + if core.window.show_minimize { + header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); + } - widgets + for element in self.header_start() { + header = header.start(element.map(Message::App)); + } + + for element in self.header_center() { + header = header.center(element.map(Message::App)); + } + + for element in self.header_end() { + header = header.end(element.map(Message::App)); + } + + header + }) + } else { + None }) - .spacing(8) - .apply(iced::widget::container) - .padding([0, 8, 8, 8]) - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .style(crate::theme::Container::Background) - .into(), - ); + // The content element contains every element beneath the header. + .push( + crate::widget::row::with_children({ + let mut widgets = Vec::with_capacity(2); - iced::widget::column(main).into() + // Insert nav bar onto the left side of the window. + if core.nav_bar_active() { + if let Some(nav_model) = self.nav_model() { + let mut nav = crate::widget::nav_bar(nav_model, |entity| { + Message::Cosmic(cosmic::Message::NavBar(entity)) + }); + + if !is_condensed { + nav = nav.max_width(300); + } + + widgets.push(nav.apply(Element::from).debug(core.debug)); + } + } + + if self.nav_model().is_none() || core.show_content() { + let main_content = self.view().debug(core.debug).map(Message::App); + + widgets.push(main_content); + } + + widgets + }) + .spacing(8) + .apply(crate::widget::container) + .padding([0, 8, 8, 8]) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .style(crate::theme::Container::Background), + ) + .into() } } diff --git a/src/app/settings.rs b/src/app/settings.rs index 99ab8de6..1819407e 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -10,6 +10,7 @@ use iced_core::Font; /// Configure a new COSMIC application. #[allow(clippy::struct_excessive_bools)] +#[must_use] #[derive(derive_setters::Setters)] pub struct Settings { /// Produces a smoother result in some widgets, at a performance cost. diff --git a/src/theme/button.rs b/src/theme/button.rs new file mode 100644 index 00000000..3aed8186 --- /dev/null +++ b/src/theme/button.rs @@ -0,0 +1,113 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use cosmic_theme::Component; +use iced_core::{Background, Color}; +use palette::{rgb::Rgb, Alpha}; + +use crate::{ + app, + widget::button::{Appearance, StyleSheet}, +}; + +#[derive(Copy, Clone, Debug, Default)] +pub enum Button { + Destructive, + Link, + Icon, + #[default] + Standard, + Suggested, + Text, +} + +pub fn appearance( + theme: &crate::Theme, + focused: bool, + style: &Button, + color: fn(&Component>) -> Color, +) -> Appearance { + let cosmic = theme.cosmic(); + let mut corner_radii = &cosmic.corner_radii.radius_xl; + let mut appearance = Appearance::new(); + + match style { + Button::Standard => { + let component = &theme.current_container().component; + appearance.background = Some(Background::Color(color(component))); + appearance.text_color = component.on.into(); + } + + Button::Icon | Button::Text => { + let component = &cosmic.text_button; + appearance.background = None; + appearance.text_color = component.on.into(); + } + + Button::Suggested => { + let component = &cosmic.accent_button; + appearance.background = Some(Background::Color(color(component))); + appearance.icon_color = Some(component.on.into()); + appearance.text_color = component.on.into(); + } + + Button::Destructive => { + let component = &cosmic.destructive_button; + appearance.background = Some(Background::Color(color(component))); + appearance.icon_color = Some(component.on.into()); + appearance.text_color = component.on.into(); + } + + Button::Link => { + appearance.background = None; + appearance.icon_color = Some(cosmic.accent.base.into()); + appearance.text_color = cosmic.accent.base.into(); + corner_radii = &cosmic.corner_radii.radius_0; + } + } + + appearance.border_radius = (*corner_radii).into(); + + if focused { + appearance.outline_width = 1.0; + appearance.outline_color = cosmic.accent.base.into(); + appearance.border_width = 2.0; + appearance.border_color = Color::TRANSPARENT; + } + + appearance +} + +impl StyleSheet for crate::Theme { + type Style = Button; + + fn active(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| component.base.into()) + } + + fn disabled(&self, style: &Self::Style) -> Appearance { + appearance(self, false, style, |component| { + let mut color = Color::from(component.base); + color.a *= 0.5; + color + }) + } + + fn drop_target(&self, style: &Self::Style) -> Appearance { + let mut appearance = self.active(false, style); + + appearance + } + + fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| component.hover.into()) + } + + fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| component.pressed.into()) + } + + fn selected(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| component.selected.into()) + } +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 16b14706..3cfa7820 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -4,17 +4,18 @@ //! Use COSMIC's themes and styles. pub mod expander; + +mod button; +pub use self::button::Button; + mod segmented_button; +pub use self::segmented_button::SegmentedButton; use std::cell::RefCell; use std::f32::consts::PI; -use std::hash::Hash; -use std::hash::Hasher; use std::rc::Rc; use std::sync::Arc; -pub use self::segmented_button::SegmentedButton; - use cosmic_config::config_subscription; use cosmic_config::CosmicConfigEntry; use cosmic_theme::composite::over; @@ -26,7 +27,7 @@ use iced_core::BorderRadius; use iced_core::Radians; use iced_futures::Subscription; use iced_style::application; -use iced_style::button; +use iced_style::button as iced_button; use iced_style::checkbox; use iced_style::container; use iced_style::menu; @@ -195,6 +196,7 @@ impl application::StyleSheet for Theme { match style { Application::Default => application::Appearance { + icon_color: cosmic.bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), }, @@ -203,13 +205,13 @@ impl application::StyleSheet for Theme { } } -/* - * TODO: Button - */ -pub enum Button { +/// Styles for the button widget from iced-rs. +#[derive(Default)] +pub enum IcedButton { Deactivated, Destructive, Positive, + #[default] Primary, Secondary, Text, @@ -218,110 +220,104 @@ pub enum Button { Transparent, Card, Custom { - active: Box button::Appearance>, - hover: Box button::Appearance>, + active: Box iced_button::Appearance>, + hover: Box iced_button::Appearance>, }, } -impl Default for Button { - fn default() -> Self { - Self::Primary - } -} - -impl Button { +impl IcedButton { #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::match_same_arms)] fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { let cosmic = theme.cosmic(); match self { - Button::Primary => &cosmic.accent_button, - Button::Secondary => &theme.current_container().component, - Button::Positive => &cosmic.success_button, - Button::Destructive => &cosmic.destructive_button, - Button::Text => &cosmic.text_button, - Button::Link => &cosmic.accent_button, - Button::LinkActive => &cosmic.accent_button, - Button::Transparent => &TRANSPARENT_COMPONENT, - Button::Deactivated => &theme.current_container().component, - Button::Card => &theme.current_container().component, - Button::Custom { .. } => &TRANSPARENT_COMPONENT, + IcedButton::Primary => &cosmic.accent_button, + IcedButton::Secondary => &theme.current_container().component, + IcedButton::Positive => &cosmic.success_button, + IcedButton::Destructive => &cosmic.destructive_button, + IcedButton::Text => &cosmic.text_button, + IcedButton::Link => &cosmic.accent_button, + IcedButton::LinkActive => &cosmic.accent_button, + IcedButton::Transparent => &TRANSPARENT_COMPONENT, + IcedButton::Deactivated => &theme.current_container().component, + IcedButton::Card => &theme.current_container().component, + IcedButton::Custom { .. } => &TRANSPARENT_COMPONENT, } } } -impl button::StyleSheet for Theme { - type Style = Button; +impl iced_button::StyleSheet for Theme { + type Style = IcedButton; - fn active(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom { active, .. } = style { + fn active(&self, style: &Self::Style) -> iced_button::Appearance { + if let IcedButton::Custom { active, .. } = style { return active(self); } let corner_radii = &self.cosmic().corner_radii; let component = style.cosmic(self); - button::Appearance { + iced_button::Appearance { border_radius: match style { - Button::Link => corner_radii.radius_0.into(), - Button::Card => corner_radii.radius_xs.into(), + IcedButton::Link => corner_radii.radius_0.into(), + IcedButton::Card => corner_radii.radius_xs.into(), _ => corner_radii.radius_xl.into(), }, background: match style { - Button::Link | Button::Text => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), + IcedButton::Link | IcedButton::Text => None, + IcedButton::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.base.into())), }, text_color: match style { - Button::Link | Button::LinkActive => component.base.into(), + IcedButton::Link | IcedButton::LinkActive => component.base.into(), _ => component.on.into(), }, - ..button::Appearance::default() + ..iced_button::Appearance::default() } } - fn hovered(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom { hover, .. } = style { + fn hovered(&self, style: &Self::Style) -> iced_button::Appearance { + if let IcedButton::Custom { hover, .. } = style { return hover(self); } let active = self.active(style); let component = style.cosmic(self); - button::Appearance { + iced_button::Appearance { background: match style { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), + IcedButton::Link => None, + IcedButton::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.hover.into())), }, ..active } } - fn focused(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom { hover, .. } = style { + fn focused(&self, style: &Self::Style) -> iced_button::Appearance { + if let IcedButton::Custom { hover, .. } = style { return hover(self); } let active = self.active(style); let component = style.cosmic(self); - button::Appearance { + iced_button::Appearance { background: match style { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), + IcedButton::Link => None, + IcedButton::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.hover.into())), }, ..active } } - fn disabled(&self, style: &Self::Style) -> button::Appearance { + fn disabled(&self, style: &Self::Style) -> iced_button::Appearance { let active = self.active(style); - if matches!(style, Button::Card) { + if matches!(style, IcedButton::Card) { return active; } - button::Appearance { + iced_button::Appearance { shadow_offset: iced_core::Vector::default(), background: active.background.map(|background| match background { Background::Color(color) => Background::Color(Color { @@ -563,6 +559,7 @@ impl container::StyleSheet for Theme { let palette = self.cosmic(); container::Appearance { + icon_color: Some(Color::from(palette.background.on)), text_color: Some(Color::from(palette.background.on)), background: Some(iced::Background::Color(palette.background.base.into())), border_radius: 2.0.into(), @@ -577,6 +574,7 @@ impl container::StyleSheet for Theme { header_top.alpha = 0.8; container::Appearance { + icon_color: Some(Color::from(palette.accent.base)), text_color: Some(Color::from(palette.background.on)), background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( Linear::new(Radians(3.0 * PI / 2.0)) @@ -592,6 +590,7 @@ impl container::StyleSheet for Theme { let palette = self.cosmic(); container::Appearance { + icon_color: Some(Color::from(palette.primary.on)), text_color: Some(Color::from(palette.primary.on)), background: Some(iced::Background::Color(palette.primary.base.into())), border_radius: 2.0.into(), @@ -603,6 +602,7 @@ impl container::StyleSheet for Theme { let palette = self.cosmic(); container::Appearance { + icon_color: Some(Color::from(palette.secondary.on)), text_color: Some(Color::from(palette.secondary.on)), background: Some(iced::Background::Color(palette.secondary.base.into())), border_radius: 2.0.into(), @@ -615,6 +615,7 @@ impl container::StyleSheet for Theme { match self.layer { cosmic_theme::Layer::Background => container::Appearance { + icon_color: Some(Color::from(palette.background.component.on)), text_color: Some(Color::from(palette.background.component.on)), background: Some(iced::Background::Color( palette.background.component.base.into(), @@ -624,6 +625,7 @@ impl container::StyleSheet for Theme { border_color: Color::TRANSPARENT, }, cosmic_theme::Layer::Primary => container::Appearance { + icon_color: Some(Color::from(palette.primary.component.on)), text_color: Some(Color::from(palette.primary.component.on)), background: Some(iced::Background::Color( palette.primary.component.base.into(), @@ -633,6 +635,7 @@ impl container::StyleSheet for Theme { border_color: Color::TRANSPARENT, }, cosmic_theme::Layer::Secondary => container::Appearance { + icon_color: Some(Color::from(palette.secondary.component.on)), text_color: Some(Color::from(palette.secondary.component.on)), background: Some(iced::Background::Color( palette.secondary.component.base.into(), @@ -1021,29 +1024,6 @@ pub enum Svg { /// No filtering is applied #[default] Default, - /// Icon fill color will match text color - Symbolic, - /// Icon fill color will match accent color - SymbolicActive, - /// Icon fill color will match on primary color - SymbolicPrimary, - /// Icon fill color will use accent color - SymbolicLink, -} - -impl Hash for Svg { - fn hash(&self, state: &mut H) { - let id = match self { - Svg::Custom(_) => 0, - Svg::Default => 1, - Svg::Symbolic => 2, - Svg::SymbolicActive => 3, - Svg::SymbolicPrimary => 4, - Svg::SymbolicLink => 5, - }; - - id.hash(state); - } } impl Svg { @@ -1060,18 +1040,6 @@ impl svg::StyleSheet for Theme { match style { Svg::Default => svg::Appearance::default(), Svg::Custom(appearance) => appearance(self), - Svg::Symbolic => svg::Appearance { - color: Some(self.current_container().on.into()), - }, - Svg::SymbolicActive => svg::Appearance { - color: Some(self.cosmic().accent.base.into()), - }, - Svg::SymbolicPrimary => svg::Appearance { - color: Some(self.cosmic().accent.on.into()), - }, - Svg::SymbolicLink => svg::Appearance { - color: Some(self.cosmic().accent.base.into()), - }, } } } diff --git a/src/widget/button.rs b/src/widget/button.rs deleted file mode 100644 index 1bd5f106..00000000 --- a/src/widget/button.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::{ - theme::{self, THEME}, - Element, Renderer, -}; -use iced::widget; - -/// A button widget with COSMIC styling -#[must_use] -pub const fn button(style: theme::Button) -> Button { - Button { - style, - message: None, - } -} - -/// A button widget with COSMIC styling -pub struct Button { - style: theme::Button, - message: Option, -} - -impl Button { - /// The message to emit on button press. - #[must_use] - pub fn on_press(mut self, message: Message) -> Self { - self.message = Some(message); - self - } - - /// A button with an icon. - pub fn icon( - self, - style: theme::Svg, - icon: &str, - size: u16, - ) -> widget::Button { - self.custom(vec![super::icon(icon, size).style(style).into()]) - } - - /// A button with text. - pub fn text(self, text: &str) -> widget::Button { - self.custom(vec![text.into()]) - } - - /// A custom button that has the desired default spacing and padding. - pub fn custom(self, children: Vec>) -> widget::Button { - let theme = THEME.with(|t| t.borrow().clone()); - let theme = theme.cosmic(); - let button = widget::button(widget::row(children).spacing(8)) - .style(self.style) - .padding([theme.space_xxs(), theme.space_s()]); - - if let Some(message) = self.message { - button.on_press(message) - } else { - button - } - } -} diff --git a/src/widget/button/hyperlink.rs b/src/widget/button/hyperlink.rs new file mode 100644 index 00000000..472d3162 --- /dev/null +++ b/src/widget/button/hyperlink.rs @@ -0,0 +1,50 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::Builder; +use super::Style; +use crate::widget::icon::{self, Handle}; +use iced_core::{font::Weight, widget::Id, Length, Padding}; +use std::borrow::Cow; + +pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>; + +pub struct Hyperlink { + trailing_icon: bool, +} + +pub fn hyperlink<'a, Message>() -> Button<'a, Message> { + Button::new(Hyperlink { + trailing_icon: false, + }) +} + +impl<'a, Message> Button<'a, Message> { + pub fn new(link: Hyperlink) -> Self { + Self { + id: Id::unique(), + label: Cow::Borrowed(""), + on_press: None, + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::from(0), + spacing: 0, + icon_size: 16, + line_height: 20, + font_size: 14, + font_weight: Weight::Normal, + style: Style::Link, + variant: link, + } + } + + pub const fn with_icon(mut self) -> Self { + self.variant.trailing_icon = true; + self + } +} + +pub fn icon() -> Handle { + icon::handle::from_svg_bytes(&include_bytes!("../../../res/external-link.svg")[..]) + .symbolic(true) +} diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs new file mode 100644 index 00000000..c03aaba9 --- /dev/null +++ b/src/widget/button/icon.rs @@ -0,0 +1,179 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{button, Builder, Style}; +use crate::{widget::icon::Handle, Element}; +use apply::Apply; +use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding}; +use std::borrow::Cow; + +pub type Button<'a, Message> = Builder<'a, Message, Icon>; + +pub struct Icon { + handle: Handle, + selected: bool, + vertical: bool, +} + +pub fn icon(handle: impl Into) -> Button<'static, Message> { + Button::new(Icon { + handle: handle.into(), + selected: false, + vertical: false, + }) +} + +impl<'a, Message> Button<'a, Message> { + pub fn new(icon: Icon) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + let padding = theme.space_xxs(); + + Self { + id: Id::unique(), + label: Cow::Borrowed(""), + on_press: None, + width: Length::Shrink, + height: Length::Fixed(46.0), + padding: Padding::from(padding), + spacing: theme.space_xxxs(), + icon_size: 16, + line_height: 20, + font_size: 14, + font_weight: Weight::Normal, + style: Style::Icon, + variant: icon, + } + }) + } + + /// Applies the **Extra Small** button size preset. + pub fn extra_small(mut self) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + + self.font_size = 14; + self.font_weight = Weight::Normal; + self.icon_size = 16; + self.line_height = 20; + self.height = Length::Fixed(36.0); + self.padding = Padding::from(theme.space_xxs()); + self.spacing = theme.space_xxxs(); + }); + + self + } + + /// Applies the **Medium** button size preset. + pub fn medium(mut self) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + + self.font_size = 24; + self.font_weight = Weight::Normal; + self.icon_size = 32; + self.line_height = 32; + self.height = Length::Fixed(56.0); + self.padding = Padding::from(theme.space_xs()); + self.spacing = theme.space_xxs(); + }); + + self + } + + /// Applies the **Large** button size preset. + pub fn large(mut self) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + + self.font_size = 28; + self.font_weight = Weight::Light; + self.icon_size = 40; + self.line_height = 36; + self.height = Length::Fixed(64.0); + self.padding = Padding::from(theme.space_xs()); + self.spacing = theme.space_xxs(); + }); + + self + } + + /// Applies the **Extra Large** button size preset. + pub fn extra_large(mut self) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + let padding = theme.space_xs(); + + self.font_size = 32; + self.font_weight = Weight::Light; + self.icon_size = 56; + self.line_height = 44; + self.height = Length::Fixed(80.0); + self.padding = Padding::from(padding); + self.spacing = theme.space_xxs(); + }); + + self + } + + pub fn selected(mut self, selected: bool) -> Self { + self.variant.selected = selected; + self + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(builder: Button<'a, Message>) -> Element<'a, Message> { + let mut content = Vec::with_capacity(2); + + content.push( + crate::widget::icon(builder.variant.handle.clone()) + .size(builder.icon_size) + .into(), + ); + + if !builder.label.is_empty() { + content.push( + crate::widget::text(builder.label) + .size(builder.font_size) + .line_height(LineHeight::Absolute(builder.line_height.into())) + .font({ + let mut font = crate::font::DEFAULT; + font.weight = builder.font_weight; + font + }) + .into(), + ); + } + + let button = if builder.variant.vertical { + crate::widget::column::with_children(content) + .padding(builder.padding) + .width(builder.width) + .height(builder.height) + .spacing(builder.spacing) + .align_items(Alignment::Center) + .apply(button) + } else { + crate::widget::row::with_children(content) + .padding(builder.padding) + .width(builder.width) + .height(builder.height) + .spacing(builder.spacing) + .align_items(Alignment::Center) + .apply(button) + }; + + button + .padding(0) + .id(builder.id) + .on_press_maybe(builder.on_press) + .style(builder.style) + .into() + } +} diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs new file mode 100644 index 00000000..853d6c67 --- /dev/null +++ b/src/widget/button/mod.rs @@ -0,0 +1,113 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod hyperlink; +pub use hyperlink::Button as LinkButton; + +mod icon; +pub use icon::icon; +pub use icon::Button as IconButton; + +mod style; +pub use style::{Appearance, StyleSheet}; + +mod text; +pub use text::Button as TextButton; +pub use text::{destructive, standard, suggested, text}; + +mod widget; +pub use widget::{draw, focus, layout, mouse_interaction, Button}; + +pub use crate::theme::Button as Style; +use crate::Element; +use iced_core::font::Weight; +use iced_core::widget::Id; +use iced_core::{Length, Padding}; +use std::borrow::Cow; + +pub fn button<'a, Message>( + content: impl Into>, +) -> Button<'a, Message, crate::Renderer> { + Button::new(content) +} + +#[must_use] +pub struct Builder<'a, Message, Variant> { + id: Id, + label: Cow<'a, str>, + // tooltip: Cow<'a, str>, + on_press: Option, + width: Length, + height: Length, + padding: Padding, + spacing: u16, + icon_size: u16, + line_height: u16, + font_size: u16, + font_weight: Weight, + style: Style, + variant: Variant, +} + +// /// A [`Button`] with an icon, which may be used in place of text. +// pub const fn icon<'a>(selected: bool) -> Button<'a> { +// Builder::new(Standard::Icon { selected }) +// } + +impl<'a, Message, Variant> Builder<'a, Message, Variant> { + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into(); + self + } + + /// Sets the width of the [`Button`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Button`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + pub fn on_press_maybe(mut self, on_press: Option) -> Self { + self.on_press = on_press; + self + } + + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self + } + + pub fn tooltip(mut self, label: impl Into>) -> Self { + self.label = label.into(); + self + } +} diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs new file mode 100644 index 00000000..2304ff15 --- /dev/null +++ b/src/widget/button/style.rs @@ -0,0 +1,90 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Change the apperance of a button. +use iced_core::{Background, BorderRadius, Color, Vector}; + +/// The appearance of a button. +#[must_use] +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The amount of offset to apply to the shadow of the button. + pub shadow_offset: Vector, + + /// The [`Background`] of the button. + pub background: Option, + + /// The border radius of the button. + pub border_radius: BorderRadius, + + /// The border width of the button. + pub border_width: f32, + + /// The border [`Color`] of the button. + pub border_color: Color, + + /// Opacity of the button. + pub opacity: f32, + + /// An outline placed around the border. + pub outline_width: f32, + + /// The [`Color`] of the outline. + pub outline_color: Color, + + /// The icon [`Color`] of the button. + pub icon_color: Option, + + /// The text [`Color`] of the button. + pub text_color: Color, +} + +impl Appearance { + // TODO: `BorderRadius` is not `const fn` compatible. + pub fn new() -> Self { + Self { + shadow_offset: Vector::new(0.0, 0.0), + background: None, + border_radius: BorderRadius::from(0.0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + opacity: 1.0, + outline_width: 0.0, + outline_color: Color::TRANSPARENT, + icon_color: None, + text_color: Color::BLACK, + } + } +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self::new() + } +} + +/// A set of rules that dictate the style of a button. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a button. + fn active(&self, focused: bool, style: &Self::Style) -> Appearance; + + /// Produces the disabled [`Appearance`] of a button. + fn disabled(&self, style: &Self::Style) -> Appearance; + + /// [`Appearance`] when the button is the target of a DND operation. + fn drop_target(&self, style: &Self::Style) -> Appearance { + self.hovered(false, style) + } + + /// Produces the hovered [`Appearance`] of a button. + fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance; + + /// Produces the pressed [`Appearance`] of a button. + fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance; + + /// [`Appearance`] when a button is selected. + fn selected(&self, focused: bool, style: &Self::Style) -> Appearance; +} diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs new file mode 100644 index 00000000..5d6a9fc9 --- /dev/null +++ b/src/widget/button/text.rs @@ -0,0 +1,123 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{button, Builder, Style}; +use crate::widget::{self, icon::Handle, row}; +use crate::{ext::CollectionWidget, Element}; +use apply::Apply; +use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding}; +use std::borrow::Cow; + +/// A [`Button`] with the highest level of attention. +/// +/// There should only be one primary button used per page. +pub type Button<'a, Message> = Builder<'a, Message, Text>; + +pub fn destructive<'a, Message>(label: impl Into>) -> Button<'a, Message> { + Button::new(Text::new()) + .label(label) + .style(Style::Destructive) +} + +pub fn suggested<'a, Message>(label: impl Into>) -> Button<'a, Message> { + Button::new(Text::new()) + .label(label) + .style(Style::Suggested) +} + +pub fn standard<'a, Message>(label: impl Into>) -> Button<'a, Message> { + Button::new(Text::new()).label(label) +} + +pub fn text<'a, Message>(label: impl Into>) -> Button<'a, Message> { + Button::new(Text::new()).label(label).style(Style::Text) +} + +pub struct Text { + pub(super) leading_icon: Option, + pub(super) trailing_icon: Option, +} + +impl Text { + pub const fn new() -> Self { + Self { + leading_icon: None, + trailing_icon: None, + } + } +} + +impl<'a, Message> Button<'a, Message> { + pub fn new(text: Text) -> Self { + crate::theme::THEME.with(|theme_cell| { + let theme = theme_cell.borrow(); + let theme = theme.cosmic(); + Self { + id: Id::unique(), + label: Cow::Borrowed(""), + on_press: None, + width: Length::Shrink, + height: Length::Fixed(theme.space_l().into()), + padding: Padding::from([0, theme.space_s()]), + spacing: theme.space_xxxs(), + icon_size: 16, + line_height: 20, + font_size: 14, + font_weight: Weight::Normal, + style: Style::Standard, + variant: text, + } + }) + } + + pub fn leading_icon(mut self, icon: impl Into) -> Self { + self.variant.leading_icon = Some(icon.into()); + self + } + + pub fn trailing_icon(mut self, icon: impl Into) -> Self { + self.variant.trailing_icon = Some(icon.into()); + self + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(mut b: Button<'a, Message>) -> Element<'a, Message> { + // TODO: Determine why this needs to be set before the label to prevent lifetime conflict. + let trailing_icon = b + .variant + .trailing_icon + .map(|i| Element::from(widget::icon(i).size(b.icon_size))); + + row::with_capacity(3) + // Optional icon to place before label. + .push_maybe( + b.variant + .leading_icon + .map(|i| widget::icon(i).size(b.icon_size)), + ) + // Optional label between icons. + .push_maybe((!b.label.is_empty()).then(|| { + let mut font = crate::font::DEFAULT; + font.weight = b.font_weight; + + crate::widget::text(b.label) + .size(b.font_size) + .line_height(LineHeight::Absolute(b.line_height.into())) + .font(font) + })) + // Optional icon to place behind the label. + .push_maybe(trailing_icon) + .padding(b.padding) + .width(b.width) + .height(b.height) + .spacing(b.spacing) + .align_items(Alignment::Center) + .apply(button) + .padding(0) + .id(b.id) + .on_press_maybe(b.on_press.take()) + .style(b.style) + .into() + } +} diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs new file mode 100644 index 00000000..94691f7c --- /dev/null +++ b/src/widget/button/widget.rs @@ -0,0 +1,677 @@ +// Copyright 2019 H�ctor Ram�n, Iced contributors +// Copyright 2023 System76 +// SPDX-License-Identifier: MIT + +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +use iced_runtime::core::widget::Id; +use iced_runtime::{keyboard, Command}; + +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::touch; +use iced_core::widget::tree::{self, Tree}; +use iced_core::widget::Operation; +use iced_core::{ + Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, + Vector, Widget, +}; +use iced_renderer::core::widget::{operation, OperationOutputWrapper}; + +pub use super::style::{Appearance, StyleSheet}; + +/// A generic widget that produces a message when pressed. +/// +/// ```no_run +/// # type Button<'a, Message> = +/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); +/// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # type Button<'a, Message> = +/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button<'a>() -> Button<'a, Message> { +/// Button::new("I'm disabled!") +/// } +/// +/// fn enabled_button<'a>() -> Button<'a, Message> { +/// disabled_button().on_press(Message::ButtonPressed) +/// } +/// ``` +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Button<'a, Message, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, + content: Element<'a, Message, Renderer>, + on_press: Option, + width: Length, + height: Length, + padding: Padding, + style: ::Style, +} + +impl<'a, Message, Renderer> Button<'a, Message, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Button`] with the given content. + pub fn new(content: impl Into>) -> Self { + Button { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, + content: content.into(), + on_press: None, + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5.0), + style: ::Style::default(), + } + } + + /// Sets the width of the [`Button`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Button`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + pub fn on_press_maybe(mut self, on_press: Option) -> Self { + self.on_press = on_press; + self + } + + /// Sets the style variant of this [`Button`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } + + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget(mut self, description: &T) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } +} + +impl<'a, Message, Renderer> Widget for Button<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| self.content.as_widget().layout(renderer, limits), + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + let state = tree.state.downcast_mut::(); + operation.focusable(state, Some(&self.id)); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ) { + return event::Status::Captured; + } + + update( + self.id.clone(), + event, + layout, + cursor, + shell, + &self.on_press, + || tree.state.downcast_mut::(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = draw( + renderer, + bounds, + cursor, + self.on_press.is_some(), + theme, + &self.style, + || tree.state.downcast_ref::(), + ); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + icon_color: styling.icon_color.unwrap_or(renderer_style.icon_color), + text_color: styling.text_color, + scale_factor: renderer_style.scale_factor, + }, + content_layout, + cursor, + &bounds, + ); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor, self.on_press.is_some()) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, + A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = self + .content + .as_widget() + .a11y_nodes(child_layout, &child_tree, p); + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); + let is_hovered = state.state.downcast_ref::().is_hovered; + + let mut node = NodeBuilder::new(Role::Button); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if self.on_press.is_none() { + node.set_disabled() + } + if is_hovered { + node.set_hovered() + } + node.set_default_action_verb(DefaultActionVerb::Click); + + A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_core::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(button: Button<'a, Message, Renderer>) -> Self { + Self::new(button) + } +} + +/// The local state of a [`Button`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_hovered: bool, + is_pressed: bool, + is_focused: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } + + /// Returns whether the [`Button`] is currently focused or not. + pub fn is_focused(self) -> bool { + self.is_focused + } + + /// Returns whether the [`Button`] is currently hovered or not. + pub fn is_hovered(self) -> bool { + self.is_hovered + } + + /// Focuses the [`Button`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`Button`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +#[allow(clippy::needless_pass_by_value)] +pub fn update<'a, Message: Clone>( + _id: Id, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + on_press: &Option, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if on_press.is_some() { + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + let state = state(); + + state.is_pressed = true; + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + #[cfg(feature = "a11y")] + Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { + let state = state(); + if let Some(Some(on_press)) = (id == event_id + && matches!(action, iced_accessibility::accesskit::Action::Default)) + .then(|| on_press.clone()) + { + state.is_pressed = false; + shell.publish(on_press); + } + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused && key_code == keyboard::KeyCode::Enter { + state.is_pressed = true; + shell.publish(on_press); + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { + let state = state(); + state.is_hovered = false; + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: iced_core::Renderer>( + renderer: &mut Renderer, + bounds: Rectangle, + cursor: mouse::Cursor, + is_enabled: bool, + style_sheet: &dyn StyleSheet