diff --git a/cosmic-settings/src/app.rs b/cosmic-settings/src/app.rs index 9e34a27..510e963 100644 --- a/cosmic-settings/src/app.rs +++ b/cosmic-settings/src/app.rs @@ -756,8 +756,9 @@ impl cosmic::Application for SettingsApp { } } - Message::SetTheme(t) => return cosmic::command::set_theme(t), - + Message::SetTheme(t) => { + return cosmic::command::set_theme(t); + } Message::OpenContextDrawer(page) => { self.core.window.show_context = true; self.active_context_page = Some(page); diff --git a/cosmic-settings/src/pages/desktop/appearance/drawer.rs b/cosmic-settings/src/pages/desktop/appearance/drawer.rs new file mode 100644 index 0000000..bdf4b4c --- /dev/null +++ b/cosmic-settings/src/pages/desktop/appearance/drawer.rs @@ -0,0 +1,486 @@ +use cosmic::app::{ContextDrawer, context_drawer}; +use cosmic::config::CosmicTk; +use cosmic::cosmic_config::{Config, ConfigSet}; +use cosmic::cosmic_theme::Spacing; +use cosmic::cosmic_theme::palette::{FromColor, Hsv, Srgb}; +use cosmic::iced_core::{Color, Length}; +use cosmic::widget::{ + ColorPickerModel, color_picker::ColorPickerUpdate, container, flex_row, settings, text, +}; +use cosmic::{Apply, Task}; +use cosmic::{Element, widget}; +use std::sync::Arc; + +use crate::app; +use crate::widget::color_picker_context_view; + +use super::{ + ContextView, Message, font_config, icon_themes, + icon_themes::{IconHandles, IconThemes}, + theme_manager, +}; + +pub struct Content { + context_view: Option, + pub custom_accent: ColorPickerModel, + pub accent_window_hint: ColorPickerModel, + pub application_background: ColorPickerModel, + pub container_background: ColorPickerModel, + pub interface_text: ColorPickerModel, + pub control_component: ColorPickerModel, + + font_config: font_config::Model, + + icons_fetched: bool, + icon_fetch_handle: Option, + + icon_theme_active: Option, + icon_global: bool, + icon_themes: IconThemes, + icon_handles: IconHandles, + tk_config: Option, +} +#[derive(Debug, Clone)] +pub enum FontMessage { + FontLoaded(Vec>, Vec>), + Search(String), + Select(Arc), +} + +#[derive(Debug, Clone)] +pub enum IconMessage { + IconLoaded((IconThemes, IconHandles)), + IconTheme(usize), + ApplyThemeGlobal(bool), +} + +crate::cache_dynamic_lazy! { + static HEX: String = fl!("hex"); + static RGB: String = fl!("rgb"); + static ICON_THEME: String = fl!("icon-theme"); + static RESET_TO_DEFAULT: String = fl!("reset-to-default"); +} + +impl From<&theme_manager::Manager> for Content { + fn from(theme_manager: &theme_manager::Manager) -> Self { + let theme = theme_manager.theme(); + Self { + context_view: None, + custom_accent: ColorPickerModel::new( + &*HEX, + &*RGB, + None, + theme_manager.get_color(&ContextView::CustomAccent), + ), + application_background: ColorPickerModel::new( + &*HEX, + &*RGB, + Some(theme.background.base.into()), + theme_manager.get_color(&ContextView::ApplicationBackground), + ), + container_background: ColorPickerModel::new( + &*HEX, + &*RGB, + None, + theme_manager.get_color(&ContextView::ContainerBackground), + ), + interface_text: ColorPickerModel::new( + &*HEX, + &*RGB, + Some(theme.background.on.into()), + theme_manager.get_color(&ContextView::InterfaceText), + ), + control_component: ColorPickerModel::new( + &*HEX, + &*RGB, + Some(theme.palette.neutral_5.into()), + theme_manager.get_color(&ContextView::ControlComponent), + ), + accent_window_hint: ColorPickerModel::new( + &*HEX, + &*RGB, + None, + theme_manager.get_color(&ContextView::AccentWindowHint), + ), + font_config: font_config::Model::new(), + icons_fetched: false, + icon_global: cosmic::config::apply_theme_global(), + icon_fetch_handle: None, + icon_theme_active: None, + icon_themes: Vec::new(), + icon_handles: Vec::new(), + tk_config: CosmicTk::config().ok(), + } + } +} + +impl Content { + pub fn current_font_family(&self, context_view: &ContextView) -> String { + match *context_view { + ContextView::SystemFont => self.font_config.interface_font.family.clone(), + ContextView::MonospaceFont => self.font_config.monospace_font.family.clone(), + _ => "".to_string(), + } + } + + pub fn update_font( + &mut self, + message: FontMessage, + context_view: Option<&ContextView>, + ) -> Task { + match message { + FontMessage::FontLoaded(interface, mono) => { + return self.font_config.font_loaded(mono, interface); + } + FontMessage::Search(input) => match context_view { + None => Task::none(), + Some(c) => self.font_config.search(input.to_string(), c), + }, + FontMessage::Select(font) => { + if let Some(context_view) = context_view { + if let Some(task) = self.font_config.select(font.to_string(), context_view) { + return task; + } + } + return Task::none(); + } + } + } + + pub fn update_color( + &mut self, + message: ColorPickerUpdate, + context_view: &ContextView, + ) -> Task { + let mut tasks = Vec::new(); + + tasks.push(match message { + ColorPickerUpdate::AppliedColor | ColorPickerUpdate::Reset => { + self.context_view = None; + cosmic::task::message(crate::pages::Message::CloseContextDrawer) + } + + ColorPickerUpdate::ActionFinished => Task::none(), + + ColorPickerUpdate::Cancel => { + self.context_view = None; + cosmic::task::message(crate::pages::Message::CloseContextDrawer) + } + + _ => Task::none(), + }); + + tasks.push(match *context_view { + ContextView::CustomAccent => self.custom_accent.update(message), + ContextView::ApplicationBackground => self.application_background.update(message), + ContextView::ContainerBackground => self.container_background.update(message), + ContextView::InterfaceText => self.interface_text.update(message), + ContextView::ControlComponent => self.control_component.update(message), + ContextView::AccentWindowHint => self.accent_window_hint.update(message), + _ => Task::none(), + }); + + cosmic::Task::batch(tasks) + } + + pub fn update_icon( + &mut self, + message: IconMessage, + _context_view: &ContextView, + ) -> Task { + match message { + IconMessage::IconTheme(id) => { + if let Some(theme) = self.icon_themes.get(id).cloned() { + self.icon_theme_active = Some(id); + + if let Some(ref config) = self.tk_config { + _ = config.set::("icon_theme", theme.id); + } + + tokio::spawn(icon_themes::set_gnome_icon_theme(theme.name)); + } + } + IconMessage::ApplyThemeGlobal(enabled) => { + if let Some(config) = self.tk_config.as_ref() { + _ = config.set("apply_theme_global", enabled); + self.icon_global = enabled; + } else { + tracing::error!( + "Failed to apply theme to GNOME config because the CosmicTK config does not exist." + ); + } + } + IconMessage::IconLoaded((icon_themes, icon_handles)) => { + let active_icon_theme = cosmic::config::icon_theme(); + + // Set the icon themes, and define the active icon theme. + self.icon_themes = icon_themes; + self.icon_theme_active = self + .icon_themes + .iter() + .position(|theme| theme.id == active_icon_theme); + self.icon_handles = icon_handles; + + return cosmic::task::message(app::Message::SetTheme( + cosmic::theme::system_preference(), + )); + } + } + Task::none() + } + + pub fn on_open(&mut self, context_view: &ContextView) -> Task { + match *context_view { + ContextView::IconsAndToolkit => { + if self.icons_fetched { + return Task::none(); + } + + self.icons_fetched = true; + let (task, handle) = cosmic::task::future(icon_themes::fetch()).abortable(); + self.icon_fetch_handle = Some(handle); + + return task; + } + ContextView::MonospaceFont | ContextView::SystemFont => { + self.font_config.reset(); + } + _ => {} + } + Task::none() + } + + pub fn on_leave(&mut self) -> Task { + if let Some(handle) = self.icon_fetch_handle.take() { + handle.abort(); + } + Task::none() + } + + // Returns the color associated with the color picker for the context view. + // Returns None if the context view is not associated to any color picker. + pub fn current_color(&self, context_view: &ContextView) -> Option { + match *context_view { + ContextView::CustomAccent => self.custom_accent.get_applied_color(), + ContextView::ApplicationBackground => self.application_background.get_applied_color(), + ContextView::ContainerBackground => self.container_background.get_applied_color(), + ContextView::InterfaceText => self.interface_text.get_applied_color(), + ContextView::ControlComponent => self.control_component.get_applied_color(), + ContextView::AccentWindowHint => self.accent_window_hint.get_applied_color(), + _ => None, + } + } + + pub fn reset(&mut self, manager: &theme_manager::Manager) { + self.application_background = ColorPickerModel::new( + &*HEX, + &*RGB, + Some(manager.theme().background.base.into()), + manager.get_color(&ContextView::ApplicationBackground), + ); + self.custom_accent = ColorPickerModel::new( + &*HEX, + &*RGB, + None, + manager.get_color(&ContextView::CustomAccent), + ); + self.container_background = ColorPickerModel::new( + &*HEX, + &*RGB, + None, + manager.get_color(&ContextView::ContainerBackground), + ); + self.interface_text = ColorPickerModel::new( + &*HEX, + &*RGB, + Some(manager.theme().background.on.into()), + manager.get_color(&ContextView::InterfaceText), + ); + self.control_component = ColorPickerModel::new( + &*HEX, + &*RGB, + Some(manager.theme().palette.neutral_5.into()), + manager.get_color(&ContextView::ControlComponent), + ); + self.accent_window_hint = ColorPickerModel::new( + &*HEX, + &*RGB, + None, + manager.get_color(&ContextView::AccentWindowHint), + ); + } + + pub fn context_drawer( + &self, + context_view: Option, + ) -> Option> { + Some(match context_view? { + ContextView::AccentWindowHint => context_drawer( + color_picker_context_view( + None, + RESET_TO_DEFAULT.as_str().into(), + Message::DrawerColor, + &self.accent_window_hint, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("window-hint-accent")), + + ContextView::ApplicationBackground => context_drawer( + color_picker_context_view( + None, + RESET_TO_DEFAULT.as_str().into(), + Message::DrawerColor, + &self.application_background, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("app-background")), + + ContextView::ContainerBackground => context_drawer( + color_picker_context_view( + Some(fl!("container-background", "desc-detail").into()), + fl!("container-background", "reset").into(), + Message::DrawerColor, + &self.container_background, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("container-background")), + + ContextView::ControlComponent => context_drawer( + color_picker_context_view( + None, + RESET_TO_DEFAULT.as_str().into(), + Message::DrawerColor, + &self.control_component, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("control-tint")), + + ContextView::CustomAccent => context_drawer( + color_picker_context_view( + None, + RESET_TO_DEFAULT.as_str().into(), + Message::DrawerColor, + &self.custom_accent, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("accent-color")), + + ContextView::InterfaceText => context_drawer( + color_picker_context_view( + None, + RESET_TO_DEFAULT.as_str().into(), + Message::DrawerColor, + &self.interface_text, + ) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("text-tint")), + + ContextView::SystemFont => context_drawer( + self.font_config + .selection_context(&ContextView::SystemFont, |name| { + Message::DrawerFont(FontMessage::Select(name)) + }) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("interface-font")) + .header(self.font_config.search_input()), + + ContextView::MonospaceFont => context_drawer( + self.font_config + .selection_context(&ContextView::MonospaceFont, |name| { + Message::DrawerFont(FontMessage::Select(name)) + }) + .map(crate::pages::Message::Appearance), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("monospace-font")) + .header(self.font_config.search_input()), + + ContextView::IconsAndToolkit => context_drawer( + self.icons_and_toolkit(), + crate::pages::Message::CloseContextDrawer, + ), + }) + } + + pub fn icons_and_toolkit(&self) -> Element<'_, crate::pages::Message> { + let Spacing { + space_xxs, + space_xs, + space_m, + .. + } = cosmic::theme::spacing(); + + let active = self.icon_theme_active; + + cosmic::iced::widget::column![ + // Export theme choice + settings::section().add( + settings::item::builder(fl!("enable-export")) + .description(fl!("enable-export", "desc")) + .toggler(self.icon_global, |b| { + Message::DrawerIcon(IconMessage::ApplyThemeGlobal(b)) + }) + ), + // Icon theme previews + widget::column::with_children(vec![ + text::heading(&*ICON_THEME).into(), + flex_row( + self.icon_themes + .iter() + .zip(self.icon_handles.iter()) + .enumerate() + .map(|(i, (theme, handles))| { + let selected = active.map(|j| i == j).unwrap_or_default(); + icon_themes::button(&theme.name, handles, i, selected, |id| { + Message::DrawerIcon(IconMessage::IconTheme(id)) + }) + }) + .collect(), + ) + .row_spacing(space_xs) + .column_spacing(space_xs) + .apply(container) + .center_x(Length::Fill) + .into() + ]) + .spacing(space_xxs) + ] + .spacing(space_m) + .width(Length::Fill) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + } +} + +fn reset_color_control( + field: &mut ColorPickerModel, + manager: &theme_manager::Manager, + context_view: ContextView, +) -> Vec> { + let mut tasks = Vec::new(); + + let color = manager.get_color(&context_view).map(Srgb::from); + + if let Some(c) = color { + tasks.push(field.update(ColorPickerUpdate::ActiveColor(Hsv::from_color(c)))); + tasks.push(field.update(ColorPickerUpdate::AppliedColor)); + } else { + tasks.push(field.update(ColorPickerUpdate::Reset)); + } + + tasks +} diff --git a/cosmic-settings/src/pages/desktop/appearance/font_config.rs b/cosmic-settings/src/pages/desktop/appearance/font_config.rs index d2ebc01..e4a0b37 100644 --- a/cosmic-settings/src/pages/desktop/appearance/font_config.rs +++ b/cosmic-settings/src/pages/desktop/appearance/font_config.rs @@ -12,6 +12,10 @@ use cosmic::{ }; use cosmic_config::ConfigSet; +use crate::app; + +use super::{ContextView, Message, drawer}; + const INTERFACE_FONT: &str = "interface_font"; const MONOSPACE_FONT: &str = "monospace_font"; @@ -62,42 +66,174 @@ pub fn load_font_families() -> (Vec>, Vec>) { (interface, mono) } -pub fn selection_context<'a>( - families: &'a [Arc], - current_font: &str, - system: bool, -) -> Element<'a, super::Message> { - let svg_accent = Rc::new(|theme: &cosmic::Theme| svg::Style { - color: Some(theme.cosmic().accent_color().into()), - }); +#[derive(Debug)] +pub struct Model { + font_search: String, + font_filter: Vec>, - let list = families.iter().fold(widget::list_column(), |list, family| { - let selected = &**family == current_font; - list.add( - settings::item_row(vec![ - widget::text::body(&**family) - .wrapping(Wrapping::Word) - .width(cosmic::iced::Length::Fill) - .into(), - if selected { - widget::icon::from_name("object-select-symbolic") - .size(16) - .icon() - .class(cosmic::theme::Svg::Custom(svg_accent.clone())) - .into() - } else { - widget::horizontal_space().width(16).into() - }, - ]) - .apply(widget::container) - .class(cosmic::theme::Container::List) - .apply(widget::button::custom) - .class(cosmic::theme::Button::Transparent) - .on_press(super::Message::FontSelect(system, family.clone())), - ) - }); + interface_font_families: Vec>, + pub interface_font: FontConfig, - list.into() + monospace_font_families: Vec>, + pub monospace_font: FontConfig, +} + +impl Model { + pub fn new() -> Model { + Model { + font_filter: Vec::new(), + font_search: String::new(), + interface_font_families: Vec::new(), + interface_font: cosmic::config::interface_font(), + monospace_font_families: Vec::new(), + monospace_font: cosmic::config::monospace_font(), + } + } + + pub fn reset(&mut self) { + self.font_search.clear(); + self.font_filter.clear(); + } + + pub fn font_loaded( + &mut self, + mono: Vec>, + interface: Vec>, + ) -> Task { + self.interface_font_families = interface; + self.monospace_font_families = mono; + + Task::none() + } + + pub fn select( + &mut self, + font: String, + context_view: &ContextView, + ) -> Option> { + match *context_view { + ContextView::MonospaceFont => { + self.monospace_font = FontConfig { + family: font.to_string(), + weight: cosmic::iced::font::Weight::Normal, + style: cosmic::iced::font::Style::Normal, + stretch: cosmic::iced::font::Stretch::Normal, + }; + + update_config(MONOSPACE_FONT, self.monospace_font.clone()); + return None; + } + ContextView::SystemFont => { + self.interface_font = FontConfig { + family: font.to_string(), + weight: cosmic::iced::font::Weight::Normal, + style: cosmic::iced::font::Style::Normal, + stretch: cosmic::iced::font::Stretch::Normal, + }; + update_config(INTERFACE_FONT, self.interface_font.clone()); + tokio::spawn(async move { + set_gnome_font_name(font.as_ref()).await; + }); + return None; + } + _ => return None, + } + } + + pub fn search(&mut self, input: String, context_view: &ContextView) -> Task { + self.font_search = input.to_lowercase(); + self.font_filter.clear(); + + let mut result: Option>> = None; + + if let Some(fonts) = self.current_font_family(context_view) { + result = Some( + fonts + .iter() + .filter(|f| f.to_lowercase().contains(&self.font_search)) + .map(|f| f.clone()) + .collect(), + ); + } + + if let Some(fonts) = result.as_mut() { + self.font_filter.append(fonts); + } + Task::none() + } + + pub fn search_input(&self) -> Element<'_, crate::pages::Message> { + widget::search_input(fl!("type-to-search"), &self.font_search) + .on_input(|input| Message::DrawerFont(drawer::FontMessage::Search(input))) + .on_clear(Message::DrawerFont(drawer::FontMessage::Search( + String::new(), + ))) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + } + + pub fn selection_context( + &self, + context_view: &ContextView, + callback: impl Fn(Arc) -> super::Message, + ) -> Element<'_, super::Message> { + let svg_accent = Rc::new(|theme: &cosmic::Theme| svg::Style { + color: Some(theme.cosmic().accent_color().into()), + }); + + let (mut families, current_font) = match *context_view { + ContextView::MonospaceFont => { + (&self.monospace_font_families, &self.monospace_font.family) + } + ContextView::SystemFont => (&self.interface_font_families, &self.interface_font.family), + _ => (&self.monospace_font_families, &self.monospace_font.family), + }; + + if !self.font_filter.is_empty() { + families = &self.font_filter; + } + + let list = families.iter().fold(widget::list_column(), |list, family| { + let selected = &**family == current_font; + list.add( + settings::item_row(vec![ + widget::text::body(&**family) + .wrapping(Wrapping::Word) + .width(cosmic::iced::Length::Fill) + .into(), + if selected { + widget::icon::from_name("object-select-symbolic") + .size(16) + .icon() + .class(cosmic::theme::Svg::Custom(svg_accent.clone())) + .into() + } else { + widget::horizontal_space().width(16).into() + }, + ]) + .apply(widget::container) + .class(cosmic::theme::Container::List) + .apply(widget::button::custom) + .class(cosmic::theme::Button::Transparent) + .on_press(callback(family.clone())), + ) + }); + list.into() + } + + fn current_font_family(&self, context_view: &ContextView) -> Option<&Vec>> { + match *context_view { + ContextView::SystemFont => Some(&self.interface_font_families), + ContextView::MonospaceFont => Some(&self.monospace_font_families), + _ => None, + } + } +} + +fn update_config(variant: &str, font: FontConfig) { + if let Ok(config) = CosmicTk::config() { + _ = config.set(variant, font); + } } /// Set the preferred icon theme for GNOME/GTK applications. @@ -107,96 +243,3 @@ pub async fn set_gnome_font_name(font_name: &str) { .status() .await; } - -#[derive(Debug, Clone)] -pub enum Message { - InterfaceFontFamily(usize), - LoadedFonts(Vec>, Vec>), - MonospaceFontFamily(usize), -} - -#[derive(Debug, Default)] -pub struct Model { - pub interface_font_families: Vec>, - pub interface_font_family: Option, - pub monospace_font_families: Vec>, - pub monospace_font_family: Option, -} - -impl Model { - pub const fn new() -> Model { - Model { - interface_font_families: Vec::new(), - interface_font_family: None, - monospace_font_families: Vec::new(), - monospace_font_family: None, - } - } - - pub fn update(&mut self, message: Message) -> Task { - match message { - Message::InterfaceFontFamily(id) => { - if let Some(family) = self.interface_font_families.get(id) { - update_config( - INTERFACE_FONT, - FontConfig { - family: family.to_string(), - weight: cosmic::iced::font::Weight::Normal, - style: cosmic::iced::font::Style::Normal, - stretch: cosmic::iced::font::Stretch::Normal, - }, - ); - - self.interface_font_family = Some(id); - - let family = family.clone(); - tokio::spawn(async move { - set_gnome_font_name(family.as_ref()).await; - }); - } - } - - Message::LoadedFonts(interface, mono) => { - self.interface_font_families = interface; - self.monospace_font_families = mono; - - let interface_font = cosmic::config::interface_font(); - let monospace_font = cosmic::config::monospace_font(); - - self.interface_font_family = - font_family_to_pos(&self.interface_font_families, &interface_font.family); - - self.monospace_font_family = - font_family_to_pos(&self.monospace_font_families, &monospace_font.family); - } - - Message::MonospaceFontFamily(id) => { - if let Some(family) = self.monospace_font_families.get(id) { - update_config( - MONOSPACE_FONT, - FontConfig { - family: family.to_string(), - weight: cosmic::iced::font::Weight::Normal, - style: cosmic::iced::font::Style::Normal, - stretch: cosmic::iced::font::Stretch::Normal, - }, - ); - - self.monospace_font_family = Some(id); - } - } - } - - Task::none() - } -} - -fn font_family_to_pos(families: &[Arc], family: &str) -> Option { - families.iter().position(|f| &**f == family) -} - -fn update_config(variant: &str, font: FontConfig) { - if let Ok(config) = CosmicTk::config() { - _ = config.set(variant, font); - } -} diff --git a/cosmic-settings/src/pages/desktop/appearance/icon_themes.rs b/cosmic-settings/src/pages/desktop/appearance/icon_themes.rs index feac57c..fee1e47 100644 --- a/cosmic-settings/src/pages/desktop/appearance/icon_themes.rs +++ b/cosmic-settings/src/pages/desktop/appearance/icon_themes.rs @@ -26,6 +26,7 @@ pub fn button( handles: &[icon::Handle], id: usize, selected: bool, + callback: impl Fn(usize) -> super::Message, ) -> Element<'static, Message> { let theme = cosmic::theme::active(); let theme = theme.cosmic(); @@ -61,7 +62,7 @@ pub fn button( .spacing(theme.space_xxxs()), None, ) - .on_press(Message::IconTheme(id)) + .on_press(callback(id)) .selected(selected) .padding(theme.space_xs()) // Image button's style mostly works, but it needs a background to fit the design @@ -231,7 +232,9 @@ pub async fn fetch() -> Message { } } - Message::Entered(icon_themes.into_iter().unzip()) + Message::DrawerIcon(super::drawer::IconMessage::IconLoaded( + icon_themes.into_iter().unzip(), + )) } /// Set the preferred icon theme for GNOME/GTK applications. diff --git a/cosmic-settings/src/pages/desktop/appearance/mod.rs b/cosmic-settings/src/pages/desktop/appearance/mod.rs index 2a0e782..cdef5ec 100644 --- a/cosmic-settings/src/pages/desktop/appearance/mod.rs +++ b/cosmic-settings/src/pages/desktop/appearance/mod.rs @@ -1,55 +1,43 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +pub mod drawer; pub mod font_config; pub mod icon_themes; +pub mod mode_and_colors; +pub mod style; +pub mod theme_manager; -use std::borrow::Cow; use std::sync::Arc; -use cosmic::app::{ContextDrawer, context_drawer}; +use cosmic::app::ContextDrawer; //TODO: use embedded cosmic-files for portability use cosmic::config::CosmicTk; use cosmic::cosmic_config::{Config, ConfigSet, CosmicConfigEntry}; -use cosmic::cosmic_theme::palette::{FromColor, Hsv, Srgb, Srgba}; +use cosmic::cosmic_theme::palette::{FromColor, Hsv, Srgb}; use cosmic::cosmic_theme::{ - CornerRadii, DARK_THEME_BUILDER_ID, Density, LIGHT_THEME_BUILDER_ID, Spacing, Theme, - ThemeBuilder, ThemeMode, + CornerRadii, DARK_THEME_BUILDER_ID, Density, LIGHT_THEME_BUILDER_ID, Theme, ThemeBuilder, }; #[cfg(feature = "xdg-portal")] use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced_core::{Alignment, Color, Length}; -use cosmic::widget::icon::{from_name, icon}; +use cosmic::iced_core::{Alignment, Length}; use cosmic::widget::{ - ColorPickerModel, button, color_picker::ColorPickerUpdate, container, flex_row, - horizontal_space, radio, row, scrollable, settings, text, + button, color_picker::ColorPickerUpdate, container, horizontal_space, radio, row, settings, + text, }; use cosmic::{Apply, Element, Task, widget}; #[cfg(feature = "wayland")] use cosmic_panel_config::CosmicPanelConfig; use cosmic_settings_page::Section; use cosmic_settings_page::{self as page, section}; -use cosmic_settings_wallpaper as wallpaper; -use icon_themes::{IconHandles, IconThemes}; use ron::ser::PrettyConfig; -use serde::Serialize; use slab::Slab; use slotmap::{Key, SlotMap}; use crate::app; -use crate::widget::color_picker_context_view; - -use super::wallpaper::widgets::color_image; - -crate::cache_dynamic_lazy! { - static HEX: String = fl!("hex"); - static RGB: String = fl!("rgb"); - static RESET_TO_DEFAULT: String = fl!("reset-to-default"); - static ICON_THEME: String = fl!("icon-theme"); -} #[derive(Clone, Copy, Debug)] -enum ContextView { +pub enum ContextView { AccentWindowHint, ApplicationBackground, ContainerBackground, @@ -65,154 +53,29 @@ enum ContextView { pub struct Page { entity: page::Entity, on_enter_handle: Option, - accent_palette: AccentPalette, can_reset: bool, - no_custom_window_hint: bool, context_view: Option, - custom_accent: ColorPickerModel, - accent_window_hint: ColorPickerModel, - application_background: ColorPickerModel, - container_background: ColorPickerModel, - interface_text: ColorPickerModel, - control_component: ColorPickerModel, + drawer: drawer::Content, roundness: Roundness, + density: Density, - font_config: font_config::Model, - font_filter: Vec>, - font_search: String, - - /** Only fetch icons once. Allows for better cleanup. Also icon fetching can take for ages. */ - icons_fetched: bool, - icon_fetch_handle: Option, - - icon_theme_active: Option, - icon_themes: IconThemes, - icon_handles: IconHandles, - - theme: Theme, - theme_mode: ThemeMode, - theme_mode_config: Option, - theme_builder: ThemeBuilder, - theme_builder_config: Option, - - auto_switch_descs: [Cow<'static, str>; 4], + theme_manager: theme_manager::Manager, tk_config: Option, - settings_config: crate::config::Config, day_time: bool, } -#[derive(Default)] -pub struct AccentPalette { - dark: Option>, - light: Option>, - theme: Vec, -} - impl Default for Page { fn default() -> Self { - let settings_config = crate::config::Config::new(); - - let theme_mode_config = ThemeMode::config().ok(); - let theme_mode = theme_mode_config - .as_ref() - .map(|c| match ThemeMode::get_entry(c) { - Ok(t) => t, - Err((errors, t)) => { - for e in errors { - tracing::error!("{e}"); - } - t - } - }) - .unwrap_or_default(); - - let accent_palette = AccentPalette { - dark: settings_config.accent_palette_dark().ok(), - light: settings_config.accent_palette_light().ok(), - theme: Vec::new(), - }; - - let mut page: Page = ( - settings_config, - theme_mode_config, - theme_mode, - accent_palette, - ) - .into(); - page.update_accent_palette(); - page + theme_manager::Manager::default().into() } } -impl - From<( - crate::config::Config, - Option, - ThemeMode, - Option, - ThemeBuilder, - Option, - AccentPalette, - )> for Page -{ - fn from( - ( - settings_config, - theme_mode_config, - theme_mode, - theme_builder_config, - mut theme_builder, - tk_config, - accent_palette, - ): ( - crate::config::Config, - Option, - ThemeMode, - Option, - ThemeBuilder, - Option, - AccentPalette, - ), - ) -> Self { - let theme = if let Ok(c) = if theme_mode.is_dark { - Theme::dark_config() - } else { - Theme::light_config() - } { - Theme::get_entry(&c).unwrap_or_default() - } else { - if theme_mode.is_dark { - Theme::dark_default() - } else { - Theme::light_default() - } - }; - theme_builder = theme_builder - .clone() - .accent(theme.accent.base.color) - .bg_color(theme.bg_color()) - .corner_radii(theme.corner_radii) - .destructive(theme.destructive.base.color) - .spacing(theme.spacing) - .success(theme.success.base.color) - .warning(theme.warning.base.color) - .neutral_tint(theme.palette.neutral_5.color) - .text_tint(theme.background.on.color); - theme_builder.gaps = theme.gaps; - - let custom_accent = theme_builder.accent.filter(|c| { - let c = Srgba::new(c.red, c.green, c.blue, 1.0); - c != theme.palette.accent_blue - && c != theme.palette.accent_green - && c != theme.palette.accent_indigo - && c != theme.palette.accent_orange - && c != theme.palette.accent_pink - && c != theme.palette.accent_purple - && c != theme.palette.accent_red - && c != theme.palette.accent_warm_grey - && c != theme.palette.accent_yellow - }); +impl From for Page { + fn from(theme_manager: theme_manager::Manager) -> Self { + let tk_config = CosmicTk::config().ok(); + let theme_mode = theme_manager.mode(); + let theme_builder = theme_manager.builder().clone(); Self { entity: page::Entity::null(), @@ -223,126 +86,16 @@ impl theme_builder == ThemeBuilder::light() }, context_view: None, + drawer: drawer::Content::from(&theme_manager), roundness: theme_builder.corner_radii.into(), - custom_accent: ColorPickerModel::new( - &*HEX, - &*RGB, - None, - custom_accent.map(Color::from), - ), - application_background: ColorPickerModel::new( - &*HEX, - &*RGB, - Some(theme.background.base.into()), - theme_builder.bg_color.map(Color::from), - ), - container_background: ColorPickerModel::new( - &*HEX, - &*RGB, - None, - theme_builder.primary_container_bg.map(Color::from), - ), - interface_text: ColorPickerModel::new( - &*HEX, - &*RGB, - Some(theme.background.on.into()), - theme_builder.text_tint.map(Color::from), - ), - control_component: ColorPickerModel::new( - &*HEX, - &*RGB, - Some(theme.palette.neutral_5.into()), - theme_builder.neutral_tint.map(Color::from), - ), - accent_window_hint: ColorPickerModel::new( - &*HEX, - &*RGB, - None, - theme_builder.window_hint.map(Color::from), - ), - no_custom_window_hint: theme_builder.window_hint.is_none(), - font_config: font_config::Model::new(), - font_filter: Vec::new(), - font_search: String::new(), - icons_fetched: false, - icon_fetch_handle: None, - icon_theme_active: None, - icon_themes: Vec::new(), - icon_handles: Vec::new(), - accent_palette, - theme, - theme_mode_config, - theme_builder_config, - theme_mode, - theme_builder, + density: cosmic::config::interface_density(), + theme_manager, tk_config, - settings_config, day_time: true, - auto_switch_descs: [ - fl!("auto-switch", "sunrise").into(), - fl!("auto-switch", "sunset").into(), - fl!("auto-switch", "next-sunrise").into(), - fl!("auto-switch", "next-sunset").into(), - ], } } } -impl - From<( - crate::config::Config, - Option, - ThemeMode, - AccentPalette, - )> for Page -{ - fn from( - (settings_config, theme_mode_config, theme_mode, accent_palette): ( - crate::config::Config, - Option, - ThemeMode, - AccentPalette, - ), - ) -> Self { - let theme_builder_config = if theme_mode.is_dark { - ThemeBuilder::dark_config() - } else { - ThemeBuilder::light_config() - } - .ok(); - let theme_builder = theme_builder_config.as_ref().map_or_else( - || { - if theme_mode.is_dark { - ThemeBuilder::dark() - } else { - ThemeBuilder::light() - } - }, - |c| match ThemeBuilder::get_entry(c) { - Ok(t) => t, - Err((errors, t)) => { - for e in errors { - tracing::error!("{e}"); - } - t - } - }, - ); - - let tk_config = CosmicTk::config().ok(); - - Self::from(( - settings_config, - theme_mode_config, - theme_mode, - theme_builder_config, - theme_builder, - tk_config, - accent_palette, - )) - } -} - #[cfg(feature = "xdg-portal")] #[derive(Clone)] pub struct SaveResponse(pub Arc); @@ -367,37 +120,29 @@ impl std::fmt::Debug for OpenResponse { #[derive(Debug, Clone)] pub enum Message { - AccentWindowHint(ColorPickerUpdate), - ApplicationBackground(ColorPickerUpdate), - ApplyThemeGlobal(bool), Autoswitch(bool), - ContainerBackground(ColorPickerUpdate), - ControlComponent(ColorPickerUpdate), - CustomAccent(ColorPickerUpdate), DarkMode(bool), Density(Density), - DisplayMonoFont, - DisplaySystemFont, - Entered((IconThemes, IconHandles)), - IconsAndToolkit, + + DrawerOpen(ContextView), + DrawerColor(ColorPickerUpdate), + DrawerFont(drawer::FontMessage), + DrawerIcon(drawer::IconMessage), + #[cfg(feature = "xdg-portal")] ExportError, #[cfg(feature = "xdg-portal")] ExportFile(SaveResponse), #[cfg(feature = "xdg-portal")] ExportSuccess, - FontConfig(font_config::Message), - FontSearch(String), - FontSelect(bool, Arc), + GapSize(u32), - IconTheme(usize), #[cfg(feature = "xdg-portal")] ImportError, #[cfg(feature = "xdg-portal")] ImportFile(OpenResponse), #[cfg(feature = "xdg-portal")] ImportSuccess(Box), - InterfaceText(ColorPickerUpdate), Left, NewTheme(Box), PaletteAccent(cosmic::iced::Color), @@ -475,379 +220,90 @@ impl From for Roundness { } impl Page { - fn icons_and_toolkit(&self) -> Element<'_, crate::pages::Message> { - let Spacing { - space_xxs, - space_xs, - space_m, - .. - } = cosmic::theme::spacing(); - - let active = self.icon_theme_active; - cosmic::iced::widget::column![ - // Export theme choice - settings::section().add( - settings::item::builder(fl!("enable-export")) - .description(fl!("enable-export", "desc")) - .toggler( - cosmic::config::apply_theme_global(), - Message::ApplyThemeGlobal - ) - ), - // Icon theme previews - widget::column::with_children(vec![ - text::heading(&*ICON_THEME).into(), - flex_row( - self.icon_themes - .iter() - .zip(self.icon_handles.iter()) - .enumerate() - .map(|(i, (theme, handles))| { - let selected = active.map(|j| i == j).unwrap_or_default(); - icon_themes::button(&theme.name, handles, i, selected) - }) - .collect(), - ) - .row_spacing(space_xs) - .column_spacing(space_xs) - .apply(container) - .center_x(Length::Fill) - .into() - ]) - .spacing(space_xxs) - ] - .spacing(space_m) - .width(Length::Fill) - .apply(Element::from) - .map(crate::pages::Message::Appearance) - } - #[allow(clippy::too_many_lines)] pub fn update(&mut self, message: Message) -> Task { let mut tasks = Vec::new(); - - let mut needs_build = false; - let mut needs_sync = false; + let mut theme_staged: Option = None; match message { - Message::DisplayMonoFont => { - self.context_view = Some(ContextView::MonospaceFont); - self.font_search.clear(); - - return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity)); - } - - Message::DisplaySystemFont => { - self.context_view = Some(ContextView::SystemFont); - self.font_search.clear(); - - return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity)); - } - - Message::FontConfig(message) => { - return self.font_config.update(message); - } - - Message::FontSearch(input) => { - self.font_search = input.to_lowercase(); - self.font_filter.clear(); - - match self.context_view { - Some(ContextView::SystemFont) => { - self.font_config - .interface_font_families - .iter() - .filter(|f| f.to_lowercase().contains(&self.font_search)) - .for_each(|f| self.font_filter.push(f.clone())); - } - - Some(ContextView::MonospaceFont) => { - self.font_config - .monospace_font_families - .iter() - .filter(|f| f.to_lowercase().contains(&self.font_search)) - .for_each(|f| self.font_filter.push(f.clone())); - } - - _ => (), - } - } - - Message::FontSelect(is_system, family) => { - if is_system { - if let Some(id) = self - .font_config - .interface_font_families - .iter() - .position(|f| f == &family) - { - return self - .font_config - .update(font_config::Message::InterfaceFontFamily(id)); - } - } else if let Some(id) = self - .font_config - .monospace_font_families - .iter() - .position(|f| f == &family) - { - return self - .font_config - .update(font_config::Message::MonospaceFontFamily(id)); - } - } - Message::NewTheme(theme) => { - self.theme = *theme; - self.theme_builder = self - .theme_builder - .clone() - .accent(self.theme.accent.base.color) - .bg_color(self.theme.bg_color()) - .corner_radii(self.theme.corner_radii) - .destructive(self.theme.destructive.base.color) - .spacing(self.theme.spacing) - .success(self.theme.success.base.color) - .warning(self.theme.warning.base.color) - .neutral_tint(self.theme.palette.neutral_5.color) - .text_tint(self.theme.background.on.color); - self.theme_builder.gaps = self.theme.gaps; + _ = self.theme_manager.dark_mode(theme.is_dark); + + self.theme_manager + .selected_customizer_mut() + .set_theme(*theme); + + self.can_reset = if self.theme_manager.mode().is_dark { + *self.theme_manager.builder() != ThemeBuilder::dark() + } else { + *self.theme_manager.builder() != ThemeBuilder::light() + }; + + return cosmic::task::message(app::Message::SetTheme( + self.theme_manager.cosmic_theme(), + )); } + + Message::Autoswitch(enabled) => self.theme_manager.auto_switch(enabled), + Message::DarkMode(enabled) => { - if let Some(config) = self.theme_mode_config.as_ref() { - if let Err(err) = self.theme_mode.set_is_dark(config, enabled) { - tracing::error!(?err, "Error setting dark mode"); - } + if let Err(err) = self.theme_manager.dark_mode(enabled) { + tracing::error!(?err, "Error setting dark mode"); + } - tasks.push(self.reload_theme_mode()); + self.drawer.reset(&self.theme_manager); + tasks.push(cosmic::task::message(app::Message::SetTheme( + self.theme_manager.cosmic_theme(), + ))); + + theme_staged = Some(theme_manager::ThemeStaged::Current); + } + + Message::DrawerOpen(context_view) => { + self.context_view = Some(context_view); + tasks.push(cosmic::task::message( + crate::app::Message::OpenContextDrawer(self.entity), + )); + tasks.push(self.drawer.on_open(&context_view)); + } + + Message::DrawerFont(message) => { + tasks.push(self.drawer.update_font(message, self.context_view.as_ref())); + } + + Message::DrawerColor(u) => { + if let Some(context_view) = self.context_view.as_ref() { + tasks.push(self.drawer.update_color(u, context_view)); + theme_staged = self + .theme_manager + .set_color(self.drawer.current_color(context_view), context_view); } } - Message::Autoswitch(enabled) => { - self.theme_mode.auto_switch = enabled; - if let Some(config) = self.theme_mode_config.as_ref() { - _ = config.set::("auto_switch", enabled); - } - } - - Message::AccentWindowHint(u) => { - needs_sync = true; - - let (task, needs_update) = - self.update_color_picker(&u, ContextView::AccentWindowHint); - - tasks.push(task); - tasks.push(self.accent_window_hint.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - let color = self.accent_window_hint.get_applied_color().map(Srgb::from); - - needs_build = self - .theme_builder - .set_window_hint(config, color) - .unwrap_or_default(); - } - } - - Message::IconTheme(id) => { - if let Some(theme) = self.icon_themes.get(id).cloned() { - self.icon_theme_active = Some(id); - - if let Some(ref config) = self.tk_config { - _ = config.set::("icon_theme", theme.id); - } - - tokio::spawn(icon_themes::set_gnome_icon_theme(theme.name)); + Message::DrawerIcon(message) => { + if let Some(context_view) = self.context_view.as_ref() { + tasks.push(self.drawer.update_icon(message, context_view)); } } Message::WindowHintSize(active_hint) => { - needs_sync = true; - - let Some(config) = self.theme_builder_config.as_ref() else { - return Task::none(); - }; - - if self - .theme_builder - .set_active_hint(config, active_hint) - .unwrap_or_default() - { - // Update the gap if it's less than the active hint - if active_hint > self.theme_builder.gaps.1 { - let mut gaps = self.theme_builder.gaps; - gaps.1 = active_hint; - if self - .theme_builder - .set_gaps(config, gaps) - .unwrap_or_default() - { - self.theme_config_write("gaps", gaps); - } - } - - // Update the active_hint in the config - self.theme_config_write("active_hint", active_hint); - } + self.theme_manager.set_active_hint(active_hint); } - Message::GapSize(gap) => { - needs_sync = true; - - let Some(config) = self.theme_builder_config.as_ref() else { - return Task::none(); - }; - - let mut gaps = self.theme_builder.gaps; - - // Ensure that the gap is never less than what the active hint size is. - gaps.1 = if gap < self.theme_builder.active_hint { - self.theme_builder.active_hint - } else { - gap - }; - - if self - .theme_builder - .set_gaps(config, gaps) - .unwrap_or_default() - { - self.theme_config_write("gaps", gaps); - } - } - - Message::ApplicationBackground(u) => { - let (task, needs_update) = - self.update_color_picker(&u, ContextView::ApplicationBackground); - - tasks.push(task); - tasks.push(self.application_background.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - needs_build = self - .theme_builder - .set_bg_color( - config, - self.application_background - .get_applied_color() - .map(Srgba::from), - ) - .unwrap_or_default(); - } - } - - Message::ContainerBackground(u) => { - let (task, needs_update) = - self.update_color_picker(&u, ContextView::ContainerBackground); - - tasks.push(task); - tasks.push(self.container_background.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - needs_build = self - .theme_builder - .set_primary_container_bg( - config, - self.container_background - .get_applied_color() - .map(Srgba::from), - ) - .unwrap_or_default(); - } - } - - Message::CustomAccent(u) => { - let (task, needs_update) = self.update_color_picker(&u, ContextView::CustomAccent); - - tasks.push(task); - tasks.push(self.custom_accent.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - needs_build = self - .theme_builder - .set_accent( - config, - self.custom_accent.get_applied_color().map(Srgb::from), - ) - .unwrap_or_default(); - } - } - - Message::InterfaceText(u) => { - let (task, needs_update) = self.update_color_picker(&u, ContextView::InterfaceText); - - tasks.push(task); - tasks.push(self.interface_text.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - needs_build = self - .theme_builder - .set_text_tint( - config, - self.interface_text.get_applied_color().map(Srgb::from), - ) - .unwrap_or_default(); - } - } - - Message::ControlComponent(u) => { - let (task, needs_update) = - self.update_color_picker(&u, ContextView::ControlComponent); - - tasks.push(task); - tasks.push(self.control_component.update::(u)); - - if needs_update { - let Some(config) = self.theme_builder_config.as_ref() else { - return cosmic::Task::batch(tasks); - }; - - needs_build = self - .theme_builder - .set_neutral_tint( - config, - self.control_component.get_applied_color().map(Srgb::from), - ) - .unwrap_or_default(); - } + self.theme_manager.set_gap_size(gap); } Message::Roundness(r) => { - needs_sync = true; self.roundness = r; - let Some(config) = self.theme_builder_config.as_ref() else { - return Task::none(); - }; - let radii = self.roundness.into(); - if self - .theme_builder - .set_corner_radii(config, radii) - .unwrap_or_default() + if let None = self + .theme_manager + .selected_customizer_mut() + .set_corner_radii(radii) { - self.theme_config_write("corner_radii", radii); + return Task::none(); } #[cfg(feature = "wayland")] @@ -857,26 +313,21 @@ impl Page { } Message::Density(density) => { - needs_sync = true; + tracing::info!("Density changed: {:?}", density); + self.density = density; if let Some(config) = self.tk_config.as_mut() { _ = config.set("interface_density", density); _ = config.set("header_size", density); } - let Some(config) = self.theme_builder_config.as_ref() else { - return Task::none(); - }; - let spacing = density.into(); - if self - .theme_builder - .set_spacing(config, spacing) - .unwrap_or_default() - { - self.theme_config_write("spacing", spacing); - } + self.theme_manager.set_spacing(spacing); + + tasks.push(cosmic::task::message(app::Message::SetTheme( + self.theme_manager.cosmic_theme(), + ))); #[cfg(feature = "wayland")] tokio::task::spawn(async move { @@ -884,22 +335,6 @@ impl Page { }); } - Message::Entered((icon_themes, icon_handles)) => { - let active_icon_theme = cosmic::config::icon_theme(); - - // Set the icon themes, and define the active icon theme. - self.icon_themes = icon_themes; - self.icon_theme_active = self - .icon_themes - .iter() - .position(|theme| theme.id == active_icon_theme); - self.icon_handles = icon_handles; - - tasks.push(cosmic::task::message(app::Message::SetTheme( - cosmic::theme::system_preference(), - ))); - } - Message::Left => { tasks.push(cosmic::task::message(app::Message::SetTheme( cosmic::theme::system_preference(), @@ -907,80 +342,40 @@ impl Page { } Message::PaletteAccent(c) => { - let Some(config) = self.theme_builder_config.as_ref() else { - return Task::none(); - }; - - needs_build = self - .theme_builder - .set_accent(config, Some(c.into())) - .unwrap_or_default(); + theme_staged = self + .theme_manager + .selected_customizer_mut() + .set_accent(Some(c).map(Srgb::from)); } Message::Reset => { - self.theme_builder = if self.theme_mode.is_dark { - cosmic::cosmic_config::Config::system( - DARK_THEME_BUILDER_ID, - ThemeBuilder::VERSION, - ) - .map_or_else( - |_| ThemeBuilder::dark(), - |config| match ThemeBuilder::get_entry(&config) { - Ok(t) => t, - Err((errs, t)) => { - for err in errs { - tracing::warn!(?err, "Error getting system theme builder"); - } - t - } - }, - ) + let theme_type = self.theme_manager.cosmic_theme().theme_type; + + let builder = if theme_type.is_dark() { + ThemeBuilder::dark() } else { - cosmic::cosmic_config::Config::system( - LIGHT_THEME_BUILDER_ID, - ThemeBuilder::VERSION, - ) - .map_or_else( - |_| ThemeBuilder::light(), - |config| match ThemeBuilder::get_entry(&config) { - Ok(t) => t, - Err((errs, t)) => { - for err in errs { - tracing::warn!(?err, "Error getting system theme builder"); - } - t - } - }, - ) - }; - if let Some(config) = self.theme_builder_config.as_ref() { - _ = self.theme_builder.write_entry(config); + ThemeBuilder::light() }; + + self.theme_manager + .selected_customizer_mut() + .set_builder(builder.clone()) + .set_theme(builder.build()) + .apply_theme(); + if let Some(config) = self.tk_config.as_mut() { _ = config.set("interface_density", Density::Standard); _ = config.set("header_size", Density::Standard); } - let config = if self.theme_mode.is_dark { - Theme::dark_config() - } else { - Theme::light_config() - }; - let new_theme = self.theme_builder.clone().build(); - if let Ok(config) = config { - _ = new_theme.write_entry(&config); - } else { - tracing::error!("Failed to get the theme config."); - } - let r = self.roundness; + self.drawer.reset(&self.theme_manager); + #[cfg(feature = "wayland")] tokio::task::spawn(async move { Self::update_panel_radii(r); Self::update_panel_spacing(Density::Standard); }); - - tasks.push(self.reload_theme_mode()); } #[cfg(feature = "xdg-portal")] @@ -1004,7 +399,7 @@ impl Page { #[cfg(feature = "xdg-portal")] Message::StartExport => { - let is_dark = self.theme_mode.is_dark; + let is_dark = self.theme_manager.mode().is_dark; let name = format!("{}.ron", if is_dark { fl!("dark") } else { fl!("light") }); tasks.push(cosmic::task::future(async move { @@ -1062,7 +457,7 @@ impl Page { return Task::none(); }; - let theme_builder = self.theme_builder.clone(); + let theme_builder = self.theme_manager.builder().clone(); tasks.push(cosmic::task::future(async move { let Ok(builder) = @@ -1097,104 +492,51 @@ impl Page { #[cfg(feature = "xdg-portal")] Message::ImportSuccess(builder) => { tracing::trace!("Import successful"); - self.theme_builder = *builder; - if let Some(config) = self.theme_builder_config.as_ref() { - _ = self.theme_builder.write_entry(config); - }; + self.theme_manager + .selected_customizer_mut() + .set_builder(*builder.clone()) + .set_theme(builder.build()) + .apply_builder() + .apply_theme(); - let config = if self.theme_mode.is_dark { - Theme::dark_config() - } else { - Theme::light_config() - }; - let new_theme = self.theme_builder.clone().build(); - if let Ok(config) = config { - _ = new_theme.write_entry(&config); - } else { - tracing::error!("Failed to get the theme config."); - } - - tasks.push(self.reload_theme_mode()); + self.drawer.reset(&self.theme_manager); + tasks.push(cosmic::task::message(app::Message::SetTheme( + self.theme_manager.cosmic_theme(), + ))); } Message::UseDefaultWindowHint(v) => { - self.no_custom_window_hint = v; - - let Some(config) = self.theme_builder_config.as_ref() else { + if !v { + let _ = self + .theme_manager + .selected_customizer_mut() + .set_window_hint(None) + .is_some_and(|_| true); return Task::none(); - }; - - needs_build = self - .theme_builder - .set_window_hint( - config, - if v { - None - } else { - let theme = if self.theme_mode.is_dark { - Theme::dark_default() - } else { - Theme::light_default() - }; - - let window_hint = self - .theme_builder - .window_hint - .filter(|c| { - let c = Srgba::new(c.red, c.green, c.blue, 1.0); - c != theme.palette.accent_blue - && c != theme.palette.accent_green - && c != theme.palette.accent_indigo - && c != theme.palette.accent_orange - && c != theme.palette.accent_pink - && c != theme.palette.accent_purple - && c != theme.palette.accent_red - && c != theme.palette.accent_warm_grey - && c != theme.palette.accent_yellow - }) - .unwrap_or( - self.custom_accent - .get_applied_color() - .unwrap_or_default() - .into(), - ); - - _ = self.accent_window_hint.update::( - ColorPickerUpdate::ActiveColor(Hsv::from_color(window_hint)), - ); - - self.accent_window_hint.get_applied_color().map(Srgb::from) - }, - ) - .unwrap_or_default(); - } - - Message::ApplyThemeGlobal(enabled) => { - if let Some(config) = self.tk_config.as_ref() { - _ = config.set("apply_theme_global", enabled); - } else { - tracing::error!( - "Failed to apply theme to GNOME config because the CosmicTK config does not exist." - ); } - return Task::none(); - } + let window_hint = self + .theme_manager + .builder() + .window_hint + .or(self.theme_manager.builder().accent); - Message::IconsAndToolkit => { - self.context_view = Some(ContextView::IconsAndToolkit); - let mut tasks = Vec::new(); - tasks.push(cosmic::task::message( - crate::app::Message::OpenContextDrawer(self.entity), - )); - if !self.icons_fetched { - self.icons_fetched = true; - let (task, handle) = cosmic::task::future(icon_themes::fetch()).abortable(); - self.icon_fetch_handle = Some(handle); - tasks.push(task); - } - return Task::batch(tasks); + _ = self.drawer.accent_window_hint.update::( + ColorPickerUpdate::ActiveColor(Hsv::from_color( + window_hint.unwrap_or_default(), + )), + ); + + _ = self + .drawer + .accent_window_hint + .update::(ColorPickerUpdate::AppliedColor); + + theme_staged = self + .theme_manager + .selected_customizer_mut() + .set_window_hint(window_hint); } Message::Daytime(day_time) => { @@ -1203,256 +545,13 @@ impl Page { } } - // If the theme builder changed, write a new theme to disk on a background thread. - if needs_build { - self.update_accent_palette(); - let theme_builder = self.theme_builder.clone(); - let is_dark = self.theme_mode.is_dark; - let current_theme = self.theme.clone(); + let mut tasks = cosmic::Task::batch(tasks); - tasks.push(cosmic::task::future(async move { - let config = if is_dark { - Theme::dark_config() - } else { - Theme::light_config() - }; - - if let Ok(config) = config { - let new_theme = theme_builder.build(); - - macro_rules! theme_transaction { - ($config:ident, $current_theme:ident, $new_theme:ident, { $($name:ident;)+ }) => { - let tx = $config.transaction(); - - $( - if $current_theme.$name != $new_theme.$name { - _ = tx.set(stringify!($name), $new_theme.$name.clone()); - } - )+ - - _ = tx.commit(); - } - } - - theme_transaction!(config, current_theme, new_theme, { - accent; - accent_button; - background; - button; - destructive; - destructive_button; - link_button; - icon_button; - palette; - primary; - secondary; - shade; - success; - text_button; - warning; - warning_button; - window_hint; - }); - - Message::NewTheme(Box::new(new_theme)).into() - } else { - tracing::error!("Failed to get the theme config."); - crate::app::Message::None - } - })); + if let Some(stage) = theme_staged { + tasks = tasks.chain(self.theme_manager.build_theme(stage)); } - self.can_reset = if self.theme_mode.is_dark { - self.theme_builder != ThemeBuilder::dark() - } else { - self.theme_builder != ThemeBuilder::light() - }; - - if needs_sync { - let theme_builder = self.theme_builder.clone(); - let is_dark = self.theme_mode.is_dark; - - tokio::task::spawn(async move { - if let Err(why) = Self::sync_theme_changes_between_modes(theme_builder, is_dark) { - tracing::error!(?why, "Error syncing theme changes."); - } - }); - } - - cosmic::Task::batch(tasks) - } - - fn update_accent_palette(&mut self) { - let palette = self.theme_builder.palette.as_ref(); - self.accent_palette.theme = vec![ - palette.accent_blue, - palette.accent_indigo, - palette.accent_purple, - palette.accent_pink, - palette.accent_red, - palette.accent_orange, - palette.accent_yellow, - palette.accent_green, - palette.accent_warm_grey, - ]; - } - - fn reload_theme_mode(&mut self) -> Task { - let entity = self.entity; - let font_config = std::mem::take(&mut self.font_config); - let icon_themes = std::mem::take(&mut self.icon_themes); - let icon_handles = std::mem::take(&mut self.icon_handles); - let icon_theme_active = self.icon_theme_active.take(); - let day_time = self.day_time; - - *self = Self::from(( - self.settings_config.clone(), - self.theme_mode_config.take(), - self.theme_mode, - std::mem::take(&mut self.accent_palette), - )); - - self.update_accent_palette(); - - self.entity = entity; - self.day_time = day_time; - self.icon_themes = icon_themes; - self.icon_handles = icon_handles; - self.icon_theme_active = icon_theme_active; - self.font_config = font_config; - - cosmic::task::message(app::Message::SetTheme(cosmic::theme::system_preference())) - } - - fn update_color_picker( - &mut self, - message: &ColorPickerUpdate, - context_view: ContextView, - ) -> (Task, bool) { - let mut needs_update = false; - - let task = match message { - ColorPickerUpdate::AppliedColor | ColorPickerUpdate::Reset => { - needs_update = true; - cosmic::task::message(crate::pages::Message::CloseContextDrawer) - } - - ColorPickerUpdate::ActionFinished => { - needs_update = true; - Task::none() - } - - ColorPickerUpdate::Cancel => { - cosmic::task::message(crate::pages::Message::CloseContextDrawer) - } - - ColorPickerUpdate::ToggleColorPicker => { - self.context_view = Some(context_view); - cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity)) - } - - _ => Task::none(), - }; - - (task, needs_update) - } - - /// Syncs changes for dark and light theme. - /// Roundness and window management settings should be consistent between dark / light mode. - fn sync_theme_changes_between_modes( - current_theme_builder: ThemeBuilder, - is_dark: bool, - ) -> Result<(), cosmic::cosmic_config::Error> { - let (other_builder_config, other_theme_config) = if is_dark { - (ThemeBuilder::light_config()?, Theme::light_config()?) - } else { - (ThemeBuilder::dark_config()?, Theme::dark_config()?) - }; - - let mut theme_builder = match ThemeBuilder::get_entry(&other_builder_config) { - Ok(t) => t, - Err((errs, t)) => { - for err in errs { - tracing::error!(?err, "Error loading theme builder"); - } - t - } - }; - - let mut theme = match Theme::get_entry(&other_theme_config) { - Ok(t) => t, - Err((errs, t)) => { - for err in errs { - tracing::error!(?err, "Error loading theme"); - } - t - } - }; - - if theme_builder.active_hint != current_theme_builder.active_hint { - if let Err(err) = theme_builder - .set_active_hint(&other_builder_config, current_theme_builder.active_hint) - { - tracing::error!(?err, "Error setting active hint"); - } - if let Err(err) = - theme.set_active_hint(&other_theme_config, current_theme_builder.active_hint) - { - tracing::error!(?err, "Error setting active hint"); - } - } - - if theme_builder.gaps != current_theme_builder.gaps { - if let Err(err) = - theme_builder.set_gaps(&other_builder_config, current_theme_builder.gaps) - { - tracing::error!(?err, "Error setting gaps"); - } - if let Err(err) = theme.set_gaps(&other_theme_config, current_theme_builder.gaps) { - tracing::error!(?err, "Error setting gaps"); - } - } - - if theme_builder.corner_radii != current_theme_builder.corner_radii { - if let Err(err) = theme_builder - .set_corner_radii(&other_builder_config, current_theme_builder.corner_radii) - { - tracing::error!(?err, "Error setting corner radii"); - } - - if let Err(err) = - theme.set_corner_radii(&other_theme_config, current_theme_builder.corner_radii) - { - tracing::error!(?err, "Error setting corner radii"); - } - } - - if theme_builder.spacing != current_theme_builder.spacing { - if let Err(err) = - theme_builder.set_spacing(&other_builder_config, current_theme_builder.spacing) - { - tracing::error!(?err, "Error setting spacing"); - } - - if let Err(err) = theme.set_spacing(&other_theme_config, current_theme_builder.spacing) - { - tracing::error!(?err, "Error setting spacing"); - } - } - - Ok(()) - } - - fn theme_config_write(&self, name: &str, value: T) { - let config_res = if self.theme_mode.is_dark { - Theme::dark_config() - } else { - Theme::light_config() - }; - - if let Ok(config) = config_res { - _ = config.set(name, value); - } + tasks } // TODO: cache panel and dock configs so that they needn't be re-read @@ -1553,8 +652,8 @@ impl page::Page for Page { sections: &mut SlotMap>, ) -> Option { Some(vec![ - sections.insert(mode_and_colors()), - sections.insert(style()), + sections.insert(mode_and_colors::section()), + sections.insert(style::section()), sections.insert(interface_density()), sections.insert(window_management()), sections.insert(experimental()), @@ -1565,7 +664,7 @@ impl page::Page for Page { #[cfg(feature = "xdg-portal")] fn header_view(&self) -> Option> { let content = row::with_capacity(2) - .spacing(self.theme_builder.spacing.space_xxs) + .spacing(self.theme_manager.builder().spacing.space_xxs) .push(button::standard(fl!("import")).on_press(Message::StartImport)) .push(button::standard(fl!("export")).on_press(Message::StartExport)) .apply(container) @@ -1589,8 +688,8 @@ impl page::Page for Page { // cosmic::task::future(icon_themes::fetch()).map(crate::pages::Message::Appearance), // Load font families cosmic::task::future(async move { - let (mono, interface) = font_config::load_font_families(); - Message::FontConfig(font_config::Message::LoadedFonts(mono, interface)) + let (interface, mono) = font_config::load_font_families(); + Message::DrawerFont(drawer::FontMessage::FontLoaded(interface, mono)) }) .map(crate::pages::Message::Appearance), ]) @@ -1601,489 +700,23 @@ impl page::Page for Page { } fn on_leave(&mut self) -> Task { + let mut tasks = Vec::new(); if let Some(handle) = self.on_enter_handle.take() { handle.abort(); } - if let Some(handle) = self.icon_fetch_handle.take() { - handle.abort(); - } - cosmic::task::message(crate::pages::Message::Appearance(Message::Left)) + tasks.push(self.drawer.on_leave()); + tasks.push(cosmic::task::message(crate::pages::Message::Appearance( + Message::Left, + ))); + + cosmic::task::batch(tasks) } fn context_drawer(&self) -> Option> { - Some(match self.context_view? { - ContextView::AccentWindowHint => context_drawer( - color_picker_context_view( - None, - RESET_TO_DEFAULT.as_str().into(), - Message::AccentWindowHint, - &self.accent_window_hint, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("window-hint-accent")), - - ContextView::ApplicationBackground => context_drawer( - color_picker_context_view( - None, - RESET_TO_DEFAULT.as_str().into(), - Message::ApplicationBackground, - &self.application_background, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("app-background")), - - ContextView::ContainerBackground => context_drawer( - color_picker_context_view( - Some(fl!("container-background", "desc-detail").into()), - fl!("container-background", "reset").into(), - Message::ContainerBackground, - &self.container_background, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("container-background")), - - ContextView::ControlComponent => context_drawer( - color_picker_context_view( - None, - RESET_TO_DEFAULT.as_str().into(), - Message::ControlComponent, - &self.control_component, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("control-tint")), - - ContextView::CustomAccent => context_drawer( - color_picker_context_view( - None, - RESET_TO_DEFAULT.as_str().into(), - Message::CustomAccent, - &self.custom_accent, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("accent-color")), - - ContextView::InterfaceText => context_drawer( - color_picker_context_view( - None, - RESET_TO_DEFAULT.as_str().into(), - Message::InterfaceText, - &self.interface_text, - ) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("text-tint")), - - ContextView::SystemFont => { - let filter = if self.font_search.is_empty() { - &self.font_config.interface_font_families - } else { - &self.font_filter - }; - let search_input = widget::search_input(fl!("type-to-search"), &self.font_search) - .on_input(Message::FontSearch) - .on_clear(Message::FontSearch(String::new())) - .apply(Element::from) - .map(crate::pages::Message::Appearance); - - let current_font = cosmic::config::interface_font(); - - context_drawer( - font_config::selection_context(filter, current_font.family.as_str(), true) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("interface-font")) - .header(search_input) - } - - ContextView::MonospaceFont => { - let filter = if self.font_search.is_empty() { - &self.font_config.monospace_font_families - } else { - &self.font_filter - }; - let search_input = widget::search_input(fl!("type-to-search"), &self.font_search) - .on_input(Message::FontSearch) - .on_clear(Message::FontSearch(String::new())) - .apply(Element::from) - .map(crate::pages::Message::Appearance); - - let current_font = cosmic::config::monospace_font(); - - context_drawer( - font_config::selection_context(filter, current_font.family.as_str(), false) - .map(crate::pages::Message::Appearance), - crate::pages::Message::CloseContextDrawer, - ) - .title(fl!("monospace-font")) - .header(search_input) - } - - ContextView::IconsAndToolkit => context_drawer( - self.icons_and_toolkit(), - crate::pages::Message::CloseContextDrawer, - ), - }) + self.drawer.context_drawer(self.context_view) } } -#[allow(clippy::too_many_lines)] -pub fn mode_and_colors() -> Section { - crate::slab!(descriptions { - auto_txt = fl!("auto"); - auto_switch = fl!("auto-switch"); - accent_color = fl!("accent-color"); - app_bg = fl!("app-background"); - container_bg = fl!("container-background"); - container_bg_desc = fl!("container-background", "desc"); - text_tint = fl!("text-tint"); - text_tint_desc = fl!("text-tint", "desc"); - control_tint = fl!("control-tint"); - control_tint_desc = fl!("control-tint", "desc"); - window_hint_toggle = fl!("window-hint-accent-toggle"); - window_hint = fl!("window-hint-accent"); - dark = fl!("dark"); - light = fl!("light"); - }); - - let dark_mode_illustration = from_name("illustration-appearance-mode-dark").handle(); - let light_mode_illustration = from_name("illustration-appearance-mode-light").handle(); - let go_next_icon = from_name("go-next-symbolic").handle(); - - Section::default() - .title(fl!("mode-and-colors")) - .descriptions(descriptions) - .view::(move |_binder, page, section| { - let Spacing { space_xxs, .. } = cosmic::theme::spacing(); - - let descriptions = §ion.descriptions; - let palette = &page.theme_builder.palette.as_ref(); - let cur_accent = page - .theme_builder - .accent - .map_or(palette.accent_blue, Srgba::from); - - let accent_palette_values = match ( - page.theme_mode.is_dark, - page.accent_palette.dark.as_ref(), - page.accent_palette.light.as_ref(), - ) { - (true, Some(dark_palette), _) => &dark_palette, - (false, _, Some(light_palette)) => &light_palette, - _ => &page.accent_palette.theme, - }; - - let mut accent_palette_row = - cosmic::widget::row::with_capacity(accent_palette_values.len()); - - for &color in accent_palette_values { - accent_palette_row = accent_palette_row.push(color_button( - Some(Message::PaletteAccent(color.into())), - color.into(), - cur_accent == color, - 48, - 48, - )); - } - - let accent_color_palette = cosmic::iced::widget::column![ - text::body(&descriptions[accent_color]), - scrollable::horizontal( - accent_palette_row - .push(if let Some(c) = page.custom_accent.get_applied_color() { - container(color_button( - Some(Message::CustomAccent(ColorPickerUpdate::ToggleColorPicker)), - c, - cosmic::iced::Color::from(cur_accent) == c, - 48, - 48, - )) - } else { - container( - page.custom_accent - .picker_button(Message::CustomAccent, None) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(48.0)), - ) - }) - .padding([0, 0, 16, 0]) - .spacing(16) - ) - ] - .padding([16, 0, 0, 0]) - .spacing(space_xxs); - - let mut section = settings::section() - .title(§ion.title) - .add( - container( - cosmic::iced::widget::row![ - cosmic::iced::widget::column![ - button::custom( - icon(dark_mode_illustration.clone()) - .width(Length::Fixed(191.0)) - .height(Length::Fixed(100.0)) - ) - .class(button::ButtonClass::Image) - .padding([8, 0]) - .selected(page.theme_mode.is_dark) - .on_press(Message::DarkMode(true)), - text::body(&descriptions[dark]) - ] - .spacing(8) - .width(Length::FillPortion(1)) - .align_x(Alignment::Center), - cosmic::iced::widget::column![ - button::custom( - icon(light_mode_illustration.clone(),) - .width(Length::Fixed(191.0)) - .height(Length::Fixed(100.0)) - ) - .class(button::ButtonClass::Image) - .selected(!page.theme_mode.is_dark) - .padding([8, 0]) - .on_press(Message::DarkMode(false)), - text::body(&descriptions[light]) - ] - .spacing(8) - .width(Length::FillPortion(1)) - .align_x(Alignment::Center) - ] - .spacing(8) - .width(Length::Fixed(478.0)) - .align_y(Alignment::Center), - ) - .center_x(Length::Fill), - ) - .add( - settings::item::builder(&descriptions[auto_switch]) - .description( - if !page.day_time && page.theme_mode.is_dark { - &page.auto_switch_descs[0] - } else if page.day_time && !page.theme_mode.is_dark { - &page.auto_switch_descs[1] - } else if page.day_time && page.theme_mode.is_dark { - &page.auto_switch_descs[2] - } else { - &page.auto_switch_descs[3] - } - .clone(), - ) - .toggler(page.theme_mode.auto_switch, Message::Autoswitch), - ) - .add(accent_color_palette) - .add( - settings::item::builder(&descriptions[app_bg]).control( - page.application_background - .picker_button(Message::ApplicationBackground, Some(24)) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(24.0)), - ), - ) - .add( - settings::item::builder(&descriptions[container_bg]) - .description(&descriptions[container_bg_desc]) - .control(if page.container_background.get_applied_color().is_some() { - Element::from( - page.container_background - .picker_button(Message::ContainerBackground, Some(24)) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(24.0)), - ) - } else { - container( - button::text(&descriptions[auto_txt]) - .trailing_icon(go_next_icon.clone()) - .on_press(Message::ContainerBackground( - ColorPickerUpdate::ToggleColorPicker, - )), - ) - .into() - }), - ) - .add( - settings::item::builder(&descriptions[text_tint]) - .description(&descriptions[text_tint_desc]) - .control( - page.interface_text - .picker_button(Message::InterfaceText, Some(24)) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(24.0)), - ), - ) - .add( - settings::item::builder(&descriptions[control_tint]) - .description(&descriptions[control_tint_desc]) - .control( - page.control_component - .picker_button(Message::ControlComponent, Some(24)) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(24.0)), - ), - ) - .add( - settings::item::builder(&descriptions[window_hint_toggle]) - .toggler(page.no_custom_window_hint, Message::UseDefaultWindowHint), - ); - if !page.no_custom_window_hint { - section = section.add( - settings::item::builder(&descriptions[window_hint]).control( - page.accent_window_hint - .picker_button(Message::AccentWindowHint, Some(24)) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(24.0)), - ), - ); - } - section - .apply(Element::from) - .map(crate::pages::Message::Appearance) - }) -} - -#[allow(clippy::too_many_lines)] -pub fn style() -> Section { - let mut descriptions = Slab::new(); - - let round = descriptions.insert(fl!("style", "round")); - let slightly_round = descriptions.insert(fl!("style", "slightly-round")); - let square = descriptions.insert(fl!("style", "square")); - - let dark_round_style = from_name("illustration-appearance-dark-style-round").handle(); - let light_round_style = from_name("illustration-appearance-light-style-round").handle(); - - let dark_slightly_round_style = - from_name("illustration-appearance-dark-style-slightly-round").handle(); - let light_slightly_round_style = - from_name("illustration-appearance-light-style-slightly-round").handle(); - - let dark_square_style = from_name("illustration-appearance-dark-style-square").handle(); - let light_square_style = from_name("illustration-appearance-light-style-square").handle(); - - fn style_container() -> cosmic::theme::Container<'static> { - cosmic::theme::Container::custom(|theme| { - let mut background = theme.cosmic().palette.neutral_9; - background.alpha = 0.1; - container::Style { - background: Some(cosmic::iced::Background::Color(background.into())), - border: cosmic::iced::Border { - radius: theme.cosmic().radius_s().into(), - ..Default::default() - }, - ..Default::default() - } - }) - } - - Section::default() - .title(fl!("style")) - .descriptions(descriptions) - .view::(move |_binder, page, section| { - let descriptions = §ion.descriptions; - - settings::section() - .title(§ion.title) - .add( - container( - cosmic::iced::widget::row![ - cosmic::iced::widget::column![ - button::custom( - icon( - if page.theme_mode.is_dark { - &dark_round_style - } else { - &light_round_style - } - .clone() - ) - .width(Length::Fill) - .height(Length::Fixed(100.0)) - ) - .selected(matches!(page.roundness, Roundness::Round)) - .class(button::ButtonClass::Image) - .padding(0) - .on_press(Message::Roundness(Roundness::Round)) - .apply(container) - .width(Length::Fixed(191.0)) - .class(style_container()), - text::body(&descriptions[round]) - ] - .spacing(8) - .width(Length::FillPortion(1)) - .align_x(Alignment::Center), - cosmic::iced::widget::column![ - button::custom( - icon( - if page.theme_mode.is_dark { - &dark_slightly_round_style - } else { - &light_slightly_round_style - } - .clone() - ) - .width(Length::Fill) - .height(Length::Fixed(100.0)) - ) - .selected(matches!(page.roundness, Roundness::SlightlyRound)) - .class(button::ButtonClass::Image) - .padding(0) - .on_press(Message::Roundness(Roundness::SlightlyRound)) - .apply(container) - .width(Length::Fixed(191.0)) - .class(style_container()), - text::body(&descriptions[slightly_round]) - ] - .spacing(8) - .width(Length::FillPortion(1)) - .align_x(Alignment::Center), - cosmic::iced::widget::column![ - button::custom( - icon( - if page.theme_mode.is_dark { - &dark_square_style - } else { - &light_square_style - } - .clone() - ) - .width(Length::Fill) - .height(Length::Fixed(100.0)) - ) - .width(Length::FillPortion(1)) - .selected(matches!(page.roundness, Roundness::Square)) - .class(button::ButtonClass::Image) - .padding(0) - .on_press(Message::Roundness(Roundness::Square)) - .apply(container) - .width(Length::Fixed(191.0)) - .class(style_container()), - text::body(&descriptions[square]) - ] - .spacing(8) - .align_x(Alignment::Center) - .width(Length::FillPortion(1)) - ] - .spacing(8) - .align_y(Alignment::Center), - ) - .center_x(Length::Fill), - ) - .apply(Element::from) - .map(crate::pages::Message::Appearance) - }) -} - pub fn interface_density() -> Section { crate::slab!(descriptions { comfortable = fl!("interface-density", "comfortable"); @@ -2094,18 +727,16 @@ pub fn interface_density() -> Section { Section::default() .title(fl!("interface-density")) .descriptions(descriptions) - .view::(move |_binder, _page, section| { + .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; - let density = cosmic::config::interface_density(); - settings::section() .title(§ion.title) .add(settings::item_row(vec![ radio( text::body(&descriptions[compact]), Density::Compact, - Some(density), + Some(page.density), Message::Density, ) .width(Length::Fill) @@ -2115,7 +746,7 @@ pub fn interface_density() -> Section { radio( text::body(&descriptions[comfortable]), Density::Standard, - Some(density), + Some(page.density), Message::Density, ) .width(Length::Fill) @@ -2125,7 +756,7 @@ pub fn interface_density() -> Section { radio( text::body(&descriptions[spacious]), Density::Spacious, - Some(density), + Some(page.density), Message::Density, ) .width(Length::Fill) @@ -2153,8 +784,8 @@ pub fn window_management() -> Section { .title(§ion.title) .add(settings::item::builder(&descriptions[active_hint]).control( widget::spin_button( - page.theme_builder.active_hint.to_string(), - page.theme_builder.active_hint, + page.theme_manager.builder().active_hint.to_string(), + page.theme_manager.builder().active_hint, 1, 0, 64, @@ -2163,10 +794,10 @@ pub fn window_management() -> Section { )) .add( settings::item::builder(&descriptions[gaps]).control(widget::spin_button( - page.theme_builder.gaps.1.to_string(), - page.theme_builder.gaps.1, + page.theme_manager.builder().gaps.1.to_string(), + page.theme_manager.builder().gaps.1, 1, - page.theme_builder.active_hint, + page.theme_manager.builder().active_hint, 500, Message::GapSize, )), @@ -2186,24 +817,24 @@ pub fn experimental() -> Section { Section::default() .title(fl!("experimental-settings")) .descriptions(descriptions) - .view::(move |_binder, _page, section| { + .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; let system_font = crate::widget::go_next_with_item( &descriptions[interface_font_txt], - text::body(cosmic::config::interface_font().family), - Message::DisplaySystemFont, + text::body(page.drawer.current_font_family(&ContextView::SystemFont)), + Message::DrawerOpen(ContextView::SystemFont), ); let mono_font = crate::widget::go_next_with_item( &descriptions[monospace_font_txt], - text::body(cosmic::config::monospace_font().family), - Message::DisplayMonoFont, + text::body(page.drawer.current_font_family(&ContextView::MonospaceFont)), + Message::DrawerOpen(ContextView::MonospaceFont), ); let icons_and_toolkit = crate::widget::go_next_item( &descriptions[icons_and_toolkit_txt], - Message::IconsAndToolkit, + Message::DrawerOpen(ContextView::IconsAndToolkit), ); settings::section() @@ -2237,26 +868,3 @@ pub fn reset_button() -> Section { }) } impl page::AutoBind for Page {} - -/// A button for selecting a color or gradient. -pub fn color_button<'a, Message: 'a + Clone>( - on_press: Option, - color: cosmic::iced::Color, - selected: bool, - width: u16, - height: u16, -) -> Element<'a, Message> { - button::custom(color_image( - wallpaper::Color::Single([color.r, color.g, color.b]), - width, - height, - None, - )) - .padding(0) - .selected(selected) - .class(button::ButtonClass::Image) - .on_press_maybe(on_press) - .width(Length::Fixed(f32::from(width))) - .height(Length::Fixed(f32::from(height))) - .into() -} diff --git a/cosmic-settings/src/pages/desktop/appearance/mode_and_colors.rs b/cosmic-settings/src/pages/desktop/appearance/mode_and_colors.rs new file mode 100644 index 0000000..3af010c --- /dev/null +++ b/cosmic-settings/src/pages/desktop/appearance/mode_and_colors.rs @@ -0,0 +1,379 @@ +use crate::pages::desktop::wallpaper::widgets::color_image; +use cosmic::cosmic_theme::Spacing; +use cosmic::cosmic_theme::palette::Srgba; +use cosmic::iced_core::{Alignment, Length}; +use cosmic::widget::icon::{from_name, icon}; +use cosmic::widget::{button, container, scrollable, settings, text}; +use cosmic::{Apply, Element}; +use cosmic_settings_page::Section; +use cosmic_settings_wallpaper as wallpaper; +use std::collections::HashMap; + +use super::{ContextView, Message, Page}; + +#[allow(clippy::too_many_lines)] +pub fn section() -> Section { + let (descriptions, label_keys) = i18n(); + + Section::default() + .title(fl!("mode-and-colors")) + .descriptions(descriptions) + .view::(move |_binder, page, section| { + let label_keys = label_keys.clone(); + let descriptions = §ion.descriptions; + let theme_manager = &page.theme_manager; + + let mut section = settings::section() + .title(§ion.title) + .add(theme_mode(&page, section, &label_keys)) + .add(auto_switch(&page, section, &label_keys)) + .add(accent_color_palette(&page, section, &label_keys)) + .add(application_background(&page, section, &label_keys)) + .add(container_background(&page, section, &label_keys)) + .add(interface_text(&page, section, &label_keys)) + .add(control_tint(&page, section, &label_keys)) + .add( + settings::item::builder(&descriptions[label_keys["window_hint_toggle"]]) + .toggler( + theme_manager.custom_window_hint().is_some(), + Message::UseDefaultWindowHint, + ), + ); + if theme_manager.custom_window_hint().is_some() { + section = section.add( + settings::item::builder(&descriptions[label_keys["window_hint"]]).control( + page.drawer + .accent_window_hint + .picker_button( + |_| Message::DrawerOpen(ContextView::AccentWindowHint), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(24.0)), + ), + ); + } + section + .apply(Element::from) + .map(crate::pages::Message::Appearance) + }) +} + +fn container_background<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + let go_next_icon = from_name("go-next-symbolic").handle(); + + settings::item::builder(&descriptions[labels["container_bg"]]) + .description(&descriptions[labels["container_bg_desc"]]) + .control( + if page + .drawer + .container_background + .get_applied_color() + .is_some() + { + Element::from( + page.drawer + .container_background + .picker_button( + |_| Message::DrawerOpen(ContextView::ContainerBackground), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(24.0)), + ) + } else { + container( + button::text(&descriptions[labels["auto"]]) + .trailing_icon(go_next_icon.clone()) + .on_press(Message::DrawerOpen(ContextView::ContainerBackground)), + ) + .into() + }, + ) +} + +fn application_background<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + + settings::item::builder(&descriptions[labels["app_bg"]]).control( + page.drawer + .application_background + .picker_button( + |_| Message::DrawerOpen(ContextView::ApplicationBackground), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(24.0)), + ) +} + +fn control_tint<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + + settings::item::builder(&descriptions[labels["control_tint"]]) + .description(&descriptions[labels["control_tint_desc"]]) + .control( + page.drawer + .control_component + .picker_button( + |_| Message::DrawerOpen(ContextView::ControlComponent), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(24.0)), + ) +} + +fn interface_text<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + + settings::item::builder(&descriptions[labels["text_tint"]]) + .description(&descriptions[labels["text_tint_desc"]]) + .control( + page.drawer + .interface_text + .picker_button( + |_| Message::DrawerOpen(ContextView::InterfaceText), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(24.0)), + ) +} +fn auto_switch<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + + settings::item::builder(&descriptions[labels["auto_switch"]]) + .description( + if !page.day_time && page.theme_manager.mode().is_dark { + &descriptions[labels["auto_switch_desc/sunrise"]] + } else if page.day_time && !page.theme_manager.mode().is_dark { + &descriptions[labels["auto_switch_desc/sunset"]] + } else if page.day_time && page.theme_manager.mode().is_dark { + &descriptions[labels["auto_switch_desc/next-sunrise"]] + } else { + &descriptions[labels["auto_switch_desc/next-sunset"]] + } + .clone(), + ) + .toggler(page.theme_manager.mode().auto_switch, Message::Autoswitch) +} + +fn accent_color_palette<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let Spacing { space_xxs, .. } = cosmic::theme::spacing(); + let descriptions = §ion.descriptions; + let palette = &page.theme_manager.builder().palette.as_ref(); + let accent = page.theme_manager.accent_palette().as_ref().unwrap(); + let cur_accent = page + .theme_manager + .builder() + .accent + .map_or(palette.accent_blue, Srgba::from); + let mut accent_palette_row = cosmic::widget::row::with_capacity(accent.len()); + + for &color in accent { + accent_palette_row = accent_palette_row.push(color_button( + Some(Message::PaletteAccent(color.into())), + color.into(), + cur_accent == color, + 48, + 48, + )); + } + + cosmic::iced::widget::column![ + text::body(&descriptions[labels["accent_color"]]), + scrollable::horizontal( + accent_palette_row + .push( + if let Some(c) = page.drawer.custom_accent.get_applied_color() { + container(color_button( + Some(Message::DrawerOpen(ContextView::CustomAccent)), + c, + cosmic::iced::Color::from(cur_accent) == c, + 48, + 48, + )) + } else { + container( + page.drawer + .custom_accent + .picker_button( + |_| Message::DrawerOpen(ContextView::CustomAccent), + Some(24), + ) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(48.0)), + ) + } + ) + .padding([0, 0, 16, 0]) + .spacing(16) + ) + ] + .padding([16, 0, 0, 0]) + .spacing(space_xxs) +} + +fn theme_mode<'a>( + page: &Page, + section: &'a Section, + labels: &HashMap, +) -> impl Into> { + let descriptions = §ion.descriptions; + let dark_mode_illustration = from_name("illustration-appearance-mode-dark").handle(); + let light_mode_illustration = from_name("illustration-appearance-mode-light").handle(); + + container( + cosmic::iced::widget::row![ + cosmic::iced::widget::column![ + button::custom( + icon(dark_mode_illustration) + .width(Length::Fixed(191.0)) + .height(Length::Fixed(100.0)) + ) + .class(button::ButtonClass::Image) + .padding([8, 0]) + .selected(page.theme_manager.mode().is_dark) + .on_press(super::Message::DarkMode(true)), + text::body(&descriptions[labels["dark"]]) + ] + .spacing(8) + .width(Length::FillPortion(1)) + .align_x(Alignment::Center), + cosmic::iced::widget::column![ + button::custom( + icon(light_mode_illustration,) + .width(Length::Fixed(191.0)) + .height(Length::Fixed(100.0)) + ) + .class(button::ButtonClass::Image) + .selected(!page.theme_manager.mode().is_dark) + .padding([8, 0]) + .on_press(super::Message::DarkMode(false)), + text::body(&descriptions[labels["light"]]) + ] + .spacing(8) + .width(Length::FillPortion(1)) + .align_x(Alignment::Center) + ] + .spacing(8) + .width(Length::Fixed(478.0)) + .align_y(Alignment::Center), + ) + .center_x(Length::Fill) +} + +/// A button for selecting a color or gradient. +pub fn color_button<'a, Message: 'a + Clone>( + on_press: Option, + color: cosmic::iced::Color, + selected: bool, + width: u16, + height: u16, +) -> Element<'a, Message> { + button::custom(color_image( + wallpaper::Color::Single([color.r, color.g, color.b]), + width, + height, + None, + )) + .padding(0) + .selected(selected) + .class(button::ButtonClass::Image) + .on_press_maybe(on_press) + .width(Length::Fixed(f32::from(width))) + .height(Length::Fixed(f32::from(height))) + .into() +} + +#[inline] +fn i18n() -> (slab::Slab, HashMap) { + let mut descriptions = slab::Slab::new(); + let keys: HashMap = HashMap::from([ + ("auto".into(), descriptions.insert(fl!("auto"))), + ( + "auto_switch".into(), + descriptions.insert(fl!("auto-switch")), + ), + ( + "auto_switch_desc/sunrise".into(), + descriptions.insert(fl!("auto-switch", "sunrise")), + ), + ( + "auto_switch_desc/sunset".into(), + descriptions.insert(fl!("auto-switch", "sunrise")), + ), + ( + "auto_switch_desc/next-sunrise".into(), + descriptions.insert(fl!("auto-switch", "next-sunrise")), + ), + ( + "auto_switch_desc/next-sunset".into(), + descriptions.insert(fl!("auto-switch", "next-sunrise")), + ), + ( + "accent_color".into(), + descriptions.insert(fl!("accent-color")), + ), + ("app_bg".into(), descriptions.insert(fl!("app-background"))), + ( + "container_bg".into(), + descriptions.insert(fl!("container-background")), + ), + ( + "container_bg_desc".into(), + descriptions.insert(fl!("container-background", "desc")), + ), + ("text_tint".into(), descriptions.insert(fl!("text-tint"))), + ( + "text_tint_desc".into(), + descriptions.insert(fl!("text-tint", "desc")), + ), + ( + "control_tint".into(), + descriptions.insert(fl!("control-tint")), + ), + ( + "control_tint_desc".into(), + descriptions.insert(fl!("control-tint", "desc")), + ), + ( + "window_hint_toggle".into(), + descriptions.insert(fl!("window-hint-accent-toggle")), + ), + ( + "window_hint".into(), + descriptions.insert(fl!("window-hint-accent")), + ), + ("dark".into(), descriptions.insert(fl!("dark"))), + ("light".into(), descriptions.insert(fl!("light"))), + ]); + + (descriptions, keys) +} diff --git a/cosmic-settings/src/pages/desktop/appearance/style.rs b/cosmic-settings/src/pages/desktop/appearance/style.rs new file mode 100644 index 0000000..c2460d8 --- /dev/null +++ b/cosmic-settings/src/pages/desktop/appearance/style.rs @@ -0,0 +1,140 @@ +use cosmic::iced_core::{Alignment, Length}; +use cosmic::widget::icon::{from_name, icon}; +use cosmic::widget::{button, container, settings, text}; +use cosmic::{Apply, Element}; +use cosmic_settings_page::Section; +use slab::Slab; + +use super::{Message, Page, Roundness}; + +#[allow(clippy::too_many_lines)] +pub fn section() -> Section { + let mut descriptions = Slab::new(); + + let round = descriptions.insert(fl!("style", "round")); + let slightly_round = descriptions.insert(fl!("style", "slightly-round")); + let square = descriptions.insert(fl!("style", "square")); + + let dark_round_style = from_name("illustration-appearance-dark-style-round").handle(); + let light_round_style = from_name("illustration-appearance-light-style-round").handle(); + + let dark_slightly_round_style = + from_name("illustration-appearance-dark-style-slightly-round").handle(); + let light_slightly_round_style = + from_name("illustration-appearance-light-style-slightly-round").handle(); + + let dark_square_style = from_name("illustration-appearance-dark-style-square").handle(); + let light_square_style = from_name("illustration-appearance-light-style-square").handle(); + + fn style_container() -> cosmic::theme::Container<'static> { + cosmic::theme::Container::custom(|theme| { + let mut background = theme.cosmic().palette.neutral_9; + background.alpha = 0.1; + container::Style { + background: Some(cosmic::iced::Background::Color(background.into())), + border: cosmic::iced::Border { + radius: theme.cosmic().radius_s().into(), + ..Default::default() + }, + ..Default::default() + } + }) + } + + Section::default() + .title(fl!("style")) + .descriptions(descriptions) + .view::(move |_binder, page, section| { + let descriptions = §ion.descriptions; + + settings::section() + .title(§ion.title) + .add( + container( + cosmic::iced::widget::row![ + cosmic::iced::widget::column![ + button::custom( + icon( + if page.theme_manager.mode().is_dark { + &dark_round_style + } else { + &light_round_style + } + .clone() + ) + .width(Length::Fill) + .height(Length::Fixed(100.0)) + ) + .selected(matches!(page.roundness, Roundness::Round)) + .class(button::ButtonClass::Image) + .padding(0) + .on_press(Message::Roundness(Roundness::Round)) + .apply(container) + .width(Length::Fixed(191.0)) + .class(style_container()), + text::body(&descriptions[round]) + ] + .spacing(8) + .width(Length::FillPortion(1)) + .align_x(Alignment::Center), + cosmic::iced::widget::column![ + button::custom( + icon( + if page.theme_manager.mode().is_dark { + &dark_slightly_round_style + } else { + &light_slightly_round_style + } + .clone() + ) + .width(Length::Fill) + .height(Length::Fixed(100.0)) + ) + .selected(matches!(page.roundness, Roundness::SlightlyRound)) + .class(button::ButtonClass::Image) + .padding(0) + .on_press(Message::Roundness(Roundness::SlightlyRound)) + .apply(container) + .width(Length::Fixed(191.0)) + .class(style_container()), + text::body(&descriptions[slightly_round]) + ] + .spacing(8) + .width(Length::FillPortion(1)) + .align_x(Alignment::Center), + cosmic::iced::widget::column![ + button::custom( + icon( + if page.theme_manager.mode().is_dark { + &dark_square_style + } else { + &light_square_style + } + .clone() + ) + .width(Length::Fill) + .height(Length::Fixed(100.0)) + ) + .width(Length::FillPortion(1)) + .selected(matches!(page.roundness, Roundness::Square)) + .class(button::ButtonClass::Image) + .padding(0) + .on_press(Message::Roundness(Roundness::Square)) + .apply(container) + .width(Length::Fixed(191.0)) + .class(style_container()), + text::body(&descriptions[square]) + ] + .spacing(8) + .align_x(Alignment::Center) + .width(Length::FillPortion(1)) + ] + .spacing(8) + .align_y(Alignment::Center), + ) + .center_x(Length::Fill), + ) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + }) +} diff --git a/cosmic-settings/src/pages/desktop/appearance/theme_manager.rs b/cosmic-settings/src/pages/desktop/appearance/theme_manager.rs new file mode 100644 index 0000000..29329ee --- /dev/null +++ b/cosmic-settings/src/pages/desktop/appearance/theme_manager.rs @@ -0,0 +1,535 @@ +use cosmic::cosmic_config::{Config, ConfigSet, CosmicConfigEntry}; +use cosmic::cosmic_theme::palette::{Srgb, Srgba}; +use cosmic::cosmic_theme::{ + CornerRadii, DARK_THEME_BUILDER_ID, LIGHT_THEME_BUILDER_ID, Spacing, Theme, ThemeBuilder, + ThemeMode, +}; +use cosmic::iced_core::Color; + +use cosmic::Task; +use cosmic::theme::ThemeType; +use std::borrow::BorrowMut; +use std::sync::Arc; + +use crate::app; + +use super::ContextView; + +pub enum ThemeStaged { + Current, + Both, +} + +#[derive(Debug)] +pub struct Manager { + mode: (ThemeMode, Option), + light: ThemeCustomizer, + dark: ThemeCustomizer, + + custom_accent: Option, +} + +#[derive(Debug)] +pub struct ThemeCustomizer { + builder: (ThemeBuilder, Option), + theme: (Theme, Option), + accent_palette: Option>, + custom_window_hint: Option, +} + +impl From<(Option, Option, Option>)> for ThemeCustomizer { + fn from( + (theme_config, builder_config, palette): ( + Option, + Option, + Option>, + ), + ) -> Self { + let theme = Theme::get_entry(theme_config.as_ref().unwrap()).unwrap_or_default(); + + let mut theme_builder = match ThemeBuilder::get_entry(builder_config.as_ref().unwrap()) { + Ok(t) => t, + Err((errors, t)) => { + for e in errors { + tracing::error!("{e}"); + } + t + } + }; + + theme_builder = theme_builder + .accent(theme.accent.base.color) + .bg_color(theme.bg_color()) + .corner_radii(theme.corner_radii) + .destructive(theme.destructive.base.color) + .spacing(theme.spacing) + .success(theme.success.base.color) + .warning(theme.warning.base.color) + .neutral_tint(theme.palette.neutral_5.color) + .text_tint(theme.background.on.color); + + theme_builder.gaps = theme.gaps; + + let mut customizer = Self { + builder: (theme_builder, builder_config), + theme: (theme, theme_config), + accent_palette: palette, + custom_window_hint: None, + }; + + if let None = customizer.accent_palette { + let palette = customizer.builder.0.palette.as_ref(); + customizer.accent_palette = Some(vec![ + palette.accent_blue, + palette.accent_indigo, + palette.accent_purple, + palette.accent_pink, + palette.accent_red, + palette.accent_orange, + palette.accent_yellow, + palette.accent_green, + palette.accent_warm_grey, + ]); + } + + customizer + } +} + +impl Default for Manager { + fn default() -> Self { + let settings_config = crate::config::Config::new(); + + let theme_mode_config = ThemeMode::config().ok(); + let theme_mode = theme_mode_config + .as_ref() + .map(|c| match ThemeMode::get_entry(c) { + Ok(t) => t, + Err((errors, t)) => { + for e in errors { + tracing::error!("{e}"); + } + t + } + }) + .unwrap_or_default(); + + let mut manager = Self { + mode: (theme_mode, theme_mode_config), + light: ( + Theme::light_config().ok(), + ThemeBuilder::light_config().ok(), + settings_config.accent_palette_light().ok(), + ) + .into(), + dark: ( + Theme::dark_config().ok(), + ThemeBuilder::dark_config().ok(), + settings_config.accent_palette_dark().ok(), + ) + .into(), + custom_accent: None, + }; + + let customizer = manager.selected_customizer(); + manager.custom_accent = customizer.builder.0.accent.filter(|c| { + let c = Srgba::new(c.red, c.green, c.blue, 1.0); + let theme = &customizer.theme.0; + c != theme.palette.accent_blue + && c != theme.palette.accent_green + && c != theme.palette.accent_indigo + && c != theme.palette.accent_orange + && c != theme.palette.accent_pink + && c != theme.palette.accent_purple + && c != theme.palette.accent_red + && c != theme.palette.accent_warm_grey + && c != theme.palette.accent_yellow + }); + + manager + } +} + +impl Manager { + pub fn build_theme<'a>(&mut self, stage: ThemeStaged) -> Task { + macro_rules! theme_transaction { + ($config:ident, $current_theme:ident, $new_theme:ident, { $($name:ident;)+ }) => { + let tx = $config.transaction(); + + $( + if $current_theme.$name != $new_theme.$name { + _ = tx.set(stringify!($name), $new_theme.$name.clone()); + } + )+ + + _ = tx.commit(); + } + } + + let mut tasks: Vec> = Vec::new(); + let customizers = match stage { + ThemeStaged::Current => vec![self.selected_customizer_mut()], + ThemeStaged::Both => vec![self.light.borrow_mut(), self.dark.borrow_mut()], + }; + + customizers.into_iter().for_each(|customizer| { + let builder = customizer.builder.0.clone(); + let (current_theme, config) = customizer.theme.clone(); + + tasks.push(cosmic::task::future(async move { + if let Some(config) = config { + let new_theme = builder.build(); + + theme_transaction!(config, current_theme, new_theme, { + accent; + accent_button; + background; + button; + destructive; + destructive_button; + link_button; + icon_button; + palette; + primary; + secondary; + shade; + success; + text_button; + warning; + warning_button; + window_hint; + }); + + app::Message::from(super::Message::NewTheme(Box::new(new_theme))) + } else { + app::Message::None + } + })); + }); + + cosmic::task::batch(tasks) + } + + #[inline] + pub fn selected_customizer(&self) -> &ThemeCustomizer { + if self.mode.0.is_dark { + &self.dark + } else { + &self.light + } + } + + #[inline] + pub fn selected_customizer_mut(&mut self) -> &mut ThemeCustomizer { + if self.mode.0.is_dark { + &mut self.dark + } else { + &mut self.light + } + } + + #[inline] + pub fn theme(&self) -> &Theme { + &self.selected_customizer().theme.0 + } + + #[inline] + pub fn mode(&self) -> &ThemeMode { + &self.mode.0 + } + + #[inline] + pub fn builder(&self) -> &ThemeBuilder { + &self.selected_customizer().builder.0 + } + + #[inline] + pub fn custom_accent(&self) -> &Option { + &self.custom_accent + } + + #[inline] + pub fn accent_palette(&self) -> &Option> { + &self.selected_customizer().accent_palette + } + + #[inline] + pub fn custom_window_hint(&self) -> &Option { + &self.selected_customizer().custom_window_hint() + } + + #[inline] + pub fn theme_mode_config(&self) -> &Option { + &self.mode.1 + } + + pub fn dark_mode(&mut self, enabled: bool) -> Result { + if let Some(config) = self.mode.1.as_ref() { + return self.mode.0.set_is_dark(config, enabled); + } + + self.mode.0.is_dark = enabled; + + let (theme_id, builder_fn): (&str, fn() -> ThemeBuilder) = if enabled { + (DARK_THEME_BUILDER_ID, ThemeBuilder::dark) + } else { + (LIGHT_THEME_BUILDER_ID, ThemeBuilder::light) + }; + + let builder = cosmic::cosmic_config::Config::system(theme_id, ThemeBuilder::VERSION) + .map_or_else( + |_| builder_fn(), + |config| match ThemeBuilder::get_entry(&config) { + Ok(t) => t, + Err((errs, t)) => { + for err in errs { + tracing::warn!(?err, "Error getting system theme builder"); + } + t + } + }, + ); + + self.selected_customizer_mut().set_builder(builder); + + Ok(true) + } + + pub fn auto_switch(&mut self, enabled: bool) { + self.mode.0.auto_switch = enabled; + + if let Some(config) = self.mode.1.as_ref() { + _ = config.set::("auto_switch", enabled); + } + } + + // TODO: Make it rollback if the first operation succeeds and the second + // one fails? + pub fn set_active_hint(&mut self, active_hint: u32) -> Option { + self.dark.set_active_hint(active_hint)?; + self.light.set_active_hint(active_hint)?; + Some(ThemeStaged::Both) + } + + // TODO: Make it rollback if the first operation succeeds and the second + // one fails? + pub fn set_spacing(&mut self, spacing: Spacing) -> Option { + self.dark.set_spacing(spacing)?; + self.light.set_spacing(spacing)?; + Some(ThemeStaged::Both) + } + + pub fn set_gap_size(&mut self, gap: u32) -> Option { + self.dark.set_gap_size(gap)?; + self.light.set_gap_size(gap)?; + Some(ThemeStaged::Both) + } + + pub fn get_color(&self, context: &ContextView) -> Option { + match *context { + ContextView::CustomAccent => self.custom_accent().map(Color::from), + ContextView::ApplicationBackground => self.builder().bg_color.map(Color::from), + ContextView::ContainerBackground => { + self.builder().primary_container_bg.map(Color::from) + } + ContextView::InterfaceText => self.builder().text_tint.map(Color::from), + ContextView::ControlComponent => self.builder().neutral_tint.map(Color::from), + ContextView::AccentWindowHint => self.builder().window_hint.map(Color::from), + _ => None, + } + } + + pub fn set_color( + &mut self, + color: Option, + context: &ContextView, + ) -> Option { + let theme_customizer = self.selected_customizer_mut(); + match *context { + ContextView::CustomAccent => theme_customizer.set_accent(color.map(Srgb::from)), + ContextView::ApplicationBackground => { + theme_customizer.set_bg_color(color.map(Srgba::from)) + } + ContextView::ContainerBackground => { + theme_customizer.set_primary_container_bg(color.map(Srgba::from)) + } + ContextView::InterfaceText => theme_customizer.set_text_tint(color.map(Srgb::from)), + ContextView::ControlComponent => { + theme_customizer.set_neutral_tint(color.map(Srgb::from)) + } + ContextView::AccentWindowHint => { + theme_customizer.set_window_hint(color.map(Srgb::from)) + } + _ => None, + } + } + + pub fn cosmic_theme(&self) -> cosmic::Theme { + cosmic::Theme { + theme_type: ThemeType::Custom(Arc::new(self.theme().clone())), + ..cosmic::Theme::default() + } + } +} + +impl ThemeCustomizer { + /// Set theme builder without writing to cosmic-config. + pub fn set_builder(&mut self, builder: ThemeBuilder) -> &mut Self { + self.builder.0 = builder; + self + } + + /// Write theme builder to cosmic-config, notifying all subscribers. + pub fn apply_builder(&mut self) -> &mut Self { + if let Some(config) = self.builder.1.as_ref() { + let _ = self.builder.0.write_entry(config); + } + + self + } + + /// Set theme without writing to cosmic-config. + pub fn set_theme(&mut self, theme: Theme) -> &mut Self { + self.theme.0 = theme; + self + } + + /// Write theme to cosmic-config, notifying all subscribers. + pub fn apply_theme(&mut self) -> &mut Self { + if let Some(config) = self.theme.1.as_ref() { + let _ = self.theme.0.write_entry(config); + } + + self + } + + pub fn set_window_hint(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.custom_window_hint = color; + self.builder.0.set_window_hint(config, color).ok()?; + self.theme + .0 + .set_window_hint(self.theme.1.as_ref()?, color) + .ok()?; + + Some(ThemeStaged::Current) + } + + pub fn custom_window_hint(&self) -> &Option { + &self.custom_window_hint + } + + pub fn set_bg_color(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_bg_color(config, color).ok()?; + Some(ThemeStaged::Current) + } + + pub fn set_primary_container_bg(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder + .0 + .set_primary_container_bg(config, color) + .ok()?; + + Some(ThemeStaged::Current) + } + + pub fn set_accent(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_accent(config, color).ok()?; + Some(ThemeStaged::Current) + } + + pub fn set_text_tint(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_text_tint(config, color).ok()?; + Some(ThemeStaged::Current) + } + + pub fn set_neutral_tint(&mut self, color: Option) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_neutral_tint(config, color).ok()?; + Some(ThemeStaged::Current) + } + + pub fn set_spacing(&mut self, spacing: Spacing) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_spacing(config, spacing).ok()?; + self.theme + .0 + .set_spacing(self.theme.1.as_ref()?, spacing) + .ok()?; + + Some(ThemeStaged::Current) + } + + pub fn set_corner_radii(&mut self, corner_radii: CornerRadii) -> Option { + let config = self.builder.1.as_ref()?; + + self.builder.0.set_corner_radii(config, corner_radii).ok()?; + + self.theme + .0 + .set_corner_radii(self.theme.1.as_ref()?, corner_radii) + .ok()?; + + Some(ThemeStaged::Current) + } + + pub fn set_gap_size(&mut self, gap: u32) -> Option { + let config = self.builder.1.as_ref()?; + let builder = &mut self.builder.0; + let mut gaps = builder.gaps; + + // Ensure that the gap is never less than what the active hint size is. + gaps.1 = if gap < builder.active_hint { + builder.active_hint + } else { + gap + }; + + if let Err(err) = builder.set_gaps(config, gaps) { + tracing::error!(?err, "Error setting the gap"); + return None; + } + + self.theme.0.set_gaps(self.theme.1.as_ref()?, gaps).ok()?; + Some(ThemeStaged::Current) + } + + // set active hints is set on all themes to be consistent between dark & light themes. + pub fn set_active_hint(&mut self, active_hint: u32) -> Option { + let config = self.builder.1.as_ref()?; + let builder = &mut self.builder.0; + + if let Err(err) = builder.set_active_hint(config, active_hint) { + tracing::error!(?err, "Error setting the active hint"); + return None; + } + + // Update the gap if it's less than the active hint + if active_hint > builder.gaps.1 { + let mut gaps = builder.gaps; + gaps.1 = active_hint; + if builder.set_gaps(config, gaps).unwrap_or_default() { + let _ = self.theme.0.set_active_hint(self.theme.1.as_ref()?, gaps.1); + } + } + + // Update the active_hint in the config + self.theme + .0 + .set_active_hint(self.theme.1.as_ref()?, active_hint) + .ok()?; + + Some(ThemeStaged::Current) + } +} diff --git a/cosmic-settings/src/pages/desktop/wallpaper/mod.rs b/cosmic-settings/src/pages/desktop/wallpaper/mod.rs index 3b79d50..b3337a7 100644 --- a/cosmic-settings/src/pages/desktop/wallpaper/mod.rs +++ b/cosmic-settings/src/pages/desktop/wallpaper/mod.rs @@ -5,7 +5,6 @@ mod config; pub mod widgets; pub use config::Config; -use futures::StreamExt; use url::Url; use std::{