From 2d0143f1aad7e7ebdf1f0bf13e42b4e96fd8d653 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Fri, 29 May 2026 05:21:47 -0400 Subject: [PATCH] feat(gui): keyboard shortcuts + a Shortcuts reference screen Window-focused shortcuts via a handle_keys() dispatch: H/V/S on the menu, Space/Enter to start hosting and C to copy the code on the Host screen, F1 to open the new Shortcuts screen, and Esc to back out (existing). Letter/Space actions only fire when no widget holds focus, so they don't clash with typing or egui's Space/Enter widget activation; popups keep their own key handling. New Screen::Shortcuts lists every binding behind a menu button. Also enforce that leaving Settings closes the theme editor, so a draft preview can't leak. Co-Authored-By: Claude Opus 4.8 --- src/gui/mod.rs | 161 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 20 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 6e6dcd6..af0cc53 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -734,6 +734,7 @@ enum Screen { Host, Viewer, Settings, + Shortcuts, } /// Quality preset choices, mirroring `cli::Quality`. Map to the `--quality` @@ -930,6 +931,24 @@ fn color_section(ui: &mut egui::Ui, title: &str, id: &str, rows: impl FnOnce(&mu .show(ui, rows); } +/// One `(keys, description)` group on the Shortcuts screen: a bold subheading +/// over a two-column grid (monospace keys, plain descriptions). +fn shortcut_section(ui: &mut egui::Ui, title: &str, id: &str, rows: &[(&str, &str)]) { + ui.add_space(8.0); + ui.label(egui::RichText::new(title).strong()); + ui.add_space(2.0); + egui::Grid::new(id) + .num_columns(2) + .spacing([16.0, 6.0]) + .show(ui, |ui| { + for (keys, desc) in rows { + ui.label(egui::RichText::new(*keys).monospace()); + ui.label(*desc); + ui.end_row(); + } + }); +} + impl PixelPassApp { /// Drain child output and reflect it into the tray. Runs on every wake, /// whether or not a window is shown, so notifications and the tray tooltip @@ -942,26 +961,13 @@ impl PixelPassApp { /// Render the current screen. Called from inside the egui frame. fn draw(&mut self, ui: &mut egui::Ui) { - // Esc backs out one level, mirroring each screen's "← Menu" button: stop - // any session and return to the menu, or (in the theme editor) close the - // editor back to the picker first so a live preview isn't left applied. - // Suppressed while a popup (colour picker, dropdown) is open, so there - // Esc just dismisses the popup rather than also navigating. - let esc = ui.input(|i| i.key_pressed(egui::Key::Escape)); - if esc && !ui.ctx().any_popup_open() { - match self.screen { - Screen::Host => { - self.stop_host(); - self.screen = Screen::Menu; - } - Screen::Viewer => { - self.stop_viewer(); - self.screen = Screen::Menu; - } - Screen::Settings if self.theme.editing => self.cancel_theme_edit(), - Screen::Settings => self.screen = Screen::Menu, - Screen::Menu => {} - } + self.handle_keys(ui); + + // Leaving Settings by any path (button, Esc, a shortcut) closes the + // editor and restores the active theme, so a half-edited draft's live + // preview never leaks onto other screens. + if self.theme.editing && self.screen != Screen::Settings { + self.cancel_theme_edit(); } // Apply whichever theme should be visible this frame. While the editor @@ -990,9 +996,120 @@ impl PixelPassApp { Screen::Host => self.host(ui), Screen::Viewer => self.viewer(ui), Screen::Settings => self.settings(ui), + Screen::Shortcuts => self.shortcuts(ui), } } + /// Window-focused keyboard shortcuts (mirrored on the Shortcuts screen). + /// Runs before the frame is drawn. Esc and F1 work regardless of focus + /// (they aren't text-editing keys); the letter/Space actions only fire when + /// no widget holds focus, so they don't clash with typing in a field or + /// with egui using Space/Enter to activate the focused widget. + fn handle_keys(&mut self, ui: &mut egui::Ui) { + use egui::Key; + // While a popup (colour picker, dropdown) is open, let it own the keys. + if ui.ctx().any_popup_open() { + return; + } + let key = |k: Key| ui.input(|i| i.key_pressed(k)); + + if key(Key::F1) { + self.screen = Screen::Shortcuts; + return; + } + if key(Key::Escape) { + match self.screen { + Screen::Host => { + self.stop_host(); + self.screen = Screen::Menu; + } + Screen::Viewer => { + self.stop_viewer(); + self.screen = Screen::Menu; + } + Screen::Settings if self.theme.editing => self.cancel_theme_edit(), + Screen::Settings | Screen::Shortcuts => self.screen = Screen::Menu, + Screen::Menu => {} + } + return; + } + + if ui.memory(|m| m.focused().is_some()) { + return; + } + + match self.screen { + Screen::Menu => { + if key(Key::H) { + self.screen = Screen::Host; + } else if key(Key::V) { + self.screen = Screen::Viewer; + self.prefill_viewer_ticket(); + } else if key(Key::S) { + self.theme.names = theme::all_themes().into_iter().map(|t| t.name).collect(); + self.screen = Screen::Settings; + } + } + Screen::Host => { + if self.host.proc.is_some() { + if key(Key::C) + && let Some(ticket) = self.host.ticket.clone() + { + self.copy_to_clipboard(&ticket); + } + } else if key(Key::Space) || key(Key::Enter) { + self.start_host(); + } + } + // View's Enter (Connect) is handled in viewer_form. + Screen::Viewer | Screen::Settings | Screen::Shortcuts => {} + } + } + + /// The Shortcuts screen: a static reference list of every key binding. + fn shortcuts(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + if ui.button("← Menu").clicked() { + self.screen = Screen::Menu; + } + ui.heading("Keyboard shortcuts"); + }); + ui.separator(); + egui::ScrollArea::vertical().show(ui, |ui| { + shortcut_section( + ui, + "Anywhere", + "sc_any", + &[("Esc", "Back / close"), ("F1", "Open this list")], + ); + shortcut_section( + ui, + "Main menu", + "sc_menu", + &[ + ("H", "Host — share my screen"), + ("V", "View — watch someone's screen"), + ("S", "Settings"), + ], + ); + shortcut_section( + ui, + "Host", + "sc_host", + &[ + ("Space / Enter", "Start hosting"), + ("C", "Copy the share code"), + ], + ); + shortcut_section( + ui, + "View", + "sc_view", + &[("Enter", "Connect to the pasted code")], + ); + }); + } + /// Mirror current activity into the tray icon's tooltip/menu. fn sync_tray_status(&mut self) { let status = if self.host.proc.is_some() { @@ -1045,6 +1162,10 @@ impl PixelPassApp { self.theme.names = theme::all_themes().into_iter().map(|t| t.name).collect(); self.screen = Screen::Settings; } + ui.add_space(8.0); + if ui.button("⌨ Keyboard shortcuts").clicked() { + self.screen = Screen::Shortcuts; + } }); }