From 5f089ef9a3e88a73323a80980cf717ada7fff9a5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 12 Dec 2023 15:05:17 +0100 Subject: [PATCH] feat(wallpaper): color dialog integration --- app/src/app.rs | 11 ++ app/src/pages/desktop/wallpaper/config.rs | 20 +- app/src/pages/desktop/wallpaper/mod.rs | 219 ++++++++++++++++++---- pages/desktop/src/wallpaper.rs | 5 +- 4 files changed, 204 insertions(+), 51 deletions(-) diff --git a/app/src/app.rs b/app/src/app.rs index 04e5295..4263e9d 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -503,16 +503,19 @@ impl cosmic::Application for SettingsApp { { return page.dnd_icon(); } + if let Some(Some(page)) = (id == *applets_inner::ADD_PANEL_APPLET_DIALOGUE_ID) .then(|| self.pages.page::()) { return page.add_applet_view(crate::pages::Message::PanelApplet); } + if let Some(Some(page)) = (id == *appearance::COLOR_PICKER_DIALOG_ID) .then(|| self.pages.page::()) { return page.color_picker_view(); } + if let Some(Some(page)) = (id == *ADD_DOCK_APPLET_DIALOGUE_ID).then(|| self.pages.page::()) { @@ -520,17 +523,25 @@ impl cosmic::Application for SettingsApp { crate::pages::Message::DockApplet(dock::applets::Message(msg)) }); } + if let Some(Some(page)) = (id == *keyboard::ADD_INPUT_SOURCE_DIALOGUE_ID) .then(|| self.pages.page::()) { return page.add_input_source_view(); } + if let Some(Some(page)) = (id == *keyboard::SPECIAL_CHARACTER_DIALOGUE_ID) .then(|| self.pages.page::()) { return page.special_character_key_view(); } + if let Some(page) = self.pages.page::() { + if id == page.color_dialog { + return page.show_color_dialog(); + } + } + panic!("unknown window ID: {id:?}"); } diff --git a/app/src/pages/desktop/wallpaper/config.rs b/app/src/pages/desktop/wallpaper/config.rs index e6ea9a0..3013340 100644 --- a/app/src/pages/desktop/wallpaper/config.rs +++ b/app/src/pages/desktop/wallpaper/config.rs @@ -16,7 +16,7 @@ const RECENT_FOLDERS: &str = "recent-folders"; #[derive(Debug, Default)] pub struct Config { context: Option, - current_folder: Option, + pub(super) current_folder: Option, custom_colors: Vec, custom_images: Vec, recent_folders: VecDeque, @@ -34,8 +34,8 @@ impl Config { } }; - if let Ok(path) = dbg!(context.get::(CURRENT_FOLDER)) { - config.current_folder = Some(path); + if let Ok(path) = context.get::>(CURRENT_FOLDER) { + config.current_folder = path; } if let Ok(colors) = context.get::>(CUSTOM_COLORS) { @@ -59,7 +59,12 @@ impl Config { pub fn current_folder(&self) -> &Path { self.current_folder .as_deref() - .unwrap_or(Path::new("/usr/share/backgrounds/")) + .unwrap_or(Self::default_folder()) + } + + #[must_use] + pub fn default_folder() -> &'static Path { + Path::new("/usr/share/backgrounds/") } /// Sets the current background folder @@ -67,9 +72,12 @@ impl Config { /// # Errors /// /// Returns an error if the on-disk configuration could not be updated. - pub fn set_current_folder(&mut self, folder: PathBuf) -> Result<(), cosmic_config::Error> { + pub fn set_current_folder( + &mut self, + folder: Option, + ) -> Result<(), cosmic_config::Error> { let result = self.update(CURRENT_FOLDER, &folder); - self.current_folder = Some(folder); + self.current_folder = folder; result } diff --git a/app/src/pages/desktop/wallpaper/mod.rs b/app/src/pages/desktop/wallpaper/mod.rs index c0635fe..e1f748a 100644 --- a/app/src/pages/desktop/wallpaper/mod.rs +++ b/app/src/pages/desktop/wallpaper/mod.rs @@ -13,8 +13,12 @@ use std::{ }; use apply::Apply; -use cosmic::{iced::Length, Element}; -use cosmic::{iced_core::alignment, iced_runtime::core::image::Handle as ImageHandle}; +use cosmic::iced::{wayland::actions::window::SctkWindowSettings, window, Color, Length}; +use cosmic::iced_sctk::commands::window::{close_window, get_window}; +use cosmic::{ + iced_core::{alignment, layout}, + iced_runtime::core::image::Handle as ImageHandle, +}; use cosmic::{ widget::{ button, dropdown, list_column, row, @@ -23,6 +27,10 @@ use cosmic::{ }, Command, }; +use cosmic::{ + widget::{color_picker::ColorPickerUpdate, ColorPickerModel}, + Element, +}; use cosmic_settings_desktop::wallpaper::{self, Entry, ScalingMode}; use cosmic_settings_page::Section; use cosmic_settings_page::{self as page, section}; @@ -50,11 +58,12 @@ pub type Image = ImageBuffer, Vec>; #[derive(Clone, Debug)] pub enum Message { ChangeFolder(Context), - ColorAdd(wallpaper::Color), ColorAddDialog, + ColorDialogUpdate(ColorPickerUpdate), ColorRemove(wallpaper::Color), ChangeCategory(Category), ColorSelect(wallpaper::Color), + DragColorDialog, Fit(usize), ImageAdd(Option>), ImageAddDialog, @@ -79,6 +88,8 @@ pub struct Page { pub background_service_config: wallpaper::Config, pub cached_display_handle: Option, pub categories: dropdown::multi::Model, + pub color_dialog: window::Id, + pub color_model: ColorPickerModel, pub config: Config, pub fit_options: Vec, pub outputs: SingleSelectModel, @@ -173,6 +184,8 @@ impl Default for Page { categories }, background_service_config: wallpaper::Config::default(), + color_dialog: window::Id::unique(), + color_model: ColorPickerModel::new(fl!("hex"), fl!("rgb"), None, Some(Color::WHITE)), config: Config::new(), fit_options: vec![fl!("fit-to-screen"), fl!("stretch"), fl!("zoom")], outputs: SingleSelectModel::default(), @@ -204,11 +217,6 @@ impl Default for Page { selection: Context::default(), }; - // Sync custom colors from config. - for color in page.config.custom_colors() { - page.selection.add_custom_color(color.clone()); - } - page.assign_recent_folders(); page @@ -355,19 +363,13 @@ impl Page { wallpaper::set(&mut self.background_service_config, entry); } - /// Updates configuration for background image. - fn config_background_entry(&self, output: String, path: PathBuf) -> Option { - let scaling_mode = match self.selected_fit { - FIT => ScalingMode::Fit([0.0, 0.0, 0.0]), - STRETCH => ScalingMode::Stretch, - ZOOM => ScalingMode::Zoom, - _ => return None, - }; - - Entry::new(output, wallpaper::Source::Path(path)) - .scaling_mode(scaling_mode) - .rotation_frequency(self.rotation_frequency) - .apply(Some) + /// Locate the ID of a background that's already stored in memory + fn background_id_from_path(&self, path: &Path) -> Option { + self.selection + .paths + .iter() + .find(|(_id, background)| *background == path) + .map(|(id, _)| id) } /// Updates configuration from the background service. @@ -405,7 +407,7 @@ impl Page { self.select_background_entry(&entry); if let Some(current) = entry_directory(self.config.current_folder(), &entry) { - if let Err(why) = self.config.set_current_folder(current) { + if let Err(why) = self.config.set_current_folder(Some(current)) { tracing::error!(?why, "cannot set current folder"); } } @@ -423,7 +425,7 @@ impl Page { if let Some(current) = entry_directory(self.config.current_folder(), background) { - if let Err(why) = self.config.set_current_folder(current) { + if let Err(why) = self.config.set_current_folder(Some(current)) { tracing::error!(?why, "cannot set current folder"); } } @@ -445,7 +447,18 @@ impl Page { match category { Category::Backgrounds => { - self.select_first_background(); + if self.config.current_folder.is_some() { + let _ = self.config.set_current_folder(None); + command = cosmic::command::future(async move { + crate::app::Message::PageMessage(crate::pages::Message::DesktopWallpaper( + Message::ChangeFolder( + change_folder(Config::default_folder().to_owned()).await, + ), + )) + }); + } else { + self.select_first_background(); + } } Category::Colors => { @@ -455,7 +468,7 @@ impl Page { Category::RecentFolder(id) => { if let Some(path) = self.config.recent_folders().get(id).cloned() { - if let Err(why) = self.config.set_current_folder(path.clone()) { + if let Err(why) = self.config.set_current_folder(Some(path.clone())) { tracing::error!(?path, ?why, "failed to set current folder"); } @@ -495,6 +508,21 @@ impl Page { }; } + /// Updates configuration for background image. + fn config_background_entry(&self, output: String, path: PathBuf) -> Option { + let scaling_mode = match self.selected_fit { + FIT => ScalingMode::Fit([0.0, 0.0, 0.0]), + STRETCH => ScalingMode::Stretch, + ZOOM => ScalingMode::Zoom, + _ => return None, + }; + + Entry::new(output, wallpaper::Source::Path(path)) + .scaling_mode(scaling_mode) + .rotation_frequency(self.rotation_frequency) + .apply(Some) + } + #[must_use] pub fn display_image_view(&self) -> cosmic::Element { match self.cached_display_handle { @@ -509,6 +537,44 @@ impl Page { #[allow(clippy::too_many_lines)] pub fn update(&mut self, message: Message) -> Command { match message { + Message::DragColorDialog => { + return cosmic::iced_sctk::commands::window::start_drag_window(self.color_dialog) + } + + Message::ColorDialogUpdate(update) => { + let cmd = match update { + ColorPickerUpdate::AppliedColor + | ColorPickerUpdate::Cancel + | ColorPickerUpdate::Reset => { + if let Some(color) = self.color_model.get_applied_color() { + let color = wallpaper::Color::Single([color.r, color.g, color.b]); + + if let Err(why) = self.config.add_custom_color(color.clone()) { + tracing::error!(?why, "could not set custom color"); + } + + self.selection.add_custom_color(color); + } + + close_window(self.color_dialog) + } + + ColorPickerUpdate::ActionFinished => { + let _res = self + .color_model + .update::(ColorPickerUpdate::AppliedColor); + Command::none() + } + + _ => Command::none(), + }; + + return Command::batch(vec![ + cmd, + self.color_model.update::(update), + ]); + } + Message::ChangeFolder(mut context) => { // Reassign custom colors and images to the new context. std::mem::swap(&mut context, &mut self.selection); @@ -531,14 +597,9 @@ impl Page { self.select_first_background(); } - Message::ColorAdd(color) => { - if let Err(why) = self.config.add_custom_color(color) { - tracing::error!(?why, "could not set custom color"); - } - } Message::ColorAddDialog => { - unimplemented!(); + return get_window(color_picker_window_settings(self.color_dialog)); } Message::ColorRemove(color) => { @@ -625,7 +686,22 @@ impl Page { self.background_service_config_update(update.0, update.1, update.2); self.config_apply(); - // Load custom content + // Sync custom colors from config. + for color in self.config.custom_colors() { + self.selection.add_custom_color(color.clone()); + } + + // Set the default selection if an image was selected. + if let Choice::Background(_) | Choice::Slideshow = self.selection.active { + let folder = self.config.current_folder(); + for (id, recent) in self.config.recent_folders().iter().enumerate() { + if recent == folder { + self.categories.selected = Some(Category::RecentFolder(id)); + } + } + } + + // Load preview images for each custom image stored in the on-disk config. return cosmic::command::batch(self.config.custom_images().iter().cloned().map( |path| { cosmic::command::future(async move { @@ -717,13 +793,13 @@ impl Page { self.cache_display_image(); } - /// Locate the ID of a background that's already stored in memory - fn background_id_from_path(&self, path: &Path) -> Option { - self.selection - .paths - .iter() - .find(|(_id, background)| *background == path) - .map(|(id, _)| id) + pub fn show_color_dialog(&self) -> Element { + color_picker_view( + &self.color_model, + Message::DragColorDialog, + Message::ColorDialogUpdate, + ) + .map(|m| crate::app::Message::PageMessage(crate::pages::Message::DesktopWallpaper(m))) } } @@ -754,7 +830,7 @@ pub struct Context { impl Context { fn add_custom_color(&mut self, color: wallpaper::Color) { if !self.custom_colors.contains(&color) { - self.add_custom_color(color); + self.custom_colors.push(color); } } @@ -923,7 +999,11 @@ pub fn settings() -> Section { (fl!("add-image"), Message::ImageAddDialog) }; - button::link(text).on_press(message) + button::link(text) + .trailing_icon(true) + .on_press(message) + .apply(cosmic::widget::container) + .align_y(alignment::Vertical::Bottom) }; children.push( @@ -985,3 +1065,60 @@ fn entry_directory(current_folder: &Path, entry: &wallpaper::Entry) -> Option PathBuf::from(current_folder), }) } + +fn color_picker_window_settings(window_id: window::Id) -> SctkWindowSettings { + SctkWindowSettings { + window_id, + app_id: Some("com.system76.CosmicSettings".to_string()), + title: Some(fl!("color-picker")), + parent: Some(window::Id::MAIN), + autosize: false, + size_limits: layout::Limits::NONE + .min_width(300.0) + .max_width(800.0) + .min_height(520.0) + .max_height(520.0), + size: (300, 520), + resizable: Some(8.0), + client_decorations: true, + transparent: true, + ..Default::default() + } +} + +// TODO: Reuse with the appearance page +pub fn color_picker_view( + model: &ColorPickerModel, + on_drag: Message, + on_message: fn(ColorPickerUpdate) -> Message, +) -> Element { + let header = cosmic::widget::header_bar() + .title(fl!("color-picker")) + .on_close(on_message(ColorPickerUpdate::AppliedColor)) + .on_drag(on_drag); + + let content = cosmic::widget::container( + model + .builder(on_message) + .width(Length::Fixed(254.0)) + .height(Length::Fixed(174.0)) + .reset_label(fl!("reset-to-default")) + .build( + fl!("recent-colors"), + fl!("copy-to-clipboard"), + fl!("copied-to-clipboard"), + ), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .style(cosmic::theme::style::Container::Background); + + cosmic::widget::column::with_capacity(2) + .push(header) + .push(content) + .width(Length::Fill) + .height(Length::Fill) + .align_items(cosmic::iced_core::Alignment::Center) + .apply(Element::from) +} diff --git a/pages/desktop/src/wallpaper.rs b/pages/desktop/src/wallpaper.rs index 505abfe..4657d12 100644 --- a/pages/desktop/src/wallpaper.rs +++ b/pages/desktop/src/wallpaper.rs @@ -4,7 +4,6 @@ use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage}; use std::{ borrow::Cow, collections::{hash_map::DefaultHasher, BTreeSet, HashMap}, - fs::DirEntry, hash::{Hash, Hasher}, io::Read, path::{Path, PathBuf}, @@ -97,8 +96,6 @@ pub fn cache_dir() -> Option { /// Loads wallpapers in parallel by spawning tasks with a rayon thread pool. #[must_use] pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaImage)> { - let cache_dir = cache_dir(); - let (tx, rx) = mpsc::channel(1); tokio::task::spawn(async move { @@ -187,7 +184,7 @@ pub async fn load_image_with_thumbnail( let _res = tx.send(Some((path, display_thumbnail, selection_thumbnail))); }); } else { - tx.send(None); + let _res = tx.send(None); } rx.await.unwrap_or(None)