diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5bb7524f0..c0e7d809c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1676,6 +1676,7 @@ pub fn now_ms() -> f64 { #[cfg(test)] mod tests { use super::*; + use egui_kittest::kittest::Queryable; fn mod_key(key: egui::Key) -> egui::Event { egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND } @@ -2641,6 +2642,46 @@ mod tests { assert_eq!(reported.get(), Some(true), "the host was told before the frame ended"); } + #[test] + fn paragraph_alignment_keeps_the_canvas_focused_for_typing() { + use wordcraft_doc::Align; + + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + typed(), + ); + for _ in 0..6 { + h.step(); + } + + for (label, alignment, text) in + [("Align Left", Align::Left, " left"), ("Center", Align::Center, " center"), ("Align Right", Align::Right, " right")] + { + h.get_by_role_and_label(egui::accesskit::Role::Button, label).click(); + h.step(); + + assert!(h.state().canvas.focused, "{label}: the canvas keeps keyboard focus"); + let paragraph = h.state().session.doc.para_at(&h.state().session.sel.focus).expect("the caret remains in its paragraph"); + assert_eq!(h.state().session.doc.styles.resolve_para(¶graph.props).align, alignment); + + h.event(egui::Event::Text(text.into())); + h.step(); + assert!(body_text(h.state()).ends_with(text), "{label}: typing continues in the document"); + } + + // Every command button does the same, not only alignment: Bold, then type. + h.get_by_role_and_label(egui::accesskit::Role::Button, "Bold").click(); + h.step(); + assert!(h.state().canvas.focused, "Bold: the canvas keeps keyboard focus"); + h.event(egui::Event::Text(" bold".into())); + h.step(); + assert!(body_text(h.state()).ends_with(" bold"), "Bold: typing continues in the document"); + } + /// #330: Word's shortcuts work from the keyboard — Ctrl+Alt+1 (⌘⌥1) applies Heading 1, and /// an Alt shortcut doesn't also type the character its key would produce. #[test] diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 50b8f8c98..43bfc4506 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -337,7 +337,7 @@ fn mi_text(ui: &mut Ui, app: &mut WordApp, text: &str, id: &str, params: Value) let enabled = crate::widgets::enabled(app, id); let resp = ui.add_enabled(enabled, egui::Button::new(text).shortcut_text(sc).min_size(vec2(200.0, 0.0))); if resp.clicked() { - let _ = app.run(id, params); + crate::widgets::run_from_ribbon(app, id, params); ui.close(); } } @@ -390,7 +390,7 @@ fn mi_check(ui: &mut Ui, app: &mut WordApp, label: &str, checked: bool, id: &str let enabled = crate::widgets::enabled(app, id); let resp = ui.add_enabled(enabled, egui::Button::new((mark, tl!(label))).selected(checked).min_size(vec2(200.0, 0.0))); if resp.clicked() { - let _ = app.run(id, params); + crate::widgets::run_from_ribbon(app, id, params); ui.close(); } } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 4df392c8c..37dab5a59 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -105,6 +105,16 @@ pub(crate) fn bg(ui: &Ui, r: Rect, resp: &Response, checked: bool, t: &Tokens) { } } +/// Runs a ribbon button's command, then hands keyboard focus back to the document so typing +/// continues without another click, as in Word (#686). Interface commands (`ui.*`) open dialogs, +/// panes and tabs that take focus themselves, so they keep it. +pub(crate) fn run_from_ribbon(app: &mut WordApp, id: &str, params: Value) { + let _ = app.run(id, params); + if !id.starts_with("ui.") { + app.canvas.want_focus = true; + } +} + /// A large ribbon button: 32 px icon over a (possibly two-line) label. pub fn big(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, params: Value, menu: bool) -> Response { big_button(ui, app, icon, label, id, params, menu, false) @@ -139,7 +149,7 @@ fn big_button(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, let resp = tooltip(app, resp, &label.replace('\n', " "), id); command_menu(&resp, app, id); if resp.clicked() && on && !menu { - let _ = app.run(id, params); + run_from_ribbon(app, id, params); } resp } @@ -162,8 +172,9 @@ pub fn small(ui: &mut Ui, app: &mut WordApp, icon: &str, label: Option<&str>, ti let resp = tooltip(app, resp, tip, id); command_menu(&resp, app, id); if resp.clicked() && on { - let _ = app.run(id, params); + run_from_ribbon(app, id, params); } + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, on, tip)); resp } @@ -195,7 +206,7 @@ pub fn split( let resp = tooltip(app, resp, tip, id); command_menu(&resp, app, id); if resp.clicked() { - let _ = app.run(id, params); + run_from_ribbon(app, id, params); } egui::Popup::menu(&aresp).show(|ui| { ui.set_min_width(180.0); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 59450e7c3..bc85f06c2 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (status bar language follows the caret and sets the proofing language; Review › Language is a menu; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (ribbon buttons return keyboard focus to the document, #686; previously trivial: screen readers can read the document canvas, #488; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch. Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our @@ -43,6 +43,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Date | Change | Summary | |---|---|---| +| 2026-10-11 | trivial | Ribbon buttons that run a command (alignment, Bold, styles…) hand keyboard focus back to the document, so typing continues without another click; they also report their name and enabled state to screen readers (#686) | | 2026-10-11 | trivial | Tables on the page: column and row borders and the resize handle drag, with a dashed guide and one undo step per drag (#217) | | 2026-10-11 | trivial | Animation effects › Use system setting also reads macOS (Reduce motion) and the Linux/BSD desktops (settings portal: the cross-desktop reduced-motion preference, GNOME's switch, KDE's animation speed; the GTK and KDE settings files where no portal answers), and follows a change without a restart | | 2026-10-11 | trivial | Animation effects setting in File › Options › Accessibility (Use system setting, On, Off), kept between runs: the app eased wheel scrolling, zooming and its fades whatever the system's setting was; off, they land at once. Follows the system's setting on Windows and in browsers; `ui.animations` for agents |