diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 576fddc57..d48c67d3d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1938,6 +1938,11 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r if resp.clicked() || resp.drag_started() { resp.request_focus(); } + // Arrow keys, Tab and Escape belong to the canvas (caret, nudge, tab character, deselect). + // Without the lock egui moves focus to a neighbouring widget, and the next Space or Return + // clicks it while typing still reaches the text. + let lock = egui::EventFilter { tab: true, horizontal_arrows: true, vertical_arrows: true, escape: true }; + ui.memory_mut(|m| m.set_focus_lock_filter(resp.id, lock)); // Keyboard for the active tool. if menu_open || (ui.ctx().text_edit_focused() && !resp.has_focus()) || !keyboard_pane(app) { return; @@ -3107,6 +3112,46 @@ mod tests { assert!(screen_options(&app, &layout, true, 1.0).document_grid.is_empty()); } + /// egui moves keyboard focus on a plain arrow key or Tab, and Space/Return then clicks the + /// focused widget (e.g. the status bar's spread buttons) while typing still reaches the text. + #[test] + fn arrow_keys_and_tab_keep_keyboard_focus_on_the_canvas() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 3})).unwrap(); + let f = app.run("frame.create", json!({"rect": [36, 36, 300, 200], "content": "text", "text": "Hello"})).unwrap(); + app.select_tool("type"); + let mut h = harness(app); + let p = { + let app = h.state(); + let st = app.session.active().unwrap(); + let layout = CanvasLayout::new(&st.doc, st.editing_parents); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + xf.to_screen(layout.xf(SpreadRef::Doc(0)) * Point::new(100.0, 60.0)) + }; + h.hover_at(p); + h.step(); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }); + h.step(); + } + h.run_steps(2); + let focus = h.ctx.memory(|m| m.focused()); + assert!(focus.is_some(), "a click on the canvas takes keyboard focus"); + let view = *h.state().view().unwrap(); + for key in [egui::Key::ArrowDown, egui::Key::Space, egui::Key::ArrowRight, egui::Key::Enter, egui::Key::Tab, egui::Key::Escape] { + h.key_press(key); + if key == egui::Key::Space { + h.event(egui::Event::Text(" ".into())); + } + h.run_steps(2); + assert_eq!(h.ctx.memory(|m| m.focused()), focus, "{key:?} keeps keyboard focus on the canvas"); + } + let v = *h.state().view().unwrap(); + assert_eq!((v.origin, v.zoom), (view.origin, view.zoom), "the view stays where it was"); + let text = h.state_mut().session.execute("story.get", &json!({"story": f["story"]})).unwrap()["text"].clone(); + assert_eq!(text, "Hello \n\t", "Space, Return and Tab reach the text"); + } + #[test] fn rulers_open_no_units_menu_without_a_document() { let mut h = harness(DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()));