fix(appearance): Multiple fixes + refactor (#1244)

- Switching theme mode is now reflected immediately in the settings application;
- Color changes are propagated immediately when selected;
- Imported theme is now loaded immediately;
- Interface density is now responsive when selected;
- Font Selection is updated to the screen when changes are committed;
- Icons toolkit's "Apply to Gnome" toggle is now responsive;

Logical fixes:

- Style (Round, Square, etc) is now sync'ed between dark & light mode;
- Interface Density is now updated for both modes;
- Window Management is also updated for both modes;

Improvements/Perf fixes:

- Removed code paths where the same configuration field was written multiple times in the same pass;
- Stopped completely rebuilding the whole page when theme needed to be rebuild;
- Simplified initialization of the Appearance module;
- Extracted theme manipulation to its own module for future performance improvement;

---------

Co-authored-by: Michael Aaron Murphy <michael@mmurphy.dev>
This commit is contained in:
8roken 2025-06-27 09:56:42 -04:00 committed by GitHub
parent 126503fe5f
commit f7d16417cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1901 additions and 1707 deletions

View file

@ -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);

View file

@ -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<ContextView>,
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<cosmic::iced::task::Handle>,
icon_theme_active: Option<usize>,
icon_global: bool,
icon_themes: IconThemes,
icon_handles: IconHandles,
tk_config: Option<Config>,
}
#[derive(Debug, Clone)]
pub enum FontMessage {
FontLoaded(Vec<Arc<str>>, Vec<Arc<str>>),
Search(String),
Select(Arc<str>),
}
#[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<app::Message> {
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<app::Message> {
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<app::Message> {
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::<String>("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<app::Message> {
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<crate::pages::Message> {
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<Color> {
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<ContextView>,
) -> Option<ContextDrawer<'_, crate::pages::Message>> {
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<Task<app::Message>> {
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
}

View file

@ -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<Arc<str>>, Vec<Arc<str>>) {
(interface, mono)
}
pub fn selection_context<'a>(
families: &'a [Arc<str>],
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<Arc<str>>,
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<Arc<str>>,
pub interface_font: FontConfig,
list.into()
monospace_font_families: Vec<Arc<str>>,
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<Arc<str>>,
interface: Vec<Arc<str>>,
) -> Task<crate::app::Message> {
self.interface_font_families = interface;
self.monospace_font_families = mono;
Task::none()
}
pub fn select(
&mut self,
font: String,
context_view: &ContextView,
) -> Option<Task<app::Message>> {
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<app::Message> {
self.font_search = input.to_lowercase();
self.font_filter.clear();
let mut result: Option<Vec<Arc<str>>> = 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<str>) -> 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<Arc<str>>> {
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<Arc<str>>, Vec<Arc<str>>),
MonospaceFontFamily(usize),
}
#[derive(Debug, Default)]
pub struct Model {
pub interface_font_families: Vec<Arc<str>>,
pub interface_font_family: Option<usize>,
pub monospace_font_families: Vec<Arc<str>>,
pub monospace_font_family: Option<usize>,
}
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<crate::app::Message> {
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<str>], family: &str) -> Option<usize> {
families.iter().position(|f| &**f == family)
}
fn update_config(variant: &str, font: FontConfig) {
if let Ok(config) = CosmicTk::config() {
_ = config.set(variant, font);
}
}

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -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<crate::pages::Message> {
let (descriptions, label_keys) = i18n();
Section::default()
.title(fl!("mode-and-colors"))
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let label_keys = label_keys.clone();
let descriptions = &section.descriptions;
let theme_manager = &page.theme_manager;
let mut section = settings::section()
.title(&section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let Spacing { space_xxs, .. } = cosmic::theme::spacing();
let descriptions = &section.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<crate::pages::Message>,
labels: &HashMap<String, usize>,
) -> impl Into<Element<'a, Message>> {
let descriptions = &section.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<Message>,
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<String>, HashMap<String, usize>) {
let mut descriptions = slab::Slab::new();
let keys: HashMap<String, usize> = 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)
}

View file

@ -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<crate::pages::Message> {
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::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
settings::section()
.title(&section.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)
})
}

View file

@ -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<Config>),
light: ThemeCustomizer,
dark: ThemeCustomizer,
custom_accent: Option<Srgb>,
}
#[derive(Debug)]
pub struct ThemeCustomizer {
builder: (ThemeBuilder, Option<Config>),
theme: (Theme, Option<Config>),
accent_palette: Option<Vec<Srgba>>,
custom_window_hint: Option<Srgb>,
}
impl From<(Option<Config>, Option<Config>, Option<Vec<Srgba>>)> for ThemeCustomizer {
fn from(
(theme_config, builder_config, palette): (
Option<Config>,
Option<Config>,
Option<Vec<Srgba>>,
),
) -> 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<app::Message> {
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<Task<app::Message>> = 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<Srgb> {
&self.custom_accent
}
#[inline]
pub fn accent_palette(&self) -> &Option<Vec<Srgba>> {
&self.selected_customizer().accent_palette
}
#[inline]
pub fn custom_window_hint(&self) -> &Option<Srgb> {
&self.selected_customizer().custom_window_hint()
}
#[inline]
pub fn theme_mode_config(&self) -> &Option<Config> {
&self.mode.1
}
pub fn dark_mode(&mut self, enabled: bool) -> Result<bool, cosmic_config::Error> {
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::<bool>("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<ThemeStaged> {
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<ThemeStaged> {
self.dark.set_spacing(spacing)?;
self.light.set_spacing(spacing)?;
Some(ThemeStaged::Both)
}
pub fn set_gap_size(&mut self, gap: u32) -> Option<ThemeStaged> {
self.dark.set_gap_size(gap)?;
self.light.set_gap_size(gap)?;
Some(ThemeStaged::Both)
}
pub fn get_color(&self, context: &ContextView) -> Option<Color> {
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<Color>,
context: &ContextView,
) -> Option<ThemeStaged> {
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<Srgb>) -> Option<ThemeStaged> {
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<Srgb> {
&self.custom_window_hint
}
pub fn set_bg_color(&mut self, color: Option<Srgba>) -> Option<ThemeStaged> {
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<Srgba>) -> Option<ThemeStaged> {
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<Srgb>) -> Option<ThemeStaged> {
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<Srgb>) -> Option<ThemeStaged> {
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<Srgb>) -> Option<ThemeStaged> {
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<ThemeStaged> {
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<ThemeStaged> {
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<ThemeStaged> {
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<ThemeStaged> {
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)
}
}

View file

@ -5,7 +5,6 @@ mod config;
pub mod widgets;
pub use config::Config;
use futures::StreamExt;
use url::Url;
use std::{