mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Remove the in-window VectorCraft menu (File first, Exit quits, Mac unchanged)
This commit is contained in:
1 parent
12f8a77a00
commit
bd895a1de7
8 files changed
+140
-48
No files matched your search
@@ -14,7 +14,6 @@
|
||||
Align to Path Zarovnat k cestě
|
||||
Cut Path at Selected Anchor Points Rozdělit cestu ve vybraných kotevních bodech
|
||||
Join Our Discord Připojit se k našemu Discordu
|
||||
Settings… Nastavení…
|
||||
Language Jazyk
|
||||
UI Brightness Jas rozhraní
|
||||
Dark Tmavý
|
||||
@@ -675,3 +674,4 @@
|
||||
Texturizer… Texturovač…
|
||||
System Title Bar Systémové záhlaví okna
|
||||
Applies at next launch. Projeví se při příštím spuštění.
|
||||
Exit Ukončit
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2119,7 +2119,6 @@
|
||||
Set Zoom Definir Zoom
|
||||
Set base color to the current color Definir cor base para a cor atual
|
||||
Set the selected color to the current color Definir a cor selecionada para a cor atual
|
||||
Settings… Configurações…
|
||||
Setup… Configuração…
|
||||
Shadows and Glows Sombras e Brilhos
|
||||
Shape Forma
|
||||
@@ -2735,3 +2734,4 @@
|
||||
Texturizer… Texturizador…
|
||||
System Title Bar Barra de título do sistema
|
||||
Applies at next launch. Aplica-se na próxima inicialização.
|
||||
Exit Sair
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -199,8 +199,8 @@ fn the_engine_follows_the_interface_language() {
|
||||
assert_eq!(app.ui_language().code(), "ja");
|
||||
}
|
||||
|
||||
/// VectorCraft › Language (`app.language`) sets the `interfaceLanguage` preference, which is what
|
||||
/// persists; the checked item follows the preference, and a bad code is an error, not a change.
|
||||
/// `app.language` sets the `interfaceLanguage` preference, which is what persists; the checked
|
||||
/// item follows the preference, and a bad code is an error, not a change.
|
||||
#[test]
|
||||
fn language_command_sets_the_preference() {
|
||||
use crate::menus::{checked, run_ui_command};
|
||||
@@ -218,9 +218,21 @@ fn language_command_sets_the_preference() {
|
||||
run_ui_command(&mut app, "app.language", &serde_json::json!({"lang": "auto"})).unwrap().unwrap();
|
||||
assert_eq!(app.session.prefs.interface_language, "auto");
|
||||
assert_eq!(checked(&app, "app.language", &serde_json::json!({"lang": "auto"})), Some(true));
|
||||
// Every registered language is in the menu, by its own name.
|
||||
let labels: Vec<String> =
|
||||
crate::menus::menu_entries(&app).into_iter().filter(|e| e.command.as_deref() == Some("app.language")).map(|e| e.label).collect();
|
||||
// Every registered language is in the Mac App menu's Language, by its own name (the in-window
|
||||
// bar has no Language menu).
|
||||
let layout = crate::native_menu::mac_layout(&app, &crate::native_menu::from_tree(&app, &crate::menus::menu_tree()), Lang::EN);
|
||||
let app_menu = layout.bar.menus.into_iter().find(|m| m.title == "VectorCraft").expect("App menu");
|
||||
fn items(nodes: &[crate::native_menu::Node], out: &mut Vec<String>) {
|
||||
for n in nodes {
|
||||
match n {
|
||||
crate::native_menu::Node::Item(it) if it.command == Some("app.language") => out.push(it.label.clone()),
|
||||
crate::native_menu::Node::Submenu { children, .. } => items(children, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut labels = vec![];
|
||||
items(&app_menu.children, &mut labels);
|
||||
for l in Lang::all() {
|
||||
assert!(labels.iter().any(|x| x == l.name()), "{} missing from {labels:?}", l.code());
|
||||
}
|
||||
|
||||
+62
-29
@@ -1926,8 +1926,10 @@ fn selected_image(app: &VectorcraftApp, keep: impl Fn(&vectorcraft_doc::ImageObj
|
||||
})
|
||||
}
|
||||
|
||||
/// The in-window and native menu tree. `english_names` is Preferences › Type › Show Font Names in
|
||||
/// English (the Type → Font labels follow it; tests use the default `true`).
|
||||
/// The in-window and native menu tree. The in-window bar skips the VectorCraft menu (see
|
||||
/// `menu_bar`); the native Mac bar is composed from the whole tree (see `native_menu`).
|
||||
/// `english_names` is Preferences › Type › Show Font Names in English (the Type → Font labels
|
||||
/// follow it; tests use the default `true`).
|
||||
pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
menu_tree_named(true)
|
||||
}
|
||||
@@ -1936,24 +1938,13 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
let panel = |label: &'static str, id: &'static str| cp(label, "window.panel", json!({ "panel": id }));
|
||||
vec![
|
||||
(
|
||||
// The application menu: About and Discord only. The in-window bar never shows it (File
|
||||
// is first there, as in Illustrator); on macOS `native_menu::mac_layout` composes the
|
||||
// App menu from this entry and the other menus (About from Help, Settings from Edit,
|
||||
// Quit from File, Language and Appearance synthesized). It stays first: `from_tree`
|
||||
// gives the first menu the App role.
|
||||
"VectorCraft",
|
||||
vec![
|
||||
c("About VectorCraft", "help.about"),
|
||||
c("Join Our Discord", "help.discord"),
|
||||
Sep,
|
||||
c("Settings…", "edit.preferences"),
|
||||
sub(
|
||||
"Language",
|
||||
std::iter::once(cp("Automatic", "app.language", json!({"lang": "auto"})))
|
||||
.chain(std::iter::once(Sep))
|
||||
.chain(crate::i18n::Lang::all().map(|l| cp(l.name(), "app.language", json!({"lang": l.code()}))))
|
||||
.collect(),
|
||||
),
|
||||
Sep,
|
||||
sub("UI Brightness", Brightness::ALL.iter().map(|b| cp(b.label(), "window.brightness", json!({"brightness": b.id()}))).collect()),
|
||||
Sep,
|
||||
c("Quit VectorCraft", "app.quit"),
|
||||
],
|
||||
vec![c("About VectorCraft", "help.about"), c("Join Our Discord", "help.discord")],
|
||||
),
|
||||
(
|
||||
"File",
|
||||
@@ -2007,6 +1998,8 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("File Info…", "file.info"),
|
||||
Sep,
|
||||
c("Print…", "file.print"),
|
||||
Sep,
|
||||
c("Exit", "app.quit"),
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -2754,11 +2747,14 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
|
||||
}
|
||||
let mut clicked: Option<(String, Value)> = None;
|
||||
let tree = menu_tree_named(app.session.prefs.font_names_in_english);
|
||||
// No application menu in the window (as in Illustrator): the bar starts at File on every
|
||||
// platform. The VectorCraft menu stays in the tree: the native Mac bar takes it from there.
|
||||
let tree: Vec<(&str, Vec<Item>)> = tree.into_iter().filter(|(title, _)| *title != "VectorCraft").collect();
|
||||
let (end, open) = egui::MenuBar::new()
|
||||
.ui(ui, |ui| {
|
||||
let mut titles = Vec::with_capacity(tree.len());
|
||||
for (i, (title, items)) in tree.iter().enumerate() {
|
||||
let text = if i == 0 {
|
||||
for (title, items) in tree.iter() {
|
||||
let text = if *title == "VectorCraft" {
|
||||
egui::RichText::new(tl!(title)).font(theme::semibold(13.0)).color(t.text)
|
||||
} else {
|
||||
egui::RichText::new(tl!(title)).size(13.0).color(t.text)
|
||||
@@ -3553,6 +3549,43 @@ mod tests {
|
||||
(0..titles.len()).filter(|&i| egui::Popup::is_id_open(ctx, titles[i].1)).collect()
|
||||
}
|
||||
|
||||
/// File ends with Exit, running the app's own quit flow (unsaved documents are asked about).
|
||||
#[test]
|
||||
fn file_ends_with_exit() {
|
||||
let tree = menu_tree();
|
||||
let (_, file) = tree.iter().find(|(t, _)| *t == "File").unwrap();
|
||||
let last = file.iter().rev().find(|i| !matches!(i, Item::Sep)).unwrap();
|
||||
assert!(matches!(last, Item::Cmd("Exit", "app.quit", _)), "{last:?}");
|
||||
}
|
||||
|
||||
/// No application menu in the window (as in Illustrator): the bar starts at File.
|
||||
#[test]
|
||||
fn the_bar_starts_at_file() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, Default::default());
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 700.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| {
|
||||
menu_bar(&mut app, ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
fn texts(s: &egui::Shape, out: &mut Vec<String>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut shown = vec![];
|
||||
out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
|
||||
let titles = ["File", "Edit", "Object", "Type", "Select", "Effect", "View", "Window", "Help"];
|
||||
for t in titles {
|
||||
assert!(shown.iter().any(|s| s == t), "{t} missing from {shown:?}");
|
||||
}
|
||||
assert!(!shown.iter().any(|s| s == "VectorCraft"), "{shown:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_another_title_switches_the_open_menu() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
@@ -3561,9 +3594,9 @@ mod tests {
|
||||
theme::apply(&ctx, Default::default());
|
||||
bar_frame(&mut app, &ctx, vec![]);
|
||||
let titles = bar_frame(&mut app, &ctx, vec![]);
|
||||
assert_eq!(titles.len(), menu_tree().len());
|
||||
// 0 is the app menu, then File, Edit, Object.
|
||||
let (file, edit, object) = (titles[1].0.center(), titles[2].0.center(), titles[3].0.center());
|
||||
// No application menu in the window: the bar starts at File on every platform.
|
||||
assert_eq!(titles.len(), menu_tree().len() - 1);
|
||||
let (file, edit, object) = (titles[0].0.center(), titles[1].0.center(), titles[2].0.center());
|
||||
let frames = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut t = bar_frame(app, &ctx, events);
|
||||
for _ in 0..2 {
|
||||
@@ -3581,17 +3614,17 @@ mod tests {
|
||||
frames(&mut app, vec![egui::Event::PointerMoved(file)]);
|
||||
frames(&mut app, vec![button(file, true)]);
|
||||
let t = frames(&mut app, vec![button(file, false)]);
|
||||
assert_eq!(open_titles(&ctx, &t), vec![1], "a click opens File");
|
||||
assert_eq!(open_titles(&ctx, &t), vec![0], "a click opens File");
|
||||
let t = frames(&mut app, vec![egui::Event::PointerMoved(edit)]);
|
||||
assert_eq!(open_titles(&ctx, &t), vec![2], "hovering Edit opens it and closes File");
|
||||
assert_eq!(open_titles(&ctx, &t), vec![1], "hovering Edit opens it and closes File");
|
||||
let t = frames(&mut app, vec![egui::Event::PointerMoved(object)]);
|
||||
assert_eq!(open_titles(&ctx, &t), vec![3], "hovering Object opens it and closes Edit");
|
||||
assert_eq!(open_titles(&ctx, &t), vec![2], "hovering Object opens it and closes Edit");
|
||||
|
||||
// A pointer resting on a title doesn't switch: File opened another way (the keyboard)
|
||||
// stays open while the pointer stays still over Object.
|
||||
egui::Popup::open_id(&ctx, t[1].1);
|
||||
egui::Popup::open_id(&ctx, t[0].1);
|
||||
let t = frames(&mut app, vec![]);
|
||||
assert_eq!(open_titles(&ctx, &t), vec![1], "a still pointer leaves the open menu alone");
|
||||
assert_eq!(open_titles(&ctx, &t), vec![0], "a still pointer leaves the open menu alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
//! - an **app menu** named VectorCraft: About; Settings, a submenu with one item per Preferences
|
||||
//! page (General… ⌘K first); Language; Appearance (the UI brightness); Services; Hide VectorCraft
|
||||
//! ⌃⌘H (⌘H stays View › Hide Edges), Hide Others ⌥⌘H, Show All; and Quit ⌘Q, still the app's own
|
||||
//! `app.quit`, so documents with unsaved changes are asked about. These leave the other menus
|
||||
//! (Edit › Preferences…, Help › About), and Join Our Discord stays under Help only;
|
||||
//! `app.quit`, so documents with unsaved changes are asked about. Language and Appearance have
|
||||
//! no in-window menu (as in PhotoCraft) and are synthesized here; Quit is File › Exit in the
|
||||
//! window and reads as Quit in the App menu. These leave the other menus (Edit › Preferences…,
|
||||
//! Help › About), and Join Our Discord stays under Help only;
|
||||
//! - **Window** gets Minimize ⌃⌘M, Zoom and Bring All to Front, and **Help** the system's search
|
||||
//! field;
|
||||
//! - a system key already bound to a command keeps the command's: the system item goes without.
|
||||
@@ -34,6 +36,7 @@ use serde_json::{Value, json};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::i18n::{Lang, tr, tr_ctx};
|
||||
use crate::menus::{self, Entry};
|
||||
use crate::theme::Brightness;
|
||||
|
||||
pub const APP_NAME: &str = "VectorCraft";
|
||||
/// Hide VectorCraft: an item of ours (AppKit's always takes ⌘H, which is View › Hide Edges).
|
||||
@@ -292,7 +295,9 @@ pub struct Layout {
|
||||
}
|
||||
|
||||
/// The Mac menu bar of `bar` (the in-window menus, [`from_tree`]): see the module docs.
|
||||
pub fn mac_layout(bar: &MenuBar, lang: Lang) -> Layout {
|
||||
/// Language and Appearance have no in-window menu (as in PhotoCraft); when the tree doesn't
|
||||
/// carry them they are synthesized below, so the App menu keeps them.
|
||||
pub fn mac_layout(app: &VectorcraftApp, bar: &MenuBar, lang: Lang) -> Layout {
|
||||
let mut bar = bar.clone();
|
||||
let mut clashes = Vec::new();
|
||||
let bound = crate::shortcuts::all_shortcuts();
|
||||
@@ -316,8 +321,10 @@ pub fn mac_layout(bar: &MenuBar, lang: Lang) -> Layout {
|
||||
let about = take_items(&mut bar, "help.about").into_iter().next();
|
||||
let settings = take_items(&mut bar, "edit.preferences").into_iter().next();
|
||||
let quit = take_items(&mut bar, "app.quit").into_iter().next();
|
||||
let language = take_submenu(&mut bar, "app.language");
|
||||
let appearance = take_submenu(&mut bar, "window.brightness");
|
||||
// Language and UI Brightness live only in the App menu: taken from the tree when it carries
|
||||
// them, else synthesized here (the in-window bar has neither).
|
||||
let language = take_submenu(&mut bar, "app.language").or_else(|| Some(language_node(app, lang)));
|
||||
let appearance = take_submenu(&mut bar, "window.brightness").or_else(|| Some(appearance_node(app, lang)));
|
||||
// Its other items (Join Our Discord) stay where the in-window bar also has them: Help.
|
||||
bar.menus.retain(|m| m.role != MenuRole::App);
|
||||
|
||||
@@ -333,7 +340,11 @@ pub fn mac_layout(bar: &MenuBar, lang: Lang) -> Layout {
|
||||
}));
|
||||
app.extend([Node::Separator, Node::Standard(Standard::Services), Node::Separator]);
|
||||
app.extend([Node::Item(hide), Node::Item(hide_others), Node::Standard(Standard::ShowAll), Node::Separator]);
|
||||
app.extend(quit.map(Node::Item));
|
||||
// Quit is File › Exit in the window; in the App menu it reads as a Mac Quit item.
|
||||
app.extend(quit.map(|mut q| {
|
||||
q.label = tr(lang, "Quit VectorCraft").into();
|
||||
Node::Item(q)
|
||||
}));
|
||||
bar.menus.insert(0, Menu { title: APP_NAME.into(), role: MenuRole::App, children: app });
|
||||
|
||||
if let Some(window) = bar.menus.iter_mut().find(|m| m.role == MenuRole::Window) {
|
||||
@@ -350,6 +361,40 @@ pub fn mac_layout(bar: &MenuBar, lang: Lang) -> Layout {
|
||||
Layout { bar, clashes }
|
||||
}
|
||||
|
||||
/// Language in the app menu, after Settings: Automatic, then every UI language in its own name,
|
||||
/// with the `interfaceLanguage` preference checked.
|
||||
fn language_node(app: &VectorcraftApp, lang: Lang) -> Node {
|
||||
let mut children = vec![Node::Item(native_item(app, "Automatic", "app.language", json!({"lang": "auto"}))), Node::Separator];
|
||||
children.extend(Lang::all().map(|l| Node::Item(native_item(app, l.name(), "app.language", json!({"lang": l.code()})))));
|
||||
Node::Submenu { label: tr(lang, "Language").into(), children }
|
||||
}
|
||||
|
||||
/// Appearance in the app menu (the UI-brightness theme, as Mac apps call it).
|
||||
fn appearance_node(app: &VectorcraftApp, lang: Lang) -> Node {
|
||||
let children =
|
||||
Brightness::ALL.iter().map(|b| Node::Item(native_item(app, b.label(), "window.brightness", json!({"brightness": b.id()})))).collect();
|
||||
Node::Submenu { label: tr_ctx(lang, "theme", "Appearance").into(), children }
|
||||
}
|
||||
|
||||
/// A native item running `command`, built like [`from_tree`] builds it (translated label,
|
||||
/// shortcut, enabled and checked as they read now).
|
||||
fn native_item(app: &VectorcraftApp, label: &'static str, command: &'static str, params: Value) -> Item {
|
||||
let item = menus::Item::Cmd(label, command, params);
|
||||
match menus::entry(app, &item) {
|
||||
Some(Entry::Item(row)) => Item {
|
||||
command: row.command.map(|(id, _)| id),
|
||||
params: row.command.map_or(Value::Null, |(_, p)| p.clone()),
|
||||
source: row.source,
|
||||
label: row.label.into_owned(),
|
||||
shortcut: row.shortcut,
|
||||
enabled: row.enabled,
|
||||
checked: row.checked,
|
||||
},
|
||||
// The commands always show; the fallback keeps the structure.
|
||||
_ => Item { command: Some(command), params: Value::Null, source: label, label: label.into(), shortcut: None, enabled: true, checked: None },
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings ▸: one item per page of the Preferences dialog (its own list), each opening it there;
|
||||
/// the first (General…) has Preferences' shortcut (⌘K), as in Illustrator.
|
||||
fn settings_pages(preferences: &Item, lang: Lang) -> Vec<Node> {
|
||||
@@ -413,7 +458,7 @@ fn take_submenu(bar: &mut MenuBar, command: &str) -> Option<Node> {
|
||||
|
||||
/// VectorCraft's menus as a Mac menu bar, as they show now.
|
||||
pub fn layout(app: &VectorcraftApp) -> Layout {
|
||||
mac_layout(&from_tree(app, &menus::menu_tree_named(app.session.prefs.font_names_in_english)), crate::i18n::current())
|
||||
mac_layout(app, &from_tree(app, &menus::menu_tree_named(app.session.prefs.font_names_in_english)), crate::i18n::current())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------- the app's side
|
||||
|
||||
@@ -25,7 +25,7 @@ fn app(doc: bool) -> VectorcraftApp {
|
||||
}
|
||||
|
||||
fn bar(app: &VectorcraftApp) -> MenuBar {
|
||||
native_menu::mac_layout(&native_menu::from_tree(app, &menus::menu_tree()), Lang::EN).bar
|
||||
native_menu::mac_layout(app, &native_menu::from_tree(app, &menus::menu_tree()), Lang::EN).bar
|
||||
}
|
||||
|
||||
/// A level's nodes as short names: an item's command (`---` a separator, `[label]` a submenu,
|
||||
@@ -182,8 +182,10 @@ fn every_in_window_item_is_in_the_mac_menu_bar_once() {
|
||||
assert_eq!(m, want, "{key:?} is listed {m} times on the Mac, {n} in the window");
|
||||
}
|
||||
let system = [native_menu::HIDE, native_menu::HIDE_OTHERS, native_menu::MINIMIZE];
|
||||
// Language and Appearance have no in-window menu (synthesized in the App menu, as in PhotoCraft).
|
||||
let synthesized = ["app.language", "window.brightness"];
|
||||
for key in b.keys() {
|
||||
assert!(a.contains_key(key) || system.contains(&key.0.as_str()), "{key:?} isn't an in-window item");
|
||||
assert!(a.contains_key(key) || system.contains(&key.0.as_str()) || synthesized.contains(&key.0.as_str()), "{key:?} isn't an in-window item");
|
||||
}
|
||||
let help = ids(menu(&mac, "Help"));
|
||||
assert!(help.contains(&"help.discord".to_string()) && !help.contains(&"help.about".to_string()), "{help:?}");
|
||||
@@ -211,7 +213,7 @@ fn a_command_keeps_a_system_key_it_is_bound_to() {
|
||||
let mut app = app(true);
|
||||
app.ui.shortcut_overrides.insert("view.grid".into(), "Ctrl+Cmd+H".into());
|
||||
crate::shortcut_editor::sync(&app.ui);
|
||||
let layout = native_menu::mac_layout(&native_menu::from_tree(&app, &menus::menu_tree()), Lang::EN);
|
||||
let layout = native_menu::mac_layout(&app, &native_menu::from_tree(&app, &menus::menu_tree()), Lang::EN);
|
||||
app.ui.shortcut_overrides.clear();
|
||||
crate::shortcut_editor::sync(&app.ui);
|
||||
assert_eq!(find(&layout.bar, native_menu::HIDE).shortcut, None);
|
||||
|
||||
@@ -266,7 +266,7 @@ mod tests {
|
||||
assert!(close.height() >= 42.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH));
|
||||
assert_eq!((min.right(), max.right()), (max.left(), close.left()));
|
||||
let widgets = w.bar_widgets();
|
||||
assert!(widgets.len() >= 12, "home, nine menus, workspace switcher, search: {widgets:?}");
|
||||
assert!(widgets.len() >= 11, "home, eight menus, workspace switcher, search: {widgets:?}");
|
||||
for (i, a) in widgets.iter().enumerate() {
|
||||
assert!(a.right() <= min.left() - 8.0, "{a:?} runs into the caption buttons at {width}");
|
||||
for b in &widgets[i + 1..] {
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ By default VectorCraft's own crates log at `info` and everything else at `warn`.
|
||||
|
||||
On macOS the menus live in the system menu bar, not in the window's app bar (which keeps the brand mark, Home, the search box and the workspace switcher next to the traffic lights). `--in-window-menus` or `VECTORCRAFT_IN_WINDOW_MENUS=1` keeps them in the window, as on Windows, Linux and the web; so does a menu bar AppKit refuses to build (logged). `ui.inspect` says which with `nativeMenuBar`.
|
||||
|
||||
- **One menu model.** `crates/ui-egui/src/native_menu.rs` (platform-free, tested on every platform) builds the menu bar from the in-window menus (`menus::menu_tree`, each item through `menus::entry`, which the in-window menus draw too) and lays it out the Mac way: the VectorCraft menu holds About, Settings ▸ (one item per Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays View › Hide Edges), Hide Others ⌥⌘H, Show All and Quit ⌘Q (the app's own `app.quit`, so unsaved documents are asked about); those leave Edit and Help. Window starts with Minimize ⌃⌘M and Zoom and ends with Bring All to Front, and Help is the system's Help menu (its search field). A system key a command (or a user's shortcut) already has stays the command's, and the log says so.
|
||||
- **One menu model.** `crates/ui-egui/src/native_menu.rs` (platform-free, tested on every platform) builds the menu bar from the in-window menus (`menus::menu_tree`, each item through `menus::entry`, which the in-window menus draw too) and lays it out the Mac way: the VectorCraft menu holds About, Settings ▸ (one item per Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays View › Hide Edges), Hide Others ⌥⌘H, Show All and Quit ⌘Q (the app's own `app.quit`, so unsaved documents are asked about); those leave Edit and Help. The in-window bar itself never shows the VectorCraft menu (it starts at File, as in Illustrator, on every platform): Language and Appearance have no in-window menu and are synthesized in `mac_layout`, and Quit is File › Exit in the window, relabelled to Quit in the App menu. Window starts with Minimize ⌃⌘M and Zoom and ends with Bring All to Front, and Help is the system's Help menu (its search field). A system key a command (or a user's shortcut) already has stays the command's, and the log says so.
|
||||
- **The native side** is `apps/vectorcraft/src/mac_menu.rs`: muda (`=0.21.1`, default features off, macOS only, no `unsafe`) builds the `NSMenu`s. Labels, enabled and checked are updated in place when anything they show may have changed (a command ran, the document, its history or selection changed, a click or a key press); a change of structure (a recent file, the language, a shortcut) rebuilds the menus.
|
||||
- **Keys stay with egui.** AppKit runs a menu's key equivalents before the window sees the key, so the item's key goes back to egui as the key press it was (⌘X/⌘C/⌘V as egui's Cut/Copy/Paste events), through `raw_input_hook`: `shortcuts::handle` runs it once, user shortcuts apply, and a focused text field keeps ⌘A, ⌘C, ⌘V and ⌘Z. A click runs the item like an in-window click. Bare keys and ⌥/⇧ chords are never key equivalents (they type).
|
||||
- Built and clippy-checked for `aarch64-apple-darwin` and `x86_64-apple-darwin` from any host: `cargo clippy -p vectorcraft --target aarch64-apple-darwin --all-targets -- -D warnings`.
|
||||
|
||||
Reference in new issue
Block a user