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:
parent
126503fe5f
commit
f7d16417cd
9 changed files with 1901 additions and 1707 deletions
|
|
@ -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) => {
|
Message::OpenContextDrawer(page) => {
|
||||||
self.core.window.show_context = true;
|
self.core.window.show_context = true;
|
||||||
self.active_context_page = Some(page);
|
self.active_context_page = Some(page);
|
||||||
|
|
|
||||||
486
cosmic-settings/src/pages/desktop/appearance/drawer.rs
Normal file
486
cosmic-settings/src/pages/desktop/appearance/drawer.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,10 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use cosmic_config::ConfigSet;
|
use cosmic_config::ConfigSet;
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
|
||||||
|
use super::{ContextView, Message, drawer};
|
||||||
|
|
||||||
const INTERFACE_FONT: &str = "interface_font";
|
const INTERFACE_FONT: &str = "interface_font";
|
||||||
const MONOSPACE_FONT: &str = "monospace_font";
|
const MONOSPACE_FONT: &str = "monospace_font";
|
||||||
|
|
||||||
|
|
@ -62,42 +66,174 @@ pub fn load_font_families() -> (Vec<Arc<str>>, Vec<Arc<str>>) {
|
||||||
(interface, mono)
|
(interface, mono)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selection_context<'a>(
|
#[derive(Debug)]
|
||||||
families: &'a [Arc<str>],
|
pub struct Model {
|
||||||
current_font: &str,
|
font_search: String,
|
||||||
system: bool,
|
font_filter: Vec<Arc<str>>,
|
||||||
) -> Element<'a, super::Message> {
|
|
||||||
let svg_accent = Rc::new(|theme: &cosmic::Theme| svg::Style {
|
|
||||||
color: Some(theme.cosmic().accent_color().into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let list = families.iter().fold(widget::list_column(), |list, family| {
|
interface_font_families: Vec<Arc<str>>,
|
||||||
let selected = &**family == current_font;
|
pub interface_font: FontConfig,
|
||||||
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())),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
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.
|
/// Set the preferred icon theme for GNOME/GTK applications.
|
||||||
|
|
@ -107,96 +243,3 @@ pub async fn set_gnome_font_name(font_name: &str) {
|
||||||
.status()
|
.status()
|
||||||
.await;
|
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub fn button(
|
||||||
handles: &[icon::Handle],
|
handles: &[icon::Handle],
|
||||||
id: usize,
|
id: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
callback: impl Fn(usize) -> super::Message,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let theme = cosmic::theme::active();
|
let theme = cosmic::theme::active();
|
||||||
let theme = theme.cosmic();
|
let theme = theme.cosmic();
|
||||||
|
|
@ -61,7 +62,7 @@ pub fn button(
|
||||||
.spacing(theme.space_xxxs()),
|
.spacing(theme.space_xxxs()),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.on_press(Message::IconTheme(id))
|
.on_press(callback(id))
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.padding(theme.space_xs())
|
.padding(theme.space_xs())
|
||||||
// Image button's style mostly works, but it needs a background to fit the design
|
// 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.
|
/// Set the preferred icon theme for GNOME/GTK applications.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
379
cosmic-settings/src/pages/desktop/appearance/mode_and_colors.rs
Normal file
379
cosmic-settings/src/pages/desktop/appearance/mode_and_colors.rs
Normal 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 = §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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<crate::pages::Message>,
|
||||||
|
labels: &HashMap<String, usize>,
|
||||||
|
) -> impl Into<Element<'a, Message>> {
|
||||||
|
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<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)
|
||||||
|
}
|
||||||
140
cosmic-settings/src/pages/desktop/appearance/style.rs
Normal file
140
cosmic-settings/src/pages/desktop/appearance/style.rs
Normal 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 = §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)
|
||||||
|
})
|
||||||
|
}
|
||||||
535
cosmic-settings/src/pages/desktop/appearance/theme_manager.rs
Normal file
535
cosmic-settings/src/pages/desktop/appearance/theme_manager.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ mod config;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
use futures::StreamExt;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue