Browse / github.com/storytold/wordcraft / patch
macOS: ⌘M minimizes the window and ⌘W closes it, as in Word for Mac
macOS: ⌘M minimizes the window and ⌘W closes it, as in Word for Mac Refs #2509. ### What On the Mac, two window shortcuts every Mac app shares don't do what they do in Word for Mac: - **⌘M** ran Increase Indent (Word for Windows' Ctrl+M) instead of minimizing the window. In Word for Mac, ⌘M is the system's Minimize; Microsoft's Word for Mac shortcut list gives **Control+Shift+M** for "Indent the paragraph" and ⌘⇧M for "Remove a paragraph indent", and lists nothing on ⌘M. - **⌘W** in the last window closed the document but left an empty WordCraft window behind (#1712's Windows behaviour). In Word for Mac, ⌘W closes the window. WordCraft has no menu bar for t
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- jpc7k1 (imported from pull request #4511, open)
- Base
d859c673c1e93bccaa6536fa3653b902ea877cea- Size
- +104 −3 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
f98659c71efb3af270ff4663
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/para.rs crates/engine/src/tests.rs crates/ui-egui/src/keys.rs crates/ui-egui/src/lib.rs crates/ui-egui/src/windows.rs
@@ -37,7 +37,8 @@ pub fn specs() -> Vec<CommandSpec> { .params(r#"{"value"?: bool (false = left to right)}"#), CommandSpec::new("para.ltr", "Left-to-Right Text Direction", "Home › Paragraph", |s, v| direction(s, v, false)) .params(r#"{"value"?: bool (false = right to left)}"#),- CommandSpec::new("para.indent", "Increase Indent", "Home › Paragraph", indent).key("Mod+M"),+ // ⌘M minimizes the window on macOS; Word for Mac indents with Control+Shift+M.+ CommandSpec::new("para.indent", "Increase Indent", "Home › Paragraph", indent).key(super::mac_or("Ctrl+Shift+M", "Mod+M")), CommandSpec::new("para.outdent", "Decrease Indent", "Home › Paragraph", outdent).key("Mod+Shift+M"), CommandSpec::new("para.hangingIndent", "Hanging Indent", "Home › Paragraph › Paragraph", |s, _| { fmt(s, &|p| {@@ -194,6 +194,14 @@ fn word_default_shortcuts_resolve() { assert_eq!(id("Shift+F3"), Some("format.changeCase")); assert_eq!(id("F12"), Some("file.saveAs")); assert_eq!(id("Mod+Alt+M"), Some("review.newComment"));+ // Word for Mac indents with Control+Shift+M; ⌘M is the system's Minimize.+ if cfg!(target_os = "macos") {+ assert_eq!(id("Ctrl+Shift+M"), Some("para.indent"));+ assert_eq!(id("Mod+M"), None);+ } else {+ assert_eq!(id("Mod+M"), Some("para.indent"));+ }+ assert_eq!(id("Mod+Shift+M"), Some("para.outdent")); assert_eq!(id("Mod+H"), Some("edit.findReplace")); } @@ -115,13 +115,15 @@ pub fn combo(m: Modifiers, key: &str, with_shift: bool) -> String { /// Shortcuts for interface commands (`ui.*`), which aren't in the engine's keymap: Alt+Q moves /// to "Tell me" command search, as in Word for Windows (#819). Not on macOS, where Option+Q types-/// a character. On macOS ⌘⌥R collapses or expands the ribbon, as in Word for Mac.+/// a character. On macOS ⌘⌥R collapses or expands the ribbon, as in Word for Mac, and ⌘M+/// minimizes the window, as in every Mac app. fn ui_shortcut(key: Key, m: Modifiers) -> Option<(&'static str, Value)> { let alt_only = m.alt && !m.command && !m.ctrl && !m.mac_cmd && !m.shift; let mac_cmd_alt = cfg!(target_os = "macos") && m.mac_cmd && m.alt && !m.ctrl && !m.shift; match key { Key::Q if alt_only && !cfg!(target_os = "macos") => Some(("ui.dialog", json!({"name": "commands"}))), Key::R if mac_cmd_alt => Some(("ui.collapseRibbon", json!({}))),+ Key::M if cfg!(target_os = "macos") && m.mac_cmd && !m.alt && !m.ctrl && !m.shift => Some(("ui.minimize", json!({}))), // Ctrl+F1 collapses or expands the ribbon, as in Word for Windows (#1678). Not on macOS, // where Ctrl+F1 is the system's keyboard-access switch. Key::F1 if m.ctrl && !m.alt && !m.shift && !m.mac_cmd && !cfg!(target_os = "macos") => Some(("ui.collapseRibbon", json!({}))),@@ -146,7 +148,11 @@ fn dispatch(app: &mut WordApp, key: Key, m: Modifiers) -> bool { if m.alt && !crate::widgets::enabled(app, spec.id) { return false; }- let _ = app.run(spec.id, json!({}));+ if spec.id == "file.close" {+ app.close_from_key();+ } else {+ let _ = app.run(spec.id, json!({}));+ } return true; } let Some(name) = name else { return false };@@ -1638,6 +1644,16 @@ mod tests { assert_eq!(ui_shortcut(Key::R, m(true, true, true)), None, "Mod+Alt+Shift+R"); } + #[test]+ fn cmd_m_minimizes_on_macos() {+ let m = |mac_cmd, shift, ctrl| egui::Modifiers { mac_cmd, command: mac_cmd, shift, ctrl, ..Default::default() };+ let minimize = Some(("ui.minimize", json!({})));+ assert_eq!(ui_shortcut(Key::M, m(true, false, false)), if cfg!(target_os = "macos") { minimize } else { None });+ assert_eq!(ui_shortcut(Key::M, m(true, true, false)), None, "⌘⇧M decreases the indent");+ assert_eq!(ui_shortcut(Key::M, m(false, true, true)), None, "⌃⇧M increases the indent");+ assert_eq!(ui_shortcut(Key::M, m(false, false, false)), None, "a plain M");+ }+ #[test] fn only_plain_paste_releases_paste_a_clipboard_picture() { let m = |command, shift, alt| egui::Modifiers { command, shift, alt, ..Default::default() };@@ -1883,6 +1883,13 @@ impl WordApp { json!({"pending": "contextMenu"}) } "ui.paneDock" => return Some(panes::dock_command(self, p)),+ // ⌘M on macOS: the window goes to the Dock.+ "ui.minimize" => {+ if let Some(ctx) = &self.ctx {+ ctx.send_viewport_cmd_to(self.active_viewport(), egui::ViewportCommand::Minimized(true));+ }+ json!({"minimized": true})+ } "ui.collapseRibbon" => { self.ui.ribbon_collapsed = !self.ui.ribbon_collapsed; json!({"collapsed": self.ui.ribbon_collapsed})@@ -2446,6 +2453,7 @@ impl WordApp { /// Per-frame logic before layout: control requests, screenshots, shortcuts, file drops. pub fn logic(&mut self, ctx: &egui::Context) {+ unstick_minimized(ctx); updates_ui::tick(self, ctx); let lang = i18n::Lang::from_pref(&self.ui.language); i18n::set_current(lang);@@ -3311,6 +3319,18 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> { /// Options, the view and interface preferences it sets, Customize Ribbon and Keyboard). /// Everything else needs a document, `ui.*` included: only the interface commands listed here /// (tabs, Backstage, dialogs, appearance, opening files) work without one.+/// ⌘M's `Minimized(true)` leaves egui's record of the window saying minimized, and on macOS+/// egui never reads the window's state back, so after the Dock brings the window back it would+/// stay "hidden": nothing drawn, no keys. A window with the keyboard isn't minimized, so the+/// record is put right. (The window's yellow button doesn't go through egui and never sets it.)+fn unstick_minimized(ctx: &egui::Context) {+ let stale: Vec<egui::ViewportId> =+ ctx.input(|i| i.raw.viewports.iter().filter(|(_, v)| v.minimized == Some(true) && v.focused == Some(true)).map(|(id, _)| *id).collect());+ for id in stale {+ ctx.send_viewport_cmd_to(id, egui::ViewportCommand::Minimized(false));+ }+}+ pub(crate) fn works_without_document(id: &str) -> bool { matches!( id,@@ -3337,6 +3357,7 @@ pub(crate) fn works_without_document(id: &str) -> bool { | "ui.dialog" | "ui.closeDialog" | "ui.collapseRibbon"+ | "ui.minimize" | "ui.quickAccessToolbar" | "ui.keytips" | "ui.dark"@@ -3421,6 +3442,27 @@ mod tests { egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND } } + /// After ⌘M, egui's record says minimized; once the window has the keyboard again the record is+ /// put right, so the window draws and takes keys. A window still in the Dock is left alone.+ #[test]+ fn a_window_back_from_the_dock_is_not_left_minimized() {+ let ctx = egui::Context::default();+ let restores = |minimized, focused| {+ let mut raw = egui::RawInput::default();+ let root = raw.viewports.entry(egui::ViewportId::ROOT).or_default();+ root.minimized = Some(minimized);+ root.focused = Some(focused);+ let out = ctx.run_ui(raw, |ui| unstick_minimized(ui.ctx()));+ let commands = out.viewport_output.get(&egui::ViewportId::ROOT).map(|v| v.commands.clone()).unwrap_or_default();+ // No painter here: dropping the font atlas delta unapplied trips an epaint debug assertion.+ out.drop_without_applying_deltas();+ commands.contains(&egui::ViewportCommand::Minimized(false))+ };+ assert!(restores(true, true), "back from the Dock");+ assert!(!restores(true, false), "still in the Dock");+ assert!(!restores(false, true), "never minimized");+ }+ /// Issue #14: Mod+- (optional hyphen) also zoomed the whole window out, so it read as "zoom out". #[test] fn word_shortcuts_do_not_zoom_the_window() {@@ -307,6 +307,18 @@ impl WordApp { self.window_count() > 1 && !self.quit_requested } + /// File › Close from its key (⌘W). On macOS it closes the window, as Word for Mac does; in the+ /// last window, with no menu bar for the app to stay behind in, that quits, asking first about+ /// unsaved changes as the window's close button does. Elsewhere, and for the Backstage's Close+ /// and an agent's `file.close`, the last window stays with no page (#1712).+ pub(crate) fn close_from_key(&mut self) {+ if cfg!(target_os = "macos") && self.window_count() <= 1 {+ self.quit();+ } else if let Err(e) = self.run("file.close", json!({})) {+ log::warn!("closing: {e}");+ }+ }+ /// Quit the app (the app menu's Quit): ask about every document with unsaved work, each in /// its window, as [`WordApp::close_requested`] does. True when the app may quit now; the /// host closes it once `quit_requested` is set.@@ -1051,6 +1063,28 @@ mod tests { assert!(a.quit_requested); } + /// Mod+W in the last window: on macOS it closes the window, as Word for Mac does, and so quits+ /// once the unsaved changes are answered; elsewhere the window stays with no page (#1712).+ #[test]+ fn mod_w_in_the_last_window_closes_it_on_macos() {+ let ctx = backend();+ let mut a = app("Only");+ focus(ViewportId::ROOT);+ frames(&ctx, &mut a, 4);+ let mac = cfg!(target_os = "macos");+ let modifiers = egui::Modifiers { ctrl: !mac, mac_cmd: mac, command: true, ..Default::default() };+ let key = egui::Event::Key { key: egui::Key::W, physical_key: None, pressed: true, repeat: false, modifiers };+ EVENTS.with(|e| e.borrow_mut().entry(ViewportId::ROOT).or_default().push(key));+ frame(&ctx, &mut a);+ assert_eq!(prompt(&a), Some("saveChanges"), "unsaved changes are asked about first");+ assert!(!a.quit_requested);+ a.run("ui.saveChanges", json!({"answer": "dontSave"})).unwrap();+ assert_eq!(a.quit_requested, mac, "the last window closes on macOS only");+ if !mac {+ assert!(!a.has_document(), "elsewhere the window stays with no page");+ }+ }+ /// Mod+W in a window with no changes closes it; the keys typed after it in the same frame /// don't reach the window that comes forward. #[test]