From 317bc7d32031ee32e3e8f80df20d483bf8258c0f Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 8 Apr 2024 02:30:46 -0400 Subject: [PATCH 1/9] Show previews of icon themes I implemented a preview for icon themes that shows a sample of available icons. Currently, the actual UI is a bit ugly, and I have to curate which icons to show as well. The basic concept works for now. --- .../src/pages/desktop/appearance.rs | 99 ++++++++++++++++--- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index fd93fd7..387dbc4 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use std::borrow::Cow; -use std::collections::BTreeSet; +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -14,12 +14,11 @@ use cosmic::cosmic_theme::{ CornerRadii, Theme, ThemeBuilder, ThemeMode, DARK_THEME_BUILDER_ID, LIGHT_THEME_BUILDER_ID, }; use cosmic::iced_core::{alignment, Color, Length}; -use cosmic::iced_widget::scrollable; +use cosmic::iced_widget::{scrollable, Column}; use cosmic::prelude::CollectionWidget; -use cosmic::widget::dropdown; -use cosmic::widget::icon::{from_name, icon}; +use cosmic::widget::icon::{self, from_name, icon}; use cosmic::widget::{ - button, color_picker::ColorPickerUpdate, container, horizontal_space, row, settings, + button, color_picker::ColorPickerUpdate, column, container, horizontal_space, row, settings, spin_button, text, ColorPickerModel, }; use cosmic::Apply; @@ -36,6 +35,7 @@ use crate::app; use super::wallpaper::widgets::color_image; type IconThemes = Vec; +type IconHandles = Vec<[icon::Handle; 3]>; crate::cache_dynamic_lazy! { static HEX: String = fl!("hex"); @@ -68,7 +68,8 @@ pub struct Page { roundness: Roundness, icon_theme_active: Option, - icon_themes: Vec, + icon_themes: IconThemes, + icon_handles: IconHandles, theme_mode: ThemeMode, theme_mode_config: Option, @@ -190,6 +191,7 @@ impl no_custom_window_hint: theme_builder.accent.is_some(), icon_theme_active: None, icon_themes: Vec::new(), + icon_handles: Vec::new(), theme_mode_config, theme_builder_config, theme_mode, @@ -267,7 +269,7 @@ pub enum Message { ControlComponent(ColorPickerUpdate), CustomAccent(ColorPickerUpdate), DarkMode(bool), - Entered(IconThemes), + Entered((IconThemes, IconHandles)), ExportError, ExportFile(Arc), ExportSuccess, @@ -569,7 +571,7 @@ impl Page { self.theme_builder_needs_update = true; Command::none() } - Message::Entered(icon_themes) => { + Message::Entered((icon_themes, icon_handles)) => { *self = Self::default(); // Set the icon themes, and define the active icon theme. @@ -578,6 +580,7 @@ impl Page { .icon_themes .iter() .position(|theme| theme == &self.tk.icon_theme); + self.icon_handles = icon_handles; Command::none() } Message::Left => Command::perform(async {}, |()| { @@ -1398,11 +1401,27 @@ pub fn style() -> Section { .add( settings::item::builder(&*ICON_THEME) .description(&*ICON_THEME_DESC) - .control(dropdown( - &page.icon_themes, - page.icon_theme_active, - Message::IconTheme, - )), + .control( + // dropdown( + // &page.icon_themes, + // page.icon_theme_active, + // Message::IconTheme, + // ) + scrollable(column::with_children( + page.icon_themes + .iter() + .zip(page.icon_handles.iter()) + .enumerate() + .map(|(i, (theme, handles))| { + icon_theme_button(theme, handles, i) + }) + .collect(), + )) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::new(), + )) + .height(Length::Fixed(64.0)), + ), ) .apply(Element::from) .map(crate::pages::Message::Appearance) @@ -1483,7 +1502,7 @@ pub fn color_button<'a, Message: 'a + Clone>( /// Find all icon themes available on the system. async fn fetch_icon_themes() -> Message { - let mut icon_themes = BTreeSet::new(); + let mut icon_themes = BTreeMap::new(); let mut buffer = String::new(); @@ -1551,12 +1570,16 @@ async fn fetch_icon_themes() -> Message { } if let Some(name) = name { - icon_themes.insert(name); + // `icon::from_name` may perform blocking I/O + let theme = name.clone(); + if let Ok(handles) = tokio::task::spawn_blocking(|| preview_handles(theme)).await { + icon_themes.insert(name, handles); + } } } } - Message::Entered(icon_themes.into_iter().collect()) + Message::Entered(icon_themes.into_iter().unzip()) } /// Set the preferred icon theme for GNOME/GTK applications. @@ -1571,3 +1594,47 @@ async fn set_gnome_icon_theme(theme: String) { .status() .await; } + +/// Generate [icon::Handle]s to use for icon theme previews. +fn preview_handles(theme: String) -> [icon::Handle; 3] { + // Cache current default and set icon theme as the new default. + let default = cosmic::icon_theme::default(); + cosmic::icon_theme::set_default(theme); + + // Evaluate handles with the current theme + let handles = [ + icon_handle("folder"), + icon_handle("folder"), + icon_handle("folder"), + ]; + + // Reset default icon theme. + cosmic::icon_theme::set_default(default); + handles +} + +fn icon_handle(icon_name: &str) -> icon::Handle { + icon::from_name(icon_name) + // Get the path to the icon for the currently set theme. + // Without the exact path, the handles will all resolve to icons from the same theme in + // [`icon_theme_button`] rather than the icons for each different theme + .path() + .map(icon::from_path) + // Fallback icon handle + .unwrap_or_else(|| icon::from_name(icon_name).handle()) +} + +/// Button with a preview of the icon theme. +fn icon_theme_button( + theme: &str, + handles: &[icon::Handle], + id: usize, +) -> Element<'static, Message> { + button( + column::with_capacity(2) + .push(text(theme.to_owned())) + .push(row::with_capacity(3).extend(handles.iter().map(|handle| handle.clone().icon()))), + ) + .on_press(Message::IconTheme(id)) + .into() +} From 0e985b51b5a0d9a416a961385b54aad779e1040d Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Tue, 9 Apr 2024 02:23:05 -0400 Subject: [PATCH 2/9] Icon theme chooser aesthetics * Display a few common icons that seem to available for each theme * Nicer spacing * Highlight active theme --- .../src/pages/desktop/appearance.rs | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index 387dbc4..717e0a8 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -34,8 +34,9 @@ use crate::app; use super::wallpaper::widgets::color_image; +const NUM_HANDLES: usize = 5; type IconThemes = Vec; -type IconHandles = Vec<[icon::Handle; 3]>; +type IconHandles = Vec<[icon::Handle; NUM_HANDLES]>; crate::cache_dynamic_lazy! { static HEX: String = fl!("hex"); @@ -1398,31 +1399,28 @@ pub fn style() -> Section { .description(&*descriptions[6]) .toggler(page.tk.apply_theme_global, Message::ApplyThemeGlobal), ) - .add( + .add({ + let active = page.icon_theme_active; settings::item::builder(&*ICON_THEME) .description(&*ICON_THEME_DESC) .control( - // dropdown( - // &page.icon_themes, - // page.icon_theme_active, - // Message::IconTheme, - // ) scrollable(column::with_children( page.icon_themes .iter() .zip(page.icon_handles.iter()) .enumerate() .map(|(i, (theme, handles))| { - icon_theme_button(theme, handles, i) + let selected = active.map(|j| i == j).unwrap_or_default(); + icon_theme_button(theme, handles, i, selected) }) .collect(), )) .direction(scrollable::Direction::Vertical( scrollable::Properties::new(), )) - .height(Length::Fixed(64.0)), - ), - ) + .height(Length::Fixed(96.0)), + ) + }) .apply(Element::from) .map(crate::pages::Message::Appearance) }) @@ -1596,7 +1594,7 @@ async fn set_gnome_icon_theme(theme: String) { } /// Generate [icon::Handle]s to use for icon theme previews. -fn preview_handles(theme: String) -> [icon::Handle; 3] { +fn preview_handles(theme: String) -> [icon::Handle; NUM_HANDLES] { // Cache current default and set icon theme as the new default. let default = cosmic::icon_theme::default(); cosmic::icon_theme::set_default(theme); @@ -1604,8 +1602,10 @@ fn preview_handles(theme: String) -> [icon::Handle; 3] { // Evaluate handles with the current theme let handles = [ icon_handle("folder"), - icon_handle("folder"), - icon_handle("folder"), + icon_handle("text-x-generic"), + icon_handle("audio-x-generic"), + icon_handle("video-x-generic"), + icon_handle("image-x-generic"), ]; // Reset default icon theme. @@ -1626,15 +1626,23 @@ fn icon_handle(icon_name: &str) -> icon::Handle { /// Button with a preview of the icon theme. fn icon_theme_button( - theme: &str, + name: &str, handles: &[icon::Handle], id: usize, + selected: bool, ) -> Element<'static, Message> { + let theme = cosmic::theme::active(); button( - column::with_capacity(2) - .push(text(theme.to_owned())) - .push(row::with_capacity(3).extend(handles.iter().map(|handle| handle.clone().icon()))), + row::with_capacity(2) + .push( + row::with_capacity(NUM_HANDLES) + .extend(handles.iter().map(|handle| handle.clone().icon())), + ) + .push(text(name.to_owned())) + .spacing(theme.cosmic().space_m()), ) .on_press(Message::IconTheme(id)) + .selected(selected) + .style(button::Style::Icon) .into() } From cbd6b2d350e3f849acfe33209cd210c9954bf127 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 12 Apr 2024 00:47:52 -0400 Subject: [PATCH 3/9] Update icon theme preview buttons to match design --- .../src/pages/desktop/appearance.rs | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index 717e0a8..4219497 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -34,9 +34,11 @@ use crate::app; use super::wallpaper::widgets::color_image; -const NUM_HANDLES: usize = 5; +const ICON_PREV_N: usize = 6; +const ICON_PREV_ROW: usize = 3; +const ICON_PREV_SIZE: u16 = 32; type IconThemes = Vec; -type IconHandles = Vec<[icon::Handle; NUM_HANDLES]>; +type IconHandles = Vec<[icon::Handle; ICON_PREV_N]>; crate::cache_dynamic_lazy! { static HEX: String = fl!("hex"); @@ -1594,18 +1596,19 @@ async fn set_gnome_icon_theme(theme: String) { } /// Generate [icon::Handle]s to use for icon theme previews. -fn preview_handles(theme: String) -> [icon::Handle; NUM_HANDLES] { - // Cache current default and set icon theme as the new default. +fn preview_handles(theme: String) -> [icon::Handle; ICON_PREV_N] { + // Cache current default and set icon theme as a temporary default let default = cosmic::icon_theme::default(); cosmic::icon_theme::set_default(theme); - // Evaluate handles with the current theme + // Evaluate handles with the temporary theme let handles = [ icon_handle("folder"), - icon_handle("text-x-generic"), + icon_handle("user-home"), + icon_handle("preferences-system"), + icon_handle("image-x-generic"), icon_handle("audio-x-generic"), icon_handle("video-x-generic"), - icon_handle("image-x-generic"), ]; // Reset default icon theme. @@ -1632,17 +1635,35 @@ fn icon_theme_button( selected: bool, ) -> Element<'static, Message> { let theme = cosmic::theme::active(); - button( - row::with_capacity(2) - .push( - row::with_capacity(NUM_HANDLES) - .extend(handles.iter().map(|handle| handle.clone().icon())), - ) - .push(text(name.to_owned())) - .spacing(theme.cosmic().space_m()), - ) - .on_press(Message::IconTheme(id)) - .selected(selected) - .style(button::Style::Icon) + let theme = theme.cosmic(); + cosmic::iced::widget::column![ + button( + cosmic::iced::widget::column![ + row::Row::new() + .extend( + handles + .iter() + .take(ICON_PREV_ROW) + .cloned() + .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + ) + .spacing(theme.space_xs()), + row::Row::new() + .extend( + handles + .iter() + .skip(ICON_PREV_ROW) + .cloned() + .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + ) + .spacing(theme.space_xs()), + ] + .spacing(theme.space_xs()), + ) + .on_press(Message::IconTheme(id)) + .selected(selected) + .style(button::Style::Icon), + text(name.to_owned()) + ] .into() } From 99a402ee1d90f3357237c37bacedbb530d437cf0 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 15 Apr 2024 02:25:33 -0400 Subject: [PATCH 4/9] Context drawer for experimental icon config Adds a context drawer for the experimental icon settings such as the icon theme picker. TODO: * Flexible display of icon previews * Reset button --- .../src/pages/desktop/appearance.rs | 95 +++++++++++++------ i18n/en/cosmic_settings.ftl | 2 + 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index 4219497..a43877b 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -55,6 +55,7 @@ enum ContextView { ContainerBackground, ControlComponent, CustomAccent, + Experimental, InterfaceText, } @@ -273,6 +274,7 @@ pub enum Message { CustomAccent(ColorPickerUpdate), DarkMode(bool), Entered((IconThemes, IconHandles)), + ExperimentalContextDrawer, ExportError, ExportFile(Arc), ExportSuccess, @@ -444,6 +446,42 @@ impl Page { .map(crate::pages::Message::Appearance) } + fn experimental_context_view( + &self, + reset: Cow<'static, str>, + ) -> Element<'_, crate::pages::Message> { + let active = self.icon_theme_active; + cosmic::iced::widget::column![ + // Export theme choice to GNOME + settings::item::builder(fl!("enable-export")) + .description(fl!("enable-export", "desc")) + .toggler(self.tk.apply_theme_global, Message::ApplyThemeGlobal), + // Icon theme previews + settings::item::builder(&*ICON_THEME) + .description(&*ICON_THEME_DESC) + .control( + scrollable(column::with_children( + 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_theme_button(theme, handles, i, selected) + }) + .collect(), + )) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::new(), + )) + .height(Length::Fixed(96.0)), + ) + ] + .width(Length::Fill) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + } + #[allow(clippy::too_many_lines)] pub fn update(&mut self, message: Message) -> Command { self.theme_builder_needs_update = false; @@ -835,6 +873,13 @@ impl Page { } Command::none() } + Message::ExperimentalContextDrawer => { + self.context_view = Some(ContextView::Experimental); + + cosmic::command::message(crate::app::Message::OpenContextDrawer( + fl!("experimental").into(), + )) + } Message::Daytime(day_time) => { self.day_time = day_time; Command::none() @@ -944,6 +989,7 @@ impl page::Page for Page { sections.insert(mode_and_colors()), sections.insert(style()), sections.insert(window_management()), + sections.insert(experimental()), sections.insert(reset_button()), ]) } @@ -1022,6 +1068,10 @@ impl page::Page for Page { |this| &this.custom_accent, ), + ContextView::Experimental => { + self.experimental_context_view(RESET_TO_DEFAULT.as_str().into()) + } + ContextView::InterfaceText => self.color_picker_context_view( None, RESET_TO_DEFAULT.as_str().into(), @@ -1304,10 +1354,6 @@ pub fn style() -> Section { fl!("style", "square").into(), fl!("frosted").into(), fl!("frosted", "desc").into(), - fl!("enable-export").into(), - fl!("enable-export", "desc").into(), - ICON_THEME.as_str().into(), - ICON_THEME_DESC.as_str().into(), ]) .view::(|_binder, page, section| { let descriptions = §ion.descriptions; @@ -1396,33 +1442,6 @@ pub fn style() -> Section { .description(&*descriptions[4]) .toggler(page.theme_builder.is_frosted, Message::Frosted), ) - .add( - settings::item::builder(&*descriptions[5]) - .description(&*descriptions[6]) - .toggler(page.tk.apply_theme_global, Message::ApplyThemeGlobal), - ) - .add({ - let active = page.icon_theme_active; - settings::item::builder(&*ICON_THEME) - .description(&*ICON_THEME_DESC) - .control( - scrollable(column::with_children( - page.icon_themes - .iter() - .zip(page.icon_handles.iter()) - .enumerate() - .map(|(i, (theme, handles))| { - let selected = active.map(|j| i == j).unwrap_or_default(); - icon_theme_button(theme, handles, i, selected) - }) - .collect(), - )) - .direction(scrollable::Direction::Vertical( - scrollable::Properties::new(), - )) - .height(Length::Fixed(96.0)), - ) - }) .apply(Element::from) .map(crate::pages::Message::Appearance) }) @@ -1457,6 +1476,20 @@ pub fn window_management() -> Section { }) } +pub fn experimental() -> Section { + Section::default().view::(|_binder, _page, _section| { + settings::view_section("") + .add( + settings::item::builder(fl!("experimental")).control( + button::icon(from_name("go-next-symbolic")) + .on_press(Message::ExperimentalContextDrawer), + ), + ) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + }) +} + #[allow(clippy::too_many_lines)] pub fn reset_button() -> Section { Section::default() diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index fe56ef4..504f9c0 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -49,6 +49,8 @@ control-tint = Control component tint frosted = Frosted glass effect on system interface .desc = Applies background blur to panel, dock, applets, launcher, and application library. +experimental = Experimental + enable-export = Apply this theme to GNOME apps. .desc = Not all toolkits support auto-switching. Non-COSMIC apps may need to be restarted after a theme change. From 46374c69c17ade8f4708d0d24db9949bd80d3809 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Wed, 17 Apr 2024 03:34:33 -0400 Subject: [PATCH 5/9] Implement context page for theme previews --- .../src/pages/desktop/appearance.rs | 122 +++++++++++------- 1 file changed, 76 insertions(+), 46 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index a43877b..b2c0657 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use ashpd::desktop::file_chooser::{FileFilter, SelectedFiles}; @@ -13,13 +13,13 @@ use cosmic::cosmic_theme::palette::{FromColor, Hsv, Srgb, Srgba}; use cosmic::cosmic_theme::{ CornerRadii, Theme, ThemeBuilder, ThemeMode, DARK_THEME_BUILDER_ID, LIGHT_THEME_BUILDER_ID, }; -use cosmic::iced_core::{alignment, Color, Length}; -use cosmic::iced_widget::{scrollable, Column}; +use cosmic::iced_core::{alignment, Background, Color, Length}; +use cosmic::iced_widget::scrollable; use cosmic::prelude::CollectionWidget; use cosmic::widget::icon::{self, from_name, icon}; use cosmic::widget::{ - button, color_picker::ColorPickerUpdate, column, container, horizontal_space, row, settings, - spin_button, text, ColorPickerModel, + button, color_picker::ColorPickerUpdate, column, container, flex_row, horizontal_space, row, + settings, spin_button, text, ColorPickerModel, }; use cosmic::Apply; use cosmic::{command, Command, Element}; @@ -446,37 +446,40 @@ impl Page { .map(crate::pages::Message::Appearance) } - fn experimental_context_view( - &self, - reset: Cow<'static, str>, - ) -> Element<'_, crate::pages::Message> { + fn experimental_context_view(&self) -> Element<'_, crate::pages::Message> { let active = self.icon_theme_active; + let theme = cosmic::theme::active(); + let theme = theme.cosmic(); cosmic::iced::widget::column![ // Export theme choice to GNOME - settings::item::builder(fl!("enable-export")) - .description(fl!("enable-export", "desc")) - .toggler(self.tk.apply_theme_global, Message::ApplyThemeGlobal), + settings::view_section("").add( + settings::item::builder(fl!("enable-export")) + .description(fl!("enable-export", "desc")) + .toggler(self.tk.apply_theme_global, Message::ApplyThemeGlobal) + ), // Icon theme previews - settings::item::builder(&*ICON_THEME) - .description(&*ICON_THEME_DESC) - .control( - scrollable(column::with_children( - 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_theme_button(theme, handles, i, selected) - }) - .collect(), - )) - .direction(scrollable::Direction::Vertical( - scrollable::Properties::new(), - )) - .height(Length::Fixed(96.0)), + // cosmic::iced::widget::column![text(&*ICON_THEME), text(&*ICON_THEME_DESC).size(10)] + // .spacing(2), + text(&*ICON_THEME), + scrollable( + 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_theme_button(theme, handles, i, selected) + }) + .collect(), ) + .row_spacing(theme.space_xs()) + .column_spacing(theme.space_xs()) + ) ] + // .padding(theme.space_s()) + .spacing(theme.space_m()) + // .align_items(cosmic::iced_core::Alignment::Center) .width(Length::Fill) .apply(Element::from) .map(crate::pages::Message::Appearance) @@ -1068,9 +1071,7 @@ impl page::Page for Page { |this| &this.custom_accent, ), - ContextView::Experimental => { - self.experimental_context_view(RESET_TO_DEFAULT.as_str().into()) - } + ContextView::Experimental => self.experimental_context_view(), ContextView::InterfaceText => self.color_picker_context_view( None, @@ -1477,17 +1478,20 @@ pub fn window_management() -> Section { } pub fn experimental() -> Section { - Section::default().view::(|_binder, _page, _section| { - settings::view_section("") - .add( - settings::item::builder(fl!("experimental")).control( - button::icon(from_name("go-next-symbolic")) - .on_press(Message::ExperimentalContextDrawer), - ), - ) - .apply(Element::from) - .map(crate::pages::Message::Appearance) - }) + Section::default() + .descriptions(vec![fl!("experimental").into()]) + .view::(|_binder, _page, section| { + let descriptions = &*section.descriptions; + settings::view_section("") + .add( + settings::item::builder(&*descriptions[0]).control( + button::icon(from_name("go-next-symbolic")) + .on_press(Message::ExperimentalContextDrawer), + ), + ) + .apply(Element::from) + .map(crate::pages::Message::Appearance) + }) } #[allow(clippy::too_many_lines)] @@ -1695,8 +1699,34 @@ fn icon_theme_button( ) .on_press(Message::IconTheme(id)) .selected(selected) - .style(button::Style::Icon), - text(name.to_owned()) + .style(button::Style::Custom { + active: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)), + disabled: Box::new(move |theme| icon_theme_style(theme, selected, false)), + hovered: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)), + pressed: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)) + }), + text(name.to_owned()).width(Length::Fixed((ICON_PREV_SIZE * 3) as _)) ] + .spacing(theme.space_xs()) .into() } + +/// Icon preview button style. +fn icon_theme_style( + theme: &cosmic::theme::Theme, + selected: bool, + _focused: bool, +) -> button::Appearance { + let cosmic = theme.cosmic(); + let mut appearance = button::Appearance::new(); + + appearance.background = Some(Background::Color(cosmic.palette.neutral_4.into())); + + if selected { + appearance.border_width = 2.0; + appearance.border_color = cosmic.accent.base.into(); + appearance.icon_color = Some(cosmic.accent.base.into()); + } + + appearance +} From d4b422228db24bdab3868d0f0b32aafcf1e6f873 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Thu, 18 Apr 2024 02:23:43 -0400 Subject: [PATCH 6/9] Tweak icon preview buttons * Preview buttons should be Image buttons with a modified style --- .../src/pages/desktop/appearance.rs | 171 ++++++++++-------- i18n/en/cosmic_settings.ftl | 2 +- 2 files changed, 101 insertions(+), 72 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index b2c0657..66333a9 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -451,7 +451,7 @@ impl Page { let theme = cosmic::theme::active(); let theme = theme.cosmic(); cosmic::iced::widget::column![ - // Export theme choice to GNOME + // Export theme choice settings::view_section("").add( settings::item::builder(fl!("enable-export")) .description(fl!("enable-export", "desc")) @@ -460,22 +460,25 @@ impl Page { // Icon theme previews // cosmic::iced::widget::column![text(&*ICON_THEME), text(&*ICON_THEME_DESC).size(10)] // .spacing(2), - text(&*ICON_THEME), - scrollable( - 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_theme_button(theme, handles, i, selected) - }) - .collect(), + cosmic::iced::widget::column![ + text(&*ICON_THEME), + scrollable( + 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_theme_button(theme, handles, i, selected) + }) + .collect(), + ) + .row_spacing(theme.space_xs()) + .column_spacing(theme.space_xxxs()) ) - .row_spacing(theme.space_xs()) - .column_spacing(theme.space_xs()) - ) + ] + .spacing(theme.space_xxs()) ] // .padding(theme.space_s()) .spacing(theme.space_m()) @@ -878,10 +881,7 @@ impl Page { } Message::ExperimentalContextDrawer => { self.context_view = Some(ContextView::Experimental); - - cosmic::command::message(crate::app::Message::OpenContextDrawer( - fl!("experimental").into(), - )) + cosmic::command::message(crate::app::Message::OpenContextDrawer("".into())) } Message::Daytime(day_time) => { self.day_time = day_time; @@ -1479,7 +1479,7 @@ pub fn window_management() -> Section { pub fn experimental() -> Section { Section::default() - .descriptions(vec![fl!("experimental").into()]) + .descriptions(vec![fl!("experimental-settings").into()]) .view::(|_binder, _page, section| { let descriptions = &*section.descriptions; settings::view_section("") @@ -1673,60 +1673,89 @@ fn icon_theme_button( ) -> Element<'static, Message> { let theme = cosmic::theme::active(); let theme = theme.cosmic(); - cosmic::iced::widget::column![ - button( - cosmic::iced::widget::column![ - row::Row::new() - .extend( - handles - .iter() - .take(ICON_PREV_ROW) - .cloned() - .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + // let image_style = cosmic::theme::Button::Image; + let background = theme.palette.neutral_4.into(); + + cosmic::widget::column() + .push( + button::Button::new_image( + cosmic::iced::widget::column![ + cosmic::widget::row::row() + .extend( + handles + .iter() + .take(ICON_PREV_ROW) + .cloned() + .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + ) + .spacing(theme.space_xs()), + row::Row::new() + .extend( + handles + .iter() + .skip(ICON_PREV_ROW) + .cloned() + .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + ) + .spacing(theme.space_xs()), + ] + .spacing(theme.space_xs()), + None, + ) + .on_press(Message::IconTheme(id)) + .selected(selected) + .style(button::Style::Custom { + active: Box::new(move |focused, theme| { + icon_theme_style( + ::active( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ), + background, ) - .spacing(theme.space_xs()), - row::Row::new() - .extend( - handles - .iter() - .skip(ICON_PREV_ROW) - .cloned() - .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + }), + disabled: Box::new(move |theme| { + icon_theme_style( + ::disabled( + theme, + &cosmic::theme::Button::Image, + ), + background, ) - .spacing(theme.space_xs()), - ] - .spacing(theme.space_xs()), + }), + hovered: Box::new(move |focused, theme| { + icon_theme_style( + ::hovered( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ), + background, + ) + }), + pressed: Box::new(move |focused, theme| { + icon_theme_style( + ::pressed( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ), + background, + ) + }), + }), ) - .on_press(Message::IconTheme(id)) - .selected(selected) - .style(button::Style::Custom { - active: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)), - disabled: Box::new(move |theme| icon_theme_style(theme, selected, false)), - hovered: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)), - pressed: Box::new(move |focused, theme| icon_theme_style(theme, selected, focused)) - }), - text(name.to_owned()).width(Length::Fixed((ICON_PREV_SIZE * 3) as _)) - ] - .spacing(theme.space_xs()) - .into() + .push(text(name.to_owned()).width(Length::Fixed((ICON_PREV_SIZE * 3) as _))) + .spacing(theme.space_xs()) + .into() } -/// Icon preview button style. -fn icon_theme_style( - theme: &cosmic::theme::Theme, - selected: bool, - _focused: bool, -) -> button::Appearance { - let cosmic = theme.cosmic(); - let mut appearance = button::Appearance::new(); - - appearance.background = Some(Background::Color(cosmic.palette.neutral_4.into())); - - if selected { - appearance.border_width = 2.0; - appearance.border_color = cosmic.accent.base.into(); - appearance.icon_color = Some(cosmic.accent.base.into()); - } - +/// Add background color to the thumbnails button. +fn icon_theme_style(mut appearance: button::Appearance, background: Color) -> button::Appearance { + appearance.background = Some(Background::Color(background)); appearance } diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 504f9c0..c7f6a6f 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -49,7 +49,7 @@ control-tint = Control component tint frosted = Frosted glass effect on system interface .desc = Applies background blur to panel, dock, applets, launcher, and application library. -experimental = Experimental +experimental-settings = Experimental settings enable-export = Apply this theme to GNOME apps. .desc = Not all toolkits support auto-switching. Non-COSMIC apps may need to be restarted after a theme change. From 78d597c56fa935719352acd53677244168a3d47b Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 19 Apr 2024 02:40:10 -0400 Subject: [PATCH 7/9] Aesthetic improvements for icon theme previews * Padded buttons * Use correct text style for "Icon theme" * Truncate long theme names --- .../src/pages/desktop/appearance.rs | 117 +++++++++--------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index 66333a9..af1327f 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -18,8 +18,8 @@ use cosmic::iced_widget::scrollable; use cosmic::prelude::CollectionWidget; use cosmic::widget::icon::{self, from_name, icon}; use cosmic::widget::{ - button, color_picker::ColorPickerUpdate, column, container, flex_row, horizontal_space, row, - settings, spin_button, text, ColorPickerModel, + button, color_picker::ColorPickerUpdate, container, flex_row, horizontal_space, row, settings, + spin_button, text, ColorPickerModel, }; use cosmic::Apply; use cosmic::{command, Command, Element}; @@ -460,8 +460,8 @@ impl Page { // Icon theme previews // cosmic::iced::widget::column![text(&*ICON_THEME), text(&*ICON_THEME_DESC).size(10)] // .spacing(2), - cosmic::iced::widget::column![ - text(&*ICON_THEME), + cosmic::widget::column::with_children(vec![ + text::heading(&*ICON_THEME).into(), scrollable( flex_row( self.icon_themes @@ -477,7 +477,8 @@ impl Page { .row_spacing(theme.space_xs()) .column_spacing(theme.space_xxxs()) ) - ] + .into() + ]) .spacing(theme.space_xxs()) ] // .padding(theme.space_s()) @@ -1673,89 +1674,91 @@ fn icon_theme_button( ) -> Element<'static, Message> { let theme = cosmic::theme::active(); let theme = theme.cosmic(); - // let image_style = cosmic::theme::Button::Image; - let background = theme.palette.neutral_4.into(); + let background = Background::Color(theme.palette.neutral_4.into()); cosmic::widget::column() .push( - button::Button::new_image( - cosmic::iced::widget::column![ - cosmic::widget::row::row() + cosmic::widget::button::custom_image_button( + cosmic::widget::column::with_children(vec![ + cosmic::widget::row() .extend( handles .iter() .take(ICON_PREV_ROW) .cloned() - .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + // TODO: Maybe allow choosable sizes/zooming + .map(|handle| handle.icon().size(ICON_PREV_SIZE)), ) - .spacing(theme.space_xs()), - row::Row::new() + .spacing(theme.space_xxs()) + .into(), + cosmic::widget::row() .extend( handles .iter() .skip(ICON_PREV_ROW) .cloned() - .map(|handle| handle.icon().size(ICON_PREV_SIZE)) + // TODO: Maybe allow choosable sizes/zooming + .map(|handle| handle.icon().size(ICON_PREV_SIZE)), ) - .spacing(theme.space_xs()), - ] + .spacing(theme.space_xxs()) + .into(), + ]) .spacing(theme.space_xs()), None, ) .on_press(Message::IconTheme(id)) .selected(selected) + .padding(theme.space_xxs()) + // Image button's style mostly works, but it needs a background to fit the design .style(button::Style::Custom { active: Box::new(move |focused, theme| { - icon_theme_style( - ::active( - theme, - focused, - selected, - &cosmic::theme::Button::Image, - ), - background, - ) + let mut appearance = ::active( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ); + appearance.background = Some(background); + appearance }), disabled: Box::new(move |theme| { - icon_theme_style( - ::disabled( - theme, - &cosmic::theme::Button::Image, - ), - background, - ) + let mut appearance = ::disabled( + theme, + &cosmic::theme::Button::Image, + ); + appearance.background = Some(background); + appearance }), hovered: Box::new(move |focused, theme| { - icon_theme_style( - ::hovered( - theme, - focused, - selected, - &cosmic::theme::Button::Image, - ), - background, - ) + let mut appearance = ::hovered( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ); + appearance.background = Some(background); + appearance }), pressed: Box::new(move |focused, theme| { - icon_theme_style( - ::pressed( - theme, - focused, - selected, - &cosmic::theme::Button::Image, - ), - background, - ) + let mut appearance = ::pressed( + theme, + focused, + selected, + &cosmic::theme::Button::Image, + ); + appearance.background = Some(background); + appearance }), }), ) - .push(text(name.to_owned()).width(Length::Fixed((ICON_PREV_SIZE * 3) as _))) + .push( + text(if name.len() > 18 { + format!("{name:.20}...") + } else { + name.into() + }) + .width(Length::Fixed((ICON_PREV_SIZE * 3) as _)), + ) .spacing(theme.space_xs()) .into() } - -/// Add background color to the thumbnails button. -fn icon_theme_style(mut appearance: button::Appearance, background: Color) -> button::Appearance { - appearance.background = Some(Background::Color(background)); - appearance -} From 900e17bb99d35a3975a2aa48f653d56f84952826 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 19 Apr 2024 23:14:26 -0400 Subject: [PATCH 8/9] fix: Evaluate icon size variants correctly --- .../src/pages/desktop/appearance.rs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index af1327f..a9b796f 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -36,7 +36,9 @@ use super::wallpaper::widgets::color_image; const ICON_PREV_N: usize = 6; const ICON_PREV_ROW: usize = 3; -const ICON_PREV_SIZE: u16 = 32; +const ICON_TRY_SIZES: [u16; 3] = [32, 48, 64]; +const ICON_THUMB_SIZE: u16 = 32; +const ICON_NAME_TRUNC: usize = 20; type IconThemes = Vec; type IconHandles = Vec<[icon::Handle; ICON_PREV_N]>; @@ -1655,12 +1657,18 @@ fn preview_handles(theme: String) -> [icon::Handle; ICON_PREV_N] { } fn icon_handle(icon_name: &str) -> icon::Handle { - icon::from_name(icon_name) - // Get the path to the icon for the currently set theme. - // Without the exact path, the handles will all resolve to icons from the same theme in - // [`icon_theme_button`] rather than the icons for each different theme - .path() - .map(icon::from_path) + ICON_TRY_SIZES + .iter() + .find_map(|&size| { + icon::from_name(icon_name) + // Set the size on the handle to evaluate the correct icon + .size(size) + // Get the path to the icon for the currently set theme. + // Without the exact path, the handles will all resolve to icons from the same theme in + // [`icon_theme_button`] rather than the icons for each different theme + .path() + .map(icon::from_path) + }) // Fallback icon handle .unwrap_or_else(|| icon::from_name(icon_name).handle()) } @@ -1687,7 +1695,7 @@ fn icon_theme_button( .take(ICON_PREV_ROW) .cloned() // TODO: Maybe allow choosable sizes/zooming - .map(|handle| handle.icon().size(ICON_PREV_SIZE)), + .map(|handle| handle.icon().size(ICON_THUMB_SIZE)), ) .spacing(theme.space_xxs()) .into(), @@ -1698,7 +1706,7 @@ fn icon_theme_button( .skip(ICON_PREV_ROW) .cloned() // TODO: Maybe allow choosable sizes/zooming - .map(|handle| handle.icon().size(ICON_PREV_SIZE)), + .map(|handle| handle.icon().size(ICON_THUMB_SIZE)), ) .spacing(theme.space_xxs()) .into(), @@ -1752,12 +1760,12 @@ fn icon_theme_button( }), ) .push( - text(if name.len() > 18 { - format!("{name:.20}...") + text(if name.len() > ICON_NAME_TRUNC { + format!("{name:.ICON_NAME_TRUNC$}...") } else { name.into() }) - .width(Length::Fixed((ICON_PREV_SIZE * 3) as _)), + .width(Length::Fixed((ICON_THUMB_SIZE * 3) as _)), ) .spacing(theme.space_xs()) .into() From 75bacc03e8f9d0549e9f06cc089967846e950285 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 22 Apr 2024 01:40:54 -0400 Subject: [PATCH 9/9] Fallback to symbolic icons for icon theme preview Some icon themes don't ship with the variants requested in the design (e.g. Adwaita doesn't have `preferences-system`) but do have `symbolic` iterations. --- .../src/pages/desktop/appearance.rs | 96 ++++++++++++++++--- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/cosmic-settings/src/pages/desktop/appearance.rs b/cosmic-settings/src/pages/desktop/appearance.rs index a9b796f..fd27b4e 100644 --- a/cosmic-settings/src/pages/desktop/appearance.rs +++ b/cosmic-settings/src/pages/desktop/appearance.rs @@ -1543,6 +1543,7 @@ pub fn color_button<'a, Message: 'a + Clone>( /// Find all icon themes available on the system. async fn fetch_icon_themes() -> Message { let mut icon_themes = BTreeMap::new(); + let mut theme_paths: BTreeMap = BTreeMap::new(); let mut buffer = String::new(); @@ -1589,6 +1590,7 @@ async fn fetch_icon_themes() -> Message { buffer.clear(); let mut name = None; + let mut valid_dirs = Vec::new(); let mut line_reader = tokio::io::BufReader::new(file); while let Ok(read) = line_reader.read_line(&mut buffer).await { @@ -1606,13 +1608,39 @@ async fn fetch_icon_themes() -> Message { } } + if valid_dirs.is_empty() { + if let Some(value) = buffer.strip_prefix("Inherits=") { + valid_dirs.extend(value.trim().split(',').map(|fallback| { + if let Some(path) = theme_paths.get(fallback) { + path.iter() + .last() + .and_then(|os| os.to_str().map(ToOwned::to_owned)) + .unwrap_or_else(|| fallback.to_owned()) + } else { + fallback.to_owned() + } + })); + } + } + buffer.clear(); } if let Some(name) = name { - // `icon::from_name` may perform blocking I/O + // Name of the directory theme was found in (e.g. Pop for Pop) + valid_dirs.push( + path.iter() + .last() + .and_then(|os| os.to_str().map(ToOwned::to_owned)) + .unwrap_or_else(|| name.clone()), + ); + theme_paths.entry(name.clone()).or_insert(path); + let theme = name.clone(); - if let Ok(handles) = tokio::task::spawn_blocking(|| preview_handles(theme)).await { + // `icon::from_name` may perform blocking I/O + if let Ok(handles) = + tokio::task::spawn_blocking(|| preview_handles(theme, valid_dirs)).await + { icon_themes.insert(name, handles); } } @@ -1636,19 +1664,23 @@ async fn set_gnome_icon_theme(theme: String) { } /// Generate [icon::Handle]s to use for icon theme previews. -fn preview_handles(theme: String) -> [icon::Handle; ICON_PREV_N] { +fn preview_handles(theme: String, inherits: Vec) -> [icon::Handle; ICON_PREV_N] { // Cache current default and set icon theme as a temporary default let default = cosmic::icon_theme::default(); cosmic::icon_theme::set_default(theme); // Evaluate handles with the temporary theme let handles = [ - icon_handle("folder"), - icon_handle("user-home"), - icon_handle("preferences-system"), - icon_handle("image-x-generic"), - icon_handle("audio-x-generic"), - icon_handle("video-x-generic"), + icon_handle("folder", "folder-symbolic", &inherits), + icon_handle("user-home", "user-home-symbolic", &inherits), + icon_handle( + "preferences-system", + "preferences-system-symbolic", + &inherits, + ), + icon_handle("image-x-generic", "images-x-generic-symbolic", &inherits), + icon_handle("audio-x-generic", "audio-x-generic-symbolic", &inherits), + icon_handle("video-x-generic", "video-x-generic-symbolic", &inherits), ]; // Reset default icon theme. @@ -1656,21 +1688,57 @@ fn preview_handles(theme: String) -> [icon::Handle; ICON_PREV_N] { handles } -fn icon_handle(icon_name: &str) -> icon::Handle { +/// Evaluate an icon handle for a specific theme. +/// +/// `alternate` is a fallback icon name such as a symbolic variant. +/// +/// `valid_dirs` should be a slice of directories from which we consider an icon to be valid. Valid +/// directories would usually be inherited themes as well as the actual theme's location. +fn icon_handle(icon_name: &str, alternate: &str, valid_dirs: &[String]) -> icon::Handle { ICON_TRY_SIZES .iter() - .find_map(|&size| { - icon::from_name(icon_name) + .zip(std::iter::repeat(icon_name).take(ICON_TRY_SIZES.len())) + // Try fallback icon name after the default + .chain( + ICON_TRY_SIZES + .iter() + .zip(std::iter::repeat(alternate)) + .take(ICON_TRY_SIZES.len()), + ) + .find_map(|(&size, name)| { + icon::from_name(name) // Set the size on the handle to evaluate the correct icon .size(size) // Get the path to the icon for the currently set theme. // Without the exact path, the handles will all resolve to icons from the same theme in // [`icon_theme_button`] rather than the icons for each different theme .path() - .map(icon::from_path) + // `libcosmic` should always return a path if the default theme is installed + // The returned path has to be verified as an icon from the set theme or an + // inherited theme + .and_then(|path| { + let mut theme_dir = &*path; + while let Some(parent) = theme_dir.parent() { + if parent.ends_with("icons") { + break; + } + theme_dir = parent; + } + + if let Some(dir_name) = + theme_dir.iter().last().and_then(std::ffi::OsStr::to_str) + { + valid_dirs + .iter() + .any(|valid| dir_name == valid) + .then(|| icon::from_path(path)) + } else { + None + } + }) }) // Fallback icon handle - .unwrap_or_else(|| icon::from_name(icon_name).handle()) + .unwrap_or_else(|| icon::from_name(icon_name).size(ICON_THUMB_SIZE).handle()) } /// Button with a preview of the icon theme.