From b4d956821c6b365adeb5bb4fd0d555a3bb90193d Mon Sep 17 00:00:00 2001 From: Danilo Spinella Date: Wed, 26 Jun 2024 12:25:22 +0200 Subject: [PATCH] feat(config): Add group option to share wallpaper Add an option to share the same wallpaper between multiple displays when using random sorting. Fixes #74. --- CHANGELOG.md | 1 + README.md | 2 + daemon/src/image_picker.rs | 231 +++++++++++++++++++++++++-------- daemon/src/ipc_server.rs | 4 +- daemon/src/main.rs | 29 ++++- daemon/src/surface.rs | 24 ++-- daemon/src/wallpaper_groups.rs | 63 +++++++++ daemon/src/wallpaper_info.rs | 3 + daemon/src/wpaperd.rs | 6 + 9 files changed, 291 insertions(+), 72 deletions(-) create mode 100644 daemon/src/wallpaper_groups.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ad0eb2..4c7b2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add `offset` configuration to move the wallpaper from its center - Add `fit-border-color` background mode - Add `initial-transition` configuration to disable the startup transition if needed +- Add `group` configuration to share the same wallpaper between multiple displays # 1.0.1 diff --git a/README.md b/README.md index 8670cc7..e3c7c44 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ represents a different display and can contain the following keys: This is only valid when path points to a directory. (_Optional_) - `sorting`, choose the sorting order. Valid options are `ascending`, `descending`, and `random`, with the default being `random`. This is only valid when path points to a directory. (_Optional_) +- `group`, assign multiple displays to same group to share the same wallpaper when using + `random` sorting; group must be a number. (_Optional_) - `mode`, choose how to display the wallpaper when the size is different than the display resolution: - `fit` shows the entire image with black corners covering the empty space left diff --git a/daemon/src/image_picker.rs b/daemon/src/image_picker.rs index 6c9f197..64f8a22 100644 --- a/daemon/src/image_picker.rs +++ b/daemon/src/image_picker.rs @@ -7,14 +7,17 @@ use std::{ }; use log::warn; +use smithay_client_toolkit::reexports::client::{protocol::wl_surface::WlSurface, QueueHandle}; use crate::{ filelist_cache::FilelistCache, + wallpaper_groups::{WallpaperGroup, WallpaperGroups}, wallpaper_info::{Sorting, WallpaperInfo}, + wpaperd::Wpaperd, }; #[derive(Debug)] -struct Queue { +pub struct Queue { buffer: VecDeque, current: usize, tail: usize, @@ -22,7 +25,7 @@ struct Queue { } impl Queue { - fn with_capacity(size: usize) -> Self { + pub fn with_capacity(size: usize) -> Self { Self { buffer: VecDeque::with_capacity(size), current: 0, @@ -121,18 +124,41 @@ enum ImagePickerAction { Previous, } +struct GroupedRandom { + surface: WlSurface, + group: Rc>, +} + +impl Drop for GroupedRandom { + fn drop(&mut self) { + self.group.borrow_mut().surfaces.remove(&self.surface); + } +} + enum ImagePickerSorting { Random(Queue), + GroupedRandom(GroupedRandom), Ascending(usize), Descending(usize), } impl ImagePickerSorting { - fn new(wallpaper_info: &WallpaperInfo, files_len: usize) -> Self { + fn new( + wallpaper_info: &WallpaperInfo, + wl_surface: &WlSurface, + files_len: usize, + groups: Rc>, + ) -> Self { match wallpaper_info.sorting { None | Some(Sorting::Random) => { Self::new_random(wallpaper_info.drawn_images_queue_size) } + Some(Sorting::GroupedRandom { group }) => Self::new_grouped_random( + groups, + group, + wl_surface, + wallpaper_info.drawn_images_queue_size, + ), Some(Sorting::Ascending) => Self::new_ascending(files_len), Some(Sorting::Descending) => Self::new_descending(), } @@ -149,6 +175,20 @@ impl ImagePickerSorting { fn new_ascending(files_len: usize) -> ImagePickerSorting { Self::Ascending(files_len - 1) } + + fn new_grouped_random( + groups: Rc>, + group: u8, + wl_surface: &WlSurface, + queue_size: usize, + ) -> Self { + Self::GroupedRandom(GroupedRandom { + surface: wl_surface.clone(), + group: groups + .borrow_mut() + .get_or_insert(group, wl_surface, queue_size), + }) + } } pub struct ImagePicker { @@ -162,18 +202,25 @@ pub struct ImagePicker { impl ImagePicker { pub const DEFAULT_DRAWN_IMAGES_QUEUE_SIZE: usize = 10; - pub fn new(wallpaper_info: &WallpaperInfo, filelist_cache: Rc>) -> Self { + pub fn new( + wallpaper_info: &WallpaperInfo, + wl_surface: &WlSurface, + filelist_cache: Rc>, + groups: Rc>, + ) -> Self { Self { current_img: PathBuf::from(""), image_changed_instant: Instant::now(), action: Some(ImagePickerAction::Next), sorting: ImagePickerSorting::new( wallpaper_info, + wl_surface, filelist_cache .clone() .borrow() .get(&wallpaper_info.path) .len(), + groups, ), filelist_cache, reload: false, @@ -181,66 +228,53 @@ impl ImagePicker { } /// Get the next image based on the sorting method - fn get_image_path(&mut self, files: &[PathBuf]) -> (usize, PathBuf) { + fn get_image_path(&mut self, files: &[PathBuf], qh: &QueueHandle) -> (usize, PathBuf) { match (&self.action, &mut self.sorting) { ( None, ImagePickerSorting::Ascending(current_index) | ImagePickerSorting::Descending(current_index), ) if self.current_img.exists() => (*current_index, self.current_img.to_path_buf()), - - (None, ImagePickerSorting::Random(_)) if self.current_img.exists() => { + (_, ImagePickerSorting::GroupedRandom(group)) + if group.group.borrow().loading_image.is_some() => + { + let group = group.group.borrow(); + let (index, loading_image) = group.loading_image.as_ref().unwrap(); + (*index, loading_image.to_path_buf()) + } + (None, ImagePickerSorting::Random(_) | ImagePickerSorting::GroupedRandom(_)) + if self.current_img.exists() => + { (0, self.current_img.to_path_buf()) } (None | Some(ImagePickerAction::Next), ImagePickerSorting::Random(queue)) => { - // Use the next images in the queue, if any - while let Some((next, index)) = queue.next() { - if next.exists() { - return (index, next.to_path_buf()); - } - } - // If there is only one image just return it - if files.len() == 1 { - return (0, files[0].to_path_buf()); - } - - // Otherwise pick a new random image that has not been drawn before - // Try 5 times, then get a random image. We do this because it might happen - // that the queue is bigger than the amount of available wallpapers - let mut tries = 5; - loop { - let index = rand::random::() % files.len(); - // search for an image that has not been drawn yet - // fail after 5 tries - if !queue.contains(&files[index]) { - break (index, files[index].to_path_buf()); - } - - // We have already tried a bunch of times - // We still need a new image, get the first one that is different than - // the current one. We also know that there is more than one image - if tries == 0 { - break loop { - let index = rand::random::() % files.len(); - if files[index] != self.current_img { - break (index, files[index].to_path_buf()); - } - }; - } - - tries -= 1; + next_random_image(&self.current_img, queue, files) + } + (None | Some(ImagePickerAction::Next), ImagePickerSorting::GroupedRandom(group)) => { + let mut group = group.group.borrow_mut(); + if self.current_img == group.current_image { + // start loading a new image + let (index, path) = + next_random_image(&self.current_img, &mut group.queue, files); + group.loading_image = Some((index, path.to_path_buf())); + group.queue_all_surfaces(qh); + (index, path) + } else { + (group.index, group.current_image.clone()) } } (Some(ImagePickerAction::Previous), ImagePickerSorting::Random(queue)) => { - while let Some((prev, index)) = queue.previous() { - if prev.exists() { - return (index, prev.to_path_buf()); - } + get_previous_image_for_random(&self.current_img, queue) + } + (Some(ImagePickerAction::Previous), ImagePickerSorting::GroupedRandom(group)) => { + let mut group = group.group.borrow_mut(); + let queue = &mut group.queue; + let (index, path) = get_previous_image_for_random(&self.current_img, queue); + if path != group.current_image { + group.loading_image = Some((index, path.to_path_buf())); + group.queue_all_surfaces(qh); } - - // We didn't find any suitable image, reset to the last working one - queue.set_current_to(&self.current_img.to_path_buf()); - (usize::MAX, self.current_image()) + (index, path) } ( None | Some(ImagePickerAction::Next), @@ -292,7 +326,11 @@ impl ImagePicker { } } - pub fn get_image_from_path(&mut self, path: &Path) -> Option<(PathBuf, usize)> { + pub fn get_image_from_path( + &mut self, + path: &Path, + qh: &QueueHandle, + ) -> Option<(PathBuf, usize)> { if path.is_dir() { let files = self.filelist_cache.borrow().get(path); @@ -301,7 +339,7 @@ impl ImagePicker { warn!("Directory {path:?} does not contain any valid image files."); None } else { - let (index, img_path) = self.get_image_path(&files); + let (index, img_path) = self.get_image_path(&files, qh); if img_path == self.current_img && !self.reload { None } else { @@ -324,11 +362,30 @@ impl ImagePicker { } } (None | Some(ImagePickerAction::Previous), ImagePickerSorting::Random { .. }) => {} + ( + None | Some(ImagePickerAction::Previous), + ImagePickerSorting::GroupedRandom(group), + ) => { + let mut group = group.group.borrow_mut(); + group.loading_image = None; + group.current_image.clone_from(&img_path); + group.index = index; + } ( _, ImagePickerSorting::Ascending(current_index) | ImagePickerSorting::Descending(current_index), ) => *current_index = index, + (Some(ImagePickerAction::Next), ImagePickerSorting::GroupedRandom(group)) => { + let mut group = group.group.borrow_mut(); + let queue = &mut group.queue; + if queue.has_reached_end() || queue.buffer.get(index).is_none() { + queue.push(img_path.clone()); + } + group.loading_image = None; + group.current_image.clone_from(&img_path); + group.index = index; + } } self.current_img = img_path; @@ -341,8 +398,9 @@ impl ImagePicker { } /// Update wallpaper by going up 1 index through the cached image paths - pub fn next_image(&mut self) { + pub fn next_image(&mut self, path: &Path, qh: &QueueHandle) { self.action = Some(ImagePickerAction::Next); + self.get_image_from_path(path, qh); } pub fn current_image(&self) -> PathBuf { @@ -380,7 +438,7 @@ impl ImagePicker { Err(_) => None, }; self.sorting = match new_sorting { - Sorting::Random => unreachable!(), + Sorting::Random | Sorting::GroupedRandom { .. } => unreachable!(), Sorting::Ascending => match index { Some(index) => ImagePickerSorting::Ascending(index), None => ImagePickerSorting::new_ascending(files.len()), @@ -427,6 +485,13 @@ impl ImagePicker { queue.resize(drawn_images_queue_size); } ImagePickerSorting::Ascending(_) | ImagePickerSorting::Descending(_) => {} + ImagePickerSorting::GroupedRandom(group) => { + group + .group + .borrow_mut() + .queue + .resize(drawn_images_queue_size); + } } } @@ -446,6 +511,62 @@ impl ImagePicker { } } +fn next_random_image( + current_image: &Path, + queue: &mut Queue, + files: &[PathBuf], +) -> (usize, PathBuf) { + // Use the next images in the queue, if any + while let Some((next, index)) = queue.next() { + if next.exists() { + return (index, next.to_path_buf()); + } + } + // If there is only one image just return it + if files.len() == 1 { + return (0, files[0].to_path_buf()); + } + + // Otherwise pick a new random image that has not been drawn before + // Try 5 times, then get a random image. We do this because it might happen + // that the queue is bigger than the amount of available wallpapers + let mut tries = 5; + loop { + let index = rand::random::() % files.len(); + // search for an image that has not been drawn yet + // fail after 5 tries + if !queue.contains(&files[index]) { + break (index, files[index].to_path_buf()); + } + + // We have already tried a bunch of times + // We still need a new image, get the first one that is different than + // the current one. We also know that there is more than one image + if tries == 0 { + break loop { + let index = rand::random::() % files.len(); + if files[index] != current_image { + break (index, files[index].to_path_buf()); + } + }; + } + + tries -= 1; + } +} + +fn get_previous_image_for_random(current_image: &Path, queue: &mut Queue) -> (usize, PathBuf) { + while let Some((prev, index)) = queue.previous() { + if prev.exists() { + return (index, prev.to_path_buf()); + } + } + + // We didn't find any suitable image, reset to the last working one + queue.set_current_to(current_image); + (usize::MAX, current_image.to_path_buf()) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. diff --git a/daemon/src/ipc_server.rs b/daemon/src/ipc_server.rs index 0faa5ae..3076bb5 100644 --- a/daemon/src/ipc_server.rs +++ b/daemon/src/ipc_server.rs @@ -117,7 +117,9 @@ pub fn handle_message( IpcMessage::NextWallpaper { monitors } => check_monitors(wpaperd, &monitors).map(|_| { for surface in collect_surfaces(wpaperd, monitors) { - surface.image_picker.next_image(); + surface + .image_picker + .next_image(&surface.wallpaper_info.path, &qh); surface.queue_draw(&qh); } diff --git a/daemon/src/main.rs b/daemon/src/main.rs index a33f6f9..7370be4 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -8,6 +8,7 @@ mod opts; mod render; mod socket; mod surface; +mod wallpaper_groups; mod wallpaper_info; mod wpaperd; @@ -45,6 +46,8 @@ use smithay_client_toolkit::reexports::{ calloop_wayland_source::WaylandSource, client::{globals::registry_queue_init, Connection, Proxy}, }; +use wallpaper_groups::WallpaperGroups; +use wallpaper_info::Sorting; use wpaperd_ipc::socket_path; use xdg::BaseDirectories; @@ -121,7 +124,16 @@ fn run(opts: Opts, xdg_dirs: BaseDirectories) -> Result<()> { FilelistCache::new(config.paths(), &mut hotwatch, event_loop.handle())?; let filelist_cache = Rc::new(RefCell::new(filelist_cache)); - let mut wpaperd = Wpaperd::new(&qh, &globals, config, egl_display, filelist_cache.clone())?; + let groups = Rc::new(RefCell::new(WallpaperGroups::new())); + + let mut wpaperd = Wpaperd::new( + &qh, + &globals, + config, + egl_display, + filelist_cache.clone(), + groups, + )?; // Start listening on the IPC socket let socket = listen_on_ipc_socket(&socket_path()?).context("spawning the ipc socket")?; @@ -183,11 +195,16 @@ fn run(opts: Opts, xdg_dirs: BaseDirectories) -> Result<()> { error!("{err:?}"); }; surface.drawn(); - } - // If the surface has already been drawn for the first time, then handle pausing/resuming - // the automatic wallpaper sequence. - else { - surface.handle_pause_state(&event_loop.handle(), qh.clone()) + } else { + // If the surface has already been drawn for the first time, then handle pausing/resuming + // the automatic wallpaper sequence. + surface.handle_pause_state(&event_loop.handle(), qh.clone()); + if matches!( + surface.wallpaper_info.sorting, + Some(Sorting::GroupedRandom { .. }) + ) { + // surface.image_picker.handle_grouped_sorting(); + } }; #[cfg(debug_assertions)] diff --git a/daemon/src/surface.rs b/daemon/src/surface.rs index bb687bd..124489e 100644 --- a/daemon/src/surface.rs +++ b/daemon/src/surface.rs @@ -19,13 +19,13 @@ use smithay_client_toolkit::{ shell::WaylandSurface, }; -use crate::wpaperd::Wpaperd; use crate::{display_info::DisplayInfo, wallpaper_info::WallpaperInfo}; use crate::{ filelist_cache::FilelistCache, render::{EglContext, Renderer}, }; use crate::{image_loader::ImageLoader, image_picker::ImagePicker}; +use crate::{wallpaper_groups::WallpaperGroups, wpaperd::Wpaperd}; #[derive(Debug)] pub enum EventSource { @@ -43,7 +43,7 @@ pub struct Surface { renderer: Renderer, pub image_picker: ImagePicker, event_source: EventSource, - wallpaper_info: WallpaperInfo, + pub wallpaper_info: WallpaperInfo, info: Rc>, image_loader: Rc>, window_drawn: bool, @@ -69,6 +69,8 @@ impl Surface { egl_display: egl::Display, filelist_cache: Rc>, image_loader: Rc>, + groups: Rc>, + qh: &QueueHandle, ) -> Self { let wl_surface = wl_layer.wl_surface().clone(); let egl_context = EglContext::new(egl_display, &wl_surface); @@ -80,7 +82,7 @@ impl Surface { // Commit the surface wl_surface.commit(); - let image_picker = ImagePicker::new(&wallpaper_info, filelist_cache); + let image_picker = ImagePicker::new(&wallpaper_info, &wl_surface, filelist_cache, groups); let image = black_image(); let info = Rc::new(RefCell::new(info)); @@ -116,7 +118,7 @@ impl Surface { // Start loading the wallpaper as soon as possible (i.e. surface creation) // It will still be loaded as a texture when we have an openGL context - if let Err(err) = surface.load_wallpaper() { + if let Err(err) = surface.load_wallpaper(qh) { warn!("{err:?}"); } @@ -134,7 +136,7 @@ impl Surface { // Use the correct context before loading the texture and drawing self.egl_context.make_current()?; - let wallpaper_loaded = self.load_wallpaper()?; + let wallpaper_loaded = self.load_wallpaper(qh)?; if self.renderer.transition_running() { // Recalculate the current progress, the transition might end now @@ -175,13 +177,13 @@ impl Surface { } // Call surface::frame when this return false - pub fn load_wallpaper(&mut self) -> Result { + pub fn load_wallpaper(&mut self, qh: &QueueHandle) -> Result { Ok(loop { // If we were not already trying to load an image if self.loading_image.is_none() { if let Some(item) = self .image_picker - .get_image_from_path(&self.wallpaper_info.path) + .get_image_from_path(&self.wallpaper_info.path, qh) { if self.image_picker.current_image() == item.0 && !self.image_picker.is_reloading() @@ -361,7 +363,7 @@ impl Surface { ); if path_changed { // ask the image_picker to pick a new a image - self.image_picker.next_image(); + self.image_picker.next_image(&self.wallpaper_info.path, &qh); self.queue_draw(qh); } if self.wallpaper_info.duration != wallpaper_info.duration { @@ -483,7 +485,9 @@ impl Surface { TimeoutAction::ToDuration(remaining_time) } else { // Change the drawn image - surface.image_picker.next_image(); + surface + .image_picker + .next_image(&surface.wallpaper_info.path, &qh); surface.queue_draw(&qh); TimeoutAction::ToDuration(duration) } @@ -521,7 +525,7 @@ impl Surface { #[inline] pub fn queue_draw(&mut self, qh: &QueueHandle) { // Start loading the next image immediately - if let Err(err) = self.load_wallpaper() { + if let Err(err) = self.load_wallpaper(qh) { warn!("{err:?}"); } self.wl_surface.frame(qh, self.wl_surface.clone()); diff --git a/daemon/src/wallpaper_groups.rs b/daemon/src/wallpaper_groups.rs new file mode 100644 index 0000000..bcf7cbc --- /dev/null +++ b/daemon/src/wallpaper_groups.rs @@ -0,0 +1,63 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + rc::Rc, +}; + +use smithay_client_toolkit::reexports::client::{protocol::wl_surface::WlSurface, QueueHandle}; + +use crate::{image_picker::Queue, wpaperd::Wpaperd}; + +pub struct WallpaperGroup { + pub index: usize, + pub current_image: PathBuf, + pub loading_image: Option<(usize, PathBuf)>, + pub surfaces: HashSet, + pub queue: Queue, +} + +impl WallpaperGroup { + pub fn new(queue_size: usize) -> Self { + Self { + index: 0, + current_image: PathBuf::from(""), + loading_image: None, + surfaces: HashSet::new(), + queue: Queue::with_capacity(queue_size), + } + } + + pub fn queue_all_surfaces(&self, qh: &QueueHandle) { + for surface in &self.surfaces { + surface.frame(qh, surface.clone()); + surface.commit(); + } + } +} + +pub struct WallpaperGroups { + groups: HashMap>>, +} + +impl WallpaperGroups { + pub fn new() -> Self { + Self { + groups: HashMap::new(), + } + } + + pub fn get_or_insert( + &mut self, + group: u8, + wl_surface: &WlSurface, + queue_size: usize, + ) -> Rc> { + self.groups + .entry(group) + .or_insert_with(|| Rc::new(RefCell::new(WallpaperGroup::new(queue_size)))); + let wp_group = self.groups.get_mut(&group).unwrap(); + wp_group.borrow_mut().surfaces.insert(wl_surface.clone()); + wp_group.clone() + } +} diff --git a/daemon/src/wallpaper_info.rs b/daemon/src/wallpaper_info.rs index 4acd780..d02f16c 100644 --- a/daemon/src/wallpaper_info.rs +++ b/daemon/src/wallpaper_info.rs @@ -47,6 +47,9 @@ impl Default for WallpaperInfo { pub enum Sorting { #[default] Random, + GroupedRandom { + group: u8, + }, Ascending, Descending, } diff --git a/daemon/src/wpaperd.rs b/daemon/src/wpaperd.rs index 60c6bc4..446195a 100644 --- a/daemon/src/wpaperd.rs +++ b/daemon/src/wpaperd.rs @@ -25,6 +25,7 @@ use crate::display_info::DisplayInfo; use crate::filelist_cache::FilelistCache; use crate::image_loader::ImageLoader; use crate::surface::Surface; +use crate::wallpaper_groups::WallpaperGroups; use crate::wallpaper_info::WallpaperInfo; pub struct Wpaperd { @@ -38,6 +39,7 @@ pub struct Wpaperd { egl_display: egl::Display, pub filelist_cache: Rc>, pub image_loader: Rc>, + pub wallpaper_groups: Rc>, } impl Wpaperd { @@ -47,6 +49,7 @@ impl Wpaperd { config: Config, egl_display: egl::Display, filelist_cache: Rc>, + wallpaper_groups: Rc>, ) -> Result { let shm_state = Shm::bind(globals, qh)?; @@ -63,6 +66,7 @@ impl Wpaperd { egl_display, filelist_cache, image_loader, + wallpaper_groups, }) } @@ -218,6 +222,8 @@ impl OutputHandler for Wpaperd { self.egl_display, self.filelist_cache.clone(), self.image_loader.clone(), + self.wallpaper_groups.clone(), + qh, )); }