From 39fe594553477056677885cea021c254625ef223 Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 16 Oct 2026 06:16:17 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Integration:=20Window=20=E2=80=BA=20Minimiz?= =?UTF-8?q?e=20(=E2=8C=98M)=20on=20macOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/menus.rs | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 05462e8a8..23f220087 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -974,7 +974,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option Option> { - if matches!(id, "window.minimize" | "window.maximize" | "window.close" | "window.drag" | "window.resize") && !app.custom_titlebar { + if is_window_control(id) && !window_control_available(app, id) { return Some(Err("custom window controls are not available in this host".into())); } if let Some(result) = crate::panel_docking::command(app, id, p) { @@ -2092,13 +2092,24 @@ pub(crate) fn waits_for_ime(app: &DesignApp, id: &str) -> bool { ) || id.starts_with("edit.paste")) } +/// The commands that act on the desktop window itself. +fn is_window_control(id: &str) -> bool { + matches!(id, "window.minimize" | "window.maximize" | "window.close" | "window.drag" | "window.resize") +} + +/// Whether the host can run a window command: all of them with our own title bar, and Minimize +/// with the macOS menu bar (Window › Minimize, ⌘M). +fn window_control_available(app: &DesignApp, id: &str) -> bool { + app.custom_titlebar || (app.native_menu && id == "window.minimize") +} + /// Is a command enabled (for menu greying)? pub fn enabled(app: &DesignApp, id: &str) -> bool { if waits_for_ime(app, id) { return false; } - if matches!(id, "window.minimize" | "window.maximize" | "window.close" | "window.drag" | "window.resize") { - return app.custom_titlebar; + if is_window_control(id) { + return window_control_available(app, id); } match designcraft_engine::find_command(id) { Some(c) => (c.enabled)(&app.session).is_ok(), @@ -2298,12 +2309,17 @@ pub const MAC_APP_MENU: &[AppMenuItem] = &[ ]; /// The menu tree for the macOS menu bar: `menu_tree` without the commands `MAC_APP_MENU` holds, -/// so each command (and its shortcut) appears once. +/// so each command (and its shortcut) appears once, and with the standard Window › Minimize (⌘M) +/// at the top of the Window menu. pub fn native_menu_tree() -> Vec<(&'static str, Vec)> { let in_app_menu = |id: &str| MAC_APP_MENU.iter().any(|m| matches!(m, AppMenuItem::Cmd { command, .. } if *command == id)); let mut tree = menu_tree(); - for (_, entries) in &mut tree { + for (title, entries) in &mut tree { without_commands(entries, &in_app_menu); + if *title == "Window" { + let minimize = Item::Cmd { label: "Minimize".into(), id: "window.minimize".into(), params: Value::Null, shortcut: Some("Cmd+M") }; + entries.splice(0..0, [minimize, Item::Sep]); + } } tree } @@ -2407,8 +2423,8 @@ pub fn menu_enabled(app: &DesignApp, id: &str) -> bool { if waits_for_ime(app, id) { return false; } - if matches!(id, "window.minimize" | "window.maximize" | "window.close" | "window.drag" | "window.resize") { - return app.custom_titlebar; + if is_window_control(id) { + return window_control_available(app, id); } if id == "app.justificationDialog" { return enabled(app, "type.para"); @@ -5502,6 +5518,19 @@ mod tests { assert!(!open, "an open submenu closes when its row becomes fully clipped"); } + #[test] + fn the_mac_window_menu_minimizes_with_cmd_m() { + let window = native_menu_tree().into_iter().find(|(t, _)| *t == "Window").map(|(_, e)| e).unwrap_or_default(); + assert!(matches!(window.first(), Some(Item::Cmd { id, shortcut: Some("Cmd+M"), .. }) if id == "window.minimize"), "{:?}", window.first()); + assert!(matches!(window.get(1), Some(Item::Sep))); + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + assert!(run_ui(&mut app, "window.minimize", &Value::Null).is_some_and(|r| r.is_err()), "no host window controls"); + app.native_menu = true; + assert!(menu_enabled(&app, "window.minimize") && !menu_enabled(&app, "window.maximize")); + run_ui(&mut app, "window.minimize", &Value::Null).unwrap().unwrap(); + assert_eq!(app.pending_window_commands, [egui::ViewportCommand::Minimized(true)]); + } + #[test] fn mac_settings_follow_about_in_the_app_menu_and_leave_edit() { let pos = |command: &str| MAC_APP_MENU.iter().position(|m| matches!(m, AppMenuItem::Cmd { command: c, .. } if *c == command)); From e99783c75635342d3010c4bc77e2a85f2184a8e8 Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 16 Oct 2026 06:19:03 +0000 Subject: [PATCH 2/3] Integration: "&" shows in native menu labels --- apps/designcraft/src/native_menu.rs | 15 ++++++++++----- crates/ui-egui/src/menus.rs | 13 +++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/designcraft/src/native_menu.rs b/apps/designcraft/src/native_menu.rs index 27e0ade9a..86a88e5c0 100644 --- a/apps/designcraft/src/native_menu.rs +++ b/apps/designcraft/src/native_menu.rs @@ -45,6 +45,11 @@ fn accel(sc: &str) -> Option { Accelerator::from_str(&sc.replace("Cmd", "CMD").replace("Alt", "ALT").replace("Shift", "SHIFT").replace("Ctrl", "CTRL")).ok() } +/// A menu or item label in the interface language, as the native menu bar takes it. +fn menu_title(app: &DesignApp, label: &str) -> String { + menus::native_label(designcraft_ui_egui::i18n::tr(&app.ui.language, label)) +} + /// A command's default shortcut, as the in-window menus show it. fn shortcut(id: &str) -> Option<&'static str> { menus::ui_label(id).and_then(|(_, sc)| sc).or_else(|| designcraft_engine::find_command(id).and_then(|c| c.shortcut)) @@ -71,7 +76,7 @@ impl NativeMenu { let _ = match *entry { AppMenuItem::Cmd { id, label, command } => { let sc = shortcut(command).and_then(accel); - let item = MenuItem::with_id(id, designcraft_ui_egui::i18n::tr(&app.ui.language, label), true, sc); + let item = MenuItem::with_id(id, menu_title(app, label), true, sc); let appended = app_menu.append(&item); items.insert(id.into(), (command.into(), Value::Null, Handle::Plain(item))); appended @@ -85,7 +90,7 @@ impl NativeMenu { } let _ = menu.append(&app_menu); for (title, entries) in menus::native_menu_tree() { - let sub = Submenu::new(designcraft_ui_egui::i18n::tr(&app.ui.language, title), true); + let sub = Submenu::new(menu_title(app, title), true); build(app, &sub, &entries, &mut items, &mut counter, title, &mut recent); let _ = menu.append(&sub); } @@ -121,7 +126,7 @@ impl NativeMenu { match it { Item::Cmd { label, id, params, .. } => { let mid = format!("dc-recent{position}"); - let item = MenuItem::with_id(mid.clone(), label, true, None); + let item = MenuItem::with_id(mid.clone(), menus::native_label(&label), true, None); let _ = r.submenu.insert(&item, position); self.items.insert(mid.clone(), (id, params, Handle::Plain(item))); r.ids.push(mid); @@ -184,7 +189,7 @@ fn build( let _ = parent.append(&PredefinedMenuItem::separator()); } Item::Sub(label, children) => { - let sub = Submenu::new(designcraft_ui_egui::i18n::tr(&app.ui.language, label), true); + let sub = Submenu::new(menu_title(app, label), true); let path = format!("{path}/{label}"); build(app, &sub, children, items, counter, &path, recent); if path == menus::OPEN_RECENT { @@ -198,7 +203,7 @@ fn build( *counter += 1; let mid = format!("dc{counter}"); let sc = if params.is_null() { shortcut.and_then(accel) } else { None }; - let label = designcraft_ui_egui::i18n::tr(&app.ui.language, label); + let label = menu_title(app, label); let handle = if menus::checked(app, id, params).is_some() { let c = CheckMenuItem::with_id(mid.clone(), label, true, false, sc); let _ = parent.append(&c); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 23f220087..acb8e3e42 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -2308,6 +2308,12 @@ pub const MAC_APP_MENU: &[AppMenuItem] = &[ AppMenuItem::Cmd { id: "dc-quit", label: "Quit DesignCraft", command: "app.quit" }, ]; +/// A label as the macOS menu bar library takes it: it reads "&" as a mnemonic marker and drops +/// it, and shows "&&" as one "&". +pub fn native_label(label: &str) -> String { + label.replace('&', "&&") +} + /// The menu tree for the macOS menu bar: `menu_tree` without the commands `MAC_APP_MENU` holds, /// so each command (and its shortcut) appears once, and with the standard Window › Minimize (⌘M) /// at the top of the Window menu. @@ -5518,6 +5524,13 @@ mod tests { assert!(!open, "an open submenu closes when its row becomes fully clipped"); } + #[test] + fn native_labels_keep_their_ampersands() { + assert_eq!(native_label("Numbering & Section Options…"), "Numbering && Section Options…"); + assert_eq!(native_label("A&&B"), "A&&&&B"); + assert_eq!(native_label("Pages"), "Pages"); + } + #[test] fn the_mac_window_menu_minimizes_with_cmd_m() { let window = native_menu_tree().into_iter().find(|(t, _)| *t == "Window").map(|(_, e)| e).unwrap_or_default(); From f309b08562e02087968e45bf604c0fe72c3fc7dd Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 16 Oct 2026 06:39:34 +0000 Subject: [PATCH 3/3] Integration: #1036 test that a selected group paints both ports of its text frame --- crates/ui-egui/src/canvas.rs | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 015adbed0..96579054d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3601,6 +3601,43 @@ mod tests { assert_eq!(out.len(), 1); } + /// A selected group paints both ports of the overset text frame inside it, on top of the + /// group's own selection marks. + #[test] + fn a_selected_group_paints_both_ports_of_its_text_frame() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let s = &mut app.session; + let text = "far too much text for a frame this small, over and over again ".repeat(8); + let a = s.execute("frame.create", &json!({"rect": [100, 100, 200, 140], "content": "text", "text": text})).unwrap()["id"].as_u64().unwrap(); + let b = s.execute("frame.create", &json!({"rect": [100, 200, 200, 260]})).unwrap()["id"].as_u64().unwrap(); + s.execute("selection.set", &json!({"ids": [a, b]})).unwrap(); + let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + s.execute("transform.rotate", &json!({"angle": 30, "ids": [g]})).unwrap(); + s.execute("selection.set", &json!({"ids": [g]})).unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + let layout = CanvasLayout::new(&doc, false); + let xf = Xf { min: pos2(0.0, 0.0), origin: Point::new(0.0, 0.0), zoom: 1.0, rot: 0, pivot: pos2(0.0, 0.0) }; + let ctx = egui::Context::default(); + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + draw_selection(&app, ui.painter(), &xf, &doc, &layout); + }); + output.textures_delta.clear(); + let white: Vec = output + .shapes + .into_iter() + .filter_map(|s| match s.shape { + egui::Shape::Rect(r) if r.fill == Color32::WHITE => Some(r.rect), + _ => None, + }) + .collect(); + for out in [false, true] { + let c = xf.to_screen(designcraft_tools::thread::port_center(&doc, &layout, designcraft_doc::ItemId(a), out, xf.zoom).unwrap()); + let port = white.iter().position(|r| (r.center() - c).length() < 0.01).unwrap_or_else(|| panic!("port (out: {out}) at {c:?}")); + assert!(!white[port + 1..].iter().any(|r| r.contains(c)), "nothing painted later covers the port (out: {out})"); + } + } + fn out_frame(doc: &Document, id: u64) -> &Item { doc.item(designcraft_doc::ItemId(id)).unwrap() }