feat: implement wallpaper settings page

This commit is contained in:
Michael Murphy 2023-06-23 17:18:05 +02:00 committed by GitHub
parent 3681a0987e
commit 900bb45758
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1009 additions and 480 deletions

View file

@ -193,8 +193,8 @@ impl Application for SettingsApp {
|(_, e)| match e {
Ok(config) => Message::PanelConfig(config),
Err((errors, config)) => {
for error in errors {
log::error!("Error loading panel config: {:?}", error);
for why in errors {
tracing::error!(?why, "panel config load error");
}
Message::PanelConfig(config)
}
@ -209,6 +209,7 @@ impl Application for SettingsApp {
let mut ret = Command::none();
match message {
Message::WindowResize(width, _height) => {
tracing::debug!(width, "new window width");
let break_point = (600.0 * self.scaling_factor) as u32;
self.window_width = width;
self.is_condensed = self.window_width < break_point;
@ -281,13 +282,11 @@ impl Application for SettingsApp {
return self.activate_page(page);
}
crate::pages::Message::Panel(message) => {
if let Some(page) = self.pages.page_mut::<panel::Page>() {
page.update(message);
}
page::update!(self.pages, message, panel::Page);
}
crate::pages::Message::Applet(message) => {
if let Some(page) = self.pages.page_mut::<applets::Page>() {
ret = page.update(message);
return page.update(message);
}
}
},

View file

@ -23,6 +23,7 @@ use cosmic::{
iced_sctk::settings::InitialSurface,
};
use i18n_embed::DesktopLanguageRequester;
use tracing_subscriber::prelude::*;
/// # Errors
///
@ -34,30 +35,12 @@ pub fn main() -> color_eyre::Result<()> {
std::env::set_var("RUST_SPANTRACE", "0");
}
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "info");
}
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");
}
init_logger();
init_localizer();
cosmic::settings::set_default_icon_theme("Pop");
let mut settings = cosmic::settings();
settings.default_text_size = 14.0;
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
title: Some(fl!("app")),
size_limits: Limits::NONE.min_width(600.0).min_height(300.0),
@ -69,3 +52,39 @@ pub fn main() -> color_eyre::Result<()> {
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();
}

View file

@ -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 = &section.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()
}

View 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 = &section.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),
})
}

View 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()
}

View file

@ -2,15 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-only
use apply::Apply;
use cosmic::widget::{divider, icon, list, settings, text};
use cosmic::{
iced::{
self,
widget::{button, column, container, horizontal_space, row, vertical_space, Button},
Length,
},
iced_widget::core::image,
use cosmic::iced::{
self,
widget::{button, column, container, horizontal_space, row, vertical_space, Button},
Length,
};
use cosmic::widget::{divider, icon, list, settings, text};
use cosmic::{theme, Element};
use cosmic_settings_page as page;
@ -136,13 +133,10 @@ pub fn unimplemented_page<Message: 'static>() -> Element<'static, Message> {
}
#[must_use]
pub fn display_container<'a, Message: 'a>(
image: image::Handle,
width: f32,
) -> cosmic::Element<'a, Message> {
pub fn display_container<'a, Message: 'a>(widget: Element<'a, Message>) -> Element<'a, Message> {
row!(
horizontal_space(Length::Fill),
container(cosmic::iced::widget::image(image).width(Length::Fixed(width)))
container(widget)
.padding(4)
.style(crate::theme::display_container()),
horizontal_space(Length::Fill),