From 4499b1d3aa820371a921a7281a605ef72329149f Mon Sep 17 00:00:00 2001 From: arabian Date: Wed, 28 Jan 2026 02:10:36 +0300 Subject: [PATCH] feat(gui): now directories can be reordered using drag and drop --- Cargo.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 ++- src/gui/draw.rs | 20 ++++++++++----- src/gui/mod.rs | 6 +++-- src/gui/update.rs | 12 +++++++++ src/types/config.rs | 6 ++--- src/types/gui.rs | 2 +- 7 files changed, 97 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26c69e6..83ca857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,7 +182,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -461,6 +461,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "concat-idents" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -810,6 +820,29 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_animation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3db554dd3784f469d804f7dc25d1b14a2e00f1608d7af60218ccbced720a6e8" +dependencies = [ + "egui", + "hello_egui_utils", + "simple-easing", +] + +[[package]] +name = "egui_dnd" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f535b8df7ca89f781954feaa505899c8955b550074ecafcaa3c040f67aec3d46" +dependencies = [ + "egui", + "egui_animation", + "simple-easing", + "web-time", +] + [[package]] name = "egui_glow" version = "0.33.3" @@ -1180,6 +1213,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hello_egui_utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d09c2c7f3aa61624b1bec320be9029f30e06769f92e39ac99a6d6e01024ae8" +dependencies = [ + "concat-idents", + "egui", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1327,6 +1370,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2247,7 +2299,9 @@ dependencies = [ "dirs", "eframe", "egui", + "egui_dnd", "egui_material_icons", + "itertools 0.14.0", "pipewire", "rfd", "rodio", @@ -2569,6 +2623,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simple-easing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" + [[package]] name = "slab" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index 6403ecf..8094254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde_json = "1.0.149" clap = { version = "4.5.54", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] } dirs = "6.0.0" +itertools = "0.14.0" rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] } pipewire = "0.9.2" @@ -28,6 +29,7 @@ rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"]} egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] } eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] } egui_material_icons = "0.5.0" +egui_dnd = "0.14.0" [[bin]] name = "pwsp-daemon" @@ -56,4 +58,4 @@ assets = [ ["assets/pwsp-gui.desktop", "usr/share/applications/pwsp.desktop", "644"], ["assets/icon.png", "usr/share/icons/hicolor/256x256/apps/pwsp.png", "644"], ["assets/pwsp-daemon.service", "usr/lib/systemd/user/pwsp-daemon.service", "644"], -] \ No newline at end of file +] diff --git a/src/gui/draw.rs b/src/gui/draw.rs index d0adeb1..26e8dba 100644 --- a/src/gui/draw.rs +++ b/src/gui/draw.rs @@ -3,10 +3,11 @@ use egui::{ Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Label, Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2, }; +use egui_dnd::dnd; use egui_material_icons::icons; use pwsp::types::audio_player::TrackInfo; use pwsp::utils::gui::format_time_pair; -use std::{error::Error, path::PathBuf, time::Instant}; +use std::{error::Error, time::Instant}; use pwsp::types::gui::AppState; @@ -301,10 +302,14 @@ impl SoundpadGui { ScrollArea::vertical().id_salt(0).show(ui, |ui| { ui.set_min_width(area_size.x); - let mut dirs: Vec = self.app_state.dirs.iter().cloned().collect(); - dirs.sort(); - for path in dirs.iter() { + let mut dirs = self.app_state.dirs.clone(); + + dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| { + let path = item.clone(); ui.horizontal(|ui| { + handle.ui(ui, |ui| { + ui.label(icons::ICON_DRAG_INDICATOR); + }); let name = path .file_name() .map(|s| s.to_string_lossy().to_string()) @@ -312,7 +317,7 @@ impl SoundpadGui { let mut dir_button_text = RichText::new(name.clone()); if let Some(current_dir) = &self.app_state.current_dir { - if current_dir.eq(path) { + if current_dir.eq(&path) { dir_button_text = dir_button_text.color(Color32::WHITE); } } @@ -322,7 +327,7 @@ impl SoundpadGui { let dir_button_response = ui.add(dir_button); if dir_button_response.clicked() { - self.open_dir(path); + self.open_dir(&path); } let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false); @@ -332,7 +337,8 @@ impl SoundpadGui { self.remove_dir(&path.clone()); } }); - } + }); + self.app_state.dirs = dirs; ui.horizontal(|ui| { let add_dirs_button = Button::new(icons::ICON_ADD).frame(false); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 2775572..a42bf60 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -4,6 +4,7 @@ mod update; use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native}; use egui::{Context, Vec2, ViewportBuilder}; +use itertools::Itertools; use pwsp::{ types::{ audio_player::PlayerState, @@ -87,15 +88,16 @@ impl SoundpadGui { let file_dialog = FileDialog::new(); if let Some(paths) = file_dialog.pick_folders() { for path in paths { - self.app_state.dirs.insert(path); + self.app_state.dirs.push(path); } + self.app_state.dirs = self.app_state.dirs.iter().unique().cloned().collect(); self.config.dirs = self.app_state.dirs.clone(); self.config.save_to_file().ok(); } } pub fn remove_dir(&mut self, path: &PathBuf) { - self.app_state.dirs.remove(path); + self.app_state.dirs.retain(|x| x != path); if let Some(current_dir) = &self.app_state.current_dir && current_dir == path { diff --git a/src/gui/update.rs b/src/gui/update.rs index 91bb61e..af55de6 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -9,6 +9,13 @@ use std::time::{Duration, Instant}; impl App for SoundpadGui { fn update(&mut self, ctx: &Context, _frame: &mut EFrame) { + // Save directories if changed + if !self.config.dirs.eq(&self.app_state.dirs) { + self.config.dirs = self.app_state.dirs.clone(); + self.config.save_to_file().ok(); + } + + // Seek and volume requests let mut seek_requests = vec![]; let mut volume_requests = vec![]; @@ -57,11 +64,13 @@ impl App for SoundpadGui { } } + // Sync audio player state { let guard = self.audio_player_state_shared.lock().unwrap(); self.audio_player_state = guard.clone(); } + // Handle scale factor changes let old_scale_factor = self.config.scale_factor; let new_scale_factor = ctx.zoom_factor().clamp(0.5, 2.0); @@ -72,8 +81,10 @@ impl App for SoundpadGui { self.config.save_to_file().ok(); } + // Handle input self.handle_input(ctx); + // Draw UI CentralPanel::default().show(ctx, |ui| { if !self.audio_player_state.is_daemon_running { self.draw_waiting_for_daemon(ui); @@ -88,6 +99,7 @@ impl App for SoundpadGui { self.draw(ui).ok(); }); + // Request repaint ctx.request_repaint_after_secs(1.0 / 60.0); } } diff --git a/src/types/config.rs b/src/types/config.rs index c13d5cb..f40affb 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -1,6 +1,6 @@ use crate::utils::config::get_config_path; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, error::Error, fs, path::PathBuf}; +use std::{error::Error, fs, path::PathBuf}; #[derive(Default, Clone, Serialize, Deserialize)] #[serde(default)] @@ -41,7 +41,7 @@ pub struct GuiConfig { pub save_scale_factor: bool, pub pause_on_exit: bool, - pub dirs: HashSet, + pub dirs: Vec, } impl Default for GuiConfig { @@ -55,7 +55,7 @@ impl Default for GuiConfig { save_scale_factor: false, pause_on_exit: false, - dirs: HashSet::default(), + dirs: vec![], } } } diff --git a/src/types/gui.rs b/src/types/gui.rs index 9425353..74db570 100644 --- a/src/types/gui.rs +++ b/src/types/gui.rs @@ -37,7 +37,7 @@ pub struct AppState { pub ignore_volume_update_until: Option, pub current_dir: Option, - pub dirs: HashSet, + pub dirs: Vec, pub selected_file: Option, pub files: HashSet,