diff --git a/src/gui/draw.rs b/src/gui/draw.rs index db4e3c2..9cd6a65 100644 --- a/src/gui/draw.rs +++ b/src/gui/draw.rs @@ -137,271 +137,285 @@ impl SoundpadGui { ui.vertical(|ui| { ui.spacing_mut().item_spacing.y = 5.0; - // --- Header --- - 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("Hotkeys").color(Color32::WHITE).monospace()); - }); - }); - + self.draw_hotkeys_header(ui); ui.separator(); - // --- Search and Add Command --- - ui.horizontal(|ui| { - ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| { - let mut selected_cmd = None; - if ui.button("Toggle Pause").clicked() { - selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None))); - } - if ui.button("Stop Playback").clicked() { - selected_cmd = Some(("cmd_stop", Request::stop(None))); - } - if ui.button("Pause Playback").clicked() { - selected_cmd = Some(("cmd_pause", Request::pause(None))); - } - if ui.button("Resume Playback").clicked() { - selected_cmd = Some(("cmd_resume", Request::resume(None))); - } - if ui.button("Toggle Loop").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("Search hotkeys...") - .desired_width(f32::INFINITY), - ); - }); - + self.draw_hotkeys_search(ui); ui.separator(); ui.add_space(5.0); - 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) - }) - .cloned() - .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("Slot") - .strong() - .monospace() - .color(Color32::LIGHT_GRAY), - ); - }); - header.col(|ui| { - ui.label( - RichText::new("Sound") - .strong() - .monospace() - .color(Color32::LIGHT_GRAY), - ); - }); - header.col(|ui| { - ui.label( - RichText::new("Key Chord") - .strong() - .monospace() - .color(Color32::LIGHT_GRAY), - ); - }); - header.col(|ui| { - ui.label( - RichText::new("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("No hotkey slots 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())); - } - }); - }); - }); - } - }); + let action = self.draw_hotkeys_table(ui); if let Some(action) = action { - 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); - } - } + 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("Hotkeys").color(Color32::WHITE).monospace()); + }); + }); + } + + fn draw_hotkeys_search(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| { + let mut selected_cmd = None; + if ui.button("Toggle Pause").clicked() { + selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None))); + } + if ui.button("Stop Playback").clicked() { + selected_cmd = Some(("cmd_stop", Request::stop(None))); + } + if ui.button("Pause Playback").clicked() { + selected_cmd = Some(("cmd_pause", Request::pause(None))); + } + if ui.button("Resume Playback").clicked() { + selected_cmd = Some(("cmd_resume", Request::resume(None))); + } + if ui.button("Toggle Loop").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("Search hotkeys...") + .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) + }) + .cloned() + .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("Slot") + .strong() + .monospace() + .color(Color32::LIGHT_GRAY), + ); + }); + header.col(|ui| { + ui.label( + RichText::new("Sound") + .strong() + .monospace() + .color(Color32::LIGHT_GRAY), + ); + }); + header.col(|ui| { + ui.label( + RichText::new("Key Chord") + .strong() + .monospace() + .color(Color32::LIGHT_GRAY), + ); + }); + header.col(|ui| { + ui.label( + RichText::new("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("No hotkey slots 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() {