From a2bbd96f304814ff9a2e510540d8c6ac2d4801fb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 11:37:22 -0400 Subject: [PATCH] UI: tool shortcuts work right after clicking the canvas (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/ui-egui/src/canvas.rs | 4 +-- crates/ui-egui/src/menus.rs | 55 +++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 595468fa..db4d1393 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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()); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 5996698c..147964e2 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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| { + 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());