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:
+141
-20
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user