feat(wallpaper): custom background images and folders

This commit is contained in:
Michael Aaron Murphy 2023-12-07 14:08:50 +01:00 committed by Michael Murphy
parent f3e929e769
commit daa730682a
10 changed files with 928 additions and 250 deletions

5
Cargo.lock generated
View file

@ -1081,6 +1081,7 @@ dependencies = [
"once_cell", "once_cell",
"regex", "regex",
"slotmap", "slotmap",
"url",
] ]
[[package]] [[package]]
@ -5764,9 +5765,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.27" version = "0.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb877ca3232bec99a6472ed63f7241de2a250165260908b2d24c09d867907a85" checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -7,7 +7,7 @@ git = "https://github.com/pop-os/libcosmic"
[workspace.dependencies.libcosmic] [workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic" git = "https://github.com/pop-os/libcosmic"
features = ["wayland", "tokio", "single-instance"] features = ["single-instance", "tokio", "wayland", "xdg-portal"]
[workspace.dependencies.cosmic-config] [workspace.dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic" git = "https://github.com/pop-os/libcosmic"

View file

@ -18,6 +18,7 @@ use crate::subscription::desktop_files;
use crate::widget::{page_title, search_header}; use crate::widget::{page_title, search_header};
use crate::PageCommands; use crate::PageCommands;
use cosmic::app::DbusActivationMessage; use cosmic::app::DbusActivationMessage;
use cosmic::dialog::file_chooser;
use cosmic::iced::Subscription; use cosmic::iced::Subscription;
use cosmic::{ use cosmic::{
app::{Command, Core}, app::{Command, Core},
@ -45,6 +46,7 @@ pub struct SettingsApp {
active_page: page::Entity, active_page: page::Entity,
config: Config, config: Config,
core: Core, core: Core,
file_chooser: Option<(file_chooser::Sender, page::Entity)>,
nav_model: nav_bar::Model, nav_model: nav_bar::Model,
pages: page::Binder<crate::pages::Message>, pages: page::Binder<crate::pages::Message>,
search: search::Model, search: search::Model,
@ -65,10 +67,11 @@ impl SettingsApp {
} }
} }
#[allow(dead_code)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
DesktopInfo, DesktopInfo,
FileChooser(FileChooser),
Error(String),
Page(page::Entity), Page(page::Entity),
PageMessage(crate::pages::Message), PageMessage(crate::pages::Message),
PanelConfig(CosmicPanelConfig), PanelConfig(CosmicPanelConfig),
@ -79,6 +82,21 @@ pub enum Message {
SetTheme(cosmic::theme::Theme), SetTheme(cosmic::theme::Theme),
} }
#[derive(Clone, Debug)]
pub enum FileChooser {
Closed,
Init(file_chooser::Sender),
Open {
title: String,
accept_label: String,
include_directories: bool,
modal: bool,
multiple_files: bool,
},
Opened,
Selected(Vec<url::Url>),
}
impl cosmic::Application for SettingsApp { impl cosmic::Application for SettingsApp {
type Executor = cosmic::executor::single::Executor; type Executor = cosmic::executor::single::Executor;
type Flags = crate::Args; type Flags = crate::Args;
@ -99,6 +117,7 @@ impl cosmic::Application for SettingsApp {
active_page: page::Entity::default(), active_page: page::Entity::default(),
config: Config::new(), config: Config::new(),
core, core,
file_chooser: None,
nav_model: nav_bar::Model::default(), nav_model: nav_bar::Model::default(),
pages: page::Binder::default(), pages: page::Binder::default(),
search: search::Model::default(), search: search::Model::default(),
@ -212,6 +231,32 @@ impl cosmic::Application for SettingsApp {
} }
}, },
), ),
file_chooser::subscription(|response| match response {
file_chooser::Message::Closed => Message::FileChooser(FileChooser::Closed),
file_chooser::Message::Opened => Message::FileChooser(FileChooser::Opened),
file_chooser::Message::Selected(files) => {
Message::FileChooser(FileChooser::Selected(files.uris().to_owned()))
}
file_chooser::Message::Err(why) => {
let mut source: &dyn std::error::Error = &why;
let mut string =
format!("open dialog subscription errored\n cause: {source}");
while let Some(new_source) = source.source() {
string.push_str(&format!("\n cause: {new_source}"));
source = new_source;
}
Message::Error(string)
}
file_chooser::Message::Init(sender) => {
Message::FileChooser(FileChooser::Init(sender))
}
}),
]) ])
} }
@ -238,29 +283,39 @@ impl cosmic::Application for SettingsApp {
crate::pages::Message::About(message) => { crate::pages::Message::About(message) => {
page::update!(self.pages, message, system::about::Page); page::update!(self.pages, message, system::about::Page);
} }
crate::pages::Message::DateAndTime(message) => { crate::pages::Message::DateAndTime(message) => {
page::update!(self.pages, message, time::date::Page); page::update!(self.pages, message, time::date::Page);
} }
crate::pages::Message::Desktop(message) => { crate::pages::Message::Desktop(message) => {
page::update!(self.pages, message, desktop::Page); page::update!(self.pages, message, desktop::Page);
} }
crate::pages::Message::DesktopWallpaper(message) => { crate::pages::Message::DesktopWallpaper(message) => {
page::update!(self.pages, message, desktop::wallpaper::Page); if let Some(page) = self.pages.page_mut::<desktop::wallpaper::Page>() {
return page.update(message).map(cosmic::app::Message::App);
}
} }
crate::pages::Message::Input(message) => { crate::pages::Message::Input(message) => {
if let Some(page) = self.pages.page_mut::<input::Page>() { if let Some(page) = self.pages.page_mut::<input::Page>() {
return page.update(message).map(cosmic::app::Message::App); return page.update(message).map(cosmic::app::Message::App);
} }
} }
crate::pages::Message::External { .. } => { crate::pages::Message::External { .. } => {
todo!("external plugins not supported yet"); todo!("external plugins not supported yet");
} }
crate::pages::Message::Page(page) => { crate::pages::Message::Page(page) => {
return self.activate_page(page); return self.activate_page(page);
} }
crate::pages::Message::Panel(message) => { crate::pages::Message::Panel(message) => {
page::update!(self.pages, message, panel::Page); page::update!(self.pages, message, panel::Page);
} }
crate::pages::Message::PanelApplet(message) => { crate::pages::Message::PanelApplet(message) => {
if let Some(page) = self.pages.page_mut::<applets_inner::Page>() { if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
return page return page
@ -268,19 +323,65 @@ impl cosmic::Application for SettingsApp {
.map(cosmic::app::Message::App); .map(cosmic::app::Message::App);
} }
} }
crate::pages::Message::Dock(message) => { crate::pages::Message::Dock(message) => {
page::update!(self.pages, message, dock::Page); page::update!(self.pages, message, dock::Page);
} }
crate::pages::Message::DockApplet(message) => { crate::pages::Message::DockApplet(message) => {
if let Some(page) = self.pages.page_mut::<dock::applets::Page>() { if let Some(page) = self.pages.page_mut::<dock::applets::Page>() {
return page.update(message).map(cosmic::app::Message::App); return page.update(message).map(cosmic::app::Message::App);
} }
} }
crate::pages::Message::Appearance(message) => { crate::pages::Message::Appearance(message) => {
if let Some(page) = self.pages.page_mut::<appearance::Page>() { if let Some(page) = self.pages.page_mut::<appearance::Page>() {
return page.update(message).map(cosmic::app::Message::App); return page.update(message).map(cosmic::app::Message::App);
} }
// TODO }
},
Message::FileChooser(message) => match message {
FileChooser::Selected(files) => {
return self.pages.page[self.active_page]
.file_chooser(files)
.map(crate::app::Message::PageMessage)
.map(cosmic::app::Message::App)
}
FileChooser::Closed => {}
FileChooser::Opened => {
if let Some((sender, _)) = self.file_chooser.as_mut() {
return sender.response().map(|_| cosmic::app::Message::None);
}
}
FileChooser::Open {
title,
accept_label,
include_directories,
modal,
multiple_files,
} => {
if let Some((sender, entity)) = self.file_chooser.as_mut() {
if let Some(dialog) = file_chooser::open_file() {
*entity = self.active_page;
return dialog
.title(title)
.accept_label(accept_label)
.include_directories(include_directories)
.modal(modal)
.multiple_files(multiple_files)
.create(sender)
.map(|_| cosmic::app::message::none());
}
}
}
FileChooser::Init(sender) => {
self.file_chooser = Some((sender, page::Entity::default()));
} }
}, },
@ -311,7 +412,7 @@ impl cosmic::Application for SettingsApp {
self.pages, self.pages,
dock::applets::Message(applets_inner::Message::PanelConfig(config,)), dock::applets::Message(applets_inner::Message::PanelConfig(config,)),
dock::applets::Page dock::applets::Page
) );
} }
Message::DesktopInfo => { Message::DesktopInfo => {
@ -335,15 +436,23 @@ impl cosmic::Application for SettingsApp {
.map(cosmic::app::Message::App); .map(cosmic::app::Message::App);
} }
} }
Message::PanelConfig(_) | Message::Search(_) => {} Message::PanelConfig(_) | Message::Search(_) => {}
Message::SetTheme(t) => return cosmic::app::command::set_theme(t), Message::SetTheme(t) => return cosmic::app::command::set_theme(t),
Message::OpenContextDrawer(title) => { Message::OpenContextDrawer(title) => {
self.core.window.show_context = true; self.core.window.show_context = true;
self.set_context_title(title.to_string()); self.set_context_title(title.to_string());
} }
Message::CloseContextDrawer => { Message::CloseContextDrawer => {
self.core.window.show_context = false; self.core.window.show_context = false;
} }
Message::Error(error) => {
tracing::error!(error, "error occurred");
}
} }
Command::none() Command::none()

View file

@ -0,0 +1,194 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic_settings_desktop::wallpaper;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
const NAME: &str = "com.system76.CosmicSettings.Wallpaper";
const VERSION: u64 = 1;
const CURRENT_FOLDER: &str = "current-folder";
const CUSTOM_COLORS: &str = "custom-colors";
const CUSTOM_IMAGES: &str = "custom-images";
const RECENT_FOLDERS: &str = "recent-folders";
#[derive(Debug, Default)]
pub struct Config {
context: Option<cosmic_config::Config>,
current_folder: Option<PathBuf>,
custom_colors: Vec<wallpaper::Color>,
custom_images: Vec<PathBuf>,
recent_folders: VecDeque<PathBuf>,
}
impl Config {
pub fn new() -> Self {
let mut config = Self::default();
let context = match cosmic_config::Config::new(NAME, VERSION) {
Ok(context) => context,
Err(why) => {
tracing::warn!(?why, "failed to get config");
return Self::default();
}
};
if let Ok(path) = dbg!(context.get::<PathBuf>(CURRENT_FOLDER)) {
config.current_folder = Some(path);
}
if let Ok(colors) = context.get::<Vec<wallpaper::Color>>(CUSTOM_COLORS) {
config.custom_colors = colors;
}
if let Ok(images) = context.get::<Vec<PathBuf>>(CUSTOM_IMAGES) {
config.custom_images = images;
}
if let Ok(folders) = dbg!(context.get::<VecDeque<PathBuf>>(RECENT_FOLDERS)) {
config.recent_folders = folders;
}
config.context = Some(context);
config
}
#[must_use]
pub fn current_folder(&self) -> &Path {
self.current_folder
.as_deref()
.unwrap_or(Path::new("/usr/share/backgrounds/"))
}
/// Sets the current background folder
///
/// # 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> {
let result = self.update(CURRENT_FOLDER, &folder);
self.current_folder = Some(folder);
result
}
#[must_use]
pub fn custom_colors(&self) -> &[wallpaper::Color] {
&self.custom_colors
}
/// Adds a custom color
///
/// # Errors
///
/// Returns an error if the on-disk configuration could not be updated.
pub fn add_custom_color(
&mut self,
color: wallpaper::Color,
) -> Result<(), cosmic_config::Error> {
if !self.custom_colors.contains(&color) {
self.custom_colors.push(color);
return self.update_custom_colors();
}
Ok(())
}
/// Removes custom background colors.
///
/// # Errors
///
/// Returns an error if the on-disk configuration could not be updated.
pub fn remove_custom_color(
&mut self,
color: &wallpaper::Color,
) -> Result<(), cosmic_config::Error> {
if let Some(position) = self.custom_colors.iter().position(|c| c == color) {
self.custom_colors.remove(position);
return self.update_custom_colors();
}
Ok(())
}
#[must_use]
pub fn custom_images(&self) -> &[PathBuf] {
&self.custom_images
}
/// Adds a custom background image
///
/// # Errors
///
/// Returns an error if the on-disk configuration could not be updated.
pub fn add_custom_image(&mut self, image: PathBuf) -> Result<(), cosmic_config::Error> {
if !self.custom_images.contains(&image) {
self.custom_images.push(image);
return self.update_custom_images();
}
Ok(())
}
/// Removes custom background images.
///
/// # Errors
///
/// Returns an error if the on-disk configuration could not be updated.
pub fn remove_custom_image(&mut self, image: &Path) -> Result<(), cosmic_config::Error> {
if let Some(position) = self.custom_images.iter().position(|p| p == image) {
self.custom_images.remove(position);
return self.update_custom_images();
}
Ok(())
}
#[must_use]
pub fn recent_folders(&self) -> &VecDeque<PathBuf> {
&self.recent_folders
}
/// Adds a folder to the recent folders list
///
/// # Errors
///
/// Returns an error if the on-disk configuration could not be updated.
pub fn add_recent_folder(&mut self, folder: PathBuf) -> Result<(), cosmic_config::Error> {
while self.recent_folders.len() > 4 {
self.recent_folders.pop_front();
}
if !self.recent_folders.contains(&folder) {
self.recent_folders.push_back(folder);
return self.update_recent_folders();
}
Ok(())
}
fn update<V: serde::Serialize>(
&self,
key: &str,
value: &V,
) -> Result<(), cosmic_config::Error> {
if let Some(context) = self.context.as_ref() {
context.set(key, value)?;
}
Ok(())
}
fn update_custom_colors(&self) -> Result<(), cosmic_config::Error> {
self.update(CUSTOM_COLORS, &self.custom_colors)
}
fn update_custom_images(&self) -> Result<(), cosmic_config::Error> {
self.update(CUSTOM_IMAGES, &self.custom_images)
}
fn update_recent_folders(&self) -> Result<(), cosmic_config::Error> {
self.update(RECENT_FOLDERS, &self.recent_folders)
}
}

View file

@ -1,33 +1,36 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
mod config;
pub mod widgets; pub mod widgets;
pub use config::Config;
use std::{ use std::{
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use apply::Apply; use apply::Apply;
use cosmic::widget::{
dropdown, list_column,
segmented_button::{self, SingleSelectModel},
segmented_selection, settings, text, toggler,
};
use cosmic::{iced::Length, Element}; use cosmic::{iced::Length, Element};
use cosmic::{iced_core::alignment, iced_runtime::core::image::Handle as ImageHandle}; use cosmic::{iced_core::alignment, iced_runtime::core::image::Handle as ImageHandle};
use cosmic::{
widget::{
button, dropdown, list_column, row,
segmented_button::{self, SingleSelectModel},
segmented_selection, settings, text, toggler,
},
Command,
};
use cosmic_settings_desktop::wallpaper::{self, Entry, ScalingMode}; use cosmic_settings_desktop::wallpaper::{self, Entry, ScalingMode};
use cosmic_settings_page::Section; use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section}; use cosmic_settings_page::{self as page, section};
use image::imageops::FilterType::Lanczos3; use image::imageops::FilterType::Lanczos3;
use image::{ImageBuffer, Rgba};
use slotmap::{DefaultKey, SecondaryMap, SlotMap}; use slotmap::{DefaultKey, SecondaryMap, SlotMap};
use static_init::dynamic; use static_init::dynamic;
const SYSTEM_WALLPAPER_DIR: &str = "/usr/share/backgrounds/pop/";
const CATEGORY_SYSTEM_WALLPAPERS: usize = 0;
const CATEGORY_COLOR: usize = 1;
const FIT: usize = 0; const FIT: usize = 0;
const STRETCH: usize = 1; const STRETCH: usize = 1;
const ZOOM: usize = 2; const ZOOM: usize = 2;
@ -42,33 +45,44 @@ const MINUTES_30: usize = 3;
const HOUR_1: usize = 4; const HOUR_1: usize = 4;
const HOUR_2: usize = 5; const HOUR_2: usize = 5;
pub type Image = ImageBuffer<Rgba<u8>, Vec<u8>>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
ChangeCategory(usize), ChangeFolder(Context),
ColorAdd(wallpaper::Color),
ColorAddDialog,
ColorRemove(wallpaper::Color),
ChangeCategory(Category),
ColorSelect(wallpaper::Color), ColorSelect(wallpaper::Color),
Fit(usize), Fit(usize),
ImageAdd(Option<Arc<(PathBuf, Image, Image)>>),
ImageAddDialog,
ImageRemove(DefaultKey),
Output(segmented_button::Entity), Output(segmented_button::Entity),
RotationFrequency(usize), RotationFrequency(usize),
SameBackground(bool), SameBackground(bool),
Select(DefaultKey), Select(DefaultKey),
Slideshow(bool), Slideshow(bool),
Update(Box<(wallpaper::Config, HashMap<String, String>, Context)>), Init(Box<(wallpaper::Config, HashMap<String, String>, Context)>),
} }
#[derive(Clone, Debug, PartialEq)]
pub enum Category { pub enum Category {
SystemBackgrounds, Backgrounds,
Colors, Colors,
RecentFolder(usize),
} }
pub struct Page { pub struct Page {
pub active_output: Option<String>, pub active_output: Option<String>,
pub active_category: usize, pub background_service_config: wallpaper::Config,
pub cached_display_handle: Option<ImageHandle>, pub cached_display_handle: Option<ImageHandle>,
pub categories: Vec<String>, pub categories: dropdown::multi::Model<String, Category>,
pub config: wallpaper::Config, pub config: Config,
pub current_directory: PathBuf,
pub fit_options: Vec<String>, pub fit_options: Vec<String>,
pub outputs: SingleSelectModel, pub outputs: SingleSelectModel,
pub recent_folders: Vec<(PathBuf, String)>,
pub rotation_frequency: u64, pub rotation_frequency: u64,
pub rotation_options: Vec<String>, pub rotation_options: Vec<String>,
pub selected_fit: usize, pub selected_fit: usize,
@ -76,17 +90,93 @@ pub struct Page {
pub selection: Context, pub selection: Context,
} }
impl page::Page<crate::pages::Message> for Page {
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(settings())])
}
fn info(&self) -> page::Info {
page::Info::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
.title(fl!("wallpaper"))
.description(fl!("wallpaper", "desc"))
}
fn file_chooser(&mut self, selections: Vec<url::Url>) -> Command<crate::pages::Message> {
if let Some(selection) = selections.first() {
let path = PathBuf::from(selection.path());
if path.is_dir() {
self.add_recent_folder(path);
} else {
if let Some(parent) = path.parent() {
self.add_recent_folder(parent.to_owned());
}
return cosmic::command::future(async move {
let result = wallpaper::load_image_with_thumbnail(&mut Vec::new(), path).await;
crate::pages::Message::DesktopWallpaper(Message::ImageAdd(result.map(Arc::new)))
});
}
}
Command::none()
}
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
let current_folder = self.config.current_folder().to_owned();
Some(Box::pin(async move {
let (background_service_config, outputs) = wallpaper::config();
let update = change_folder(current_folder).await;
crate::pages::Message::DesktopWallpaper(Message::Init(Box::new((
background_service_config,
outputs,
update,
))))
}))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl Default for Page { impl Default for Page {
fn default() -> Self { fn default() -> Self {
Page { let mut page = Page {
active_output: None, active_output: None,
active_category: CATEGORY_SYSTEM_WALLPAPERS,
cached_display_handle: None, cached_display_handle: None,
categories: vec![fl!("system-backgrounds"), fl!("colors")], categories: {
config: wallpaper::Config::default(), let mut categories = dropdown::multi::model();
current_directory: PathBuf::from(SYSTEM_WALLPAPER_DIR),
categories.insert(dropdown::multi::list(
None,
vec![(fl!("system-backgrounds"), Category::Backgrounds)],
));
categories.insert(dropdown::multi::list(
None,
vec![(fl!("colors"), Category::Colors)],
));
categories.insert(dropdown::multi::list(
Some(fl!("recent-folders")),
Vec::with_capacity(5),
));
categories.selected = Some(Category::Backgrounds);
categories
},
background_service_config: wallpaper::Config::default(),
config: Config::new(),
fit_options: vec![fl!("fit-to-screen"), fl!("stretch"), fl!("zoom")], fit_options: vec![fl!("fit-to-screen"), fl!("stretch"), fl!("zoom")],
outputs: SingleSelectModel::default(), outputs: SingleSelectModel::default(),
recent_folders: Vec::new(),
rotation_frequency: 300, rotation_frequency: 300,
rotation_options: vec![ rotation_options: vec![
// FIX: fluent is inserting extra unicode characters in formatting // FIX: fluent is inserting extra unicode characters in formatting
@ -112,32 +202,42 @@ impl Default for Page {
selected_fit: 0, selected_fit: 0,
selected_rotation: 0, selected_rotation: 0,
selection: Context::default(), 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
} }
} }
#[derive(Clone, Debug, PartialEq)]
enum Choice {
Background(DefaultKey),
Color(wallpaper::Color),
Slideshow,
}
impl Default for Choice {
fn default() -> Self {
Self::Background(DefaultKey::default())
}
}
#[derive(Clone, Debug, Default)]
pub struct Context {
active: Choice,
paths: SlotMap<DefaultKey, PathBuf>,
display_images: SecondaryMap<DefaultKey, image::RgbaImage>,
selection_handles: SecondaryMap<DefaultKey, ImageHandle>,
}
impl Page { impl Page {
fn add_recent_folder(&mut self, folder: PathBuf) {
if let Err(why) = self.config.add_recent_folder(folder) {
tracing::error!(?why, "cannot add recent folder to config");
}
self.assign_recent_folders();
}
fn assign_recent_folders(&mut self) {
let recent_list = &mut self.categories.lists[2];
recent_list.options.clear();
for (id, folder) in self.config.recent_folders().iter().enumerate() {
if let Some(name) = folder.file_name() {
let name = name.to_string_lossy();
recent_list
.options
.push((name.to_string(), Category::RecentFolder(id)));
}
}
}
fn cache_display_image(&mut self) { fn cache_display_image(&mut self) {
let choice = match self.selection.active { let choice = match self.selection.active {
Choice::Background(id) => self.selection.display_images.get(id), Choice::Background(id) => self.selection.display_images.get(id),
@ -204,7 +304,7 @@ impl Page {
} }
fn config_output(&self) -> Option<String> { fn config_output(&self) -> Option<String> {
if self.config.same_on_all { if self.background_service_config.same_on_all {
Some(String::from("all")) Some(String::from("all"))
} else { } else {
self.outputs.active_data::<String>().cloned() self.outputs.active_data::<String>().cloned()
@ -217,13 +317,16 @@ impl Page {
return; return;
}; };
if self.config.same_on_all { if self.background_service_config.same_on_all {
self.config.outputs.clear(); self.background_service_config.outputs.clear();
} }
let entry = match self.selection.active { let entry = match self.selection.active {
Choice::Slideshow => { Choice::Slideshow => {
match self.config_background_entry(output.clone(), self.current_directory.clone()) { match self.config_background_entry(
output.clone(),
self.config.current_folder().to_path_buf(),
) {
Some(entry) => entry, Some(entry) => entry,
None => return, None => return,
} }
@ -245,13 +348,14 @@ impl Page {
}; };
if output != "all" { if output != "all" {
self.config.backgrounds.clear(); self.background_service_config.backgrounds.clear();
self.config.outputs.clear(); self.background_service_config.outputs.clear();
} }
wallpaper::set(&mut self.config, entry); wallpaper::set(&mut self.background_service_config, entry);
} }
/// Updates configuration for background image.
fn config_background_entry(&self, output: String, path: PathBuf) -> Option<Entry> { fn config_background_entry(&self, output: String, path: PathBuf) -> Option<Entry> {
let scaling_mode = match self.selected_fit { let scaling_mode = match self.selected_fit {
FIT => ScalingMode::Fit([0.0, 0.0, 0.0]), FIT => ScalingMode::Fit([0.0, 0.0, 0.0]),
@ -266,13 +370,14 @@ impl Page {
.apply(Some) .apply(Some)
} }
fn config_update( /// Updates configuration from the background service.
fn background_service_config_update(
&mut self, &mut self,
config: wallpaper::Config, background_service_config: wallpaper::Config,
displays: HashMap<String, String>, displays: HashMap<String, String>,
selection: Context, selection: Context,
) { ) {
self.config = config; self.background_service_config = background_service_config;
self.selection = selection; self.selection = selection;
self.outputs.clear(); self.outputs.clear();
@ -293,49 +398,78 @@ impl Page {
self.outputs.activate(id); self.outputs.activate(id);
} }
if self.config.same_on_all || self.config.backgrounds.is_empty() { if self.background_service_config.same_on_all
let entry = self.config.default_background.clone(); || self.background_service_config.backgrounds.is_empty()
{
let entry = self.background_service_config.default_background.clone();
self.select_background_entry(&entry); self.select_background_entry(&entry);
if let Some(current) = entry_directory(&entry) { if let Some(current) = entry_directory(self.config.current_folder(), &entry) {
self.current_directory = current; if let Err(why) = self.config.set_current_folder(current) {
tracing::error!(?why, "cannot set current folder");
}
} }
} else if let Some(data) = self.outputs.active_data::<String>() { } else if let Some(data) = self.outputs.active_data::<String>() {
let mut backgrounds = Vec::new(); let mut backgrounds = Vec::new();
std::mem::swap(&mut self.config.backgrounds, &mut backgrounds); std::mem::swap(
&mut self.background_service_config.backgrounds,
&mut backgrounds,
);
for background in &backgrounds { for background in &backgrounds {
if &background.output == data { if &background.output == data {
self.active_output = Some(data.clone()); self.active_output = Some(data.clone());
self.select_background_entry(background); self.select_background_entry(background);
if let Some(current) = entry_directory(background) { if let Some(current) = entry_directory(self.config.current_folder(), background)
self.current_directory = current; {
if let Err(why) = self.config.set_current_folder(current) {
tracing::error!(?why, "cannot set current folder");
}
} }
break; break;
} }
} }
std::mem::swap(&mut self.config.backgrounds, &mut backgrounds); std::mem::swap(
&mut self.background_service_config.backgrounds,
&mut backgrounds,
);
} }
} }
/// Changes the selection category, such as wallpaper select or color select. /// Changes the selection category, such as wallpaper select or color select.
fn change_category(&mut self, pos: usize) { fn change_category(&mut self, category: Category) -> Command<crate::app::Message> {
self.active_category = pos; let mut command = Command::none();
match pos {
CATEGORY_SYSTEM_WALLPAPERS => { match category {
Category::Backgrounds => {
self.select_first_background(); self.select_first_background();
} }
CATEGORY_COLOR => { Category::Colors => {
self.selection.active = Choice::Color(wallpaper::DEFAULT_COLORS[0].clone()); self.selection.active = Choice::Color(wallpaper::DEFAULT_COLORS[0].clone());
self.cache_display_image(); self.cache_display_image();
} }
_ => (), 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()) {
tracing::error!(?path, ?why, "failed to set current folder");
}
command = cosmic::command::future(async move {
crate::app::Message::PageMessage(crate::pages::Message::DesktopWallpaper(
Message::ChangeFolder(change_folder(path).await),
))
});
}
}
} }
self.categories.selected = Some(category);
command
} }
/// Changes the output being configured /// Changes the output being configured
@ -346,7 +480,7 @@ impl Page {
} }
} }
// Changes the slideshow background rotation frequency /// Changes the slideshow background rotation frequency
pub fn change_rotation_frequency(&mut self, option: usize) { pub fn change_rotation_frequency(&mut self, option: usize) {
self.selected_rotation = option; self.selected_rotation = option;
@ -373,9 +507,86 @@ impl Page {
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn update(&mut self, message: Message) { pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
match message { match message {
Message::ChangeCategory(pos) => self.change_category(pos), Message::ChangeFolder(mut context) => {
// Reassign custom colors and images to the new context.
std::mem::swap(&mut context, &mut self.selection);
for color in context.custom_colors {
self.selection.add_custom_color(color);
}
for image in context.custom_images {
let path = context.paths.remove(image);
let display = context.display_images.remove(image);
let selection = context.selection_handles.remove(image);
if let Some(((display, selection), path)) = display.zip(selection).zip(path) {
let key = self.selection.paths.insert(path);
self.selection.display_images.insert(key, display);
self.selection.selection_handles.insert(key, selection);
}
}
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!();
}
Message::ColorRemove(color) => {
self.selection.remove_custom_color(&color);
if let Err(why) = self.config.remove_custom_color(&color) {
tracing::error!(?why, "could not remove custom color from config");
}
}
Message::ImageAdd(result) => {
let result = result.and_then(Arc::into_inner);
let Some((path, display, selection)) = result else {
tracing::warn!("image not found for provided wallpaper");
return Command::none();
};
if let Err(why) = self.config.add_custom_image(path.clone()) {
tracing::error!(?path, ?why, "could add custom image to config");
}
self.selection.add_custom_image(path, display, selection);
}
Message::ImageAddDialog => {
return cosmic::command::message(crate::Message::FileChooser(
crate::app::FileChooser::Open {
title: fl!("wallpaper-dialog-image"),
accept_label: fl!("wallpaper-dialog-image", "accept"),
include_directories: false,
modal: false,
multiple_files: false,
},
));
}
Message::ImageRemove(image) => {
if let Some(path) = self.selection.remove_custom_image(image) {
if let Err(why) = self.config.remove_custom_image(&path) {
tracing::error!(?why, "could not remove custom image from config");
}
}
}
Message::ChangeCategory(category) => {
return self.change_category(category);
}
Message::ColorSelect(color) => { Message::ColorSelect(color) => {
self.selection.active = Choice::Color(color); self.selection.active = Choice::Color(color);
@ -392,8 +603,8 @@ impl Page {
Message::RotationFrequency(pos) => self.change_rotation_frequency(pos), Message::RotationFrequency(pos) => self.change_rotation_frequency(pos),
Message::SameBackground(value) => { Message::SameBackground(value) => {
self.config.same_on_all = value; self.background_service_config.same_on_all = value;
self.config.backgrounds.clear(); self.background_service_config.backgrounds.clear();
} }
Message::Select(id) => { Message::Select(id) => {
@ -410,10 +621,30 @@ impl Page {
} }
} }
Message::Update(update) => self.config_update(update.0, update.1, update.2), Message::Init(update) => {
self.background_service_config_update(update.0, update.1, update.2);
self.config_apply();
// Load custom content
return cosmic::command::batch(self.config.custom_images().iter().cloned().map(
|path| {
cosmic::command::future(async move {
let result =
wallpaper::load_image_with_thumbnail(&mut Vec::new(), path).await;
crate::app::Message::PageMessage(
crate::pages::Message::DesktopWallpaper(Message::ImageAdd(
result.map(Arc::new),
)),
)
})
},
));
}
} }
self.config_apply(); self.config_apply();
Command::none()
} }
/// Selects the given background entry. /// Selects the given background entry.
@ -430,7 +661,7 @@ impl Page {
wallpaper::Source::Color(ref color) => { wallpaper::Source::Color(ref color) => {
self.selection.active = Choice::Color(color.clone()); self.selection.active = Choice::Color(color.clone());
self.active_category = CATEGORY_COLOR; self.categories.selected = Some(Category::Colors);
self.cache_display_image(); self.cache_display_image();
} }
} }
@ -438,11 +669,17 @@ impl Page {
/// Selects the first background from the wallpaper select options. /// Selects the first background from the wallpaper select options.
fn select_first_background(&mut self) { fn select_first_background(&mut self) {
if let Some((entity, path)) = self.selection.paths.iter().next() { let (entity, path) = match self.selection.custom_images.last() {
if let Some(output) = self.config_output() { Some(entity) => (*entity, &self.selection.paths[*entity]),
if let Some(entry) = self.config_background_entry(output, path.clone()) { None => match self.selection.paths.iter().next() {
self.select_background(&entry, entity, path.is_dir()); Some(value) => value,
} None => return,
},
};
if let Some(output) = self.config_output() {
if let Some(entry) = self.config_background_entry(output, path.clone()) {
self.select_background(&entry, entity, path.is_dir());
} }
} }
} }
@ -490,49 +727,89 @@ impl Page {
} }
} }
impl page::Page<crate::pages::Message> for Page { #[derive(Clone, Debug, PartialEq)]
fn content( enum Choice {
&self, Background(DefaultKey),
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>, Color(wallpaper::Color),
) -> Option<page::Content> { Slideshow,
Some(vec![sections.insert(settings())]) }
}
fn info(&self) -> page::Info { impl Default for Choice {
page::Info::new("wallpaper", "preferences-desktop-wallpaper-symbolic") fn default() -> Self {
.title(fl!("wallpaper")) Self::Background(DefaultKey::default())
.description(fl!("wallpaper", "desc"))
}
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
Some(Box::pin(async move {
let (config, outputs) = wallpaper::config();
let mut backgrounds = wallpaper::load_each_from_path(SYSTEM_WALLPAPER_DIR.into());
let mut update = Context::default();
while let Some((path, display_image, selection_image)) = backgrounds.recv().await {
let id = update.paths.insert(path);
update.display_images.insert(id, display_image);
let selection_handle = ImageHandle::from_pixels(
selection_image.width(),
selection_image.height(),
selection_image.into_vec(),
);
update.selection_handles.insert(id, selection_handle);
}
crate::pages::Message::DesktopWallpaper(Message::Update(Box::new((
config, outputs, update,
))))
}))
} }
} }
impl page::AutoBind<crate::pages::Message> for Page {} #[derive(Clone, Debug, Default)]
pub struct Context {
active: Choice,
custom_images: Vec<DefaultKey>,
custom_colors: Vec<wallpaper::Color>,
paths: SlotMap<DefaultKey, PathBuf>,
is_custom: SecondaryMap<DefaultKey, bool>,
display_images: SecondaryMap<DefaultKey, image::RgbaImage>,
selection_handles: SecondaryMap<DefaultKey, ImageHandle>,
}
impl Context {
fn add_custom_color(&mut self, color: wallpaper::Color) {
if !self.custom_colors.contains(&color) {
self.add_custom_color(color);
}
}
fn add_custom_image(&mut self, path: PathBuf, display: Image, selection: Image) {
let key = self.paths.insert(path);
self.is_custom.insert(key, true);
self.display_images.insert(key, display);
self.custom_images.push(key);
self.selection_handles.insert(
key,
ImageHandle::from_pixels(selection.width(), selection.height(), selection.into_vec()),
);
}
fn remove_custom_color(&mut self, color: &wallpaper::Color) {
if let Some(id) = self.custom_colors.iter().position(|c| c == color) {
self.custom_colors.remove(id);
}
}
fn remove_custom_image(&mut self, image: DefaultKey) -> Option<PathBuf> {
if let Some(true) = self.is_custom.remove(image) {
if let Some(id) = self.custom_images.iter().position(|i| i == &image) {
self.custom_images.remove(id);
}
self.display_images.remove(image);
self.selection_handles.remove(image);
return self.paths.remove(image);
}
None
}
}
pub async fn change_folder(current_folder: PathBuf) -> Context {
let mut update = Context::default();
let mut backgrounds = wallpaper::load_each_from_path(current_folder);
while let Some((path, display_image, selection_image)) = backgrounds.recv().await {
let id = update.paths.insert(path);
update.display_images.insert(id, display_image);
let selection_handle = ImageHandle::from_pixels(
selection_image.width(),
selection_image.height(),
selection_image.into_vec(),
);
update.selection_handles.insert(id, selection_handle);
}
update
}
#[dynamic] #[dynamic]
static WALLPAPER_SAME: String = fl!("wallpaper", "same"); static WALLPAPER_SAME: String = fl!("wallpaper", "same");
@ -580,7 +857,7 @@ pub fn settings() -> Section<crate::pages::Message> {
}, },
)); ));
children.push(if page.config.same_on_all { children.push(if page.background_service_config.same_on_all {
text(fl!("all-displays")) text(fl!("all-displays"))
.font(cosmic::font::FONT_SEMIBOLD) .font(cosmic::font::FONT_SEMIBOLD)
.horizontal_alignment(alignment::Horizontal::Center) .horizontal_alignment(alignment::Horizontal::Center)
@ -604,7 +881,11 @@ pub fn settings() -> Section<crate::pages::Message> {
let mut column = list_column() let mut column = list_column()
.add(settings::item( .add(settings::item(
&*WALLPAPER_SAME, &*WALLPAPER_SAME,
toggler(None, page.config.same_on_all, Message::SameBackground), toggler(
None,
page.background_service_config.same_on_all,
Message::SameBackground,
),
)) ))
.add(settings::item(&*WALLPAPER_FIT, background_fit)); .add(settings::item(&*WALLPAPER_FIT, background_fit));
@ -632,17 +913,30 @@ pub fn settings() -> Section<crate::pages::Message> {
} }
}); });
let category_selection = dropdown( let category_selection =
&page.categories, dropdown::multi::dropdown(&page.categories, Message::ChangeCategory);
Some(page.active_category),
Message::ChangeCategory, let add_button = {
let (text, message) = if Some(Category::Colors) == page.categories.selected {
(fl!("add-color"), Message::ColorAddDialog)
} else {
(fl!("add-image"), Message::ImageAddDialog)
};
button::link(text).on_press(message)
};
children.push(
row::with_capacity(2)
.push(category_selection)
.push(cosmic::widget::horizontal_space(Length::Fill))
.push(add_button)
.into(),
); );
children.push(category_selection.into()); match page.categories.selected {
match page.active_category {
// Displays system wallpapers that are available to select from // Displays system wallpapers that are available to select from
CATEGORY_SYSTEM_WALLPAPERS => { Some(Category::Backgrounds | Category::RecentFolder(_)) => {
children.push(widgets::wallpaper_select_options( children.push(widgets::wallpaper_select_options(
page, page,
if let Choice::Background(selection) = page.selection.active { if let Choice::Background(selection) = page.selection.active {
@ -654,8 +948,9 @@ pub fn settings() -> Section<crate::pages::Message> {
} }
// Displays colors and gradients that are available to select from // Displays colors and gradients that are available to select from
CATEGORY_COLOR => { Some(Category::Colors) => {
children.push(widgets::color_select_options( children.push(widgets::color_select_options(
&page.selection,
if let Choice::Color(ref color) = page.selection.active { if let Choice::Color(ref color) = page.selection.active {
Some(color) Some(color)
} else { } else {
@ -664,7 +959,7 @@ pub fn settings() -> Section<crate::pages::Message> {
)); ));
} }
_ => (), None => (),
} }
cosmic::widget::column::with_children(children) cosmic::widget::column::with_children(children)
@ -675,7 +970,7 @@ pub fn settings() -> Section<crate::pages::Message> {
} }
/// Sets the current wallpaper directory. /// Sets the current wallpaper directory.
fn entry_directory(entry: &wallpaper::Entry) -> Option<PathBuf> { fn entry_directory(current_folder: &Path, entry: &wallpaper::Entry) -> Option<PathBuf> {
Some(match entry.source { Some(match entry.source {
wallpaper::Source::Path(ref path) => { wallpaper::Source::Path(ref path) => {
if path.is_dir() { if path.is_dir() {
@ -687,6 +982,6 @@ fn entry_directory(entry: &wallpaper::Entry) -> Option<PathBuf> {
} }
} }
wallpaper::Source::Color(_) => PathBuf::from(SYSTEM_WALLPAPER_DIR), wallpaper::Source::Color(_) => PathBuf::from(current_folder),
}) })
} }

View file

@ -15,8 +15,19 @@ const COLUMN_SPACING: u16 = 12;
const ROW_SPACING: u16 = 16; const ROW_SPACING: u16 = 16;
/// A button for selecting a color or gradient. /// A button for selecting a color or gradient.
pub fn color_button(color: wallpaper::Color, selected: bool) -> Element<'static, Message> { pub fn color_button(
button(color_image(color.clone(), COLOR_WIDTH, COLOR_WIDTH, 8.0)) color: wallpaper::Color,
removable: bool,
selected: bool,
) -> Element<'static, Message> {
let content = color_image(color.clone(), COLOR_WIDTH, COLOR_WIDTH, 8.0);
let on_remove = if removable {
Some(Message::ColorRemove(color.clone()))
} else {
None
};
button::custom_image_button(content, on_remove)
.padding(0) .padding(0)
.selected(selected) .selected(selected)
.style(button::Style::Image) .style(button::Style::Image)
@ -65,12 +76,26 @@ pub fn color_image<'a, M: 'a>(
} }
/// Color selection list /// Color selection list
pub fn color_select_options(selected: Option<&wallpaper::Color>) -> Element<'static, Message> { pub fn color_select_options(
context: &super::Context,
selected: Option<&wallpaper::Color>,
) -> Element<'static, Message> {
let mut vec = Vec::with_capacity(wallpaper::DEFAULT_COLORS.len()); let mut vec = Vec::with_capacity(wallpaper::DEFAULT_COLORS.len());
// Place removable custom colors first
for color in context.custom_colors.iter().rev() {
vec.push(color_button(
color.clone(),
true,
selected.map_or(false, |selection| selection == color),
));
}
// Then non-removable default colors
for color in wallpaper::DEFAULT_COLORS { for color in wallpaper::DEFAULT_COLORS {
vec.push(color_button( vec.push(color_button(
color.clone(), color.clone(),
false,
selected.map_or(false, |selection| selection == color), selected.map_or(false, |selection| selection == color),
)); ));
} }
@ -85,10 +110,28 @@ pub fn wallpaper_select_options(
) -> Element<Message> { ) -> Element<Message> {
let mut vec = Vec::with_capacity(page.selection.selection_handles.len()); let mut vec = Vec::with_capacity(page.selection.selection_handles.len());
// Place removable custom images first
for id in page.selection.custom_images.iter().rev() {
let handle = &page.selection.selection_handles[*id];
vec.push(wallpaper_button(
handle,
*id,
true,
selected.map_or(false, |selection| id == &selection),
));
}
// Then place non-removable images from the current folder
for (id, handle) in &page.selection.selection_handles { for (id, handle) in &page.selection.selection_handles {
if page.selection.is_custom.contains_key(id) {
continue;
}
vec.push(wallpaper_button( vec.push(wallpaper_button(
handle, handle,
id, id,
false,
selected.map_or(false, |selection| id == selection), selected.map_or(false, |selection| id == selection),
)); ));
} }
@ -106,9 +149,19 @@ fn flex_select_row(elements: Vec<Element<Message>>) -> Element<Message> {
.into() .into()
} }
fn wallpaper_button(handle: &ImageHandle, id: DefaultKey, selected: bool) -> Element<Message> { fn wallpaper_button(
handle: &ImageHandle,
id: DefaultKey,
removable: bool,
selected: bool,
) -> Element<Message> {
cosmic::widget::button::image(handle.clone()) cosmic::widget::button::image(handle.clone())
.selected(selected) .selected(selected)
.on_press(Message::Select(id)) .on_press(Message::Select(id))
.on_remove_maybe(if removable {
Some(Message::ImageRemove(id))
} else {
None
})
.into() .into()
} }

View file

@ -13,68 +13,76 @@ desktop = Desktop
appearance = Appearance appearance = Appearance
.desc = Accent colors and COSMIC theming. .desc = Accent colors and COSMIC theming.
import = Import
export = Export
mode-and-colors = Mode and Colors
auto-switch = Automatically switch from Light to Dark mode
.desc = Switches to Light mode at sunrise
accent-color = Accent color accent-color = Accent color
app-background = Application or window background app-background = Application or window background
auto = Auto auto = Auto
close = Close close = Close
color-picker = Color Picker
copied-to-clipboard = Copied to clipboard
copy-to-clipboard = Copy to clipboard
dark = Dark
export = Export
hex = Hex
import = Import
light = Light
mode-and-colors = Mode and Colors
recent-colors = Recent colors
reset-default = Reset to default
reset-to-default = Reset to default
rgb = RGB
window-hint-accent = Active window hint color
window-hint-accent-toggle = Use theme accent color as active window hint
auto-switch = Automatically switch from Light to Dark mode
.desc = Switches to Light mode at sunrise
container-background = Container background container-background = Container background
.desc-detail = Container background color is used for navigation sidebar, side drawer, dialogs and similar widgets. By default, it is automatically derived from the Application or window background. .desc-detail = Container background color is used for navigation sidebar, side drawer, dialogs and similar widgets. By default, it is automatically derived from the Application or window background.
.reset = Reset to auto .reset = Reset to auto
.desc = Primary container color is used for navigation sidebar, side drawer, dialogs and similar widgets. .desc = Primary container color is used for navigation sidebar, side drawer, dialogs and similar widgets.
text-tint = Interface text tint
.desc = Color used to derive interface text colors that have sufficient contrast on various surfaces.
control-tint = Control component tint control-tint = Control component tint
.desc = Used for backgrounds of standard buttons, search inputs, text inputs, and similar components. .desc = Used for backgrounds of standard buttons, search inputs, text inputs, and similar components.
window-hint-accent-toggle = Use theme accent color as active window hint
window-hint-accent = Active window hint color frosted = Frosted glass effect on system interface
dark = Dark .desc = Applies background blur to panel, dock, applets, launcher, and application library.
light = Light
color-picker = Color Picker text-tint = Interface text tint
hex = Hex .desc = Color used to derive interface text colors that have sufficient contrast on various surfaces.
rgb = RGB
recent-colors = Recent colors
reset-to-default = Reset to default
copy-to-clipboard = Copy to clipboard
copied-to-clipboard = Copied to clipboard
style = Style style = Style
.round = Round .round = Round
.slightly-round = Slightly round .slightly-round = Slightly round
.square = Square .square = Square
frosted = Frosted glass effect on system interface
.desc = Applies background blur to panel, dock, applets, launcher, and application library.
reset-default = Reset to default
# interface density left out for now # interface density left out for now
window-management = Window Management window-management = Window Management
.active-hint = Active window hint size .active-hint = Active window hint size
.gaps = Gaps around tiled windows .gaps = Gaps around tiled windows
## Desktop: Notifications ## Desktop: Notifications
notifications = Notifications notifications = Notifications
.desc = Do Not Disturb, lockscreen notifications, and per-application settings. .desc = Do Not Disturb, lockscreen notifications, and per-application settings.
## Desktop: Options ## Desktop: Options
desktop-panel-options = Desktop and Panel desktop-panel-options = Desktop and Panel
.desc = Super Key action, hot corners, window control options. .desc = Super Key action, hot corners, window control options.
desktop-panels-and-applets = Desktop Panels and Applets
dock = Dock
.desc = Panel with pinned applications.
hot-corner = Hot Corner
.top-left-corner = Enable top-left hot corner for Workspaces
super-key-action = Super Key Action super-key-action = Super Key Action
.launcher = Launcher .launcher = Launcher
.workspaces = Workspaces .workspaces = Workspaces
.applications = Applications .applications = Applications
hot-corner = Hot Corner
.top-left-corner = Enable top-left hot corner for Workspaces
top-panel = Top Panel top-panel = Top Panel
.workspaces = Show Workspaces Button .workspaces = Show Workspaces Button
.applications = Show Applications Button .applications = Show Applications Button
@ -83,32 +91,39 @@ window-controls = Window Controls
.minimize = Show Minimize Button .minimize = Show Minimize Button
.maximize = Show Maximize Button .maximize = Show Maximize Button
desktop-panels-and-applets = Desktop Panels and Applets
dock = Dock
.desc = Panel with pinned applications.
## Desktop: Panel ## Desktop: Panel
panel = Panel panel = Panel
.desc = Top bar with desktop controls and menus. .desc = Top bar with desktop controls and menus.
add = Add
add-applet = Add Applet
all = All
applets = Applets
center-segment = Center Segment
drop-here = Drop applets here
end-segment = End Segment
large = Large
no-applets-found = No applets found...
panel-bottom = Bottom
panel-left = Left
panel-right = Right
panel-top = Top
search-applets = Search applets...
small = Small
start-segment = Start Segment
panel-appearance = Appearance
.match = Match desktop
.light = Light
.dark = Dark
panel-behavior-and-position = Behavior and Positions panel-behavior-and-position = Behavior and Positions
.autohide = Automatically hide panel .autohide = Automatically hide panel
.dock-autohide = Automatically hide dock .dock-autohide = Automatically hide dock
.position = Position on screen .position = Position on screen
.display = Show on display .display = Show on display
panel-top = Top
panel-bottom = Bottom
panel-left = Left
panel-right = Right
panel-appearance = Appearance
.match = Match desktop
.light = Light
.dark = Dark
panel-style = Style panel-style = Style
.anchor-gap = Gap between panel and screen edges .anchor-gap = Gap between panel and screen edges
.dock-anchor-gap = Gap between dock and screen edges .dock-anchor-gap = Gap between dock and screen edges
@ -118,9 +133,6 @@ panel-style = Style
.size = Size .size = Size
.background-opacity = Background opacity .background-opacity = Background opacity
small = Small
large = Large
panel-applets = Configuration panel-applets = Configuration
.dock-desc = Configure dock applets. .dock-desc = Configure dock applets.
.desc = Configure panel applets. .desc = Configure panel applets.
@ -129,19 +141,6 @@ panel-missing = Panel Configuration is Missing
.desc = The panel configuration file is missing due to use of a custom configuration or it is corrupted. .desc = The panel configuration file is missing due to use of a custom configuration or it is corrupted.
.fix = Reset to default .fix = Reset to default
applets = Applets
start-segment = Start Segment
center-segment = Center Segment
end-segment = End Segment
add = Add
add-applet = Add Applet
search-applets = Search applets...
no-applets-found = No applets found...
all = All
drop-here = Drop applets here
## Desktop: Wallpaper ## Desktop: Wallpaper
wallpaper = Wallpaper wallpaper = Wallpaper
@ -151,13 +150,19 @@ wallpaper = Wallpaper
.slide = Slideshow .slide = Slideshow
.change = Change image every .change = Change image every
add-color = Add color
add-image = Add image
all-displays = All Displays all-displays = All Displays
colors = Colors colors = Colors
fit-to-screen = Fit to Screen fit-to-screen = Fit to Screen
recent-folders = Recent Folders
stretch = Stretch stretch = Stretch
system-backgrounds = System backgrounds system-backgrounds = System backgrounds
zoom = Zoom zoom = Zoom
wallpaper-dialog-image = Choose wallpaper image
.accept = _Add
x-minutes = { $number } minutes x-minutes = { $number } minutes
x-hours = { $number -> x-hours = { $number ->
[1] 1 hour [1] 1 hour

View file

@ -11,3 +11,4 @@ libcosmic = { workspace = true }
generator = "0.7.4" generator = "0.7.4"
downcast-rs = "1.2.0" downcast-rs = "1.2.0"
once_cell = "1.17.2" once_cell = "1.17.2"
url = "2.5.0"

View file

@ -44,6 +44,11 @@ pub trait Page<Message: 'static>: Downcast {
None None
} }
/// Response from a file chooser dialog request.
fn file_chooser(&mut self, _selected: Vec<url::Url>) -> Command<Message> {
Command::none()
}
#[must_use] #[must_use]
#[allow(unused)] #[allow(unused)]
fn load(&self, page: crate::Entity) -> Option<crate::Task<Message>> { fn load(&self, page: crate::Entity) -> Option<crate::Task<Message>> {

View file

@ -1,6 +1,6 @@
pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source}; pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source};
use image::{DynamicImage, RgbaImage}; use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{hash_map::DefaultHasher, BTreeSet, HashMap}, collections::{hash_map::DefaultHasher, BTreeSet, HashMap},
@ -101,7 +101,7 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
let (tx, rx) = mpsc::channel(1); let (tx, rx) = mpsc::channel(1);
tokio::task::spawn_blocking(move || { tokio::task::spawn(async move {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let mut paths = vec![path]; let mut paths = vec![path];
let mut wallpapers = BTreeSet::new(); let mut wallpapers = BTreeSet::new();
@ -125,52 +125,8 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
} }
for path in wallpapers { for path in wallpapers {
let image_operation = load_thumbnail(&mut buffer, cache_dir.as_deref(), &path); if let Some(value) = load_image_with_thumbnail(&mut buffer, path).await {
let _res = tx.send(value).await;
if let Some(image_operation) = image_operation {
let tokio_handle = tokio::runtime::Handle::current();
let tx = tx.clone();
rayon::spawn_fifo(move || {
let display_thumbnail = match image_operation {
ImageOperation::Cached(thumbnail) => thumbnail.to_rgba8(),
ImageOperation::GenerateThumbnail { path, image } => {
let image = image.thumbnail(300, 169).to_rgba8();
if let Some(path) = path {
// Save thumbnail to disk without blocking.
tokio_handle.spawn_blocking({
let image = image.clone();
move || {
if let Err(why) = image.save(&path) {
tracing::error!(
?path,
?why,
"failed to save image thumbnail"
);
let _res = std::fs::remove_file(&path);
}
}
});
}
image
}
};
let mut selection_thumbnail = image::imageops::resize(
&display_thumbnail,
158,
105,
image::imageops::FilterType::Lanczos3,
);
round(&mut selection_thumbnail, [8, 8, 8, 8]);
let _res = tx.blocking_send((path, display_thumbnail, selection_thumbnail));
});
} }
} }
}); });
@ -178,6 +134,65 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
rx rx
} }
pub async fn load_image_with_thumbnail(
buffer: &mut Vec<u8>,
path: PathBuf,
) -> Option<(
PathBuf,
ImageBuffer<Rgba<u8>, Vec<u8>>,
ImageBuffer<Rgba<u8>, Vec<u8>>,
)> {
let cache_dir = cache_dir();
let image_operation = load_thumbnail(buffer, cache_dir.as_deref(), &path);
let (tx, rx) = tokio::sync::oneshot::channel();
if let Some(image_operation) = image_operation {
let tokio_handle = tokio::runtime::Handle::current();
rayon::spawn_fifo(move || {
let display_thumbnail = match image_operation {
ImageOperation::Cached(thumbnail) => thumbnail.to_rgba8(),
ImageOperation::GenerateThumbnail { path, image } => {
let image = image.thumbnail(300, 169).to_rgba8();
if let Some(path) = path {
// Save thumbnail to disk without blocking.
tokio_handle.spawn_blocking({
let image = image.clone();
move || {
if let Err(why) = image.save(&path) {
tracing::error!(?path, ?why, "failed to save image thumbnail");
let _res = std::fs::remove_file(&path);
}
}
});
}
image
}
};
let mut selection_thumbnail = image::imageops::resize(
&display_thumbnail,
158,
105,
image::imageops::FilterType::Lanczos3,
);
round(&mut selection_thumbnail, [8, 8, 8, 8]);
let _res = tx.send(Some((path, display_thumbnail, selection_thumbnail)));
});
} else {
tx.send(None);
}
rx.await.unwrap_or(None)
}
enum ImageOperation { enum ImageOperation {
GenerateThumbnail { GenerateThumbnail {
path: Option<PathBuf>, path: Option<PathBuf>,