collective

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

diff --git a/crates/engine/src/cmd/para.rs b/crates/engine/src/cmd/para.rs--- a/crates/engine/src/cmd/para.rs+++ b/crates/engine/src/cmd/para.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| {diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs--- a/crates/engine/src/tests.rs+++ b/crates/engine/src/tests.rs@@ -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")); } diff --git a/crates/ui-egui/src/keys.rs b/crates/ui-egui/src/keys.rs--- a/crates/ui-egui/src/keys.rs+++ b/crates/ui-egui/src/keys.rs@@ -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() };diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs--- a/crates/ui-egui/src/lib.rs+++ b/crates/ui-egui/src/lib.rs@@ -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() {diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs--- a/crates/ui-egui/src/windows.rs+++ b/crates/ui-egui/src/windows.rs@@ -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]