use crate::gui::SoundpadGui; 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_extras::{Column, TableBuilder}; use egui_material_icons::icons::*; use pwsp::types::gui::AudioPlayerState; use pwsp::types::socket::Request; use pwsp::types::{audio_player::TrackInfo, gui::AppState}; use pwsp::utils::gui::{format_time_pair, make_request_async}; use rust_i18n::t; use std::{ cmp::Ordering, path::{Path, PathBuf}, time::Instant, }; enum TrackAction { Pause(u32), Resume(u32), ToggleLoop(u32), Stop(u32), } enum HotkeyAction { Remove(String), Capture(String), ClearChord(String), Play(String), } enum FileAction { Play(PathBuf, bool), StopAndPlay(u32, PathBuf, bool), AssignHotkey(PathBuf), } impl SoundpadGui { fn get_volume_icon(volume: f32) -> &'static str { if volume > 0.7 { ICON_VOLUME_UP.codepoint } else if volume <= 0.0 { ICON_VOLUME_OFF.codepoint } else if volume < 0.3 { ICON_VOLUME_MUTE.codepoint } else { ICON_VOLUME_DOWN.codepoint } } pub fn draw(&mut self, ui: &mut Ui) { self.draw_header(ui); self.draw_body(ui); ui.separator(); self.draw_footer(ui); } pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) { ui.centered_and_justified(|ui| { ui.label( RichText::new("Waiting for PWSP daemon to start...") .size(34.0) .monospace(), ); }); } pub fn draw_hotkey_capture(&mut self, ui: &mut Ui) { ui.vertical_centered(|ui| { ui.add_space(ui.available_height() / 3.0); ui.label( RichText::new(t!("gui.hotkeys.capture.header")) .size(18.0) .color(Color32::YELLOW) .monospace(), ); ui.add_space(10.0); let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot { format!("{} '{}'", t!("gui.hotkeys.capture.for"), slot) } else if let Some(path) = &self.app_state.assigning_hotkey_for_file { format!( "{} '{}'", t!("gui.hotkeys.capture.for"), path.file_name().unwrap_or_default().to_string_lossy() ) } else { String::new() }; ui.label(RichText::new(target).size(16.0)); ui.add_space(10.0); ui.label(t!("gui.hotkeys.capture.cancel")); }); } pub fn draw_settings(&mut self, ui: &mut Ui) { ui.vertical(|ui| { ui.spacing_mut().item_spacing.y = 5.0; // --------- Back Button and Title ---------- ui.horizontal_top(|ui| { let back_button = Button::new(ICON_ARROW_BACK).frame(false); let back_button_response = ui.add(back_button); if back_button_response.clicked() { self.app_state.show_settings = false; } ui.add_space(ui.available_width() / 2.0 - 40.0); ui.label( RichText::new(t!("gui.settings.header")) .color(Color32::WHITE) .monospace(), ); }); // -------------------------------- ui.separator(); ui.add_space(20.0); // --------- Checkboxes ---------- let save_volume_response = ui.checkbox( &mut self.config.save_volume, t!("gui.settings.remember_volume"), ); let save_input_response = ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic")); let save_scale_response = ui.checkbox( &mut self.config.save_scale_factor, t!("gui.settings.remember_ui_scale"), ); let pause_on_exit_response = ui.checkbox( &mut self.config.pause_on_exit, t!("gui.settings.pause_on_window_close"), ); if save_volume_response.changed() || save_input_response.changed() || save_scale_response.changed() || pause_on_exit_response.changed() { self.config.save_to_file().ok(); } // -------------------------------- ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.label(t!( "gui.settings.version", version = env!("CARGO_PKG_VERSION") )); }); }); } pub fn draw_hotkeys(&mut self, ui: &mut Ui) { ui.vertical(|ui| { ui.spacing_mut().item_spacing.y = 5.0; self.draw_hotkeys_header(ui); ui.separator(); self.draw_hotkeys_search(ui); ui.separator(); ui.add_space(5.0); let action = self.draw_hotkeys_table(ui); if let Some(action) = action { self.handle_hotkey_action(action); } }); } fn draw_hotkeys_header(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { let back_button = Button::new(ICON_ARROW_BACK).frame(false); if ui.add(back_button).clicked() { self.app_state.show_hotkeys = false; } ui.vertical_centered(|ui| { ui.label( RichText::new(t!("gui.hotkeys.header")) .color(Color32::WHITE) .monospace(), ); }); }); } fn draw_hotkeys_search(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { ui.menu_button( format!( "{} {}", ICON_ADD.codepoint, t!("gui.hotkeys.add_command_select") ), |ui| { let mut selected_cmd = None; if ui.button(t!("gui.hotkeys.toggle_pause_command")).clicked() { selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None))); } if ui.button(t!("gui.hotkeys.stop_playback_command")).clicked() { selected_cmd = Some(("cmd_stop", Request::stop(None))); } if ui .button(t!("gui.hotkeys.pause_playback_command")) .clicked() { selected_cmd = Some(("cmd_pause", Request::pause(None))); } if ui .button(t!("gui.hotkeys.resume_playback_command")) .clicked() { selected_cmd = Some(("cmd_resume", Request::resume(None))); } if ui.button(t!("gui.hotkeys.toggle_loop_command")).clicked() { selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None))); } if let Some((slot_name, req)) = selected_cmd { make_request_async(Request::set_hotkey_action(slot_name, &req)); self.app_state .hotkey_config .set_slot(slot_name.to_string(), req); self.app_state.assigning_hotkey_slot = Some(slot_name.to_string()); self.app_state.hotkey_capture_active = true; ui.close(); } }, ); ui.add_space(10.0); ui.add( TextEdit::singleline(&mut self.app_state.hotkey_search_query) .hint_text(t!("gui.hotkeys.search_placeholder")) .desired_width(f32::INFINITY), ); }); } fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option { let conflicts = self.app_state.hotkey_config.find_conflicts(); let conflict_slots: std::collections::HashSet<&str> = conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect(); let search = self.app_state.hotkey_search_query.to_lowercase(); let mut action: Option = None; let slots: Vec<_> = self .app_state .hotkey_config .slots .iter() .filter(|s| { if search.is_empty() { return true; } s.slot.to_lowercase().contains(&search) || format!("{:?}", s.action).to_lowercase().contains(&search) || s.key_chord .as_deref() .unwrap_or("") .to_lowercase() .contains(&search) }) .collect(); let available_width = ui.available_width(); let col_width = (available_width / 4.0).max(80.0); TableBuilder::new(ui) .striped(true) .column(Column::exact(col_width).clip(true)) // Slot .column(Column::exact(col_width).clip(true)) // Sound / Action name .column(Column::exact(col_width).clip(true)) // Key Chord .column(Column::exact(col_width).clip(true)) // Actions .header(30.0, |mut header| { header.col(|ui| { ui.label( RichText::new(t!("gui.hotkeys.column_slot")) .strong() .monospace() .color(Color32::LIGHT_GRAY), ); }); header.col(|ui| { ui.label( RichText::new(t!("gui.hotkeys.column_sound")) .strong() .monospace() .color(Color32::LIGHT_GRAY), ); }); header.col(|ui| { ui.label( RichText::new(t!("gui.hotkeys.column_key_chord")) .strong() .monospace() .color(Color32::LIGHT_GRAY), ); }); header.col(|ui| { ui.label( RichText::new(t!("gui.hotkeys.column_actions")) .strong() .monospace() .color(Color32::LIGHT_GRAY), ); }); }) .body(|mut body| { if slots.is_empty() { body.row(30.0, |mut row| { row.col(|_| {}); row.col(|ui| { ui.label( RichText::new(t!("gui.hotkeys.no_hotkeys_configured")) .color(Color32::GRAY), ); }); row.col(|_| {}); row.col(|_| {}); }); return; } for slot in &slots { body.row(30.0, |mut row| { // Column 1: Slot row.col(|ui| { ui.horizontal(|ui| { if conflict_slots.contains(slot.slot.as_str()) { ui.label( RichText::new(ICON_WARNING.codepoint) .color(Color32::from_rgb(255, 165, 0)), ) .on_hover_text("Key chord conflict"); } ui.add( Label::new(RichText::new(&slot.slot).monospace()).truncate(), ); }); }); // Column 2: Sound / Action name row.col(|ui| { let action_name = match slot.action.name.as_str() { "play" => { if let Some(file_path_str) = slot.action.args.get("file_path") { Path::new(file_path_str) .file_name() .unwrap_or_default() .to_string_lossy() .to_string() } else { "Play".to_string() } } "toggle_pause" => "Toggle Pause".to_string(), "pause" => "Pause Playback".to_string(), "resume" => "Resume Playback".to_string(), "stop" => "Stop Playback".to_string(), "toggle_loop" => "Toggle Loop".to_string(), other => other.to_string(), }; ui.add(Label::new(RichText::new(action_name).monospace()).truncate()); }); // Column 3: Key Chord row.col(|ui| { let chord_text = slot.key_chord.as_deref().unwrap_or("(none)"); ui.add( Label::new(RichText::new(chord_text).monospace().color( if slot.key_chord.is_some() { Color32::from_rgb(100, 200, 100) } else { Color32::GRAY }, )) .truncate(), ); }); // Column 4: Actions row.col(|ui| { ui.horizontal(|ui| { if ui .add(Button::new(ICON_DELETE).frame(false)) .on_hover_text("Remove slot") .clicked() { action = Some(HotkeyAction::Remove(slot.slot.clone())); } if ui .add(Button::new(ICON_KEYBOARD).frame(false)) .on_hover_text("Set key chord") .clicked() { action = Some(HotkeyAction::Capture(slot.slot.clone())); } if slot.key_chord.is_some() && ui .add(Button::new(ICON_BACKSPACE).frame(false)) .on_hover_text("Clear key chord") .clicked() { action = Some(HotkeyAction::ClearChord(slot.slot.clone())); } if ui .add(Button::new(ICON_PLAY_ARROW).frame(false)) .on_hover_text("Play") .clicked() { action = Some(HotkeyAction::Play(slot.slot.clone())); } }); }); }); } }); action } fn handle_hotkey_action(&mut self, action: HotkeyAction) { match action { HotkeyAction::Remove(slot) => { make_request_async(Request::clear_hotkey(&slot)); self.app_state.hotkey_config.remove_slot(&slot); } HotkeyAction::Capture(slot) => { self.app_state.assigning_hotkey_slot = Some(slot); self.app_state.hotkey_capture_active = true; } HotkeyAction::ClearChord(slot) => { make_request_async(Request::clear_hotkey_key(&slot)); self.app_state.hotkey_config.set_key_chord(&slot, None); } HotkeyAction::Play(slot) => { self.play_hotkey_slot(&slot); } } } fn draw_header(&mut self, ui: &mut Ui) { ui.vertical_centered_justified(|ui| { if self.audio_player_state.tracks.is_empty() { ui.label("No tracks playing"); return; } let mut action = None; for track in &self.audio_player_state.tracks { CollapsingHeader::new( RichText::new( track .path .file_stem() .unwrap_or_default() .to_str() .unwrap_or_default(), ) .color(Color32::WHITE) .family(FontFamily::Monospace), ) .default_open(true) .show(ui, |ui| { if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, track) { action = Some(act); } }); ui.separator(); } if let Some(action) = action { match action { TrackAction::Pause(id) => self.pause(Some(id)), TrackAction::Resume(id) => self.resume(Some(id)), TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)), TrackAction::Stop(id) => self.stop(Some(id)), } } }); } fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option { let mut action = None; let play_button = Button::new(if track.paused { ICON_PLAY_ARROW } else { ICON_PAUSE }) .corner_radius(15.0); if ui.add_sized([30.0, 30.0], play_button).clicked() { action = Some(if track.paused { TrackAction::Resume(track.id) } else { TrackAction::Pause(track.id) }); } let loop_button = Button::new( RichText::new(if track.looped { ICON_REPEAT_ONE } else { ICON_REPEAT }) .size(18.0), ) .frame(false); if ui.add_sized([15.0, 30.0], loop_button).clicked() { action = Some(TrackAction::ToggleLoop(track.id)); } action } fn draw_position_control( ui: &mut Ui, ui_state: &mut pwsp::types::gui::TrackUiState, track: &TrackInfo, default_slider_width: f32, ) { let duration = track.duration.unwrap_or(1.0); let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration) .show_value(false) .step_by(0.01); let position_slider_width = ui.available_width() - (30.0 * 3.0) - default_slider_width - (ui.spacing().item_spacing.x * 6.0); ui.spacing_mut().slider_width = position_slider_width; if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() { ui_state.position_dragged = true; } let time_label = Label::new(RichText::new(format_time_pair(track.position, duration)).monospace()); ui.add_sized([30.0, 30.0], time_label); } fn draw_volume_control( ui: &mut Ui, ui_state: &mut pwsp::types::gui::TrackUiState, track: &TrackInfo, default_slider_width: f32, ) { let volume_icon = Self::get_volume_icon(track.volume); let volume_label = Label::new(RichText::new(volume_icon).size(18.0)); ui.add_sized([30.0, 30.0], volume_label) .on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0)); let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0) .show_value(false) .step_by(0.01); ui.spacing_mut().slider_width = default_slider_width - 30.0; ui.spacing_mut().item_spacing.x = 0.0; if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() { ui_state.volume_dragged = true; } } fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option { let stop_button = Button::new(ICON_CLOSE).frame(false); if ui.add_sized([30.0, 30.0], stop_button).clicked() { Some(TrackAction::Stop(track.id)) } else { None } } fn draw_track_control( ui: &mut Ui, app_state: &mut AppState, track: &TrackInfo, ) -> Option { let ui_state = app_state.track_ui_states.entry(track.id).or_default(); let should_update_position = !ui_state.position_dragged && ui_state .ignore_position_update_until .map(|t| Instant::now() > t) .unwrap_or(true); if should_update_position { ui_state.position_slider_value = track.position; } let should_update_volume = !ui_state.volume_dragged && ui_state .ignore_volume_update_until .map(|t| Instant::now() > t) .unwrap_or(true); if should_update_volume { ui_state.volume_slider_value = track.volume; } let mut action = None; ui.horizontal_top(|ui| { if let Some(act) = Self::draw_playback_controls(ui, track) { action = Some(act); } let default_slider_width = ui.spacing().slider_width; Self::draw_position_control(ui, ui_state, track, default_slider_width); Self::draw_volume_control(ui, ui_state, track, default_slider_width); if let Some(act) = Self::draw_stop_control(ui, track) { action = Some(act); } }); action } 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 = 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(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() { self.open_dir(&path); } 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() { self.open_dir(&path); } 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; 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(&mut self, ui: &mut Ui, area_size: Vec2) { ui.vertical(|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); }); ui.separator(); 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_tree_node( ui: &mut Ui, path: std::path::PathBuf, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, ) { if path.is_dir() { 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); } }); } else { 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_footer(&mut self, ui: &mut Ui) { ui.add_space(5.0); ui.horizontal(|ui| { // ---------- Microphone selection ---------- let mics = &self.audio_player_state.all_inputs_sorted; let mut selected_input = self.audio_player_state.current_input.to_owned(); let prev_input = selected_input.to_owned(); ComboBox::from_label(t!("gui.choose_mic_select")) .height(30.0) .selected_text( self.audio_player_state .all_inputs .get(&selected_input) .unwrap_or(&String::new()), ) .show_ui(ui, |ui| { for (name, nick) in mics { ui.selectable_value(&mut selected_input, name.clone(), nick); } }); if selected_input != prev_input { self.set_input(selected_input); } // -------------------------------- // ---------- Master Volume Slider ---------- let volume_icon = Self::get_volume_icon(self.audio_player_state.volume); let volume_label = Label::new(RichText::new(volume_icon).size(18.0)); ui.add_sized([18.0, 18.0], volume_label) .on_hover_text(format!( "Master Volume: {:.0}%", self.audio_player_state.volume * 100.0 )); let should_update_volume = !self.app_state.volume_dragged && self .app_state .ignore_volume_update_until .map(|t| Instant::now() > t) .unwrap_or(true); if should_update_volume { self.app_state.volume_slider_value = self.audio_player_state.volume; } let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0) .show_value(false) .step_by(0.01); let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider); if volume_slider_response.drag_stopped() { self.app_state.volume_dragged = true; } // ------------------------------------------ ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0); // ---------- Hotkeys button ---------- let hotkeys_button = Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false); let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button); if hotkeys_button_response.clicked() { self.app_state.show_hotkeys = true; } hotkeys_button_response.on_hover_text("Hotkeys (H)"); // -------------------------------- // ---------- Settings button ---------- let settings_button = Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false); let settings_button_response = ui.add_sized([18.0, 18.0], settings_button); if settings_button_response.clicked() { self.app_state.show_settings = true; } // -------------------------------- }); } }