feat(wallpaper): custom background images and folders
This commit is contained in:
parent
f3e929e769
commit
daa730682a
10 changed files with 928 additions and 250 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -1081,6 +1081,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"regex",
|
||||
"slotmap",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5764,9 +5765,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.27"
|
||||
version = "0.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb877ca3232bec99a6472ed63f7241de2a250165260908b2d24c09d867907a85"
|
||||
checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ git = "https://github.com/pop-os/libcosmic"
|
|||
|
||||
[workspace.dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
features = ["wayland", "tokio", "single-instance"]
|
||||
features = ["single-instance", "tokio", "wayland", "xdg-portal"]
|
||||
|
||||
[workspace.dependencies.cosmic-config]
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
|
|
|
|||
117
app/src/app.rs
117
app/src/app.rs
|
|
@ -18,6 +18,7 @@ use crate::subscription::desktop_files;
|
|||
use crate::widget::{page_title, search_header};
|
||||
use crate::PageCommands;
|
||||
use cosmic::app::DbusActivationMessage;
|
||||
use cosmic::dialog::file_chooser;
|
||||
use cosmic::iced::Subscription;
|
||||
use cosmic::{
|
||||
app::{Command, Core},
|
||||
|
|
@ -45,6 +46,7 @@ pub struct SettingsApp {
|
|||
active_page: page::Entity,
|
||||
config: Config,
|
||||
core: Core,
|
||||
file_chooser: Option<(file_chooser::Sender, page::Entity)>,
|
||||
nav_model: nav_bar::Model,
|
||||
pages: page::Binder<crate::pages::Message>,
|
||||
search: search::Model,
|
||||
|
|
@ -65,10 +67,11 @@ impl SettingsApp {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
DesktopInfo,
|
||||
FileChooser(FileChooser),
|
||||
Error(String),
|
||||
Page(page::Entity),
|
||||
PageMessage(crate::pages::Message),
|
||||
PanelConfig(CosmicPanelConfig),
|
||||
|
|
@ -79,6 +82,21 @@ pub enum Message {
|
|||
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 {
|
||||
type Executor = cosmic::executor::single::Executor;
|
||||
type Flags = crate::Args;
|
||||
|
|
@ -99,6 +117,7 @@ impl cosmic::Application for SettingsApp {
|
|||
active_page: page::Entity::default(),
|
||||
config: Config::new(),
|
||||
core,
|
||||
file_chooser: None,
|
||||
nav_model: nav_bar::Model::default(),
|
||||
pages: page::Binder::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) => {
|
||||
page::update!(self.pages, message, system::about::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::DateAndTime(message) => {
|
||||
page::update!(self.pages, message, time::date::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Desktop(message) => {
|
||||
page::update!(self.pages, message, desktop::Page);
|
||||
}
|
||||
|
||||
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) => {
|
||||
if let Some(page) = self.pages.page_mut::<input::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::External { .. } => {
|
||||
todo!("external plugins not supported yet");
|
||||
}
|
||||
|
||||
crate::pages::Message::Page(page) => {
|
||||
return self.activate_page(page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Panel(message) => {
|
||||
page::update!(self.pages, message, panel::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::PanelApplet(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<applets_inner::Page>() {
|
||||
return page
|
||||
|
|
@ -268,19 +323,65 @@ impl cosmic::Application for SettingsApp {
|
|||
.map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Dock(message) => {
|
||||
page::update!(self.pages, message, dock::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::DockApplet(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<dock::applets::Page>() {
|
||||
return page.update(message).map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Appearance(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<appearance::Page>() {
|
||||
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,
|
||||
dock::applets::Message(applets_inner::Message::PanelConfig(config,)),
|
||||
dock::applets::Page
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Message::DesktopInfo => {
|
||||
|
|
@ -335,15 +436,23 @@ impl cosmic::Application for SettingsApp {
|
|||
.map(cosmic::app::Message::App);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PanelConfig(_) | Message::Search(_) => {}
|
||||
|
||||
Message::SetTheme(t) => return cosmic::app::command::set_theme(t),
|
||||
|
||||
Message::OpenContextDrawer(title) => {
|
||||
self.core.window.show_context = true;
|
||||
self.set_context_title(title.to_string());
|
||||
}
|
||||
|
||||
Message::CloseContextDrawer => {
|
||||
self.core.window.show_context = false;
|
||||
}
|
||||
|
||||
Message::Error(error) => {
|
||||
tracing::error!(error, "error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
|
|
|
|||
194
app/src/pages/desktop/wallpaper/config.rs
Normal file
194
app/src/pages/desktop/wallpaper/config.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +1,36 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod config;
|
||||
pub mod widgets;
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
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_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_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use image::imageops::FilterType::Lanczos3;
|
||||
use image::{ImageBuffer, Rgba};
|
||||
use slotmap::{DefaultKey, SecondaryMap, SlotMap};
|
||||
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 STRETCH: usize = 1;
|
||||
const ZOOM: usize = 2;
|
||||
|
|
@ -42,33 +45,44 @@ const MINUTES_30: usize = 3;
|
|||
const HOUR_1: usize = 4;
|
||||
const HOUR_2: usize = 5;
|
||||
|
||||
pub type Image = ImageBuffer<Rgba<u8>, Vec<u8>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
ChangeCategory(usize),
|
||||
ChangeFolder(Context),
|
||||
ColorAdd(wallpaper::Color),
|
||||
ColorAddDialog,
|
||||
ColorRemove(wallpaper::Color),
|
||||
ChangeCategory(Category),
|
||||
ColorSelect(wallpaper::Color),
|
||||
Fit(usize),
|
||||
ImageAdd(Option<Arc<(PathBuf, Image, Image)>>),
|
||||
ImageAddDialog,
|
||||
ImageRemove(DefaultKey),
|
||||
Output(segmented_button::Entity),
|
||||
RotationFrequency(usize),
|
||||
SameBackground(bool),
|
||||
Select(DefaultKey),
|
||||
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 {
|
||||
SystemBackgrounds,
|
||||
Backgrounds,
|
||||
Colors,
|
||||
RecentFolder(usize),
|
||||
}
|
||||
|
||||
pub struct Page {
|
||||
pub active_output: Option<String>,
|
||||
pub active_category: usize,
|
||||
pub background_service_config: wallpaper::Config,
|
||||
pub cached_display_handle: Option<ImageHandle>,
|
||||
pub categories: Vec<String>,
|
||||
pub config: wallpaper::Config,
|
||||
pub current_directory: PathBuf,
|
||||
pub categories: dropdown::multi::Model<String, Category>,
|
||||
pub config: Config,
|
||||
pub fit_options: Vec<String>,
|
||||
pub outputs: SingleSelectModel,
|
||||
pub recent_folders: Vec<(PathBuf, String)>,
|
||||
pub rotation_frequency: u64,
|
||||
pub rotation_options: Vec<String>,
|
||||
pub selected_fit: usize,
|
||||
|
|
@ -76,17 +90,93 @@ pub struct Page {
|
|||
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 {
|
||||
fn default() -> Self {
|
||||
Page {
|
||||
let mut page = Page {
|
||||
active_output: None,
|
||||
active_category: CATEGORY_SYSTEM_WALLPAPERS,
|
||||
cached_display_handle: None,
|
||||
categories: vec![fl!("system-backgrounds"), fl!("colors")],
|
||||
config: wallpaper::Config::default(),
|
||||
current_directory: PathBuf::from(SYSTEM_WALLPAPER_DIR),
|
||||
categories: {
|
||||
let mut categories = dropdown::multi::model();
|
||||
|
||||
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")],
|
||||
outputs: SingleSelectModel::default(),
|
||||
recent_folders: Vec::new(),
|
||||
rotation_frequency: 300,
|
||||
rotation_options: vec![
|
||||
// FIX: fluent is inserting extra unicode characters in formatting
|
||||
|
|
@ -112,32 +202,42 @@ impl Default for Page {
|
|||
selected_fit: 0,
|
||||
selected_rotation: 0,
|
||||
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 {
|
||||
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) {
|
||||
let choice = match self.selection.active {
|
||||
Choice::Background(id) => self.selection.display_images.get(id),
|
||||
|
|
@ -204,7 +304,7 @@ impl Page {
|
|||
}
|
||||
|
||||
fn config_output(&self) -> Option<String> {
|
||||
if self.config.same_on_all {
|
||||
if self.background_service_config.same_on_all {
|
||||
Some(String::from("all"))
|
||||
} else {
|
||||
self.outputs.active_data::<String>().cloned()
|
||||
|
|
@ -217,13 +317,16 @@ impl Page {
|
|||
return;
|
||||
};
|
||||
|
||||
if self.config.same_on_all {
|
||||
self.config.outputs.clear();
|
||||
if self.background_service_config.same_on_all {
|
||||
self.background_service_config.outputs.clear();
|
||||
}
|
||||
|
||||
let entry = match self.selection.active {
|
||||
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,
|
||||
None => return,
|
||||
}
|
||||
|
|
@ -245,13 +348,14 @@ impl Page {
|
|||
};
|
||||
|
||||
if output != "all" {
|
||||
self.config.backgrounds.clear();
|
||||
self.config.outputs.clear();
|
||||
self.background_service_config.backgrounds.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> {
|
||||
let scaling_mode = match self.selected_fit {
|
||||
FIT => ScalingMode::Fit([0.0, 0.0, 0.0]),
|
||||
|
|
@ -266,13 +370,14 @@ impl Page {
|
|||
.apply(Some)
|
||||
}
|
||||
|
||||
fn config_update(
|
||||
/// Updates configuration from the background service.
|
||||
fn background_service_config_update(
|
||||
&mut self,
|
||||
config: wallpaper::Config,
|
||||
background_service_config: wallpaper::Config,
|
||||
displays: HashMap<String, String>,
|
||||
selection: Context,
|
||||
) {
|
||||
self.config = config;
|
||||
self.background_service_config = background_service_config;
|
||||
self.selection = selection;
|
||||
self.outputs.clear();
|
||||
|
||||
|
|
@ -293,49 +398,78 @@ impl Page {
|
|||
self.outputs.activate(id);
|
||||
}
|
||||
|
||||
if self.config.same_on_all || self.config.backgrounds.is_empty() {
|
||||
let entry = self.config.default_background.clone();
|
||||
if self.background_service_config.same_on_all
|
||||
|| self.background_service_config.backgrounds.is_empty()
|
||||
{
|
||||
let entry = self.background_service_config.default_background.clone();
|
||||
self.select_background_entry(&entry);
|
||||
|
||||
if let Some(current) = entry_directory(&entry) {
|
||||
self.current_directory = current;
|
||||
if let Some(current) = entry_directory(self.config.current_folder(), &entry) {
|
||||
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>() {
|
||||
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 {
|
||||
if &background.output == data {
|
||||
self.active_output = Some(data.clone());
|
||||
self.select_background_entry(background);
|
||||
|
||||
if let Some(current) = entry_directory(background) {
|
||||
self.current_directory = current;
|
||||
if let Some(current) = entry_directory(self.config.current_folder(), background)
|
||||
{
|
||||
if let Err(why) = self.config.set_current_folder(current) {
|
||||
tracing::error!(?why, "cannot set current folder");
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
fn change_category(&mut self, pos: usize) {
|
||||
self.active_category = pos;
|
||||
match pos {
|
||||
CATEGORY_SYSTEM_WALLPAPERS => {
|
||||
fn change_category(&mut self, category: Category) -> Command<crate::app::Message> {
|
||||
let mut command = Command::none();
|
||||
|
||||
match category {
|
||||
Category::Backgrounds => {
|
||||
self.select_first_background();
|
||||
}
|
||||
|
||||
CATEGORY_COLOR => {
|
||||
Category::Colors => {
|
||||
self.selection.active = Choice::Color(wallpaper::DEFAULT_COLORS[0].clone());
|
||||
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
|
||||
|
|
@ -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) {
|
||||
self.selected_rotation = option;
|
||||
|
||||
|
|
@ -373,9 +507,86 @@ impl Page {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
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) => {
|
||||
self.selection.active = Choice::Color(color);
|
||||
|
|
@ -392,8 +603,8 @@ impl Page {
|
|||
Message::RotationFrequency(pos) => self.change_rotation_frequency(pos),
|
||||
|
||||
Message::SameBackground(value) => {
|
||||
self.config.same_on_all = value;
|
||||
self.config.backgrounds.clear();
|
||||
self.background_service_config.same_on_all = value;
|
||||
self.background_service_config.backgrounds.clear();
|
||||
}
|
||||
|
||||
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();
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Selects the given background entry.
|
||||
|
|
@ -430,7 +661,7 @@ impl Page {
|
|||
|
||||
wallpaper::Source::Color(ref color) => {
|
||||
self.selection.active = Choice::Color(color.clone());
|
||||
self.active_category = CATEGORY_COLOR;
|
||||
self.categories.selected = Some(Category::Colors);
|
||||
self.cache_display_image();
|
||||
}
|
||||
}
|
||||
|
|
@ -438,11 +669,17 @@ impl Page {
|
|||
|
||||
/// Selects the first background from the wallpaper select options.
|
||||
fn select_first_background(&mut self) {
|
||||
if let Some((entity, path)) = self.selection.paths.iter().next() {
|
||||
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());
|
||||
}
|
||||
let (entity, path) = match self.selection.custom_images.last() {
|
||||
Some(entity) => (*entity, &self.selection.paths[*entity]),
|
||||
None => match self.selection.paths.iter().next() {
|
||||
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 {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(settings())])
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Choice {
|
||||
Background(DefaultKey),
|
||||
Color(wallpaper::Color),
|
||||
Slideshow,
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
|
||||
.title(fl!("wallpaper"))
|
||||
.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 Default for Choice {
|
||||
fn default() -> Self {
|
||||
Self::Background(DefaultKey::default())
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
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"))
|
||||
.font(cosmic::font::FONT_SEMIBOLD)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
|
|
@ -604,7 +881,11 @@ pub fn settings() -> Section<crate::pages::Message> {
|
|||
let mut column = list_column()
|
||||
.add(settings::item(
|
||||
&*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));
|
||||
|
||||
|
|
@ -632,17 +913,30 @@ pub fn settings() -> Section<crate::pages::Message> {
|
|||
}
|
||||
});
|
||||
|
||||
let category_selection = dropdown(
|
||||
&page.categories,
|
||||
Some(page.active_category),
|
||||
Message::ChangeCategory,
|
||||
let category_selection =
|
||||
dropdown::multi::dropdown(&page.categories, 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.active_category {
|
||||
match page.categories.selected {
|
||||
// Displays system wallpapers that are available to select from
|
||||
CATEGORY_SYSTEM_WALLPAPERS => {
|
||||
Some(Category::Backgrounds | Category::RecentFolder(_)) => {
|
||||
children.push(widgets::wallpaper_select_options(
|
||||
page,
|
||||
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
|
||||
CATEGORY_COLOR => {
|
||||
Some(Category::Colors) => {
|
||||
children.push(widgets::color_select_options(
|
||||
&page.selection,
|
||||
if let Choice::Color(ref color) = page.selection.active {
|
||||
Some(color)
|
||||
} else {
|
||||
|
|
@ -664,7 +959,7 @@ pub fn settings() -> Section<crate::pages::Message> {
|
|||
));
|
||||
}
|
||||
|
||||
_ => (),
|
||||
None => (),
|
||||
}
|
||||
|
||||
cosmic::widget::column::with_children(children)
|
||||
|
|
@ -675,7 +970,7 @@ pub fn settings() -> Section<crate::pages::Message> {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
wallpaper::Source::Path(ref path) => {
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,19 @@ const COLUMN_SPACING: u16 = 12;
|
|||
const ROW_SPACING: u16 = 16;
|
||||
|
||||
/// A button for selecting a color or gradient.
|
||||
pub fn color_button(color: wallpaper::Color, selected: bool) -> Element<'static, Message> {
|
||||
button(color_image(color.clone(), COLOR_WIDTH, COLOR_WIDTH, 8.0))
|
||||
pub fn color_button(
|
||||
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)
|
||||
.selected(selected)
|
||||
.style(button::Style::Image)
|
||||
|
|
@ -65,12 +76,26 @@ pub fn color_image<'a, M: 'a>(
|
|||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
// 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 {
|
||||
vec.push(color_button(
|
||||
color.clone(),
|
||||
false,
|
||||
selected.map_or(false, |selection| selection == color),
|
||||
));
|
||||
}
|
||||
|
|
@ -85,10 +110,28 @@ pub fn wallpaper_select_options(
|
|||
) -> Element<Message> {
|
||||
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 {
|
||||
if page.selection.is_custom.contains_key(id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec.push(wallpaper_button(
|
||||
handle,
|
||||
id,
|
||||
false,
|
||||
selected.map_or(false, |selection| id == selection),
|
||||
));
|
||||
}
|
||||
|
|
@ -106,9 +149,19 @@ fn flex_select_row(elements: Vec<Element<Message>>) -> Element<Message> {
|
|||
.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())
|
||||
.selected(selected)
|
||||
.on_press(Message::Select(id))
|
||||
.on_remove_maybe(if removable {
|
||||
Some(Message::ImageRemove(id))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,68 +13,76 @@ desktop = Desktop
|
|||
appearance = Appearance
|
||||
.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
|
||||
app-background = Application or window background
|
||||
auto = Auto
|
||||
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
|
||||
.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
|
||||
.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
|
||||
.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
|
||||
dark = Dark
|
||||
light = Light
|
||||
color-picker = Color Picker
|
||||
hex = Hex
|
||||
rgb = RGB
|
||||
recent-colors = Recent colors
|
||||
reset-to-default = Reset to default
|
||||
copy-to-clipboard = Copy to clipboard
|
||||
copied-to-clipboard = Copied to clipboard
|
||||
|
||||
frosted = Frosted glass effect on system interface
|
||||
.desc = Applies background blur to panel, dock, applets, launcher, and application library.
|
||||
|
||||
text-tint = Interface text tint
|
||||
.desc = Color used to derive interface text colors that have sufficient contrast on various surfaces.
|
||||
|
||||
style = Style
|
||||
.round = Round
|
||||
.slightly-round = Slightly round
|
||||
.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
|
||||
|
||||
window-management = Window Management
|
||||
.active-hint = Active window hint size
|
||||
.gaps = Gaps around tiled windows
|
||||
|
||||
## Desktop: Notifications
|
||||
|
||||
notifications = Notifications
|
||||
.desc = Do Not Disturb, lockscreen notifications, and per-application settings.
|
||||
|
||||
|
||||
## Desktop: Options
|
||||
|
||||
desktop-panel-options = Desktop and Panel
|
||||
.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
|
||||
.launcher = Launcher
|
||||
.workspaces = Workspaces
|
||||
.applications = Applications
|
||||
|
||||
hot-corner = Hot Corner
|
||||
.top-left-corner = Enable top-left hot corner for Workspaces
|
||||
|
||||
top-panel = Top Panel
|
||||
.workspaces = Show Workspaces Button
|
||||
.applications = Show Applications Button
|
||||
|
|
@ -83,32 +91,39 @@ window-controls = Window Controls
|
|||
.minimize = Show Minimize Button
|
||||
.maximize = Show Maximize Button
|
||||
|
||||
desktop-panels-and-applets = Desktop Panels and Applets
|
||||
|
||||
|
||||
dock = Dock
|
||||
.desc = Panel with pinned applications.
|
||||
|
||||
## Desktop: Panel
|
||||
|
||||
panel = Panel
|
||||
.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
|
||||
.autohide = Automatically hide panel
|
||||
.dock-autohide = Automatically hide dock
|
||||
.position = Position on screen
|
||||
.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
|
||||
.anchor-gap = Gap between panel and screen edges
|
||||
.dock-anchor-gap = Gap between dock and screen edges
|
||||
|
|
@ -118,9 +133,6 @@ panel-style = Style
|
|||
.size = Size
|
||||
.background-opacity = Background opacity
|
||||
|
||||
small = Small
|
||||
large = Large
|
||||
|
||||
panel-applets = Configuration
|
||||
.dock-desc = Configure dock 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.
|
||||
.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
|
||||
|
||||
wallpaper = Wallpaper
|
||||
|
|
@ -151,13 +150,19 @@ wallpaper = Wallpaper
|
|||
.slide = Slideshow
|
||||
.change = Change image every
|
||||
|
||||
add-color = Add color
|
||||
add-image = Add image
|
||||
all-displays = All Displays
|
||||
colors = Colors
|
||||
fit-to-screen = Fit to Screen
|
||||
recent-folders = Recent Folders
|
||||
stretch = Stretch
|
||||
system-backgrounds = System backgrounds
|
||||
zoom = Zoom
|
||||
|
||||
wallpaper-dialog-image = Choose wallpaper image
|
||||
.accept = _Add
|
||||
|
||||
x-minutes = { $number } minutes
|
||||
x-hours = { $number ->
|
||||
[1] 1 hour
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ libcosmic = { workspace = true }
|
|||
generator = "0.7.4"
|
||||
downcast-rs = "1.2.0"
|
||||
once_cell = "1.17.2"
|
||||
url = "2.5.0"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ pub trait Page<Message: 'static>: Downcast {
|
|||
None
|
||||
}
|
||||
|
||||
/// Response from a file chooser dialog request.
|
||||
fn file_chooser(&mut self, _selected: Vec<url::Url>) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(unused)]
|
||||
fn load(&self, page: crate::Entity) -> Option<crate::Task<Message>> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source};
|
||||
|
||||
use image::{DynamicImage, RgbaImage};
|
||||
use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
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);
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
tokio::task::spawn(async move {
|
||||
let mut buffer = Vec::new();
|
||||
let mut paths = vec![path];
|
||||
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 {
|
||||
let image_operation = load_thumbnail(&mut buffer, cache_dir.as_deref(), &path);
|
||||
|
||||
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));
|
||||
});
|
||||
if let Some(value) = load_image_with_thumbnail(&mut buffer, path).await {
|
||||
let _res = tx.send(value).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -178,6 +134,65 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
|
|||
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 {
|
||||
GenerateThumbnail {
|
||||
path: Option<PathBuf>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue