feat: implement wallpaper settings page
This commit is contained in:
parent
3681a0987e
commit
900bb45758
12 changed files with 1009 additions and 480 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
|
@ -616,6 +616,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorgrad"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a5f405d474b9d05e0a093d3120e77e9bf26461b57a84b40aa2a221ac5617fb6"
|
||||||
|
dependencies = [
|
||||||
|
"csscolorparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "com-rs"
|
name = "com-rs"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -712,13 +721,15 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-bg-config"
|
name = "cosmic-bg-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/cosmic-bg#9958bb035bf4a108d2fc537a6f1134e5bc21262c"
|
source = "git+https://github.com/pop-os/cosmic-bg#69c7b5fdaea935fff6d5564cc7bce797dd1d1408"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"colorgrad",
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
"derive_setters",
|
"derive_setters",
|
||||||
"image",
|
"image",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2695,15 +2706,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "matchers"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
|
||||||
dependencies = [
|
|
||||||
"regex-automata",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -3638,24 +3640,9 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.7.2",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
|
||||||
dependencies = [
|
|
||||||
"regex-syntax 0.6.29",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.6.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
@ -4538,14 +4525,10 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -1,10 +1,8 @@
|
||||||
# COSMIC Settings
|
# COSMIC Settings
|
||||||
|
|
||||||
> Prototype of a proof of concept that is an active work in progress.
|
The settings application for the [COSMIC desktop environment](https://github.com/pop-os/cosmic-epoch). Developed with [libcosmic](https://github.com/pop-os/libcosmic), using the [iced](https://iced.rs/) GUI library.
|
||||||
|
|
||||||
The settings application for the [COSMIC desktop environment](https://github.com/pop-os/cosmic-epoch). Developed with [libcosmic](https://github.com/pop-os/libcosmic) in the [iced](https://iced.rs/) GUI library.
|
## Build & Install
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
To compile, a stable Rust compiler and [just](https://github.com/casey/just) are required.
|
To compile, a stable Rust compiler and [just](https://github.com/casey/just) are required.
|
||||||
|
|
||||||
|
|
@ -23,11 +21,20 @@ Some C libraries are also required for font support at the moment.
|
||||||
Then it can be compiled and installed like so.
|
Then it can be compiled and installed like so.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
just build-release
|
just
|
||||||
sudo just prefix=/usr install
|
sudo just install
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are packaging for Linux distribution, you can use the `rootdir` variable to change the root path, in addition to the prefix.
|
## Packagers
|
||||||
|
|
||||||
|
If packaging for a Linux distribution, vendor dependencies locally with the `vendor` rule, and build with the vendored sources using the `build-vendored` rule.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just vendor
|
||||||
|
just build-vendored
|
||||||
|
```
|
||||||
|
|
||||||
|
When installing files, use the `rootdir` and `prefix` variables to change installation paths.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
just rootdir=debian/cosmic-settings prefix=/usr install
|
just rootdir=debian/cosmic-settings prefix=/usr install
|
||||||
|
|
@ -37,6 +44,20 @@ just rootdir=debian/cosmic-settings prefix=/usr install
|
||||||
|
|
||||||
Translation files may be found in the [i18n directory](./i18n). New translations may copy the [English (en) localization](./i18n/en) of the project and rename `en` to the desired [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). Translations may be submitted through GitHub as an issue or pull request. Submissions by email or other means are also acceptable; with the preferred name and email to associate with the changes.
|
Translation files may be found in the [i18n directory](./i18n). New translations may copy the [English (en) localization](./i18n/en) of the project and rename `en` to the desired [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). Translations may be submitted through GitHub as an issue or pull request. Submissions by email or other means are also acceptable; with the preferred name and email to associate with the changes.
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
|
||||||
|
Run the cosmic-settings binary with `just run` so that logs will be emitted to stderr, and crashes will generate detailed backtraces. Applications shouldn't crash, so when writing code, avoid use of `unwrap()` and `expect()`. Instead, log errors with `tracing::error!()` or `tracing::warn!()`.
|
||||||
|
|
||||||
|
This project is split across the following workspace members:
|
||||||
|
|
||||||
|
- [app](./app/): cosmic-settings GUI frontend and binary
|
||||||
|
- [page](./page/): library for creating and handling settings pages
|
||||||
|
- [pages](./pages/): libraries for page-specific logic
|
||||||
|
|
||||||
|
When creating a new page, UI-specific code will go directly into **app**, and page-specific logic will go into a crate under the **pages** directory. This is mainly to isolate page-specific crate dependencies and logic from the UI; so that the source code specific to the UI is easier to maintain and refactor.
|
||||||
|
|
||||||
|
Eventually, pages may be separated into plugins, and this will help with that migration.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the [GNU Public License 3.0](https://choosealicense.com/licenses/gpl-3.0).
|
Licensed under the [GNU Public License 3.0](https://choosealicense.com/licenses/gpl-3.0).
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ downcast-rs = "1.2.0"
|
||||||
# TODO: migrate this dependency to the pages/desktop crate.
|
# TODO: migrate this dependency to the pages/desktop crate.
|
||||||
cosmic-panel-config = { workspace = true }
|
cosmic-panel-config = { workspace = true }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"]}
|
tracing-subscriber = "0.3.17"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,8 @@ impl Application for SettingsApp {
|
||||||
|(_, e)| match e {
|
|(_, e)| match e {
|
||||||
Ok(config) => Message::PanelConfig(config),
|
Ok(config) => Message::PanelConfig(config),
|
||||||
Err((errors, config)) => {
|
Err((errors, config)) => {
|
||||||
for error in errors {
|
for why in errors {
|
||||||
log::error!("Error loading panel config: {:?}", error);
|
tracing::error!(?why, "panel config load error");
|
||||||
}
|
}
|
||||||
Message::PanelConfig(config)
|
Message::PanelConfig(config)
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +209,7 @@ impl Application for SettingsApp {
|
||||||
let mut ret = Command::none();
|
let mut ret = Command::none();
|
||||||
match message {
|
match message {
|
||||||
Message::WindowResize(width, _height) => {
|
Message::WindowResize(width, _height) => {
|
||||||
|
tracing::debug!(width, "new window width");
|
||||||
let break_point = (600.0 * self.scaling_factor) as u32;
|
let break_point = (600.0 * self.scaling_factor) as u32;
|
||||||
self.window_width = width;
|
self.window_width = width;
|
||||||
self.is_condensed = self.window_width < break_point;
|
self.is_condensed = self.window_width < break_point;
|
||||||
|
|
@ -281,13 +282,11 @@ impl Application for SettingsApp {
|
||||||
return self.activate_page(page);
|
return self.activate_page(page);
|
||||||
}
|
}
|
||||||
crate::pages::Message::Panel(message) => {
|
crate::pages::Message::Panel(message) => {
|
||||||
if let Some(page) = self.pages.page_mut::<panel::Page>() {
|
page::update!(self.pages, message, panel::Page);
|
||||||
page.update(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
crate::pages::Message::Applet(message) => {
|
crate::pages::Message::Applet(message) => {
|
||||||
if let Some(page) = self.pages.page_mut::<applets::Page>() {
|
if let Some(page) = self.pages.page_mut::<applets::Page>() {
|
||||||
ret = page.update(message);
|
return page.update(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use cosmic::{
|
||||||
iced_sctk::settings::InitialSurface,
|
iced_sctk::settings::InitialSurface,
|
||||||
};
|
};
|
||||||
use i18n_embed::DesktopLanguageRequester;
|
use i18n_embed::DesktopLanguageRequester;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
|
|
@ -34,30 +35,12 @@ pub fn main() -> color_eyre::Result<()> {
|
||||||
std::env::set_var("RUST_SPANTRACE", "0");
|
std::env::set_var("RUST_SPANTRACE", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if std::env::var_os("RUST_LOG").is_none() {
|
init_logger();
|
||||||
std::env::set_var("RUST_LOG", "info");
|
init_localizer();
|
||||||
}
|
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.pretty()
|
|
||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
||||||
.with_writer(std::io::stderr)
|
|
||||||
.without_time()
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_file(true)
|
|
||||||
.with_target(false)
|
|
||||||
.with_thread_names(true)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let localizer = crate::localize::localizer();
|
|
||||||
let requested_languages = DesktopLanguageRequester::requested_languages();
|
|
||||||
|
|
||||||
if let Err(why) = localizer.select(&requested_languages) {
|
|
||||||
tracing::error!(%why, "error while loading fluent localizations");
|
|
||||||
}
|
|
||||||
|
|
||||||
cosmic::settings::set_default_icon_theme("Pop");
|
cosmic::settings::set_default_icon_theme("Pop");
|
||||||
let mut settings = cosmic::settings();
|
let mut settings = cosmic::settings();
|
||||||
|
settings.default_text_size = 14.0;
|
||||||
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
|
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
|
||||||
title: Some(fl!("app")),
|
title: Some(fl!("app")),
|
||||||
size_limits: Limits::NONE.min_width(600.0).min_height(300.0),
|
size_limits: Limits::NONE.min_width(600.0).min_height(300.0),
|
||||||
|
|
@ -69,3 +52,39 @@ pub fn main() -> color_eyre::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_localizer() {
|
||||||
|
let localizer = crate::localize::localizer();
|
||||||
|
let requested_languages = DesktopLanguageRequester::requested_languages();
|
||||||
|
|
||||||
|
if let Err(why) = localizer.select(&requested_languages) {
|
||||||
|
tracing::error!(%why, "error while loading fluent localizations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logger() {
|
||||||
|
let log_level = std::env::var("RUST_LOG")
|
||||||
|
.ok()
|
||||||
|
.and_then(|level| level.parse::<tracing::Level>().ok())
|
||||||
|
.unwrap_or(tracing::Level::INFO);
|
||||||
|
|
||||||
|
let log_format = tracing_subscriber::fmt::format()
|
||||||
|
.pretty()
|
||||||
|
.without_time()
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_file(true)
|
||||||
|
.with_target(false)
|
||||||
|
.with_thread_names(true);
|
||||||
|
|
||||||
|
let log_filter = tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.event_format(log_format)
|
||||||
|
.with_filter(tracing_subscriber::filter::filter_fn(move |metadata| {
|
||||||
|
let target = metadata.target();
|
||||||
|
metadata.level() == &tracing::Level::ERROR
|
||||||
|
|| ((target.starts_with("cosmic_settings") || target.starts_with("cosmic_bg"))
|
||||||
|
&& metadata.level() <= &log_level)
|
||||||
|
}));
|
||||||
|
|
||||||
|
tracing_subscriber::registry().with(log_filter).init();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,389 +0,0 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
use std::{collections::HashMap, path::PathBuf, time::Instant};
|
|
||||||
|
|
||||||
use apply::Apply;
|
|
||||||
use cosmic::{
|
|
||||||
iced::alignment::Horizontal,
|
|
||||||
iced::widget::{column, row},
|
|
||||||
iced::Length,
|
|
||||||
iced_runtime::core::image::Handle as ImageHandle,
|
|
||||||
widget::{
|
|
||||||
list_column,
|
|
||||||
segmented_button::{self, SingleSelectModel},
|
|
||||||
settings, toggler,
|
|
||||||
},
|
|
||||||
Element,
|
|
||||||
};
|
|
||||||
use cosmic_settings_desktop::wallpaper::{self, Entry, Output, ScalingMode};
|
|
||||||
use cosmic_settings_page::Section;
|
|
||||||
use cosmic_settings_page::{self as page, section};
|
|
||||||
use slotmap::{DefaultKey, SecondaryMap, SlotMap};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum Message {
|
|
||||||
Fit(String),
|
|
||||||
Output(segmented_button::Entity),
|
|
||||||
RotationFrequency(String),
|
|
||||||
SameBackground(bool),
|
|
||||||
Select(DefaultKey),
|
|
||||||
Slideshow(bool),
|
|
||||||
Update((wallpaper::Config, HashMap<String, String>, Context)),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Page {
|
|
||||||
pub active_output: Option<String>,
|
|
||||||
pub config: wallpaper::Config,
|
|
||||||
pub current_directory: PathBuf,
|
|
||||||
pub fit_options: Vec<String>,
|
|
||||||
pub outputs: SingleSelectModel,
|
|
||||||
pub rotation_frequency: u64,
|
|
||||||
pub rotation_options: Vec<String>,
|
|
||||||
pub same_background: bool,
|
|
||||||
pub selected_fit: usize,
|
|
||||||
pub selected_rotation: usize,
|
|
||||||
pub selection: Context,
|
|
||||||
pub slideshow: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIT: usize = 0;
|
|
||||||
const STRETCH: usize = 1;
|
|
||||||
const ZOOM: usize = 2;
|
|
||||||
|
|
||||||
const MINUTES_5: usize = 0;
|
|
||||||
const MINUTES_10: usize = 1;
|
|
||||||
const MINUTES_15: usize = 2;
|
|
||||||
const MINUTES_30: usize = 3;
|
|
||||||
const HOUR_1: usize = 4;
|
|
||||||
const HOUR_2: usize = 5;
|
|
||||||
|
|
||||||
impl Default for Page {
|
|
||||||
fn default() -> Self {
|
|
||||||
Page {
|
|
||||||
active_output: None,
|
|
||||||
config: wallpaper::Config::default(),
|
|
||||||
current_directory: PathBuf::from("/usr/share/backgrounds/pop/"),
|
|
||||||
fit_options: vec![fl!("fit-to-screen"), fl!("stretch"), fl!("zoom")],
|
|
||||||
outputs: SingleSelectModel::default(),
|
|
||||||
rotation_frequency: 300,
|
|
||||||
rotation_options: vec![
|
|
||||||
fl!("x-minutes", number = 5),
|
|
||||||
fl!("x-minutes", number = 10),
|
|
||||||
fl!("x-minutes", number = 15),
|
|
||||||
fl!("x-minutes", number = 30),
|
|
||||||
fl!("x-hours", number = 1),
|
|
||||||
fl!("x-hours", number = 2),
|
|
||||||
],
|
|
||||||
same_background: true,
|
|
||||||
selected_fit: 0,
|
|
||||||
selected_rotation: 0,
|
|
||||||
selection: Context::default(),
|
|
||||||
slideshow: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct Context {
|
|
||||||
active: DefaultKey,
|
|
||||||
handles: SlotMap<DefaultKey, ImageHandle>,
|
|
||||||
paths: SecondaryMap<DefaultKey, PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
/// Applies the current settings to cosmic-bg.
|
|
||||||
pub fn apply(&mut self) {
|
|
||||||
let path = if self.slideshow {
|
|
||||||
&self.current_directory
|
|
||||||
} else if let Some(path) = self.selection.paths.get(self.selection.active) {
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = if self.same_background {
|
|
||||||
Output::All
|
|
||||||
} else if let Some(name) = self.outputs.active_data::<String>() {
|
|
||||||
Output::Name(name.clone())
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scaling_mode = match self.selected_fit {
|
|
||||||
FIT => ScalingMode::Fit([0.0, 0.0, 0.0]),
|
|
||||||
STRETCH => ScalingMode::Stretch,
|
|
||||||
ZOOM => ScalingMode::Zoom,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let entry = Entry::new(output.clone(), path.clone())
|
|
||||||
.scaling_mode(scaling_mode)
|
|
||||||
.rotation_frequency(self.rotation_frequency);
|
|
||||||
|
|
||||||
if output != Output::All {
|
|
||||||
self.config.backgrounds.clear();
|
|
||||||
self.config.outputs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
wallpaper::set(&mut self.config, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message) {
|
|
||||||
match message {
|
|
||||||
Message::Fit(option) => {
|
|
||||||
self.selected_fit = self
|
|
||||||
.fit_options
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, key)| **key == option)
|
|
||||||
.map_or(0, |(indice, _)| indice);
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::Output(id) => {
|
|
||||||
self.outputs.activate(id);
|
|
||||||
if let Some(name) = self.outputs.data::<String>(id) {
|
|
||||||
self.active_output = Some(name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::RotationFrequency(option) => {
|
|
||||||
self.selected_rotation = self
|
|
||||||
.fit_options
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, key)| **key == option)
|
|
||||||
.map_or(0, |(indice, _)| indice);
|
|
||||||
|
|
||||||
self.rotation_frequency = match self.selected_rotation {
|
|
||||||
MINUTES_5 => 300,
|
|
||||||
MINUTES_10 => 600,
|
|
||||||
MINUTES_15 => 900,
|
|
||||||
MINUTES_30 => 1800,
|
|
||||||
HOUR_1 => 3600,
|
|
||||||
HOUR_2 => 7200,
|
|
||||||
_ => 10800,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::SameBackground(value) => {
|
|
||||||
self.same_background = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::Select(id) => {
|
|
||||||
self.selection.active = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::Slideshow(value) => {
|
|
||||||
self.slideshow = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::Update((config, outputs, selection)) => {
|
|
||||||
self.config = config;
|
|
||||||
self.selection = selection;
|
|
||||||
self.outputs.clear();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut first = None;
|
|
||||||
for (name, model) in outputs {
|
|
||||||
let entity = self
|
|
||||||
.outputs
|
|
||||||
.insert()
|
|
||||||
.text(format!("{model} ({name})"))
|
|
||||||
.data(name);
|
|
||||||
|
|
||||||
if first.is_none() {
|
|
||||||
first = Some(entity.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = first {
|
|
||||||
self.outputs.activate(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(entry) = self
|
|
||||||
.config
|
|
||||||
.backgrounds
|
|
||||||
.iter()
|
|
||||||
.find(|b| b.output == Output::All)
|
|
||||||
{
|
|
||||||
self.same_background = true;
|
|
||||||
for (entity, path) in self.selection.paths.iter() {
|
|
||||||
if path == &entry.source {
|
|
||||||
self.selection.active = entity;
|
|
||||||
|
|
||||||
match entry.scaling_mode {
|
|
||||||
ScalingMode::Fit(_) => self.selected_fit = FIT,
|
|
||||||
ScalingMode::Stretch => self.selected_fit = STRETCH,
|
|
||||||
ScalingMode::Zoom => self.selected_fit = ZOOM,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.slideshow = path.is_dir();
|
|
||||||
|
|
||||||
match entry.rotation_frequency {
|
|
||||||
600 => self.selected_rotation = MINUTES_10,
|
|
||||||
900 => self.selected_rotation = MINUTES_15,
|
|
||||||
1800 => self.selected_rotation = MINUTES_30,
|
|
||||||
3600 => self.selected_rotation = HOUR_1,
|
|
||||||
7200 => self.selected_rotation = HOUR_2,
|
|
||||||
_ => self.selected_rotation = MINUTES_5,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.rotation_frequency = entry.rotation_frequency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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("/usr/share/backgrounds/pop/".into());
|
|
||||||
|
|
||||||
let mut update = Context::default();
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
while let Some((path, image)) = backgrounds.recv().await {
|
|
||||||
let handle =
|
|
||||||
ImageHandle::from_pixels(image.width(), image.height(), image.into_vec());
|
|
||||||
|
|
||||||
let id = update.handles.insert(handle);
|
|
||||||
update.paths.insert(id, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"loaded wallpapers in {:?}",
|
|
||||||
Instant::now().duration_since(start)
|
|
||||||
);
|
|
||||||
|
|
||||||
crate::pages::Message::DesktopWallpaper(Message::Update((config, outputs, update)))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
|
||||||
|
|
||||||
pub fn settings() -> Section<crate::pages::Message> {
|
|
||||||
Section::default()
|
|
||||||
.descriptions(vec![
|
|
||||||
fl!("wallpaper", "same"),
|
|
||||||
fl!("wallpaper", "fit"),
|
|
||||||
fl!("wallpaper", "slide"),
|
|
||||||
fl!("wallpaper", "change"),
|
|
||||||
])
|
|
||||||
.view::<Page>(|_binder, page, section| {
|
|
||||||
let descriptions = §ion.descriptions;
|
|
||||||
|
|
||||||
let mut image_column = Vec::with_capacity(page.selection.handles.len() / 4);
|
|
||||||
let mut image_handles = page.selection.handles.iter();
|
|
||||||
|
|
||||||
while let Some((id, handle)) = image_handles.next() {
|
|
||||||
let mut image_row = Vec::with_capacity(4);
|
|
||||||
|
|
||||||
image_row.push(wallpaper_button(handle, id));
|
|
||||||
|
|
||||||
for (id, handle) in image_handles.by_ref().take(3) {
|
|
||||||
image_row.push(wallpaper_button(handle, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
image_column.push(row(image_row).spacing(16).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut children = Vec::with_capacity(3);
|
|
||||||
|
|
||||||
if let Some(image) = page.selection.handles.get(page.selection.active) {
|
|
||||||
children.push(crate::widget::display_container(image.clone(), 300.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
children.push(if page.same_background {
|
|
||||||
cosmic::widget::text("All Displays")
|
|
||||||
.horizontal_alignment(Horizontal::Center)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.apply(cosmic::iced::widget::container)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.padding([0, 0, 16, 0])
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
cosmic::widget::horiontal_view_switcher(&page.outputs)
|
|
||||||
.on_activate(Message::Output)
|
|
||||||
.into()
|
|
||||||
});
|
|
||||||
|
|
||||||
let background_fit = cosmic::iced::widget::pick_list(
|
|
||||||
&page.fit_options,
|
|
||||||
page.fit_options.get(page.selected_fit).cloned(),
|
|
||||||
Message::Fit,
|
|
||||||
);
|
|
||||||
|
|
||||||
children.push({
|
|
||||||
let column = list_column()
|
|
||||||
.add(settings::item(
|
|
||||||
&descriptions[0],
|
|
||||||
toggler(None, page.same_background, Message::SameBackground),
|
|
||||||
))
|
|
||||||
.add(settings::item(&descriptions[1], background_fit))
|
|
||||||
.add(settings::item(
|
|
||||||
&descriptions[2],
|
|
||||||
toggler(None, page.slideshow, Message::Slideshow),
|
|
||||||
));
|
|
||||||
|
|
||||||
if page.slideshow {
|
|
||||||
column
|
|
||||||
.add(settings::item(
|
|
||||||
&descriptions[3],
|
|
||||||
cosmic::iced::widget::pick_list(
|
|
||||||
&page.rotation_options,
|
|
||||||
page.rotation_options.get(page.selected_rotation).cloned(),
|
|
||||||
Message::RotationFrequency,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
column.into()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
children.push(column(image_column).spacing(12).padding(0).into());
|
|
||||||
|
|
||||||
cosmic::iced::widget::column(children)
|
|
||||||
.spacing(22)
|
|
||||||
.padding(0)
|
|
||||||
.max_width(683)
|
|
||||||
.apply(Element::from)
|
|
||||||
.map(crate::pages::Message::DesktopWallpaper)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wallpaper_button(handle: &ImageHandle, id: DefaultKey) -> Element<Message> {
|
|
||||||
let image = cosmic::iced::widget::image(handle.clone()).apply(cosmic::iced::Element::from);
|
|
||||||
|
|
||||||
cosmic::iced::widget::button(image)
|
|
||||||
.width(Length::Fixed(158.0))
|
|
||||||
.height(Length::Fixed(105.0))
|
|
||||||
.style(cosmic::theme::Button::Transparent)
|
|
||||||
.on_press(Message::Select(id))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
622
app/src/pages/desktop/wallpaper/mod.rs
Normal file
622
app/src/pages/desktop/wallpaper/mod.rs
Normal file
|
|
@ -0,0 +1,622 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
use apply::Apply;
|
||||||
|
use cosmic::widget::{
|
||||||
|
list_column,
|
||||||
|
segmented_button::{self, SingleSelectModel},
|
||||||
|
settings, toggler,
|
||||||
|
};
|
||||||
|
use cosmic::{iced::alignment::Horizontal, iced::Length, Element};
|
||||||
|
use cosmic::{iced_core::alignment::Vertical, iced_runtime::core::image::Handle as ImageHandle};
|
||||||
|
use cosmic_settings_desktop::wallpaper::{self, Entry, ScalingMode};
|
||||||
|
use cosmic_settings_page::Section;
|
||||||
|
use cosmic_settings_page::{self as page, section};
|
||||||
|
use slotmap::{DefaultKey, SecondaryMap, SlotMap};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const MINUTES_5: usize = 0;
|
||||||
|
const MINUTES_10: usize = 1;
|
||||||
|
const MINUTES_15: usize = 2;
|
||||||
|
const MINUTES_30: usize = 3;
|
||||||
|
const HOUR_1: usize = 4;
|
||||||
|
const HOUR_2: usize = 5;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
ChangeCategory(String),
|
||||||
|
ColorSelect(wallpaper::Color),
|
||||||
|
Fit(String),
|
||||||
|
Output(segmented_button::Entity),
|
||||||
|
RotationFrequency(String),
|
||||||
|
SameBackground(bool),
|
||||||
|
Select(DefaultKey),
|
||||||
|
Slideshow(bool),
|
||||||
|
Update(Box<(wallpaper::Config, HashMap<String, String>, Context)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Category {
|
||||||
|
SystemBackgrounds,
|
||||||
|
Colors,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Page {
|
||||||
|
pub active_output: Option<String>,
|
||||||
|
pub active_category: usize,
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
pub config: wallpaper::Config,
|
||||||
|
pub current_directory: PathBuf,
|
||||||
|
pub fit_options: Vec<String>,
|
||||||
|
pub outputs: SingleSelectModel,
|
||||||
|
pub rotation_frequency: u64,
|
||||||
|
pub rotation_options: Vec<String>,
|
||||||
|
pub selected_fit: usize,
|
||||||
|
pub selected_rotation: usize,
|
||||||
|
pub selection: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Page {
|
||||||
|
fn default() -> Self {
|
||||||
|
Page {
|
||||||
|
active_output: None,
|
||||||
|
active_category: CATEGORY_SYSTEM_WALLPAPERS,
|
||||||
|
categories: vec![fl!("system-backgrounds"), fl!("colors")],
|
||||||
|
config: wallpaper::Config::default(),
|
||||||
|
current_directory: PathBuf::from(SYSTEM_WALLPAPER_DIR),
|
||||||
|
fit_options: vec![fl!("fit-to-screen"), fl!("stretch"), fl!("zoom")],
|
||||||
|
outputs: SingleSelectModel::default(),
|
||||||
|
rotation_frequency: 300,
|
||||||
|
rotation_options: vec![
|
||||||
|
// FIX: fluent is inserting extra unicode characters in formatting
|
||||||
|
fl!("x-minutes", number = 5)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
fl!("x-minutes", number = 10)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
fl!("x-minutes", number = 15)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
fl!("x-minutes", number = 30)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
fl!("x-hours", number = 1)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
fl!("x-hours", number = 2)
|
||||||
|
.replace('\u{2068}', "")
|
||||||
|
.replace('\u{2069}', ""),
|
||||||
|
],
|
||||||
|
selected_fit: 0,
|
||||||
|
selected_rotation: 0,
|
||||||
|
selection: Context::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_handles: SecondaryMap<DefaultKey, ImageHandle>,
|
||||||
|
selection_handles: SecondaryMap<DefaultKey, ImageHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Page {
|
||||||
|
fn config_output(&self) -> Option<String> {
|
||||||
|
if self.config.same_on_all {
|
||||||
|
Some(String::from("all"))
|
||||||
|
} else {
|
||||||
|
self.outputs.active_data::<String>().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the current settings to cosmic-bg.
|
||||||
|
pub fn config_apply(&mut self) {
|
||||||
|
let Some(output) = self.config_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.config.same_on_all {
|
||||||
|
self.config.outputs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = match self.selection.active {
|
||||||
|
Choice::Slideshow => {
|
||||||
|
match self.config_background_entry(output.clone(), self.current_directory.clone()) {
|
||||||
|
Some(entry) => entry,
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Choice::Background(key) => {
|
||||||
|
if let Some(path) = self.selection.paths.get(key) {
|
||||||
|
match self.config_background_entry(output.clone(), path.clone()) {
|
||||||
|
Some(entry) => entry,
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Choice::Color(ref color) => {
|
||||||
|
Entry::new(output.clone(), wallpaper::Source::Color(color.clone()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if output != "all" {
|
||||||
|
self.config.backgrounds.clear();
|
||||||
|
self.config.outputs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaper::set(&mut self.config, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]),
|
||||||
|
STRETCH => ScalingMode::Stretch,
|
||||||
|
ZOOM => ScalingMode::Zoom,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Entry::new(output, wallpaper::Source::Path(path))
|
||||||
|
.scaling_mode(scaling_mode)
|
||||||
|
.rotation_frequency(self.rotation_frequency)
|
||||||
|
.apply(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_update(
|
||||||
|
&mut self,
|
||||||
|
config: wallpaper::Config,
|
||||||
|
displays: HashMap<String, String>,
|
||||||
|
selection: Context,
|
||||||
|
) {
|
||||||
|
self.config = config;
|
||||||
|
self.selection = selection;
|
||||||
|
self.outputs.clear();
|
||||||
|
|
||||||
|
let mut first = None;
|
||||||
|
for (name, model) in displays {
|
||||||
|
let entity = self
|
||||||
|
.outputs
|
||||||
|
.insert()
|
||||||
|
.text(format!("{model} ({name})"))
|
||||||
|
.data(name);
|
||||||
|
|
||||||
|
if first.is_none() {
|
||||||
|
first = Some(entity.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = first {
|
||||||
|
self.outputs.activate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.same_on_all || self.config.backgrounds.is_empty() {
|
||||||
|
let entry = self.config.default_background.clone();
|
||||||
|
self.select_background_entry(&entry);
|
||||||
|
|
||||||
|
if let Some(current) = entry_directory(&entry) {
|
||||||
|
self.current_directory = current;
|
||||||
|
}
|
||||||
|
} else if let Some(data) = self.outputs.active_data::<String>() {
|
||||||
|
let mut backgrounds = Vec::new();
|
||||||
|
std::mem::swap(&mut self.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mem::swap(&mut self.config.backgrounds, &mut backgrounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the selection category, such as wallpaper select or color select.
|
||||||
|
fn change_category(&mut self, category: &str) {
|
||||||
|
if let Some(pos) = self.categories.iter().position(|c| c == category) {
|
||||||
|
self.active_category = pos;
|
||||||
|
match pos {
|
||||||
|
CATEGORY_SYSTEM_WALLPAPERS => {
|
||||||
|
self.select_first_background();
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY_COLOR => {
|
||||||
|
self.selection.active = Choice::Color(wallpaper::DEFAULT_COLORS[0].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the output being configured
|
||||||
|
pub fn change_output(&mut self, entity: segmented_button::Entity) {
|
||||||
|
self.outputs.activate(entity);
|
||||||
|
if let Some(name) = self.outputs.data::<String>(entity) {
|
||||||
|
self.active_output = Some(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes the slideshow background rotation frequency
|
||||||
|
pub fn change_rotation_frequency(&mut self, option: &str) {
|
||||||
|
self.selected_rotation = self
|
||||||
|
.rotation_options
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, key)| **key == option)
|
||||||
|
.map_or(0, |(indice, _)| indice);
|
||||||
|
|
||||||
|
self.rotation_frequency = match self.selected_rotation {
|
||||||
|
MINUTES_5 => 300,
|
||||||
|
MINUTES_10 => 600,
|
||||||
|
MINUTES_15 => 900,
|
||||||
|
MINUTES_30 => 1800,
|
||||||
|
HOUR_1 => 3600,
|
||||||
|
HOUR_2 => 7200,
|
||||||
|
_ => 10800,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn update(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::ChangeCategory(category) => self.change_category(&category),
|
||||||
|
|
||||||
|
Message::ColorSelect(color) => {
|
||||||
|
self.selection.active = Choice::Color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Fit(option) => {
|
||||||
|
self.selected_fit = self
|
||||||
|
.fit_options
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, key)| **key == option)
|
||||||
|
.map_or(0, |(indice, _)| indice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Output(id) => self.change_output(id),
|
||||||
|
|
||||||
|
Message::RotationFrequency(option) => self.change_rotation_frequency(&option),
|
||||||
|
|
||||||
|
Message::SameBackground(value) => {
|
||||||
|
self.config.same_on_all = value;
|
||||||
|
self.config.backgrounds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Select(id) => {
|
||||||
|
self.selection.active = Choice::Background(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Slideshow(enable) => {
|
||||||
|
if enable {
|
||||||
|
self.selection.active = Choice::Slideshow;
|
||||||
|
} else {
|
||||||
|
self.select_first_background();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Update(update) => self.config_update(update.0, update.1, update.2),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.config_apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the given background entry.
|
||||||
|
fn select_background_entry(&mut self, entry: &wallpaper::Entry) {
|
||||||
|
match entry.source {
|
||||||
|
wallpaper::Source::Path(ref path) => {
|
||||||
|
if path.is_dir() {
|
||||||
|
self.selection.active = Choice::Slideshow;
|
||||||
|
} else if let Some(entity) = self.background_id_from_path(path) {
|
||||||
|
self.select_background(entry, entity, path.is_dir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaper::Source::Color(ref color) => {
|
||||||
|
self.selection.active = Choice::Color(color.clone());
|
||||||
|
self.active_category = CATEGORY_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the given background
|
||||||
|
fn select_background(
|
||||||
|
&mut self,
|
||||||
|
entry: &wallpaper::Entry,
|
||||||
|
entity: DefaultKey,
|
||||||
|
is_slideshow: bool,
|
||||||
|
) {
|
||||||
|
self.selection.active = if is_slideshow {
|
||||||
|
Choice::Slideshow
|
||||||
|
} else {
|
||||||
|
Choice::Background(entity)
|
||||||
|
};
|
||||||
|
|
||||||
|
match entry.scaling_mode {
|
||||||
|
ScalingMode::Fit(_) => self.selected_fit = FIT,
|
||||||
|
ScalingMode::Stretch => self.selected_fit = STRETCH,
|
||||||
|
ScalingMode::Zoom => self.selected_fit = ZOOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
match entry.rotation_frequency {
|
||||||
|
600 => self.selected_rotation = MINUTES_10,
|
||||||
|
900 => self.selected_rotation = MINUTES_15,
|
||||||
|
1800 => self.selected_rotation = MINUTES_30,
|
||||||
|
3600 => self.selected_rotation = HOUR_1,
|
||||||
|
7200 => self.selected_rotation = HOUR_2,
|
||||||
|
_ => self.selected_rotation = MINUTES_5,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rotation_frequency = entry.rotation_frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locate the ID of a background that's already stored in memory
|
||||||
|
fn background_id_from_path(&self, path: &Path) -> Option<DefaultKey> {
|
||||||
|
self.selection
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.find(|(_id, background)| *background == path)
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
|
||||||
|
Some(Box::pin(async move {
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let display_handle = ImageHandle::from_pixels(
|
||||||
|
display_image.width(),
|
||||||
|
display_image.height(),
|
||||||
|
display_image.into_vec(),
|
||||||
|
);
|
||||||
|
|
||||||
|
update.display_handles.insert(id, display_handle);
|
||||||
|
|
||||||
|
let selection_handle = ImageHandle::from_pixels(
|
||||||
|
selection_image.width(),
|
||||||
|
selection_image.height(),
|
||||||
|
selection_image.into_vec(),
|
||||||
|
);
|
||||||
|
update.selection_handles.insert(id, selection_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"loaded wallpapers in {:?}",
|
||||||
|
Instant::now().duration_since(start)
|
||||||
|
);
|
||||||
|
|
||||||
|
crate::pages::Message::DesktopWallpaper(Message::Update(Box::new((
|
||||||
|
config, outputs, update,
|
||||||
|
))))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn settings() -> Section<crate::pages::Message> {
|
||||||
|
Section::default()
|
||||||
|
.descriptions(vec![
|
||||||
|
fl!("wallpaper", "same"),
|
||||||
|
fl!("wallpaper", "fit"),
|
||||||
|
fl!("wallpaper", "slide"),
|
||||||
|
fl!("wallpaper", "change"),
|
||||||
|
])
|
||||||
|
.view::<Page>(|_binder, page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
|
||||||
|
let mut children = Vec::with_capacity(3);
|
||||||
|
|
||||||
|
let mut show_slideshow_toggle = true;
|
||||||
|
let mut slideshow_enabled = false;
|
||||||
|
|
||||||
|
let display_demo = match page.selection.active {
|
||||||
|
// Shows background options, with the slideshow toggle enabled
|
||||||
|
Choice::Slideshow => {
|
||||||
|
slideshow_enabled = true;
|
||||||
|
page.selection
|
||||||
|
.display_handles
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.map(|handle| {
|
||||||
|
cosmic::iced::widget::image(handle.clone())
|
||||||
|
.width(Length::Fixed(300.0))
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows background options, with the slideshow toggle visible
|
||||||
|
Choice::Background(key) => page.selection.display_handles.get(key).map(|handle| {
|
||||||
|
cosmic::iced::widget::image(handle.clone())
|
||||||
|
.width(Length::Fixed(300.0))
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Displays color options, and hides the slideshow toggle
|
||||||
|
Choice::Color(ref color) => {
|
||||||
|
show_slideshow_toggle = false;
|
||||||
|
Some(widgets::color_image(color.clone(), 300, 169, 0.0))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(element) = display_demo {
|
||||||
|
children.push(crate::widget::display_container(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(if page.config.same_on_all {
|
||||||
|
cosmic::widget::text(fl!("all-displays"))
|
||||||
|
.font(cosmic::font::FONT_SEMIBOLD)
|
||||||
|
.horizontal_alignment(Horizontal::Center)
|
||||||
|
.vertical_alignment(Vertical::Center)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.apply(cosmic::iced::widget::container)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(32.0))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
cosmic::widget::horizontal_segmented_selection(&page.outputs)
|
||||||
|
.on_activate(Message::Output)
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
let background_fit = cosmic::iced::widget::pick_list(
|
||||||
|
&page.fit_options,
|
||||||
|
page.fit_options.get(page.selected_fit).cloned(),
|
||||||
|
Message::Fit,
|
||||||
|
);
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
let mut column = list_column()
|
||||||
|
.add(settings::item(
|
||||||
|
&descriptions[0],
|
||||||
|
toggler(None, page.config.same_on_all, Message::SameBackground),
|
||||||
|
))
|
||||||
|
.add(settings::item(&descriptions[1], background_fit));
|
||||||
|
|
||||||
|
if show_slideshow_toggle {
|
||||||
|
column = column.add(settings::item(
|
||||||
|
&descriptions[2],
|
||||||
|
toggler(None, slideshow_enabled, Message::Slideshow),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rotation frequency pick list should only be shown when the slideshow is enabled.
|
||||||
|
if slideshow_enabled {
|
||||||
|
column
|
||||||
|
.add(settings::item(
|
||||||
|
&descriptions[3],
|
||||||
|
cosmic::iced::widget::pick_list(
|
||||||
|
&page.rotation_options,
|
||||||
|
page.rotation_options.get(page.selected_rotation).cloned(),
|
||||||
|
Message::RotationFrequency,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
column.into()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let category_selection = cosmic::iced::widget::pick_list(
|
||||||
|
&page.categories,
|
||||||
|
Some(page.categories[page.active_category].clone()),
|
||||||
|
Message::ChangeCategory,
|
||||||
|
);
|
||||||
|
|
||||||
|
children.push(category_selection.into());
|
||||||
|
|
||||||
|
match page.active_category {
|
||||||
|
// Displays system wallpapers that are available to select from
|
||||||
|
CATEGORY_SYSTEM_WALLPAPERS => {
|
||||||
|
children.push(widgets::wallpaper_select_options(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Displays colors and gradients that are available to select from
|
||||||
|
CATEGORY_COLOR => {
|
||||||
|
children.push(widgets::color_select_options());
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmic::iced::widget::column(children)
|
||||||
|
.spacing(22)
|
||||||
|
.padding(0)
|
||||||
|
.max_width(683)
|
||||||
|
.apply(Element::from)
|
||||||
|
.map(crate::pages::Message::DesktopWallpaper)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the current wallpaper directory.
|
||||||
|
fn entry_directory(entry: &wallpaper::Entry) -> Option<PathBuf> {
|
||||||
|
Some(match entry.source {
|
||||||
|
wallpaper::Source::Path(ref path) => {
|
||||||
|
if path.is_dir() {
|
||||||
|
path.clone()
|
||||||
|
} else if let Some(path) = path.parent() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaper::Source::Color(_) => PathBuf::from(SYSTEM_WALLPAPER_DIR),
|
||||||
|
})
|
||||||
|
}
|
||||||
113
app/src/pages/desktop/wallpaper/widgets.rs
Normal file
113
app/src/pages/desktop/wallpaper/widgets.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use super::Message;
|
||||||
|
use apply::Apply;
|
||||||
|
use cosmic::iced_core::{self, gradient::Linear, Background, BorderRadius, Color, Degrees, Length};
|
||||||
|
use cosmic::iced_runtime::core::image::Handle as ImageHandle;
|
||||||
|
use cosmic::{
|
||||||
|
iced,
|
||||||
|
iced_widget::{column, row},
|
||||||
|
Element,
|
||||||
|
};
|
||||||
|
use cosmic_settings_desktop::wallpaper;
|
||||||
|
use slotmap::DefaultKey;
|
||||||
|
|
||||||
|
/// A button for selecting a color or gradient.
|
||||||
|
pub fn color_button(color: wallpaper::Color) -> Element<'static, Message> {
|
||||||
|
iced::widget::button(color_image(color.clone(), 70, 70, 8.0))
|
||||||
|
.width(Length::Fixed(71.0))
|
||||||
|
.height(Length::Fixed(71.0))
|
||||||
|
.style(cosmic::theme::Button::Transparent)
|
||||||
|
.on_press(Message::ColorSelect(color))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sized container that's filled with a color or gradient.
|
||||||
|
pub fn color_image(
|
||||||
|
color: wallpaper::Color,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
border_radius: f32,
|
||||||
|
) -> Element<'static, Message> {
|
||||||
|
iced::widget::container(iced::widget::space::Space::new(width, height))
|
||||||
|
.style(cosmic::theme::Container::custom(move |_theme| {
|
||||||
|
iced::widget::container::Appearance {
|
||||||
|
text_color: None,
|
||||||
|
background: Some(match &color {
|
||||||
|
wallpaper::Color::Single([r, g, b]) => {
|
||||||
|
Background::Color(Color::from_rgb(*r, *g, *b))
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpaper::Color::Gradient(wallpaper::Gradient { colors, radius }) => {
|
||||||
|
let stop_increment = 1.0 / (colors.len() - 1) as f32;
|
||||||
|
let mut stop = 0.0;
|
||||||
|
|
||||||
|
let mut linear = Linear::new(Degrees(*radius));
|
||||||
|
|
||||||
|
for &[r, g, b] in &**colors {
|
||||||
|
linear = linear.add_stop(stop, iced::Color::from_rgb(r, g, b));
|
||||||
|
stop += stop_increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
Background::Gradient(iced_core::Gradient::Linear(linear))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
border_radius: BorderRadius::from(border_radius),
|
||||||
|
border_width: 0.0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color selection list
|
||||||
|
pub fn color_select_options() -> Element<'static, Message> {
|
||||||
|
let mut color_column = Vec::with_capacity(wallpaper::DEFAULT_COLORS.len() / 8);
|
||||||
|
let mut colors = wallpaper::DEFAULT_COLORS.iter();
|
||||||
|
|
||||||
|
while let Some(color) = colors.next() {
|
||||||
|
let mut color_row = Vec::with_capacity(8);
|
||||||
|
|
||||||
|
color_row.push(color_button(color.clone()));
|
||||||
|
|
||||||
|
for color in colors.by_ref().take(7) {
|
||||||
|
color_row.push(color_button(color.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
color_column.push(row(color_row).spacing(16).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
column(color_column).spacing(12).padding(0).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Background selection list
|
||||||
|
pub fn wallpaper_select_options(page: &super::Page) -> Element<Message> {
|
||||||
|
let mut image_column = Vec::with_capacity(page.selection.selection_handles.len() / 4);
|
||||||
|
let mut image_handles = page.selection.selection_handles.iter();
|
||||||
|
|
||||||
|
while let Some((id, handle)) = image_handles.next() {
|
||||||
|
let mut image_row = Vec::with_capacity(4);
|
||||||
|
|
||||||
|
image_row.push(wallpaper_button(handle, id));
|
||||||
|
|
||||||
|
for (id, handle) in image_handles.by_ref().take(3) {
|
||||||
|
image_row.push(wallpaper_button(handle, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
image_column.push(row(image_row).spacing(16).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
column(image_column).spacing(12).padding(0).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wallpaper_button(handle: &ImageHandle, id: DefaultKey) -> Element<Message> {
|
||||||
|
let image = iced::widget::image(handle.clone()).apply(iced::Element::from);
|
||||||
|
|
||||||
|
iced::widget::button(image)
|
||||||
|
.width(Length::Fixed(158.0))
|
||||||
|
.height(Length::Fixed(105.0))
|
||||||
|
.style(cosmic::theme::Button::Transparent)
|
||||||
|
.on_press(Message::Select(id))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
@ -2,15 +2,12 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use cosmic::widget::{divider, icon, list, settings, text};
|
use cosmic::iced::{
|
||||||
use cosmic::{
|
self,
|
||||||
iced::{
|
widget::{button, column, container, horizontal_space, row, vertical_space, Button},
|
||||||
self,
|
Length,
|
||||||
widget::{button, column, container, horizontal_space, row, vertical_space, Button},
|
|
||||||
Length,
|
|
||||||
},
|
|
||||||
iced_widget::core::image,
|
|
||||||
};
|
};
|
||||||
|
use cosmic::widget::{divider, icon, list, settings, text};
|
||||||
use cosmic::{theme, Element};
|
use cosmic::{theme, Element};
|
||||||
use cosmic_settings_page as page;
|
use cosmic_settings_page as page;
|
||||||
|
|
||||||
|
|
@ -136,13 +133,10 @@ pub fn unimplemented_page<Message: 'static>() -> Element<'static, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn display_container<'a, Message: 'a>(
|
pub fn display_container<'a, Message: 'a>(widget: Element<'a, Message>) -> Element<'a, Message> {
|
||||||
image: image::Handle,
|
|
||||||
width: f32,
|
|
||||||
) -> cosmic::Element<'a, Message> {
|
|
||||||
row!(
|
row!(
|
||||||
horizontal_space(Length::Fill),
|
horizontal_space(Length::Fill),
|
||||||
container(cosmic::iced::widget::image(image).width(Length::Fixed(width)))
|
container(widget)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.style(crate::theme::display_container()),
|
.style(crate::theme::display_container()),
|
||||||
horizontal_space(Length::Fill),
|
horizontal_space(Length::Fill),
|
||||||
|
|
|
||||||
|
|
@ -104,12 +104,14 @@ wallpaper = Wallpaper
|
||||||
.slide = Slideshow
|
.slide = Slideshow
|
||||||
.change = Change image every
|
.change = Change image every
|
||||||
|
|
||||||
|
all-displays = All Displays
|
||||||
|
colors = Colors
|
||||||
fit-to-screen = Fit to Screen
|
fit-to-screen = Fit to Screen
|
||||||
stretch = Stretch
|
stretch = Stretch
|
||||||
|
system-backgrounds = System backgrounds
|
||||||
zoom = Zoom
|
zoom = Zoom
|
||||||
|
|
||||||
x-minutes = { $number } minutes
|
x-minutes = { $number } minutes
|
||||||
|
|
||||||
x-hours = { $number ->
|
x-hours = { $number ->
|
||||||
[1] 1 hour
|
[1] 1 hour
|
||||||
*[other] { $number } hours
|
*[other] { $number } hours
|
||||||
|
|
|
||||||
6
justfile
6
justfile
|
|
@ -29,9 +29,7 @@ desktop-src := 'resources' / desktop
|
||||||
desktop-dest := clean(rootdir / prefix) / 'share' / 'applications' / desktop
|
desktop-dest := clean(rootdir / prefix) / 'share' / 'applications' / desktop
|
||||||
|
|
||||||
[private]
|
[private]
|
||||||
help:
|
default: build-release
|
||||||
echo $RUSTFLAGS
|
|
||||||
@just -l
|
|
||||||
|
|
||||||
# Remove Cargo build artifacts
|
# Remove Cargo build artifacts
|
||||||
clean:
|
clean:
|
||||||
|
|
@ -74,7 +72,7 @@ install: (install-bin bin-src bin-dest) (install-file desktop-src desktop-dest)
|
||||||
|
|
||||||
# Run the application for testing purposes
|
# Run the application for testing purposes
|
||||||
run *args:
|
run *args:
|
||||||
env RUST_BACKTRACE=full cargo run --release {{args}}
|
env RUST_LOG=debug RUST_BACKTRACE=full cargo run --release {{args}}
|
||||||
|
|
||||||
# Run `cargo test`
|
# Run `cargo test`
|
||||||
test:
|
test:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
pub use cosmic_bg_config::{Config, Entry, Output, ScalingMode};
|
pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source};
|
||||||
|
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::{hash_map::DefaultHasher, HashMap},
|
collections::{hash_map::DefaultHasher, HashMap},
|
||||||
fs::DirEntry,
|
fs::DirEntry,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
|
@ -9,6 +11,39 @@ use std::{
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::{self, Receiver};
|
use tokio::sync::mpsc::{self, Receiver};
|
||||||
|
|
||||||
|
pub const DEFAULT_COLORS: &[Color] = &[
|
||||||
|
Color::Single([0.580, 0.922, 0.922]),
|
||||||
|
Color::Single([0.000, 0.286, 0.427]),
|
||||||
|
Color::Single([1.000, 0.678, 0.000]),
|
||||||
|
Color::Single([0.282, 0.725, 0.78]),
|
||||||
|
Color::Single([0.333, 0.278, 0.259]),
|
||||||
|
Color::Single([0.969, 0.878, 0.384]),
|
||||||
|
Color::Single([0.063, 0.165, 0.298]),
|
||||||
|
Color::Single([1.000, 0.843, 0.631]),
|
||||||
|
Color::Single([0.976, 0.227, 0.514]),
|
||||||
|
Color::Single([1.000, 0.612, 0.867]),
|
||||||
|
Color::Single([0.812, 0.490, 1.000]),
|
||||||
|
Color::Single([0.835, 0.549, 1.000]),
|
||||||
|
Color::Single([0.243, 0.533, 1.000]),
|
||||||
|
Color::Single([0.584, 0.769, 0.988]),
|
||||||
|
Color::Gradient(Gradient {
|
||||||
|
colors: Cow::Borrowed(&[[1.000, 0.678, 0.000], [0.282, 0.725, 0.78]]),
|
||||||
|
radius: 270.0,
|
||||||
|
}),
|
||||||
|
Color::Gradient(Gradient {
|
||||||
|
colors: Cow::Borrowed(&[[1.000, 0.843, 0.631], [0.58, 0.922, 0.922]]),
|
||||||
|
radius: 270.0,
|
||||||
|
}),
|
||||||
|
Color::Gradient(Gradient {
|
||||||
|
colors: Cow::Borrowed(&[[1.000, 0.612, 0.867], [0.976, 0.29, 0.514]]),
|
||||||
|
radius: 270.0,
|
||||||
|
}),
|
||||||
|
Color::Gradient(Gradient {
|
||||||
|
colors: Cow::Borrowed(&[[0.584, 0.769, 0.988], [0.063, 0.165, 0.298]]),
|
||||||
|
radius: 270.0,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
pub fn config() -> (Config, HashMap<String, String>) {
|
pub fn config() -> (Config, HashMap<String, String>) {
|
||||||
let mut displays = HashMap::new();
|
let mut displays = HashMap::new();
|
||||||
|
|
||||||
|
|
@ -36,11 +71,13 @@ pub fn config() -> (Config, HashMap<String, String>) {
|
||||||
pub fn set(config: &mut Config, entry: Entry) {
|
pub fn set(config: &mut Config, entry: Entry) {
|
||||||
if let Ok(context) = Config::helper() {
|
if let Ok(context) = Config::helper() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"setting wallpaper for {} to {}",
|
output = entry.output.to_string(),
|
||||||
entry.output.to_string(),
|
source = ?entry.source,
|
||||||
entry.source.display()
|
"setting wallpaper",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let _res = Config::set_same_on_all(&context, config.same_on_all);
|
||||||
|
|
||||||
if let Err(why) = config.set_entry(&context, entry) {
|
if let Err(why) = config.set_entry(&context, entry) {
|
||||||
tracing::error!(?why, "failed to set background");
|
tracing::error!(?why, "failed to set background");
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +96,7 @@ pub fn cache_dir() -> Option<PathBuf> {
|
||||||
|
|
||||||
/// Loads wallpapers in parallel by spawning tasks with a rayon thread pool.
|
/// Loads wallpapers in parallel by spawning tasks with a rayon thread pool.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage)> {
|
pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaImage)> {
|
||||||
let cache_dir = Arc::new(cache_dir());
|
let cache_dir = Arc::new(cache_dir());
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(1);
|
let (tx, rx) = mpsc::channel(1);
|
||||||
|
|
@ -82,10 +119,23 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage)> {
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let cache_dir = cache_dir.clone();
|
let cache_dir = cache_dir.clone();
|
||||||
rayon::spawn_fifo(move || {
|
rayon::spawn_fifo(move || {
|
||||||
let thumbnail =
|
let display_thumbnail =
|
||||||
load_thumbnail(cache_dir.as_deref(), &path, &entry, 300, 169);
|
load_thumbnail(cache_dir.as_deref(), &path, &entry, 300, 169);
|
||||||
if let Some(image) = thumbnail {
|
|
||||||
let _res = tx.blocking_send((path, image));
|
if let Some(display_thumbnail) = display_thumbnail {
|
||||||
|
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,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -141,3 +191,120 @@ pub fn load_thumbnail(
|
||||||
image.into_rgba8()
|
image.into_rgba8()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2
|
||||||
|
fn round(img: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>, radius: [u32; 4]) {
|
||||||
|
let (width, height) = img.dimensions();
|
||||||
|
assert!(radius[0] + radius[1] <= width);
|
||||||
|
assert!(radius[3] + radius[2] <= width);
|
||||||
|
assert!(radius[0] + radius[3] <= height);
|
||||||
|
assert!(radius[1] + radius[2] <= height);
|
||||||
|
|
||||||
|
// top left
|
||||||
|
border_radius(img, radius[0], |x, y| (x - 1, y - 1));
|
||||||
|
// top right
|
||||||
|
border_radius(img, radius[1], |x, y| (width - x, y - 1));
|
||||||
|
// bottom right
|
||||||
|
border_radius(img, radius[2], |x, y| (width - x, height - y));
|
||||||
|
// bottom left
|
||||||
|
border_radius(img, radius[3], |x, y| (x - 1, height - y));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn border_radius(
|
||||||
|
img: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>,
|
||||||
|
r: u32,
|
||||||
|
coordinates: impl Fn(u32, u32) -> (u32, u32),
|
||||||
|
) {
|
||||||
|
if r == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let r0 = r;
|
||||||
|
|
||||||
|
// 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8!
|
||||||
|
let r = 16 * r;
|
||||||
|
|
||||||
|
let mut x = 0;
|
||||||
|
let mut y = r - 1;
|
||||||
|
let mut p: i32 = 2 - r as i32;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
let mut alpha: u16 = 0;
|
||||||
|
let mut skip_draw = true;
|
||||||
|
|
||||||
|
let draw = |img: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>, alpha, x, y| {
|
||||||
|
debug_assert!((1..=256).contains(&alpha));
|
||||||
|
let pixel_alpha = &mut img[coordinates(r0 - x, r0 - y)].0[3];
|
||||||
|
*pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8;
|
||||||
|
};
|
||||||
|
|
||||||
|
'l: loop {
|
||||||
|
// (comments for bottom_right case:)
|
||||||
|
// remove contents below current position
|
||||||
|
{
|
||||||
|
let i = x / 16;
|
||||||
|
for j in y / 16 + 1..r0 {
|
||||||
|
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove contents right of current position mirrored
|
||||||
|
{
|
||||||
|
let j = x / 16;
|
||||||
|
for i in y / 16 + 1..r0 {
|
||||||
|
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw when moving to next pixel in x-direction
|
||||||
|
if !skip_draw {
|
||||||
|
draw(img, alpha, x / 16 - 1, y / 16);
|
||||||
|
draw(img, alpha, y / 16, x / 16 - 1);
|
||||||
|
alpha = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..16 {
|
||||||
|
skip_draw = false;
|
||||||
|
|
||||||
|
if x >= y {
|
||||||
|
break 'l;
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha += y as u16 % 16 + 1;
|
||||||
|
if p < 0 {
|
||||||
|
x += 1;
|
||||||
|
p += (2 * x + 2) as i32;
|
||||||
|
} else {
|
||||||
|
// draw when moving to next pixel in y-direction
|
||||||
|
if y % 16 == 0 {
|
||||||
|
draw(img, alpha, x / 16, y / 16);
|
||||||
|
draw(img, alpha, y / 16, x / 16);
|
||||||
|
skip_draw = true;
|
||||||
|
alpha = (x + 1) as u16 % 16 * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += 1;
|
||||||
|
p -= (2 * (y - x) + 2) as i32;
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// one corner pixel left
|
||||||
|
if x / 16 == y / 16 {
|
||||||
|
// column under current position possibly not yet accounted
|
||||||
|
if x == y {
|
||||||
|
alpha += y as u16 % 16 + 1;
|
||||||
|
}
|
||||||
|
let s = y as u16 % 16 + 1;
|
||||||
|
let alpha = 2 * alpha - s * s;
|
||||||
|
draw(img, alpha, x / 16, y / 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove remaining square of content in the corner
|
||||||
|
let range = y / 16 + 1..r0;
|
||||||
|
for i in range.clone() {
|
||||||
|
for j in range.clone() {
|
||||||
|
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue