diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 88984fa7e40..0e27afbbaab 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -28,7 +28,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; quote! { - config.set(stringify!(#field_name), &self.#field_name)?; + cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?; } }); @@ -36,7 +36,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let field_name = &field.ident; let field_type = &field.ty; quote! { - match config.get::<#field_type>(stringify!(#field_name)) { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { Ok(#field_name) => default.#field_name = #field_name, Err(e) => errors.push(e), } @@ -60,13 +60,13 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let gen = quote! { impl CosmicConfigEntry for #name { - fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { + fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> { let tx = config.transaction(); #(#write_each_config_field)* tx.commit() } - fn get_entry(config: &Config) -> Result, Self)> { + fn get_entry(config: &cosmic_config::Config) -> Result, Self)> { let mut default = Self::default(); let mut errors = Vec::new(); diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 9abff0b01ba..374bf29d3a8 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -390,8 +390,6 @@ async fn start_listening< Ok(w) => w, Err(_) => return ConfigState::Failed, }; - let msg = T::get_entry(&config); - _ = output.send((id, msg)).await; match T::get_entry(&config) { Ok(t) => { diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index a6613217487..0ded54116e5 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -23,4 +23,4 @@ serde = { version = "1.0.129", features = ["derive"] } ron = "0.8" lazy_static = "1.4.0" csscolorparser = {version = "0.6.2", features = ["serde"]} -cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription"] } +cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription", "macro"] } diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 073031ba44e..61092081800 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -143,6 +143,14 @@ pub struct CosmicPaletteInner { /// A wider spread of dark colors for more general use. pub neutral_10: C, + // Utility Colors + /// Utility bright green + pub bright_green: C, + /// Utility bright red + pub bright_red: C, + /// Utility bright orange + pub bright_orange: C, + /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. pub ext_warm_grey: C, @@ -159,6 +167,12 @@ pub struct CosmicPaletteInner { /// Colors used for themes, app icons, illustrations, and other brand purposes. pub ext_indigo: C, + /// Potential Accent Color Combos + pub accent_blue: C, + /// Potential Accent Color Combos + pub accent_red: C, + /// Potential Accent Color Combos + pub accent_green: C, /// Potential Accent Color Combos pub accent_warm_grey: C, /// Potential Accent Color Combos @@ -195,6 +209,9 @@ impl From> for CosmicPaletteInner { neutral_8: p.neutral_8.into(), neutral_9: p.neutral_9.into(), neutral_10: p.neutral_10.into(), + bright_green: p.bright_green.into(), + bright_red: p.bright_red.into(), + bright_orange: p.bright_orange.into(), ext_warm_grey: p.ext_warm_grey.into(), ext_orange: p.ext_orange.into(), ext_yellow: p.ext_yellow.into(), @@ -202,6 +219,9 @@ impl From> for CosmicPaletteInner { ext_purple: p.ext_purple.into(), ext_pink: p.ext_pink.into(), ext_indigo: p.ext_indigo.into(), + accent_blue: p.accent_blue.into(), + accent_red: p.accent_red.into(), + accent_green: p.accent_green.into(), accent_warm_grey: p.accent_warm_grey.into(), accent_orange: p.accent_orange.into(), accent_yellow: p.accent_yellow.into(), diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 9f283121cc9..b24cea4983c 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -55,6 +55,15 @@ Dark ( neutral_10: ( c: "#FFFFFF", ), + bright_green: ( + c: "#5EDB8C", + ), + bright_red: ( + c: "#FFA090", + ), + bright_orange: ( + c: "#FFA37D", + ), ext_warm_grey: ( c: "#9B8E8A", ), @@ -76,23 +85,32 @@ Dark ( ext_indigo: ( c: "#3E88FF", ), + accent_blue: ( + c: "#63D0DF", + ), + accent_green: ( + c: "#92CF9C", + ), accent_warm_grey: ( - c: "#554742", + c: "#CABAB4", ), accent_orange: ( - c: "#AF5C02", + c: "#FFAD00", ), accent_yellow: ( - c: "#966800", + c: "#F7E062", ), accent_purple: ( - c: "#813FFF", + c: "#E79CFE", ), accent_pink: ( - c: "#F93A83", + c: "#FF9CB1", + ), + accent_red: ( + c: "#FDA1A0", ), accent_indigo: ( - c: "#3E88FF", + c: "#A1C0EB", ), ) -) \ No newline at end of file +) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index cd9f0f937d6..07b5a64b7cd 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -55,6 +55,15 @@ Light ( neutral_10: ( c: "#000000", ), + bright_green: ( + c: "#00572C", + ), + bright_red: ( + c: "#890418", + ), + bright_orange: ( + c: "#792C00", + ), ext_warm_grey: ( c: "#9B8E8A", ), @@ -76,23 +85,32 @@ Light ( ext_indigo: ( c: "#95C4FC", ), + accent_blue: ( + c: "#00525A", + ), + accent_red: ( + c: "#78292E", + ), + accent_green: ( + c: "#185529", + ), accent_warm_grey: ( - c: "#ADA29E", + c: "#554742", ), accent_orange: ( - c: "#FFD7A1", + c: "#624000", ), accent_yellow: ( - c: "#FFF19E", + c: "#534800", ), accent_purple: ( - c: "#D58CFF", + c: "#68217C", ), accent_pink: ( - c: "#FF9CDD", + c: "#86043A", ), accent_indigo: ( - c: "#95C4FC", + c: "#2E496D", ), ) -) \ No newline at end of file +) diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index 5751e231f75..19370deeb6e 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,11 +1,13 @@ pub use corner::*; pub use cosmic_palette::*; pub use derivation::*; +pub use mode::*; pub use spacing::*; pub use theme::*; mod corner; mod cosmic_palette; mod derivation; +mod mode; mod spacing; mod theme; diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs new file mode 100644 index 00000000000..85853dd6c41 --- /dev/null +++ b/cosmic-theme/src/model/mode.rs @@ -0,0 +1,46 @@ +use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; + +/// ID for the ThemeMode config +pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; + +/// The config for cosmic theme dark / light settings +#[derive( + Debug, Clone, Copy, PartialEq, Eq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, +)] +pub struct ThemeMode { + /// The theme dark mode setting. + pub is_dark: bool, + /// The theme auto-switch dark and light mode setting. + pub auto_switch: bool, +} + +impl Default for ThemeMode { + fn default() -> Self { + Self { + is_dark: true, + auto_switch: false, + } + } +} + +impl ThemeMode { + /// Check if the theme is currently using dark mode + pub fn is_dark(config: &Config) -> Result { + config.get::("is_dark") + } + + /// version of the theme + pub fn version() -> u64 { + 1 + } + + /// Set auto-switch from light to dark mode + pub fn set_auto_switch(config: &Config, value: bool) -> Result<(), cosmic_config::Error> { + config.set("auto_switch", value) + } + + /// Get the config for the theme mode + pub fn config() -> Result { + Config::new(THEME_MODE_ID, Self::version()) + } +} diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 1411e98aafe..451463d0daa 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,12 +1,24 @@ use crate::{ composite::over, steps::*, Component, Container, CornerRadii, CosmicPalette, - CosmicPaletteInner, Spacing, DARK_PALETTE, LIGHT_PALETTE, NAME, + CosmicPaletteInner, Spacing, ThemeMode, DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use palette::{IntoColor, Srgb, Srgba}; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; +/// ID for the current dark ThemeBuilder config +pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder"; + +/// ID for the current dark Theme config +pub const DARK_THEME_ID: &str = "com.system76.CosmicTheme.Dark"; + +/// ID for the current light ThemeBuilder config +pub const LIGHT_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Light.Builder"; + +/// ID for the current light Theme config +pub const LIGHT_THEME_ID: &str = "com.system76.CosmicTheme.Light"; + #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] /// Theme layer type pub enum Layer { @@ -64,9 +76,17 @@ pub struct Theme { pub is_dark: bool, /// is high contrast pub is_high_contrast: bool, + /// cosmic-comp window gaps size (outer, inner) + pub gaps: (u32, u32), + /// cosmic-comp active hint window outline width + pub active_hint: u32, + /// cosmic-comp custom window hint color + pub window_hint: Option, + /// enables blurred transparency + pub is_frosted: bool, } -impl CosmicConfigEntry for Theme { +impl cosmic_config::CosmicConfigEntry for Theme { fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { let self_ = self.clone(); // TODO do as transaction @@ -80,11 +100,22 @@ impl CosmicConfigEntry for Theme { tx.set("success", self_.success)?; tx.set("destructive", self_.destructive)?; tx.set("warning", self_.warning)?; + tx.set("accent_button", self_.accent_button)?; + tx.set("success_button", self_.success_button)?; + tx.set("warning_button", self_.warning_button)?; + tx.set("destructive_button", self_.destructive_button)?; + tx.set("icon_button", self_.icon_button)?; + tx.set("link_button", self_.link_button)?; + tx.set("text_button", self_.text_button)?; + tx.set("button", self_.button)?; 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.set("active_hint", self_.active_hint)?; + tx.set("gaps", self_.gaps)?; + tx.set("window_hint", self_.window_hint)?; tx.commit() } @@ -125,6 +156,38 @@ impl CosmicConfigEntry for Theme { Ok(warning) => default.warning = warning, Err(e) => errors.push(e), } + match config.get::>("success_button") { + Ok(b) => default.success_button = b, + Err(e) => errors.push(e), + } + match config.get::>("accent_button") { + Ok(b) => default.accent_button = b, + Err(e) => errors.push(e), + } + match config.get::>("destructive_button") { + Ok(b) => default.destructive_button = b, + Err(e) => errors.push(e), + } + match config.get::>("warning_button") { + Ok(warning) => default.warning_button = warning, + Err(e) => errors.push(e), + } + match config.get::>("icon_button") { + Ok(b) => default.link_button = b, + Err(e) => errors.push(e), + } + match config.get::>("link_button") { + Ok(b) => default.link_button = b, + Err(e) => errors.push(e), + } + match config.get::>("text_button") { + Ok(b) => default.text_button = b, + Err(e) => errors.push(e), + } + match config.get::>("button") { + Ok(b) => default.button = b, + Err(e) => errors.push(e), + } match config.get::>("palette") { Ok(palette) => default.palette = palette, Err(e) => errors.push(e), @@ -145,7 +208,18 @@ impl CosmicConfigEntry for Theme { Ok(corner_radii) => default.corner_radii = corner_radii, Err(e) => errors.push(e), } - + match config.get::("active_hint") { + Ok(active_hint) => default.active_hint = active_hint, + Err(e) => errors.push(e), + } + match config.get::<(u32, u32)>("gaps") { + Ok(gaps) => default.gaps = gaps, + Err(e) => errors.push(e), + } + match config.get::>("window_hint") { + Ok(window_hint) => default.window_hint = window_hint, + Err(e) => errors.push(e), + } if errors.is_empty() { Ok(default) } else { @@ -176,6 +250,16 @@ impl Theme { pub fn id() -> &'static str { NAME } + + /// Get the config for the current dark theme + pub fn dark_config() -> Result { + Config::new(DARK_THEME_ID, Self::version()) + } + + /// Get the config for the current light theme + pub fn light_config() -> Result { + Config::new(LIGHT_THEME_ID, Self::version()) + } } impl Theme { @@ -410,6 +494,20 @@ impl Theme { pub fn radius_xl(&self) -> [f32; 4] { self.corner_radii.radius_xl } + + /// get the active theme + pub fn get_active() -> Result, Self)> { + let config = + Config::new(Self::id(), Self::version()).map_err(|e| (vec![e], Self::default()))?; + let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; + let config = if is_dark { + Self::dark_config() + } else { + Self::light_config() + } + .map_err(|e| (vec![e], Self::default()))?; + Self::get_entry(&config) + } } impl From> for Theme @@ -422,20 +520,47 @@ where } /// Helper for building customized themes -#[derive(Debug, Serialize, Deserialize)] +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + cosmic_config::cosmic_config_derive::CosmicConfigEntry, + PartialEq, +)] pub struct ThemeBuilder { - palette: CosmicPalette, - spacing: Spacing, - corner_radii: CornerRadii, - neutral_tint: Option, - bg_color: Option, - primary_container_bg: Option, - secondary_container_bg: Option, - text_tint: Option, - accent: Option, - success: Option, - warning: Option, - destructive: Option, + /// override the palette for the builder + pub palette: CosmicPalette, + /// override spacing for the builder + pub spacing: Spacing, + /// override corner radii for the builder + pub corner_radii: CornerRadii, + /// override neutral_tint for the builder + pub neutral_tint: Option, + /// override bg_color for the builder + pub bg_color: Option, + /// override the primary container bg color for the builder + pub primary_container_bg: Option, + /// override the secontary container bg color for the builder + pub secondary_container_bg: Option, + /// override the text tint for the builder + pub text_tint: Option, + /// override the accent color for the builder + pub accent: Option, + /// override the success color for the builder + pub success: Option, + /// override the warning color for the builder + pub warning: Option, + /// override the destructive color for the builder + pub destructive: Option, + /// enabled blurred transparency + pub is_frosted: bool, // TODO handle + /// cosmic-comp window gaps size (outer, inner) + pub gaps: (u32, u32), + /// cosmic-comp active hint window outline width + pub active_hint: u32, + /// cosmic-comp custom window hint color + pub window_hint: Option, } impl Default for ThemeBuilder { @@ -453,6 +578,11 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), + is_frosted: false, + // cosmic-comp theme settings + gaps: (0, 4), + active_hint: 4, + window_hint: None, } } } @@ -478,7 +608,7 @@ impl ThemeBuilder { pub fn dark_high_contrast() -> Self { let palette: CosmicPalette = DARK_PALETTE.to_owned().into(); Self { - palette: CosmicPalette::HighContrastLight(palette.inner()), + palette: CosmicPalette::HighContrastDark(palette.inner()), ..Default::default() } } @@ -575,6 +705,10 @@ impl ThemeBuilder { success, warning, destructive, + gaps, + active_hint, + window_hint, + is_frosted, } = self; let is_dark = palette.is_dark(); @@ -885,9 +1019,28 @@ impl ThemeBuilder { corner_radii, is_dark, is_high_contrast, + gaps, + active_hint, + window_hint, + is_frosted, }; theme.spacing = spacing; theme.corner_radii = corner_radii; theme } + + /// Get the builder for the dark config + pub fn dark_config() -> Result { + Config::new(DARK_THEME_BUILDER_ID, Self::version()) + } + + /// Get the builder for the light config + pub fn light_config() -> Result { + Config::new(LIGHT_THEME_BUILDER_ID, Self::version()) + } + + /// version of the theme builder + pub fn version() -> u64 { + 1 + } } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 192842d84a7..842e0c44df1 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -8,8 +8,9 @@ use cosmic::{ iced_core::Color, theme::ThemeType, widget::{ - button, cosmic_container::container, icon, segmented_button, segmented_selection, settings, - spin_button, toggler, view_switcher, ColorPickerModel, ColorPickerUpdate, + button, color_picker::ColorPickerUpdate, cosmic_container::container, icon, + segmented_button, segmented_selection, settings, spin_button, toggler, view_switcher, + ColorPickerModel, }, Element, }; @@ -160,12 +161,7 @@ impl Default for State { "card 4".to_string(), ], timeline: Rc::new(RefCell::new(Default::default())), - color_picker_model: ColorPickerModel::new( - "Hex", - "RGB", - Color::new(0.8, 0.3, 0.8, 1.0), - None, - ), + color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None), } } } @@ -524,7 +520,9 @@ impl State { .on_input(Message::InputChanged) .into(), self.color_picker_model - .picker_button(Message::ColorPickerUpdate) + .picker_button(Message::ColorPickerUpdate, None) + .width(Length::Fixed(128.0)) + .height(Length::Fixed(128.0)) .into(), if self.color_picker_model.get_is_active() { self.color_picker_model diff --git a/examples/design-demo b/examples/design-demo index 13b5a947df9..ed4af4e6431 160000 --- a/examples/design-demo +++ b/examples/design-demo @@ -1 +1 @@ -Subproject commit 13b5a947df963232cafb938de62e81b1df0dbac8 +Subproject commit ed4af4e6431e30702f6de8031f3a3a11b82da3f0 diff --git a/iced b/iced index 9fafd1815da..1e45d23ac83 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 9fafd1815da3ad2c31889a32e5daccaddd79ee9d +Subproject commit 1e45d23ac83ed1416a6ceb2c65c4f64a416577f9 diff --git a/src/app/core.rs b/src/app/core.rs index f23217c9910..7cab04cc7f5 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -1,6 +1,9 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use cosmic_config::CosmicConfigEntry; +use cosmic_theme::ThemeMode; + use crate::Theme; /// Status of the nav bar and its panels. @@ -49,6 +52,9 @@ pub struct Core { /// Last known system theme pub(super) system_theme: Theme, + /// Theme mode + pub(super) system_theme_mode: ThemeMode, + pub(super) title: String, pub window: Window, @@ -70,6 +76,16 @@ impl Default for Core { scale_factor: 1.0, title: String::new(), system_theme: crate::theme::active(), + system_theme_mode: ThemeMode::config() + .map(|c| { + ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| { + for e in errors { + tracing::error!("{e}"); + } + mode + }) + }) + .unwrap_or_default(), window: Window { context_title: String::new(), header_title: String::new(), @@ -169,4 +185,15 @@ impl Core { self.window.width = new_width; self.is_condensed_update(); } + + /// Get the current system theme + pub fn system_theme(&self) -> &Theme { + &self.system_theme + } + + #[must_use] + /// Get the current system theme mode + pub fn system_theme_mode(&self) -> ThemeMode { + self.system_theme_mode + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e1cc257f58d..edda1a6e6fa 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -5,6 +5,7 @@ use super::{command, Application, ApplicationExt, Core, Subscription}; use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; use crate::{keyboard_nav, Element}; +use cosmic_theme::ThemeMode; #[cfg(feature = "wayland")] use iced::event::wayland::{self, WindowEvent}; #[cfg(feature = "wayland")] @@ -44,6 +45,8 @@ pub enum Message { ToggleNavBarCondensed, /// Notification of system theme changes. SystemThemeChange(Theme), + /// Notification of system theme mode changes. + SystemThemeModeChange(ThemeMode), /// Updates the tracked window geometry. WindowResize(window::Id, u32, u32), /// Tracks updates to window state. @@ -152,9 +155,24 @@ where keyboard_nav::subscription() .map(Message::KeyboardNav) .map(super::Message::Cosmic), - theme::subscription(0) + theme::subscription(0, self.app.core().system_theme_mode.is_dark) .map(Message::SystemThemeChange) .map(super::Message::Cosmic), + cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( + 0, + cosmic_theme::THEME_MODE_ID.into(), + cosmic_theme::ThemeMode::version(), + ) + .map(|(_, u)| match u { + Ok(t) => Message::SystemThemeModeChange(t), + Err((errors, t)) => { + for e in errors { + tracing::error!("{e}"); + } + Message::SystemThemeModeChange(t) + } + }) + .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), ]) } @@ -195,6 +213,7 @@ impl Cosmic { iced::Command::single(Action::Window(WindowAction::Close)) } + #[allow(clippy::too_many_lines)] fn cosmic_update(&mut self, message: Message) -> iced::Command> { match message { Message::WindowResize(id, width, height) => { @@ -295,7 +314,7 @@ impl Cosmic { THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); - // Anly apply update if the theme is set to load a system theme + // Only apply update if the theme is set to load a system theme if let ThemeType::System(_) = cosmic_theme.theme_type { cosmic_theme.set_theme(theme.theme_type); } @@ -310,6 +329,21 @@ impl Cosmic { self.app.on_app_exit(); return self.close(); } + Message::SystemThemeModeChange(mode) => { + let core = self.app.core_mut(); + let changed = core.system_theme_mode.is_dark != mode.is_dark; + core.system_theme_mode = mode; + if changed { + 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 let ThemeType::System(_) = cosmic_theme.theme_type { + cosmic_theme.set_theme(crate::theme::system_preference().theme_type); + } + }); + } + } } iced::Command::none() diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 979e941c8fd..db691170e61 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -4,11 +4,11 @@ //! Contains the [`Theme`] type and its widget stylesheet implementations. pub mod style; +use cosmic_theme::ThemeMode; pub use style::*; use cosmic_config::config_subscription; use cosmic_config::CosmicConfigEntry; -use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; use iced_futures::Subscription; @@ -68,10 +68,15 @@ pub fn is_high_contrast() -> bool { } /// Watches for changes to the system's theme preference. -pub fn subscription(id: u64) -> Subscription { - config_subscription::>( - id, - crate::cosmic_theme::NAME.into(), +pub fn subscription(id: u64, is_dark: bool) -> Subscription { + config_subscription::<_, crate::cosmic_theme::Theme>( + (id, is_dark), + if is_dark { + cosmic_theme::DARK_THEME_ID + } else { + cosmic_theme::LIGHT_THEME_ID + } + .into(), crate::cosmic_theme::Theme::::version(), ) .map(|(_, res)| { @@ -88,10 +93,21 @@ pub fn subscription(id: u64) -> Subscription { /// Loads the preferred system theme from `cosmic-config`. pub fn system_preference() -> Theme { - let Ok(helper) = crate::cosmic_config::Config::new( - crate::cosmic_theme::NAME, - crate::cosmic_theme::Theme::::version(), - ) else { + let Ok(mode_config) = ThemeMode::config() else { + return Theme::dark(); + }; + + let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else { + return Theme::dark(); + }; + + let helper = if is_dark { + crate::cosmic_theme::Theme::::dark_config() + } else { + crate::cosmic_theme::Theme::::light_config() + }; + + let Ok(helper) = helper else { return Theme::dark(); }; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index e732ad3782c..46dbfb12857 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -412,7 +412,12 @@ impl container::StyleSheet for Theme { .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_radius: BorderRadius::from([ + palette.corner_radii.radius_xs[0], + palette.corner_radii.radius_xs[3], + 0.0, + 0.0, + ]), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index fd448784fc0..2bf23260169 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -17,20 +17,21 @@ use iced_core::gradient::{ColorStop, Linear}; use iced_core::renderer::Quad; use iced_core::widget::{tree, Tree}; use iced_core::{ - layout, mouse, renderer, Clipboard, Color, Layout, Length, Radians, Rectangle, Renderer, Shell, - Vector, Widget, + layout, mouse, renderer, Background, Clipboard, Color, Layout, Length, Radians, Rectangle, + Renderer, Shell, Vector, Widget, }; #[cfg(feature = "wayland")] use iced_sctk::commands::data_device::set_selection; use iced_style::slider::{HandleShape, RailBackground}; -use iced_widget::{canvas, column, scrollable, vertical_space, Row}; +use iced_widget::{canvas, column, horizontal_space, row, scrollable, vertical_space, Row}; use lazy_static::lazy_static; use palette::{FromColor, RgbHue}; +use super::button::StyleSheet; use super::divider::horizontal; -use super::icon::from_name; +use super::icon::{self, from_name}; use super::segmented_button::{self, Model, SingleSelect}; -use super::{button, segmented_selection, text, text_input, tooltip}; +use super::{button, segmented_selection, text, text_input, tooltip, Icon}; // TODO is this going to look correct enough? lazy_static! { @@ -72,9 +73,9 @@ pub struct ColorPickerModel { #[setters(skip)] input_color: String, #[setters(skip)] - applied_color: Color, + applied_color: Option, #[setters(skip)] - initial_color: Color, + fallback_color: Option, #[setters(skip)] recent_colors: Vec, active: bool, @@ -91,11 +92,11 @@ impl ColorPickerModel { pub fn new( hex: impl Into> + Clone, rgb: impl Into> + Clone, - fallback_color: Color, + fallback_color: Option, initial_color: Option, ) -> Self { - let initial = initial_color.unwrap_or(fallback_color); - let initial_srgb = palette::Srgb::from(initial); + let initial = initial_color.or(fallback_color); + let initial_srgb = palette::Srgb::from(initial.unwrap_or(Color::BLACK)); let hsv = palette::Hsv::from_color(initial_srgb); Self { segmented_model: segmented_button::Model::builder() @@ -103,10 +104,10 @@ impl ColorPickerModel { .insert(move |b| b.text(rgb.clone())) .build(), active_color: hsv, - save_next: Some(initial), + save_next: None, input_color: color_to_string(hsv, true), - applied_color: fallback_color, - initial_color: initial, + applied_color: initial, + fallback_color, recent_colors: Vec::new(), // TODO should all color pickers show the same recent colors? active: false, width: Length::Fixed(300.0), @@ -118,13 +119,15 @@ impl ColorPickerModel { /// Get a color picker button that displays the applied color /// - pub fn picker_button<'a, Message: 'a, T: Fn(ColorPickerUpdate) -> Message>( + pub fn picker_button<'a, Message: 'static, T: Fn(ColorPickerUpdate) -> Message>( &self, f: T, + icon_portion: Option, ) -> crate::widget::Button<'a, Message, crate::Renderer> { color_button( Some(f(ColorPickerUpdate::ToggleColorPicker)), self.applied_color, + Length::FillPortion(icon_portion.unwrap_or(12)), ) } @@ -142,8 +145,10 @@ impl ColorPickerModel { } ColorPickerUpdate::AppliedColor => { let srgb = palette::Srgb::from_color(self.active_color); - self.recent_colors.push(self.applied_color); - self.applied_color = Color::from(srgb); + if let Some(applied_color) = self.applied_color.take() { + self.recent_colors.push(applied_color); + } + self.applied_color = Some(Color::from(srgb)); self.active = false; } ColorPickerUpdate::ActivateSegmented(e) => { @@ -167,10 +172,10 @@ impl ColorPickerModel { ColorPickerUpdate::Reset => { self.must_clear_cache.store(true, Ordering::SeqCst); - let initial_srgb = palette::Srgb::from(self.initial_color); + let initial_srgb = palette::Srgb::from(self.fallback_color.unwrap_or(Color::BLACK)); let hsv = palette::Hsv::from_color(initial_srgb); self.active_color = hsv; - self.applied_color = self.initial_color; + self.applied_color = self.fallback_color; self.copied_at = None; } ColorPickerUpdate::Cancel => { @@ -216,7 +221,7 @@ impl ColorPickerModel { /// Get the applied color of the picker #[must_use] - pub fn get_applied_color(&self) -> Color { + pub fn get_applied_color(&self) -> Option { self.applied_color } @@ -355,7 +360,8 @@ where .leading_icon( color_button( None, - Color::from(palette::Srgb::from_color(self.active_color)) + Some(Color::from(palette::Srgb::from_color(self.active_color))), + Length::FillPortion(12) ) .into() ) @@ -411,7 +417,8 @@ where let hsv = palette::Hsv::from_color(initial_srgb); color_button( Some(on_update(ColorPickerUpdate::ActiveColor(hsv))), - *c, + Some(*c), + Length::FillPortion(12), ) .into() }) @@ -738,93 +745,122 @@ fn color_to_string(c: palette::Hsv, is_hex: bool) -> String { } } -fn color_button<'a, Message: 'a>( +pub fn color_button<'a, Message: 'static>( on_press: Option, - color: Color, + color: Option, + icon_portion: Length, ) -> crate::widget::Button<'a, Message, crate::Renderer> { let spacing = THEME.with(|t| t.borrow().cosmic().spacing); - button(vertical_space(Length::Fixed(f32::from(spacing.space_s)))) - .width(Length::Fixed(f32::from(spacing.space_s))) - .height(Length::Fixed(f32::from(spacing.space_s))) - .on_press_maybe(on_press) - .style(crate::theme::Button::Custom { - active: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); - - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - button::Appearance { - shadow_offset: Vector::default(), - background: Some(color.into()), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.on_bg_color().into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - } - }), - disabled: Box::new(move |theme| { - let cosmic = theme.cosmic(); - - button::Appearance { - shadow_offset: Vector::default(), - background: Some(color.into()), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.on_bg_color().into(), - outline_width: 0.0, - outline_color: Color::TRANSPARENT, - icon_color: None, - text_color: None, - } - }), - hovered: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); + button(if color.is_some() { + Element::from(vertical_space(Length::Fixed(f32::from(spacing.space_s)))) + } else { + Element::from(column![ + vertical_space(Length::FillPortion(6)), + row![ + horizontal_space(Length::FillPortion(6)), + Icon::from( + icon::from_name("list-add-symbolic") + .prefer_svg(true) + .symbolic(true) + .size(64) + ) + .width(icon_portion) + .height(Length::Fill) + .content_fit(iced_core::ContentFit::Contain), + horizontal_space(Length::FillPortion(6)), + ] + .height(icon_portion) + .width(Length::Fill), + vertical_space(Length::FillPortion(6)), + ]) + }) + .width(Length::Fixed(f32::from(spacing.space_s))) + .height(Length::Fixed(f32::from(spacing.space_s))) + .on_press_maybe(on_press) + .style(crate::theme::Button::Custom { + active: Box::new(move |focused, theme| { + let cosmic = theme.cosmic(); + + let (outline_width, outline_color) = if focused { + (1.0, cosmic.accent_color().into()) + } else { + (0.0, Color::TRANSPARENT) + }; + let standard = theme.active(focused, &Button::Standard); + button::Appearance { + shadow_offset: Vector::default(), + background: color.map(Background::from).or(standard.background), + border_radius: cosmic.radius_xs().into(), + border_width: 1.0, + border_color: cosmic.on_bg_color().into(), + outline_width, + outline_color, + icon_color: None, + text_color: None, + } + }), + disabled: Box::new(move |theme| { + let cosmic = theme.cosmic(); + + let standard = theme.disabled(&Button::Standard); + button::Appearance { + shadow_offset: Vector::default(), + background: color.map(Background::from).or(standard.background), + border_radius: cosmic.radius_xs().into(), + border_width: 1.0, + border_color: cosmic.on_bg_color().into(), + outline_width: 0.0, + outline_color: Color::TRANSPARENT, + icon_color: None, + text_color: None, + } + }), + hovered: Box::new(move |focused, theme| { + let cosmic = theme.cosmic(); + + let (outline_width, outline_color) = if focused { + (1.0, cosmic.accent_color().into()) + } else { + (0.0, Color::TRANSPARENT) + }; - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - button::Appearance { - shadow_offset: Vector::default(), - background: Some(color.into()), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.on_bg_color().into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - } - }), - pressed: Box::new(move |focused, theme| { - let cosmic = theme.cosmic(); + let standard = theme.hovered(focused, &Button::Standard); + button::Appearance { + shadow_offset: Vector::default(), + background: color.map(Background::from).or(standard.background), + border_radius: cosmic.radius_xs().into(), + border_width: 1.0, + border_color: cosmic.on_bg_color().into(), + outline_width, + outline_color, + icon_color: None, + text_color: None, + } + }), + pressed: Box::new(move |focused, theme| { + let cosmic = theme.cosmic(); + + let (outline_width, outline_color) = if focused { + (1.0, cosmic.accent_color().into()) + } else { + (0.0, Color::TRANSPARENT) + }; - let (outline_width, outline_color) = if focused { - (1.0, cosmic.accent_color().into()) - } else { - (0.0, Color::TRANSPARENT) - }; - button::Appearance { - shadow_offset: Vector::default(), - background: Some(color.into()), - border_radius: cosmic.radius_xs().into(), - border_width: 1.0, - border_color: cosmic.on_bg_color().into(), - outline_width, - outline_color, - icon_color: None, - text_color: None, - } - }), - }) + let standard = theme.pressed(focused, &Button::Standard); + button::Appearance { + shadow_offset: Vector::default(), + background: color.map(Background::from).or(standard.background), + border_radius: cosmic.radius_xs().into(), + border_width: 1.0, + border_color: cosmic.on_bg_color().into(), + outline_width, + outline_color, + icon_color: None, + text_color: None, + } + }), + }) } impl<'a, Message> From> for iced::Element<'a, Message, crate::Renderer> diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index d39e2a73a4e..2ee928b3c6e 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -8,6 +8,8 @@ use crate::{ Element, }; use derive_setters::Setters; +use iced_core::Length; +use iced_widget::container; /// A settings item aligned in a row #[must_use] @@ -70,15 +72,20 @@ impl<'a, Message: 'static> Item<'a, Message> { let column = column::with_capacity(2) .spacing(2) .push(text(self.title)) - .push(text(description).size(10)); + .push(text(description).size(10)) + .width(Length::FillPortion(12)); contents.push(column.into()); } else { contents.push(text(self.title).into()); } - contents.push(horizontal_space(iced::Length::Fill).into()); - contents.push(widget.into()); + contents.push( + container(widget.into()) + .width(Length::FillPortion(4)) + .align_x(iced_core::alignment::Horizontal::Right) + .into(), + ); item_row(contents) }