Keep shortcuts working after Tab

With no widget focused, egui answers Tab by giving keyboard focus to the
first widget in the window, and Shift+Tab to the last one. shortcuts::handle
ignores every key while a widget has focus, so after Tab (to the next clip
boundary) R/T zoom and every other shortcut did nothing until a click
elsewhere or Escape. Space and Enter clicked the focused widget instead: on
macOS that is the toolbar's Shuffle button, so Space switched the session to
Shuffle; on Windows and Linux it opened the SoundCraft menu.

While the shortcuts own the keyboard (nothing focused, no dialog open),
handle() now cancels egui's focus move for the frame. Tab still moves between
fields once one has focus, and inside dialogs.
This commit is contained in:
grovebit
2026-10-12 23:16:11 +01:00
parent 6cf896e76c
commit 6fd17d7c0e
2 changed files with 33 additions and 0 deletions
+2
View File
@@ -86,6 +86,8 @@ pub fn handle(app: &mut SoundApp, ctx: &egui::Context) {
let _ = app.run(c.0, json!({}));
}
}
// Tab moves the insertion point here, so egui must not also hand focus to the first widget: that would block every shortcut.
ctx.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
}
fn command_for(key: Key, mods: Modifiers, mac: bool) -> Option<&'static str> {
+31
View File
@@ -25,6 +25,37 @@ fn ctrl_shift_arrows_nudge_the_selected_clip_gain() {
}
}
fn type_in_app(ctx: &egui::Context, app: &mut SoundApp, keys: &[(Key, Modifiers)]) {
let events = keys
.iter()
.flat_map(|&(key, modifiers)| [true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }))
.collect();
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
app.logic(ui.ctx());
app.ui(ui);
})
.textures_delta
.clear();
}
#[test]
fn tab_leaves_the_keys_to_the_shortcuts() {
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
let ctx = egui::Context::default();
for _ in 0..3 {
type_in_app(&ctx, &mut app, &[]);
}
for tab in [Modifiers::NONE, Modifiers::SHIFT] {
type_in_app(&ctx, &mut app, &[(Key::Tab, tab)]);
assert_eq!(ctx.memory(|m| m.focused()), None, "{tab:?}");
let zoom = app.engine.session().edit.zoom.samples_per_px;
type_in_app(&ctx, &mut app, &[(Key::T, Modifiers::NONE)]);
assert_eq!(app.engine.session().edit.zoom.samples_per_px, zoom / 2.0, "{tab:?}");
type_in_app(&ctx, &mut app, &[(Key::R, Modifiers::NONE)]);
assert_eq!(app.engine.session().edit.zoom.samples_per_px, zoom, "{tab:?}");
}
}
#[test]
fn cmd_equals_toggles_mix_and_edit_without_zooming() {
let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };