UI: tool shortcuts work right after clicking the canvas (#1)

Only a focused text field blocks global shortcuts; the canvas (or a
button) holding egui focus after a click no longer swallows V/F/… until
Esc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-04 11:38:17 -04:00
co-authored by Claude Opus 5.5
parent 8490965e71
commit a2bbd96f30
2 changed files with 56 additions and 3 deletions
+2 -2
View File
@@ -1317,7 +1317,7 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
let Some(v) = app.view().copied() else { return };
let xf = Xf::new(rect, &v);
let wants_text = app.session.wants_text();
let space = ui.input(|i| i.key_down(egui::Key::Space)) && !wants_text && !ui.ctx().egui_wants_keyboard_input();
let space = ui.input(|i| i.key_down(egui::Key::Space)) && !wants_text && !ui.ctx().text_edit_focused();
// Scroll / zoom.
if resp.hovered() {
let (scroll, zoom_delta, m, hover) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta(), i.modifiers, i.pointer.hover_pos()));
@@ -1415,7 +1415,7 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
resp.request_focus();
}
// Keyboard for the active tool.
if ui.ctx().egui_wants_keyboard_input() && !resp.has_focus() {
if ui.ctx().text_edit_focused() && !resp.has_focus() {
return;
}
let evs = ui.input(|i| i.events.clone());
+54 -1
View File
@@ -1780,7 +1780,9 @@ fn parse_shortcut(sc: &str) -> Option<(egui::Modifiers, egui::Key)> {
/// Global keyboard shortcuts: menu commands and single-key tool shortcuts.
pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
if ctx.egui_wants_keyboard_input() || app.ui.dialog.is_some() || app.ui.palette.is_some() {
// Only a focused text field takes the keys: the canvas (or a button) having focus after a click
// must not swallow tool shortcuts until Esc clears it.
if ctx.text_edit_focused() || app.ui.dialog.is_some() || app.ui.palette.is_some() {
return;
}
let typing = app.session.wants_text();
@@ -2118,6 +2120,57 @@ mod tests {
assert!((app.view().unwrap().zoom - z0).abs() < 1e-9, "back at the zoom it started from");
}
#[test]
fn tool_shortcuts_work_after_drawing_on_the_canvas() {
// Clicking the canvas gives it keyboard focus; single-key tool shortcuts must still switch
// tools without pressing Esc first (#1).
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
let ctx = egui::Context::default();
let frame = |app: &mut crate::DesignApp, events: Vec<egui::Event>| {
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))),
events,
..Default::default()
};
let mut out = ctx.run_ui(input, |ui| {
app.logic(&ui.ctx().clone());
app.ui(ui);
});
out.textures_delta.clear();
};
let key = |k: egui::Key| {
vec![
egui::Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() },
egui::Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Default::default() },
]
};
let button = |pos: egui::Pos2, pressed: bool| egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed,
modifiers: Default::default(),
};
frame(&mut app, vec![]);
frame(&mut app, vec![]);
frame(&mut app, key(egui::Key::F));
assert_eq!(app.session.tool_id(), "rectangleFrame");
// Draw a frame.
let a = app.canvas_rect.unwrap().center();
let b = a + egui::vec2(120.0, 80.0);
frame(&mut app, vec![egui::Event::PointerMoved(a), button(a, true)]);
for i in 1..=4 {
frame(&mut app, vec![egui::Event::PointerMoved(a + (b - a) * (i as f32 / 4.0))]);
}
frame(&mut app, vec![button(b, false)]);
frame(&mut app, vec![]);
assert!(app.session.active().is_some_and(|d| !d.selection.items.is_empty()), "drew a frame");
frame(&mut app, key(egui::Key::V));
assert_eq!(app.session.tool_id(), "selection", "V switches tools straight after drawing");
frame(&mut app, key(egui::Key::F));
assert_eq!(app.session.tool_id(), "rectangleFrame");
}
#[test]
fn sample_scripts_run() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());