use crate::gui::SoundpadGui; use egui::{ Align, AtomExt, Button, CollapsingHeader, Color32, CursorIcon, Layout, RichText, ScrollArea, Sense, TextEdit, Ui, Vec2, }; use egui_dnd::dnd; use egui_material_icons::icons::*; use pwsp::types::{gui::AppState, gui::AudioPlayerState}; use rust_i18n::t; use std::{cmp::Ordering, path::Path, path::PathBuf}; pub(crate) enum FileAction { Play(PathBuf, bool), StopAndPlay(u32, PathBuf, bool), AssignHotkey(PathBuf), } impl SoundpadGui { pub fn draw_body(&mut self, ui: &mut Ui) { let left_panel_width = self .config .left_panel_width .max(100.0) .min(ui.available_width() - 100.0); let dirs_size = Vec2::new(left_panel_width, ui.available_height() - 40.0); ui.horizontal(|ui| { self.draw_dirs(ui, dirs_size); let (rect, response) = ui.allocate_at_least( Vec2::new(ui.spacing().item_spacing.x, ui.available_height()), Sense::click_and_drag(), ); if ui.is_rect_visible(rect) { let stroke = ui.visuals().widgets.noninteractive.bg_stroke; ui.painter().vline(rect.center().x, rect.y_range(), stroke); } let vertical_separator_response = response.on_hover_and_drag_cursor(CursorIcon::ResizeHorizontal); if vertical_separator_response.dragged() { self.config.left_panel_width += vertical_separator_response.drag_delta().x; self.config.left_panel_width = self.config.left_panel_width.clamp(100.0, 500.0); } if vertical_separator_response.drag_stopped() { self.config.save_to_file().ok(); } let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0); self.draw_files(ui, files_size); }); } fn draw_dirs(&mut self, ui: &mut Ui, area_size: Vec2) { ui.vertical(|ui| { ui.set_min_width(area_size.x); ui.set_min_height(area_size.y); ScrollArea::vertical().id_salt(0).show(ui, |ui| { ui.set_min_width(area_size.x); let mut dirs = std::mem::take(&mut self.app_state.dirs); let mut dir_to_open = None; dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| { let path = item; ui.horizontal(|ui| { handle.ui(ui, |ui| { ui.label(ICON_DRAG_INDICATOR.codepoint); }); let name = path .file_name() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| path.to_string_lossy().to_string()); let mut dir_button_text = RichText::new(name.clone()); if let Some(current_dir) = &self.app_state.current_dir && current_dir.eq(&*path) { dir_button_text = dir_button_text.color(Color32::WHITE); } let dir_button = Button::new(dir_button_text.atom_max_width(area_size.x)).frame(false); let dir_button_response = ui.add(dir_button); if dir_button_response.clicked() { dir_to_open = Some(path.clone()); } let delete_dir_button = Button::new(ICON_DELETE).frame(false); let delete_dir_button_response = ui.add_sized([18.0, 18.0], delete_dir_button); if delete_dir_button_response.clicked() { self.app_state.dirs_to_remove.insert(path.clone()); } // Context menu dir_button_response.context_menu(|ui| { if ui .button(format!( "{} {}", ICON_OPEN_IN_NEW.codepoint, t!("gui.context.dirs.open") )) .clicked() { dir_to_open = Some(path.clone()); } if ui .button(format!( "{} {}", ICON_OPEN_IN_BROWSER.codepoint, t!("gui.context.dirs.open_in_fm") )) .clicked() && let Err(e) = opener::open(&path) { eprintln!("Failed to open file manager: {}", e); } ui.separator(); if ui .button(format!( "{} {}", ICON_DELETE.codepoint, t!("gui.context.dirs.remove") )) .clicked() { self.app_state.dirs_to_remove.insert(path.clone()); } }); }); }); self.app_state.dirs = dirs; if let Some(path) = dir_to_open { self.open_dir(&path); } ui.horizontal(|ui| { let add_dirs_button = Button::new(ICON_ADD).frame(false); let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button); if add_dirs_button_response.clicked() { self.add_dirs(); } }); ui.with_layout(Layout::bottom_up(Align::Min), |ui| { let play_file_button = Button::new(t!("gui.play_file_button")); let play_file_button_response = ui.add(play_file_button); if play_file_button_response.clicked() { self.open_file(); } }); }); }); } fn draw_files_search_field(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { let search_field_response = ui.add_sized( [ui.available_width(), 22.0], TextEdit::singleline(&mut self.app_state.search_query) .hint_text(t!("gui.search_placeholder")), ); if self.app_state.force_focus_search { search_field_response.request_focus(); self.app_state.force_focus_search = false; } self.app_state.search_field_id = Some(search_field_response.id); }); } fn draw_files_list(&mut self, ui: &mut Ui, area_size: Vec2) { ScrollArea::vertical().id_salt(1).show(ui, |ui| { ui.set_min_width(area_size.x); ui.set_min_height(area_size.y); ui.vertical(|ui| { let mut actions = Vec::new(); let files = self.get_filtered_files(); for entry_path in files { Self::draw_tree_node( ui, entry_path, &mut self.app_state, &self.audio_player_state, &mut actions, ); } for action in actions { match action { FileAction::Play(path, concurrent) => self.play_file(&path, concurrent), FileAction::StopAndPlay(id, path, concurrent) => { self.stop(Some(id)); self.play_file(&path, concurrent); } FileAction::AssignHotkey(path) => { self.app_state.assigning_hotkey_for_file = Some(path); self.app_state.hotkey_capture_active = true; } } } }); }); } fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) { ui.vertical(|ui| { self.draw_files_search_field(ui); ui.separator(); self.draw_files_list(ui, area_size); }); } fn draw_tree_node_dir( ui: &mut Ui, path: std::path::PathBuf, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, ) { let dir_name = path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); CollapsingHeader::new(dir_name) .id_salt(&path) .show(ui, |ui| { let children = if let Some(cached) = app_state.dir_cache.get(&path) { cached.clone() } else { let mut read = Vec::new(); if let Ok(entries) = std::fs::read_dir(&path) { for entry in entries.filter_map(|e| e.ok()) { read.push(entry.path()); } } read.sort_by(|a, b| { let a_is_dir = a.is_dir(); let b_is_dir = b.is_dir(); if a_is_dir && !b_is_dir { Ordering::Less } else if !a_is_dir && b_is_dir { Ordering::Greater } else { a.cmp(b) } }); app_state.dir_cache.insert(path.clone(), read.clone()); read }; let search_query = app_state.search_query.to_lowercase(); let search_query = search_query.trim(); for child in children { if !child.is_dir() { if !crate::gui::SUPPORTED_EXTENSIONS.contains( &child .extension() .unwrap_or_default() .to_str() .unwrap_or_default(), ) { continue; } if !search_query.is_empty() { let file_name = child .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); if !file_name.to_lowercase().contains(search_query) { continue; } } } Self::draw_tree_node(ui, child, app_state, audio_player_state, actions); } }); } fn draw_tree_node_file( ui: &mut Ui, path: std::path::PathBuf, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, ) { let file_name = path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); ui.horizontal(|ui| { // Hotkey badge let mut hotkey_badge = None; for slot in &app_state.hotkey_config.slots { if slot.action.name == "play" && let Some(file_path_str) = slot.action.args.get("file_path") && Path::new(file_path_str) == path { if let Some(chord) = &slot.key_chord { hotkey_badge = Some(format!("[{}]", chord)); } else { hotkey_badge = Some(format!("[{}]", slot.slot)); } break; } } if let Some(badge) = &hotkey_badge { ui.label( RichText::new(badge) .small() .monospace() .color(Color32::from_rgb(100, 200, 100)), ); } let file_button_text = RichText::new(&file_name); let file_button = Button::new(file_button_text).frame(false).truncate(); let file_button_response = ui.add(file_button); if file_button_response.clicked() { ui.input(|i| { if i.modifiers.ctrl { actions.push(FileAction::Play(path.clone(), true)); } else if i.modifiers.shift && let Some(last_track) = audio_player_state.tracks.last() { actions.push(FileAction::StopAndPlay( last_track.id, path.clone(), true, )); } else { actions.push(FileAction::Play(path.clone(), false)); } }); } // Context menu file_button_response.context_menu(|ui| { if ui .button(format!( "{} {}", ICON_BOLT.codepoint, t!("gui.context.files.play_solo") )) .clicked() { actions.push(FileAction::Play(path.clone(), false)); } if ui .button(format!( "{} {}", ICON_ADD.codepoint, t!("gui.context.files.add_new") )) .clicked() { actions.push(FileAction::Play(path.clone(), true)); } if ui .button(format!( "{} {}", ICON_SWAP_HORIZ.codepoint, t!("gui.context.files.replace_last") )) .clicked() && let Some(last_track) = audio_player_state.tracks.last() { actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true)); } ui.separator(); if ui .button(format!( "{} {}", ICON_OPEN_IN_BROWSER.codepoint, t!("gui.context.files.show_in_fm") )) .clicked() && let Err(e) = opener::reveal(&path) { eprintln!("Failed to open file manager: {}", e); } ui.separator(); if ui .button(format!( "{} {}", ICON_KEYBOARD.codepoint, t!("gui.context.files.asign_hotkey") )) .clicked() { actions.push(FileAction::AssignHotkey(path.clone())); ui.close(); } }); }); } fn draw_tree_node( ui: &mut Ui, path: std::path::PathBuf, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, ) { if path.is_dir() { Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions); } else { Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions); } } }