Merge branch 'fix-macwin' into next-integ

This commit is contained in:
aiongg committed 2026-10-16 06:40:54 +00:00
commit 852db92b05
3 files changed
+96 -12

No files matched your search

+10 -5
View File
@@ -45,6 +45,11 @@ fn accel(sc: &str) -> Option<Accelerator> {
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);
+37
View File
@@ -3607,6 +3607,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<Rect> = 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()
}
+49 -7
View File
@@ -974,7 +974,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
}
fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
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(),
@@ -2297,13 +2308,24 @@ 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.
/// 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<Item>)> {
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 +2429,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 +5524,26 @@ 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();
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));