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 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 05:21:47 -04:00
parent e8273b364e
commit 2d0143f1aa
+141 -20
View File
@@ -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;
}
});
}