Browse / github.com/storytold/figcraft / patch
feat(macos): the main menu in the menu bar (#784)
macOS: Should populate system menu bar Currently, on macOS, Figcraft does not populate the system menu bar, leaving it barren: <img width="822" height="229" alt="Image" src="https://github.com/user-attachments/assets/2a533479-ad97-4aaf-9d24-6b63aa589dd6" /> For comparison, Figma does populate it: <img width="822" height="176" alt="Image" src="https://github.com/user-attachments/assets/7ffe1a56-a326-418c-a7b1-d072bf5d4f90" />
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- volkovskey (imported from pull request #805, open)
- Base
86e91d0b715cb7ff6bad30adf1c6148bd622d7a8- Size
- +752 −18 in 26 files
- Review
- pending
- Shared
- 2026-10-28
- Id
3db802aa402aa6c1ce44c63e
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock Cargo.toml apps/figcraft/Cargo.toml apps/figcraft/src/mac_menu.rs apps/figcraft/src/main.rs crates/ui-egui/Cargo.toml crates/ui-egui/locales/cs.toml crates/ui-egui/locales/de.toml crates/ui-egui/locales/en.toml crates/ui-egui/locales/es.toml crates/ui-egui/locales/fi.toml crates/ui-egui/locales/fr.toml crates/ui-egui/locales/id.toml crates/ui-egui/locales/ja.toml crates/ui-egui/locales/ko.toml crates/ui-egui/locales/nl.toml crates/ui-egui/locales/pt-BR.toml crates/ui-egui/locales/ru.toml crates/ui-egui/locales/sv.toml crates/ui-egui/locales/tr.toml crates/ui-egui/locales/zh-Hans.toml crates/ui-egui/src/lib.rs crates/ui-egui/src/menus.rs crates/ui-egui/src/native_menu.rs crates/ui-egui/src/native_menu_tests.rs crates/ui-egui/src/plugins_ui.rs
@@ -861,6 +861,29 @@ name = "craft-docpath" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63" +[[package]]+name = "craft-menu"+version = "0.1.0"+source = "git+https://github.com/storytold/craft-libs?rev=2bff0abbfc6046a700128c2710d78a58c6c53c39#2bff0abbfc6046a700128c2710d78a58c6c53c39"+dependencies = [+ "craft-quit",+ "egui",+ "muda",+ "objc2 0.6.5",+ "objc2-app-kit 0.3.2",+]++[[package]]+name = "craft-quit"+version = "0.1.0"+source = "git+https://github.com/storytold/craft-libs?rev=2bff0abbfc6046a700128c2710d78a58c6c53c39#2bff0abbfc6046a700128c2710d78a58c6c53c39"+dependencies = [+ "dispatch2",+ "egui",+ "objc2 0.6.5",+ "objc2-app-kit 0.3.2",+]+ [[package]] name = "crc32fast" version = "1.5.2"@@ -870,6 +893,15 @@ dependencies = [ "cfg-if", ] +[[package]]+name = "crossbeam-channel"+version = "0.5.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"+dependencies = [+ "crossbeam-utils",+]+ [[package]] name = "crossbeam-deque" version = "0.8.8"@@ -1416,6 +1448,7 @@ name = "figcraft" version = "0.7.0" dependencies = [ "arboard",+ "craft-menu", "eframe", "egui", "env_logger",@@ -1702,6 +1735,7 @@ name = "figcraft-ui-egui" version = "0.7.0" dependencies = [ "base64 0.22.1",+ "craft-menu", "egui", "egui_extras", "figcraft-color",@@ -2540,6 +2574,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]]+name = "keyboard-types"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"+dependencies = [+ "bitflags 2.13.2",+ "serde",+]+ [[package]] name = "khronos-egl" version = "6.0.0"@@ -2750,6 +2794,24 @@ dependencies = [ "pxfm", ] +[[package]]+name = "muda"+version = "0.21.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"+dependencies = [+ "crossbeam-channel",+ "dpi",+ "keyboard-types",+ "objc2 0.6.5",+ "objc2-app-kit 0.3.2",+ "objc2-core-foundation",+ "objc2-foundation 0.3.2",+ "png",+ "thiserror 2.0.21",+ "windows-sys 0.60.2",+]+ [[package]] name = "naga" version = "30.0.1"@@ -34,6 +34,8 @@ figcraft-plugin = { path = "crates/plugin" } # Which paths stored in a document may be followed (craft-libs). craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }+# The macOS menu bar (craft-libs; at storytold/craft-libs#66 until it lands).+craft-menu = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "2bff0abbfc6046a700128c2710d78a58c6c53c39" } # foundation kurbo = { version = "0.13", features = ["serde"] }@@ -37,6 +37,8 @@ windows = { version = "0.62", default-features = false, features = ["UI_ViewMana # unsafe, which the workspace forbids). Pinned exactly: review the diff before bumping. [target.'cfg(target_os = "macos")'.dependencies] fmv-macos-events = "=0.1.0"+# The main menu in the macOS menu bar.+craft-menu = { workspace = true } [dev-dependencies] wat = { workspace = true }new file mode 100644@@ -0,0 +1,64 @@+//! The main menu in the macOS menu bar (craft-menu): installed with the window, synced each frame,+//! acting on the window in front.++use craft_menu::{Layout, LayoutOptions, NativeMenu};+use egui::ViewportId;+use figcraft_ui_egui::FigApp;+use figcraft_ui_egui::native_menu;+use figcraft_ui_egui::windows::with_app;++/// The Mac layout of the main menu for the window in front, with the labels the layout writes+/// translated.+fn layout(fig: &mut FigApp, windows: bool) -> Layout {+ let label = |english: &'static str| native_menu::mac_label(english);+ craft_menu::mac_layout(&native_menu::menu_bar_in_front(fig, windows), &LayoutOptions { label: &label, ..LayoutOptions::new("FigCraft") })+}++/// Put the main menu in the menu bar. `None` when AppKit refused it: the in-window menu stays.+pub fn install(ctx: &egui::Context) -> Option<NativeMenu> {+ let bar = with_app(|fig| layout(fig, !ctx.embed_viewports()).bar)?;+ craft_menu::mac::install(ctx, &bar)+}++/// Before a window's frame: the menu bar's key equivalents go into the window in front's input+/// only. In the main window's input while another window is in front, a paste the menu bar asked+/// for goes on to that window.+pub fn raw_input(menu: &mut NativeMenu, ctx: &egui::Context, raw: &mut egui::RawInput) {+ let front = with_app(|fig| native_menu::front(fig)).unwrap_or(ViewportId::ROOT);+ if raw.viewport_id == front {+ menu.raw_input(raw);+ } else if raw.viewport_id == ViewportId::ROOT {+ let pastes = native_menu::take_pastes(&mut raw.events);+ if !pastes.is_empty() {+ with_app(|fig| native_menu::send_keys(fig, ctx, pastes));+ }+ }+}++/// One frame (the main window's): hand the window in front the key equivalents waiting for it, run+/// the items chosen since the last frame there, then bring the menus up to date (laid out again+/// only after input, a menu event or another window coming to the front).+pub fn frame(menu: &mut NativeMenu, ctx: &egui::Context) {+ let front = with_app(|fig| native_menu::front(fig)).unwrap_or(ViewportId::ROOT);+ if front != ViewportId::ROOT {+ // A menu event wakes only the main window: the window in front may not draw until its next input.+ let mut keys = egui::RawInput { viewport_id: front, ..Default::default() };+ menu.raw_input(&mut keys);+ if !keys.events.is_empty() {+ with_app(|fig| native_menu::send_keys(fig, ctx, keys.events));+ }+ }+ for id in menu.take_clicks(ctx) {+ with_app(|fig| native_menu::run_in_front(fig, &id));+ }+ let input = if craft_menu::had_input(ctx) { ctx.cumulative_frame_nr() } else { 0 };+ let state = input ^ front.0.value();+ let windows = !ctx.embed_viewports();+ let clashes = menu.sync_with(state, || {+ with_app(|fig| layout(fig, windows)).unwrap_or_else(|| Layout { bar: craft_menu::MenuBar { menus: Vec::new() }, clashes: Vec::new() })+ });+ for c in clashes {+ log::info!("menu: {} is {} on macOS and {} in FigCraft; {}", c.shortcut, c.system, c.command, c.resolution);+ }+ menu.set_text_editing(ctx.egui_wants_keyboard_input());+}@@ -22,6 +22,8 @@ mod gamepad; mod graphics; #[cfg(target_os = "linux")] mod linux_window;+#[cfg(target_os = "macos")]+mod mac_menu; mod recovery; mod services; mod startup;@@ -60,6 +62,9 @@ struct App { /// Documents and quit requests from Finder, the Dock and Open With. #[cfg(target_os = "macos")] apple_events: fmv_macos_events::Inbox,+ /// The main menu in the menu bar (`None` when AppKit refused it).+ #[cfg(target_os = "macos")]+ native_menu: Option<craft_menu::NativeMenu>, } impl eframe::App for App {@@ -87,6 +92,10 @@ impl eframe::App for App { ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone())); self.title = title; }+ #[cfg(target_os = "macos")]+ if let Some(menu) = &mut self.native_menu {+ mac_menu::frame(menu, ctx);+ } let now = ctx.input(|i| i.time); if let Some(wait) = with_app(|fig| recovery::lock(&self.recovery).as_mut().and_then(|r| r.tick(&fig.session, now))).flatten() { ctx.request_repaint_after(std::time::Duration::from_secs_f64(wait));@@ -94,6 +103,12 @@ impl eframe::App for App { } fn raw_input_hook(&mut self, _ctx: &egui::Context, raw_input: &mut egui::RawInput) {+ // The menu bar's key equivalents, as the key presses egui would have had without it (for+ // the window in front).+ #[cfg(target_os = "macos")]+ if let Some(menu) = &mut self.native_menu {+ mac_menu::raw_input(menu, _ctx, raw_input);+ } // Pointer and key events sent over the control channel (`ui.input`), for the window // about to draw. if let Some(events) = with_app(|fig| figcraft_ui_egui::windows::take_input(fig, raw_input.viewport_id)) {@@ -251,6 +266,9 @@ fn main() -> eframe::Result { recovery, #[cfg(target_os = "macos")] apple_events: apple_events.connect(&_cc.egui_ctx),+ // In the creator, on the main thread, so winit's default menu doesn't stay up.+ #[cfg(target_os = "macos")]+ native_menu: mac_menu::install(&_cc.egui_ctx), })) }), );@@ -9,6 +9,7 @@ description = "FigCraft egui frontend (the Figma-style UI: canvas, toolbar, laye workspace = true [dependencies]+craft-menu = { workspace = true } figcraft-geom = { workspace = true } figcraft-color = { workspace = true } figcraft-doc = { workspace = true }@@ -22,6 +22,7 @@ status = "draft — needs native review" "About" = "O aplikaci" "About FigCraft" = "O aplikaci FigCraft" "About this server" = "O tomto serveru"+"About {app}" = "O aplikaci {app}" "Action" = "Akce" "Actions" = "Akce" "Actions for {name}" = "Akce pro {name}"@@ -178,6 +179,7 @@ status = "draft — needs native review" "Branch {name}" = "Větev {name}" "Branches" = "Větve" "Bound to {name}. Click to detach the variable." = "Navázáno na {name}. Kliknutím odpojíte proměnnou."+"Bring All to Front" = "Přenést vše dopředu" "Bring forward" = "Přenést o úroveň výš" "Bring the main file's changes into the branch" = "Přenést změny hlavního souboru do větve" "Bring to front" = "Přenést do popředí"@@ -195,6 +197,7 @@ status = "draft — needs native review" "Can view" = "Může prohlížet" "Checking the server…" = "Kontrola serveru…" "Clear from this list" = "Vymazat z tohoto seznamu"+"Close Window" = "Zavřít okno" "Code or invite link" = "Kód nebo odkaz s pozvánkou" "Color profile: {name}" = "Barevný profil: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Barevné a textové styly, proměnné pro světlý a tmavý režim, tlačítka, vstupní pole a karty"@@ -567,11 +570,13 @@ status = "draft — needs native review" "Help page" = "Stránka nápovědy" "Here: {names}" = "Zde: {names}" "Hidden" = "Skryté"+"Hide Others" = "Skrýt ostatní" "Hide contents" = "Skrýt obsah" "Hide from publishing" = "Skrýt při publikování" "Hide section contents" = "Skrýt obsah sekce" "Hide these tips (Getting started in the Help menu shows them again)" = "Skrýt tyto tipy (znovu je zobrazíte volbou Začínáme v nabídce Nápověda)" "Hide when publishing" = "Skrýt při publikování"+"Hide {app}" = "Skrýt {app}" "Highlight layers on hover" = "Zvýraznit vrstvy pod ukazatelem" "Highlights" = "Světla" "Hold Alt" = "Podržte Alt"@@ -1021,6 +1026,7 @@ status = "draft — needs native review" "Push" = "Vytlačení" "Push changes to main component" = "Přenést změny do hlavní komponenty" "Put text on path" = "Umístit text na cestu"+"Quit {app}" = "Ukončit {app}" "R" = "R" "Radial" = "Kruhový" "Radius" = "Poloměr"@@ -1183,11 +1189,13 @@ status = "draft — needs native review" "Server address (ws://host:47900/)" = "Adresa serveru (ws://host:47900/)" "Server address" = "Adresa serveru" "Server for online sharing" = "Server pro online sdílení"+"Services" = "Služby" "Set as default" = "Nastavit jako výchozí" "Set variable" = "Nastavit proměnnou" "Set variable mode" = "Nastavit režim proměnných" "Set variable {name}" = "Nastavit proměnnou {name}" "Set {name} to {value}" = "Nastavit {name} na {value}"+"Settings…" = "Nastavení…" "Shadows" = "Stíny" "Shape" = "Tvar" "Shape builder" = "Tvorba tvarů"@@ -1204,6 +1212,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Sdílení online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Sdílení zastaveno: soubor zůstává na serveru, přístupný pouze vám" "Show" = "Zobrazit"+"Show All" = "Zobrazit vše" "Show all templates" = "Zobrazit všechny šablony" "Show as grid" = "Zobrazit jako mřížku" "Show as list" = "Zobrazit jako seznam"@@ -1473,6 +1482,7 @@ status = "draft — needs native review" "Width" = "Šířka" "Width point" = "Bod šířky" "Width profile" = "Profil šířky"+"Window" = "Okno" "Wireframe kit" = "Sada pro wireframy" "With the layers' export settings" = "S nastavením exportu vrstev" "Wrap" = "Zalamovat"@@ -23,6 +23,7 @@ status = "draft — needs native review" "About" = "Über" "About FigCraft" = "Über FigCraft" "About this server" = "Über diesen Server"+"About {app}" = "Über {app}" "Action" = "Aktion" "Actions" = "Aktionen" "Actions for {name}" = "Aktionen für {name}"@@ -174,6 +175,7 @@ status = "draft — needs native review" "Branch updated from main" = "Branch aus der Hauptdatei aktualisiert" "Branch {name}" = "Branch {name}" "Branches" = "Branches"+"Bring All to Front" = "Alle nach vorne bringen" "Bring forward" = "Eine Ebene nach vorne" "Bring the main file's changes into the branch" = "Übernimmt die Änderungen der Hauptdatei in den Branch" "Bring to front" = "In den Vordergrund"@@ -191,6 +193,7 @@ status = "draft — needs native review" "Can view" = "Kann ansehen" "Checking the server…" = "Server wird geprüft…" "Clear from this list" = "Aus dieser Liste entfernen"+"Close Window" = "Fenster schließen" "Code or invite link" = "Code oder Einladungslink" "Color profile: {name}" = "Farbprofil: {name}" "CSS custom properties…" = "Benutzerdefinierte CSS-Eigenschaften…"@@ -555,9 +558,11 @@ status = "draft — needs native review" "Help page" = "Hilfeseite" "Here: {names}" = "Hier: {names}" "Hidden" = "Ausgeblendet"+"Hide Others" = "Andere ausblenden" "Hide contents" = "Inhalt ausblenden" "Hide from publishing" = "Nicht veröffentlichen" "Hide section contents" = "Abschnittsinhalt ausblenden"+"Hide {app}" = "{app} ausblenden" "Highlight layers on hover" = "Ebenen beim Überfahren hervorheben" "Highlights" = "Lichter" "Hold Alt" = "Alt gedrückt halten"@@ -987,6 +992,7 @@ status = "draft — needs native review" "Push" = "Schieben" "Push changes to main component" = "Änderungen an Hauptkomponente übertragen" "Put text on path" = "Text auf Pfad setzen"+"Quit {app}" = "{app} beenden" "R" = "R" "Radial" = "Radial" "Radius" = "Radius"@@ -1146,11 +1152,13 @@ status = "draft — needs native review" "Server address" = "Serveradresse" "Server address (ws://host:47900/)" = "Serveradresse (ws://host:47900/)" "Server for online sharing" = "Server für das Online-Teilen"+"Services" = "Dienste" "Set as default" = "Als Standard festlegen" "Set variable" = "Variable festlegen" "Set variable mode" = "Variablenmodus festlegen" "Set variable {name}" = "Variable {name} festlegen" "Set {name} to {value}" = "{name} auf {value} setzen"+"Settings…" = "Einstellungen…" "Shadows" = "Tiefen" "Shape" = "Form" "Shape builder" = "Formerstellung"@@ -1166,6 +1174,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Wird online geteilt · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Teilen beendet: Die Datei bleibt auf dem Server, nur für Sie zugänglich" "Show" = "Anzeigen"+"Show All" = "Alle einblenden" "Show as grid" = "Als Raster anzeigen" "Show as list" = "Als Liste anzeigen" "Show autosaves" = "Automatische Speicherungen anzeigen"@@ -1425,6 +1434,7 @@ status = "draft — needs native review" "Width" = "Breite" "Width point" = "Breitenpunkt" "Width profile" = "Breitenprofil"+"Window" = "Fenster" "With the layers' export settings" = "Mit den Exporteinstellungen der Ebenen" "Wrap" = "Umbrechen" "Wrap in new section" = "In neuen Abschnitt einschließen"@@ -25,6 +25,7 @@ name = "English" "About" = "About" "About FigCraft" = "About FigCraft" "About this server" = "About this server"+"About {app}" = "About {app}" "Action" = "Action" "Actions" = "Actions" "Actions for {name}" = "Actions for {name}"@@ -183,6 +184,7 @@ name = "English" "Branch {name}" = "Branch {name}" "Branches" = "Branches" "Bound to {name}. Click to detach the variable." = "Bound to {name}. Click to detach the variable."+"Bring All to Front" = "Bring All to Front" "Bring forward" = "Bring forward" "Bring the main file's changes into the branch" = "Bring the main file's changes into the branch" "Bring to front" = "Bring to front"@@ -201,6 +203,7 @@ name = "English" "Checking the fonts installed on this computer…" = "Checking the fonts installed on this computer…" "Checking the server…" = "Checking the server…" "Clear from this list" = "Clear from this list"+"Close Window" = "Close Window" "Code or invite link" = "Code or invite link" "Color profile: {name}" = "Color profile: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Colour and text styles, Light and Dark variables, buttons, inputs and cards"@@ -581,11 +584,13 @@ name = "English" "Help page" = "Help page" "Here: {names}" = "Here: {names}" "Hidden" = "Hidden"+"Hide Others" = "Hide Others" "Hide contents" = "Hide contents" "Hide from publishing" = "Hide from publishing" "Hide section contents" = "Hide section contents" "Hide these tips (Getting started in the Help menu shows them again)" = "Hide these tips (Getting started in the Help menu shows them again)" "Hide when publishing" = "Hide when publishing"+"Hide {app}" = "Hide {app}" "Highlight layers on hover" = "Highlight layers on hover" "Highlights" = "Highlights" "Hold Alt" = "Hold Alt"@@ -1044,6 +1049,7 @@ name = "English" "Push" = "Push" "Push changes to main component" = "Push changes to main component" "Put text on path" = "Put text on path"+"Quit {app}" = "Quit {app}" "R" = "R" "Radial" = "Radial" "Radius" = "Radius"@@ -1210,11 +1216,13 @@ name = "English" "Server address (ws://host:47900/)" = "Server address (ws://host:47900/)" "Server address" = "Server address" "Server for online sharing" = "Server for online sharing"+"Services" = "Services" "Set as default" = "Set as default" "Set variable" = "Set variable" "Set variable mode" = "Set variable mode" "Set variable {name}" = "Set variable {name}" "Set {name} to {value}" = "Set {name} to {value}"+"Settings…" = "Settings…" "Shadows" = "Shadows" "Shape" = "Shape" "Shape builder" = "Shape builder"@@ -1231,6 +1239,7 @@ name = "English" "Sharing online · {people}" = "Sharing online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Sharing stopped: the file stays on the server, open only to you" "Show" = "Show"+"Show All" = "Show All" "Show all templates" = "Show all templates" "Show as grid" = "Show as grid" "Show as list" = "Show as list"@@ -1509,6 +1518,7 @@ name = "English" "Width" = "Width" "Width point" = "Width point" "Width profile" = "Width profile"+"Window" = "Window" "Wireframe kit" = "Wireframe kit" "With the layers' export settings" = "With the layers' export settings" "Wrap" = "Wrap"@@ -8,6 +8,9 @@ name = "Español" status = "draft — needs native review" [ui]+"About {app}" = "Acerca de {app}"+"Bring All to Front" = "Traer todo al frente"+"Close Window" = "Cerrar ventana" "Compatibility notes" = "Notas de compatibilidad" "Couldn't download the variables" = "No se pudieron descargar las variables" @@ -582,11 +585,13 @@ status = "draft — needs native review" "Help page" = "Página de ayuda" "Here: {names}" = "Aquí: {names}" "Hidden" = "Oculta"+"Hide Others" = "Ocultar otros" "Hide contents" = "Ocultar contenido" "Hide from publishing" = "Ocultar al publicar" "Hide section contents" = "Ocultar contenido de la sección" "Hide these tips (Getting started in the Help menu shows them again)" = "Ocultar estos consejos (Primeros pasos en el menú Ayuda los vuelve a mostrar)" "Hide when publishing" = "Ocultar al publicar"+"Hide {app}" = "Ocultar {app}" "Highlight layers on hover" = "Resaltar capas al pasar el cursor" "Highlights" = "Luces" "Hold Alt" = "Mantén Alt"@@ -1045,6 +1050,7 @@ status = "draft — needs native review" "Push" = "Empujar" "Push changes to main component" = "Enviar cambios al componente principal" "Put text on path" = "Poner texto en trazado"+"Quit {app}" = "Salir de {app}" "R" = "R" "Radial" = "Radial" "Radius" = "Radio"@@ -1211,11 +1217,13 @@ status = "draft — needs native review" "Server address (ws://host:47900/)" = "Dirección del servidor (ws://host:47900/)" "Server address" = "Dirección del servidor" "Server for online sharing" = "Servidor para compartir en línea"+"Services" = "Servicios" "Set as default" = "Establecer como predeterminado" "Set variable" = "Establecer variable" "Set variable mode" = "Establecer modo de variable" "Set variable {name}" = "Establecer variable {name}" "Set {name} to {value}" = "Establecer {name} en {value}"+"Settings…" = "Ajustes…" "Shadows" = "Sombras" "Shape" = "Forma" "Shape builder" = "Generador de formas"@@ -1232,6 +1240,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Compartiendo en línea · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Se dejó de compartir: el archivo se queda en el servidor, abierto solo para ti" "Show" = "Mostrar"+"Show All" = "Mostrar todo" "Show all templates" = "Mostrar todas las plantillas" "Show as grid" = "Mostrar como cuadrícula" "Show as list" = "Mostrar como lista"@@ -1510,6 +1519,7 @@ status = "draft — needs native review" "Width" = "Ancho" "Width point" = "Punto de ancho" "Width profile" = "Perfil de ancho"+"Window" = "Ventana" "Wireframe kit" = "Kit de wireframes" "With the layers' export settings" = "Con los ajustes de exportación de las capas" "Wrap" = "Ajustar"@@ -27,6 +27,7 @@ status = "draft — needs native review" "About" = "Tietoja" "About FigCraft" = "Tietoja FigCraftista" "About this server" = "Tietoja tästä palvelimesta"+"About {app}" = "Tietoja: {app}" "Action" = "Toiminto" "Actions" = "Toiminnot" "Actions for {name}" = "Kohteen {name} toiminnot"@@ -185,6 +186,7 @@ status = "draft — needs native review" "Branch updated from main" = "Haara päivitetty päähaarasta" "Branch {name}" = "Haara {name}" "Branches" = "Haarat"+"Bring All to Front" = "Tuo kaikki eteen" "Bring forward" = "Tuo eteenpäin" "Bring the main file's changes into the branch" = "Tuo päätiedoston muutokset haaraan" "Bring to front" = "Tuo etummaiseksi"@@ -240,6 +242,7 @@ status = "draft — needs native review" "Clip content" = "Leikkaa sisältö rajoihin" "Clip to shape" = "Leikkaa muotoon" "Close" = "Sulje"+"Close Window" = "Sulje ikkuna" "Close overlay" = "Sulje peittokuva" "Close preview" = "Sulje esikatselu" "Close tab" = "Sulje välilehti"@@ -583,11 +586,13 @@ status = "draft — needs native review" "Help page" = "Ohjesivu" "Here: {names}" = "Paikalla: {names}" "Hidden" = "Kätketty"+"Hide Others" = "Kätke muut" "Hide contents" = "Kätke sisältö" "Hide from publishing" = "Jätä julkaisematta" "Hide section contents" = "Kätke osion sisältö" "Hide these tips (Getting started in the Help menu shows them again)" = "Kätke nämä vinkit (Ohje-valikon Aloittaminen näyttää ne uudelleen)" "Hide when publishing" = "Kätke julkaistaessa"+"Hide {app}" = "Kätke {app}" "Highlight layers on hover" = "Korosta tasot osoitettaessa" "Highlights" = "Huippuvalot" "Hold Alt" = "Pidä Alt painettuna"@@ -1046,6 +1051,7 @@ status = "draft — needs native review" "Push" = "Työntö" "Push changes to main component" = "Siirrä muutokset pääkomponenttiin" "Put text on path" = "Sijoita teksti polulle"+"Quit {app}" = "Lopeta {app}" "R" = "R" "Radial" = "Säteittäinen" "Radius" = "Säde"@@ -1212,11 +1218,13 @@ status = "draft — needs native review" "Server address" = "Palvelimen osoite" "Server address (ws://host:47900/)" = "Palvelimen osoite (ws://host:47900/)" "Server for online sharing" = "Verkkojakamisen palvelin"+"Services" = "Palvelut" "Set as default" = "Aseta oletukseksi" "Set variable" = "Aseta muuttuja" "Set variable mode" = "Aseta muuttujan tila" "Set variable {name}" = "Aseta muuttuja {name}" "Set {name} to {value}" = "Aseta {name} arvoon {value}"+"Settings…" = "Asetukset…" "Shadows" = "Varjot" "Shape" = "Muoto" "Shape builder" = "Muodonrakennin"@@ -1233,6 +1241,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Jaetaan verkossa · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Jakaminen lopetettu: tiedosto jää palvelimelle vain sinun käyttöösi" "Show" = "Näytä"+"Show All" = "Näytä kaikki" "Show all templates" = "Näytä kaikki mallit" "Show as grid" = "Näytä ruudukkona" "Show as list" = "Näytä luettelona"@@ -1511,6 +1520,7 @@ status = "draft — needs native review" "Width" = "Leveys" "Width point" = "Leveyspiste" "Width profile" = "Leveysprofiili"+"Window" = "Ikkuna" "Wireframe kit" = "Rautalankapaketti" "With the layers' export settings" = "Tasojen vientiasetuksilla" "Wrap" = "Rivitys"@@ -26,6 +26,7 @@ status = "draft — needs native review" "About" = "À propos" "About FigCraft" = "À propos de FigCraft" "About this server" = "À propos de ce serveur"+"About {app}" = "À propos de {app}" "Action" = "Action" "Actions" = "Actions" "Actions for {name}" = "Actions pour {name}"@@ -183,6 +184,7 @@ status = "draft — needs native review" "Branch updated from main" = "Branche mise à jour depuis le fichier principal" "Branch {name}" = "Branche {name}" "Branches" = "Branches"+"Bring All to Front" = "Tout ramener au premier plan" "Bring forward" = "Avancer" "Bring the main file's changes into the branch" = "Apporte dans la branche les modifications du fichier principal" "Bring to front" = "Mettre au premier plan"@@ -200,6 +202,7 @@ status = "draft — needs native review" "Can view" = "Peut consulter" "Checking the server…" = "Vérification auprès du serveur…" "Clear from this list" = "Effacer de cette liste"+"Close Window" = "Fermer la fenêtre" "Code or invite link" = "Code ou lien d’invitation" "Color profile: {name}" = "Profil colorimétrique : {name}" "CSS custom properties…" = "Propriétés personnalisées CSS…"@@ -580,11 +583,13 @@ status = "draft — needs native review" "Help page" = "Page d’aide" "Here: {names}" = "Présents : {names}" "Hidden" = "Masqué"+"Hide Others" = "Masquer les autres" "Hide contents" = "Masquer le contenu" "Hide from publishing" = "Exclure de la publication" "Hide section contents" = "Masquer le contenu de la section" "Hide these tips (Getting started in the Help menu shows them again)" = "Masquer ces astuces (Premiers pas dans le menu Aide les affiche de nouveau)" "Hide when publishing" = "Masquer à la publication"+"Hide {app}" = "Masquer {app}" "Highlight layers on hover" = "Mettre en évidence les calques au survol" "Highlights" = "Hautes lumières" "Hold Alt" = "Maintenir Alt"@@ -1041,6 +1046,7 @@ status = "draft — needs native review" "Push" = "Pousser" "Push changes to main component" = "Transmettre les modifications au composant principal" "Put text on path" = "Placer le texte sur un tracé"+"Quit {app}" = "Quitter {app}" "R" = "R" "Radial" = "Radial" "Radius" = "Rayon"@@ -1205,11 +1211,13 @@ status = "draft — needs native review" "Server address" = "Adresse du serveur" "Server address (ws://host:47900/)" = "Adresse du serveur (ws://host:47900/)" "Server for online sharing" = "Serveur pour le partage en ligne"+"Services" = "Services" "Set as default" = "Définir par défaut" "Set variable" = "Définir la variable" "Set variable mode" = "Définir le mode de variable" "Set variable {name}" = "Définir la variable {name}" "Set {name} to {value}" = "Définir {name} sur {value}"+"Settings…" = "Réglages…" "Shadows" = "Ombres" "Shape" = "Forme" "Shape builder" = "Générateur de formes"@@ -1226,6 +1234,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Partage en ligne · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Partage arrêté : le fichier reste sur le serveur, accessible à vous seul" "Show" = "Afficher"+"Show All" = "Tout afficher" "Show all templates" = "Afficher tous les modèles" "Show as grid" = "Afficher en grille" "Show as list" = "Afficher en liste"@@ -1502,6 +1511,7 @@ status = "draft — needs native review" "Width" = "Largeur" "Width point" = "Point de largeur" "Width profile" = "Profil de largeur"+"Window" = "Fenêtre" "Wireframe kit" = "Kit de wireframes" "With the layers' export settings" = "Avec les réglages d’exportation des calques" "Wrap" = "Renvoi à la ligne"@@ -26,6 +26,7 @@ status = "draft — needs native review" "About" = "Tentang" "About FigCraft" = "Tentang FigCraft" "About this server" = "Tentang server ini"+"About {app}" = "Tentang {app}" "Action" = "Tindakan" "Actions" = "Tindakan" "Actions for {name}" = "Tindakan untuk {name}"@@ -182,6 +183,7 @@ status = "draft — needs native review" "Branch {name}" = "Branch {name}" "Branches" = "Branch" "Bound to {name}. Click to detach the variable." = "Terikat ke {name}. Klik untuk melepas variabel."+"Bring All to Front" = "Bawa semua ke depan" "Bring forward" = "Bawa maju" "Bring the main file's changes into the branch" = "Bawa perubahan file utama ke branch" "Bring to front" = "Bawa ke paling depan"@@ -199,6 +201,7 @@ status = "draft — needs native review" "Can view" = "Dapat melihat" "Checking the server…" = "Memeriksa server…" "Clear from this list" = "Hapus dari daftar ini"+"Close Window" = "Tutup jendela" "Code or invite link" = "Kode atau link undangan" "Color profile: {name}" = "Profil warna: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Gaya warna dan teks, variabel Light dan Dark, tombol, input, dan kartu"@@ -571,11 +574,13 @@ status = "draft — needs native review" "Help page" = "Halaman bantuan" "Here: {names}" = "Di sini: {names}" "Hidden" = "Tersembunyi"+"Hide Others" = "Sembunyikan lainnya" "Hide contents" = "Sembunyikan konten" "Hide from publishing" = "Sembunyikan dari publikasi" "Hide section contents" = "Sembunyikan konten section" "Hide these tips (Getting started in the Help menu shows them again)" = "Sembunyikan tips ini (tampilkan lagi lewat Memulai di menu Bantuan)" "Hide when publishing" = "Sembunyikan saat dipublikasikan"+"Hide {app}" = "Sembunyikan {app}" "Highlight layers on hover" = "Sorot layer saat hover" "Highlights" = "Sorotan" "Hold Alt" = "Tahan Alt"@@ -1025,6 +1030,7 @@ status = "draft — needs native review" "Push" = "Dorong" "Push changes to main component" = "Terapkan perubahan ke main component" "Put text on path" = "Tempatkan teks di path"+"Quit {app}" = "Keluar dari {app}" "R" = "R" "Radial" = "Radial" "Radius" = "Radius"@@ -1187,11 +1193,13 @@ status = "draft — needs native review" "Server address (ws://host:47900/)" = "Alamat server (ws://host:47900/)" "Server address" = "Alamat server" "Server for online sharing" = "Server untuk berbagi online"+"Services" = "Layanan" "Set as default" = "Jadikan default" "Set variable" = "Atur variabel" "Set variable mode" = "Atur mode variabel" "Set variable {name}" = "Atur variabel {name}" "Set {name} to {value}" = "Atur {name} ke {value}"+"Settings…" = "Pengaturan…" "Shadows" = "Bayangan" "Shape" = "Bentuk" "Shape builder" = "Shape builder"@@ -1208,6 +1216,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Berbagi online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Berbagi dihentikan: file tetap di server, hanya dapat dibuka oleh Anda" "Show" = "Tampilkan"+"Show All" = "Tampilkan semua" "Show all templates" = "Tampilkan semua templat" "Show as grid" = "Tampilkan sebagai grid" "Show as list" = "Tampilkan sebagai daftar"@@ -1477,6 +1486,7 @@ status = "draft — needs native review" "Width" = "Lebar" "Width point" = "Titik lebar" "Width profile" = "Profil lebar"+"Window" = "Jendela" "Wireframe kit" = "Kit wireframe" "With the layers' export settings" = "Dengan pengaturan ekspor layer" "Wrap" = "Wrap"@@ -28,6 +28,7 @@ status = "draft — needs native review" "About" = "情報" "About FigCraft" = "FigCraft について" "About this server" = "このサーバーについて"+"About {app}" = "{app} について" "Action" = "アクション" "Actions" = "アクション" "Actions for {name}" = "{name} の操作"@@ -184,6 +185,7 @@ status = "draft — needs native review" "Branch updated from main" = "メインからブランチを更新済み" "Branch {name}" = "ブランチ {name}" "Branches" = "ブランチ"+"Bring All to Front" = "すべてを手前に移動" "Bring forward" = "前面へ" "Bring the main file's changes into the branch" = "メインファイルの変更をブランチに取り込む" "Bring to front" = "最前面へ"@@ -201,6 +203,7 @@ status = "draft — needs native review" "Can view" = "閲覧可" "Checking the server…" = "サーバーを確認中…" "Clear from this list" = "このリストから消去"+"Close Window" = "ウインドウを閉じる" "Code or invite link" = "コードまたは招待リンク" "Color profile: {name}" = "カラープロファイル:{name}" "CSS custom properties…" = "CSS カスタムプロパティ…"@@ -575,11 +578,13 @@ status = "draft — needs native review" "Help page" = "ヘルプページ" "Here: {names}" = "参加中:{names}" "Hidden" = "非表示"+"Hide Others" = "ほかを隠す" "Hide contents" = "コンテンツを非表示" "Hide from publishing" = "公開対象から除外" "Hide section contents" = "セクションのコンテンツを非表示" "Hide these tips (Getting started in the Help menu shows them again)" = "ヒントを非表示(ヘルプメニューの「使い始めるには」で再表示)" "Hide when publishing" = "公開時に非表示"+"Hide {app}" = "{app} を隠す" "Highlight layers on hover" = "ホバー時にレイヤーをハイライト" "Highlights" = "ハイライト" "Hold Alt" = "Alt を押したまま"@@ -1030,6 +1035,7 @@ status = "draft — needs native review" "Push" = "プッシュ" "Push changes to main component" = "変更をメインコンポーネントにプッシュ" "Put text on path" = "テキストをパスに沿わせる"+"Quit {app}" = "{app} を終了" "R" = "R" "Radial" = "放射状" "Radius" = "半径"@@ -1194,11 +1200,13 @@ status = "draft — needs native review" "Server address" = "サーバーのアドレス" "Server address (ws://host:47900/)" = "サーバーのアドレス(ws://host:47900/)" "Server for online sharing" = "オンライン共有用のサーバー"+"Services" = "サービス" "Set as default" = "デフォルトに設定" "Set variable" = "変数を設定" "Set variable mode" = "変数モードを設定" "Set variable {name}" = "変数 {name} を設定" "Set {name} to {value}" = "{name} を {value} に設定"+"Settings…" = "設定…" "Shadows" = "シャドウ" "Shape" = "シェイプ" "Shape builder" = "シェイプビルダー"@@ -1215,6 +1223,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "オンラインで共有中 · {people}" "Sharing stopped: the file stays on the server, open only to you" = "共有を停止:ファイルはサーバーに残り、開けるのはあなただけ" "Show" = "表示"+"Show All" = "すべてを表示" "Show all templates" = "すべてのテンプレートを表示" "Show as grid" = "グリッドで表示" "Show as list" = "リストで表示"@@ -1487,6 +1496,7 @@ status = "draft — needs native review" "Width" = "幅" "Width point" = "幅のポイント" "Width profile" = "幅プロファイル"+"Window" = "ウインドウ" "Wireframe kit" = "ワイヤーフレームキット" "With the layers' export settings" = "レイヤーの書き出し設定で" "Wrap" = "折り返し"@@ -24,6 +24,7 @@ status = "draft — needs native review" "About" = "정보" "About FigCraft" = "FigCraft 정보" "About this server" = "이 서버 정보"+"About {app}" = "{app} 정보" "Action" = "액션" "Actions" = "액션" "Actions for {name}" = "{name} 작업"@@ -174,6 +175,7 @@ status = "draft — needs native review" "Branch updated from main" = "메인에서 브랜치 업데이트됨" "Branch {name}" = "브랜치 {name}" "Branches" = "브랜치"+"Bring All to Front" = "모두 앞으로 가져오기" "Bring forward" = "앞으로 가져오기" "Bring the main file's changes into the branch" = "메인 파일의 변경 사항을 브랜치로 가져오기" "Bring to front" = "맨 앞으로 가져오기"@@ -191,6 +193,7 @@ status = "draft — needs native review" "Can view" = "보기 가능" "Checking the server…" = "서버 확인 중…" "Clear from this list" = "이 목록에서 지우기"+"Close Window" = "윈도우 닫기" "Code or invite link" = "코드 또는 초대 링크" "Color profile: {name}" = "색상 프로필: {name}" "CSS custom properties…" = "CSS 사용자 정의 속성…"@@ -551,9 +554,11 @@ status = "draft — needs native review" "Help page" = "도움말 페이지" "Here: {names}" = "참여 중: {names}" "Hidden" = "숨김"+"Hide Others" = "기타 가리기" "Hide contents" = "콘텐츠 숨기기" "Hide from publishing" = "게시에서 숨기기" "Hide section contents" = "섹션 콘텐츠 숨기기"+"Hide {app}" = "{app} 가리기" "Highlight layers on hover" = "마우스를 올리면 레이어 강조" "Highlights" = "밝은 영역" "Hold Alt" = "Alt 누른 채로"@@ -981,6 +986,7 @@ status = "draft — needs native review" "Push" = "밀어내기" "Push changes to main component" = "변경 사항을 메인 컴포넌트에 푸시" "Put text on path" = "텍스트를 패스에 배치"+"Quit {app}" = "{app} 종료" "R" = "R" "Radial" = "방사형" "Radius" = "반경"@@ -1140,11 +1146,13 @@ status = "draft — needs native review" "Server address" = "서버 주소" "Server address (ws://host:47900/)" = "서버 주소(ws://host:47900/)" "Server for online sharing" = "온라인 공유용 서버"+"Services" = "서비스" "Set as default" = "기본값으로 설정" "Set variable" = "변수 설정" "Set variable mode" = "변수 모드 설정" "Set variable {name}" = "변수 {name} 설정" "Set {name} to {value}" = "{name}을(를) {value}(으)로 설정"+"Settings…" = "설정…" "Shadows" = "어두운 영역" "Shape" = "도형" "Shape builder" = "도형 빌더"@@ -1160,6 +1168,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "온라인으로 공유 중 · {people}" "Sharing stopped: the file stays on the server, open only to you" = "공유 중지됨: 파일은 서버에 남으며 나만 열 수 있음" "Show" = "표시"+"Show All" = "모두 보기" "Show as grid" = "그리드로 보기" "Show as list" = "목록으로 보기" "Show autosaves" = "자동 저장 표시"@@ -1416,6 +1425,7 @@ status = "draft — needs native review" "Width" = "너비" "Width point" = "너비 점" "Width profile" = "너비 프로필"+"Window" = "윈도우" "With the layers' export settings" = "레이어의 내보내기 설정으로" "Wrap" = "줄 바꿈" "Wrap in new section" = "새 섹션으로 감싸기"@@ -27,6 +27,7 @@ status = "draft — needs native review" "About" = "Over" "About FigCraft" = "Over FigCraft" "About this server" = "Over deze server"+"About {app}" = "Over {app}" "Action" = "Actie" "Actions" = "Acties" "Actions for {name}" = "Acties voor {name}"@@ -185,6 +186,7 @@ status = "draft — needs native review" "Branch updated from main" = "Branch bijgewerkt vanaf main" "Branch {name}" = "Branch {name}" "Branches" = "Branches"+"Bring All to Front" = "Alles op voorgrond" "Bring forward" = "Naar voren brengen" "Bring the main file's changes into the branch" = "De wijzigingen uit het hoofdbestand in de branch brengen" "Bring to front" = "Op de voorgrond plaatsen"@@ -240,6 +242,7 @@ status = "draft — needs native review" "Clip content" = "Inhoud bijsnijden" "Clip to shape" = "Bijsnijden op vorm" "Close" = "Sluiten"+"Close Window" = "Sluit venster" "Close overlay" = "Overlay sluiten" "Close preview" = "Voorvertoning sluiten" "Close tab" = "Tabblad sluiten"@@ -583,11 +586,13 @@ status = "draft — needs native review" "Help page" = "Helppagina" "Here: {names}" = "Hier: {names}" "Hidden" = "Verborgen"+"Hide Others" = "Verberg andere" "Hide contents" = "Inhoud verbergen" "Hide from publishing" = "Verbergen bij publiceren" "Hide section contents" = "Sectie-inhoud verbergen" "Hide these tips (Getting started in the Help menu shows them again)" = "Deze tips verbergen (Aan de slag in het menu Help toont ze opnieuw)" "Hide when publishing" = "Verbergen bij publiceren"+"Hide {app}" = "Verberg {app}" "Highlight layers on hover" = "Lagen uitlichten bij aanwijzen" "Highlights" = "Hooglichten" "Hold Alt" = "Houd Alt ingedrukt"@@ -1046,6 +1051,7 @@ status = "draft — needs native review" "Push" = "Pushen" "Push changes to main component" = "Wijzigingen naar hoofdcomponent doorvoeren" "Put text on path" = "Tekst op pad plaatsen"+"Quit {app}" = "Stop {app}" "R" = "R" "Radial" = "Radiaal" "Radius" = "Radius"@@ -1212,11 +1218,13 @@ status = "draft — needs native review" "Server address" = "Serveradres" "Server address (ws://host:47900/)" = "Serveradres (ws://host:47900/)" "Server for online sharing" = "Server voor online delen"+"Services" = "Voorzieningen" "Set as default" = "Als standaard instellen" "Set variable" = "Variabele instellen" "Set variable mode" = "Variabelemodus instellen" "Set variable {name}" = "Variabele {name} instellen" "Set {name} to {value}" = "{name} instellen op {value}"+"Settings…" = "Instellingen…" "Shadows" = "Schaduwen" "Shape" = "Vorm" "Shape builder" = "Shape builder"@@ -1233,6 +1241,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Delen online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Delen gestopt: het bestand blijft op de server staan, alleen voor jou toegankelijk" "Show" = "Weergeven"+"Show All" = "Toon alles" "Show all templates" = "Alle sjablonen tonen" "Show as grid" = "Als raster weergeven" "Show as list" = "Weergeven als lijst"@@ -1511,6 +1520,7 @@ status = "draft — needs native review" "Width" = "Breedte" "Width point" = "Breedtepunt" "Width profile" = "Breedteprofiel"+"Window" = "Venster" "Wireframe kit" = "Wireframekit" "With the layers' export settings" = "Met de exportinstellingen van de lagen" "Wrap" = "Omloop"@@ -22,6 +22,7 @@ status = "draft — needs native review" "About" = "Sobre" "About FigCraft" = "Sobre o FigCraft" "About this server" = "Sobre este servidor"+"About {app}" = "Sobre o {app}" "Action" = "Ação" "Actions" = "Ações" "Actions for {name}" = "Ações para {name}"@@ -172,6 +173,7 @@ status = "draft — needs native review" "Branch updated from main" = "Branch atualizada a partir do principal" "Branch {name}" = "Branch {name}" "Branches" = "Branches"+"Bring All to Front" = "Trazer tudo para a frente" "Bring forward" = "Trazer para a frente" "Bring the main file's changes into the branch" = "Traz para a branch as alterações do arquivo principal" "Bring to front" = "Trazer para o primeiro plano"@@ -189,6 +191,7 @@ status = "draft — needs native review" "Can view" = "Pode visualizar" "Checking the server…" = "Verificando o servidor…" "Clear from this list" = "Limpar desta lista"+"Close Window" = "Fechar janela" "Code or invite link" = "Código ou link de convite" "Color profile: {name}" = "Perfil de cor: {name}" "CSS custom properties…" = "Propriedades personalizadas CSS…"@@ -550,9 +553,11 @@ status = "draft — needs native review" "Help page" = "Página de ajuda" "Here: {names}" = "Aqui: {names}" "Hidden" = "Oculta"+"Hide Others" = "Ocultar outros" "Hide contents" = "Ocultar conteúdo" "Hide from publishing" = "Não publicar" "Hide section contents" = "Ocultar conteúdo da seção"+"Hide {app}" = "Ocultar {app}" "Highlight layers on hover" = "Destacar camadas ao passar o cursor" "Highlights" = "Realces" "Hold Alt" = "Segure Alt"@@ -980,6 +985,7 @@ status = "draft — needs native review" "Push" = "Empurrar" "Push changes to main component" = "Enviar alterações ao componente principal" "Put text on path" = "Colocar texto no caminho"+"Quit {app}" = "Encerrar {app}" "R" = "R" "Radial" = "Radial" "Radius" = "Raio"@@ -1139,11 +1145,13 @@ status = "draft — needs native review" "Server address" = "Endereço do servidor" "Server address (ws://host:47900/)" = "Endereço do servidor (ws://host:47900/)" "Server for online sharing" = "Servidor para compartilhamento online"+"Services" = "Serviços" "Set as default" = "Definir como padrão" "Set variable" = "Definir variável" "Set variable mode" = "Definir modo de variável" "Set variable {name}" = "Definir variável {name}" "Set {name} to {value}" = "Definir {name} como {value}"+"Settings…" = "Ajustes…" "Shadows" = "Sombras" "Shape" = "Forma" "Shape builder" = "Construtor de formas"@@ -1159,6 +1167,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "Compartilhando online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Compartilhamento encerrado: o arquivo continua no servidor, aberto somente para você" "Show" = "Mostrar"+"Show All" = "Mostrar tudo" "Show as grid" = "Mostrar em grade" "Show as list" = "Mostrar em lista" "Show autosaves" = "Mostrar salvamentos automáticos"@@ -1415,6 +1424,7 @@ status = "draft — needs native review" "Width" = "Largura" "Width point" = "Ponto de largura" "Width profile" = "Perfil de largura"+"Window" = "Janela" "With the layers' export settings" = "Com as configurações de exportação das camadas" "Wrap" = "Quebrar linha" "Wrap in new section" = "Envolver em uma nova seção"@@ -23,6 +23,7 @@ name = "Русский" "About" = "О программе" "About FigCraft" = "О FigCraft" "About this server" = "Об этом сервере"+"About {app}" = "О программе {app}" "Action" = "Действие" "Actions" = "Действия" "Actions for {name}" = "Действия для {name}"@@ -179,6 +180,7 @@ name = "Русский" "Branch {name}" = "Ветка {name}" "Branches" = "Ветки" "Bound to {name}. Click to detach the variable." = "Привязано к {name}. Нажмите, чтобы отвязать переменную."+"Bring All to Front" = "Все окна на передний план" "Bring forward" = "На уровень вперёд" "Bring the main file's changes into the branch" = "Перенести изменения основного файла в ветку" "Bring to front" = "На передний план"@@ -196,6 +198,7 @@ name = "Русский" "Can view" = "Может просматривать" "Checking the server…" = "Проверка сервера…" "Clear from this list" = "Убрать из этого списка"+"Close Window" = "Закрыть окно" "Code or invite link" = "Код или ссылка приглашения" "Color profile: {name}" = "Цветовой профиль: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Цветовые и текстовые стили, переменные светлой и тёмной тем, кнопки, поля ввода и карточки"@@ -568,11 +571,13 @@ name = "Русский" "Help page" = "Страница справки" "Here: {names}" = "Здесь: {names}" "Hidden" = "Скрыто"+"Hide Others" = "Скрыть остальные" "Hide contents" = "Скрыть содержимое" "Hide from publishing" = "Исключить из публикации" "Hide section contents" = "Скрыть содержимое секции" "Hide these tips (Getting started in the Help menu shows them again)" = "Скрыть подсказки (их можно вернуть через «Начало работы» в меню справки)" "Hide when publishing" = "Скрывать при публикации"+"Hide {app}" = "Скрыть {app}" "Highlight layers on hover" = "Подсвечивать слои при наведении" "Highlights" = "Света" "Hold Alt" = "Удерживайте Alt"@@ -1024,6 +1029,7 @@ name = "Русский" "Push" = "Вытеснение" "Push changes to main component" = "Перенести изменения в основной компонент" "Put text on path" = "Разместить текст на пути"+"Quit {app}" = "Завершить {app}" "R" = "R" "Radial" = "Радиальный" "Radius" = "Радиус"@@ -1188,11 +1194,13 @@ name = "Русский" "Server address (ws://host:47900/)" = "Адрес сервера (ws://host:47900/)" "Server address" = "Адрес сервера" "Server for online sharing" = "Сервер для онлайн-публикации"+"Services" = "Службы" "Set as default" = "Задать по умолчанию" "Set variable" = "Задать переменную" "Set variable mode" = "Задать режим переменных" "Set variable {name}" = "Задать переменную {name}" "Set {name} to {value}" = "Задать {name}: {value}"+"Settings…" = "Настройки…" "Shadows" = "Тени" "Shape" = "Фигура" "Shape builder" = "Конструктор фигур"@@ -1209,6 +1217,7 @@ name = "Русский" "Sharing online · {people}" = "Общий доступ онлайн · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Общий доступ остановлен: файл остаётся на сервере, доступен только вам" "Show" = "Показать"+"Show All" = "Показать все" "Show all templates" = "Показать все шаблоны" "Show as grid" = "Показать сеткой" "Show as list" = "Показать списком"@@ -1479,6 +1488,7 @@ name = "Русский" "Width" = "Ширина" "Width point" = "Точка ширины" "Width profile" = "Профиль ширины"+"Window" = "Окно" "Wireframe kit" = "Набор вайрфреймов" "With the layers' export settings" = "С настройками экспорта слоёв" "Wrap" = "Перенос"@@ -25,6 +25,7 @@ name = "Svenska" "About" = "Om" "About FigCraft" = "Om FigCraft" "About this server" = "Om den här servern"+"About {app}" = "Om {app}" "Action" = "Åtgärd" "Actions" = "Åtgärder" "Actions for {name}" = "Åtgärder för {name}"@@ -182,6 +183,7 @@ name = "Svenska" "Branch {name}" = "Gren {name}" "Branches" = "Grenar" "Bound to {name}. Click to detach the variable." = "Bunden till {name}. Klicka för att koppla bort variabeln."+"Bring All to Front" = "Lägg alla överst" "Bring forward" = "Flytta framåt" "Bring the main file's changes into the branch" = "Hämta huvudfilens ändringar till grenen" "Bring to front" = "Flytta längst fram"@@ -199,6 +201,7 @@ name = "Svenska" "Can view" = "Kan visa" "Checking the server…" = "Kontrollerar servern…" "Clear from this list" = "Ta bort från listan"+"Close Window" = "Stäng fönster" "Code or invite link" = "Kod eller inbjudningslänk" "Color profile: {name}" = "Färgprofil: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Färg- och textstilar, ljusa och mörka variabler, knappar, inmatningsfält och kort"@@ -577,11 +580,13 @@ name = "Svenska" "Help page" = "Hjälpsida" "Here: {names}" = "Här: {names}" "Hidden" = "Dold"+"Hide Others" = "Göm övriga" "Hide contents" = "Dölj innehåll" "Hide from publishing" = "Dölj vid publicering" "Hide section contents" = "Dölj avsnittets innehåll" "Hide these tips (Getting started in the Help menu shows them again)" = "Dölj de här tipsen (Kom igång i Hjälp-menyn visar dem igen)" "Hide when publishing" = "Dölj vid publicering"+"Hide {app}" = "Göm {app}" "Highlight layers on hover" = "Framhäv lager vid hovring" "Highlights" = "Högdagrar" "Hold Alt" = "Håll ned Alt"@@ -1034,6 +1039,7 @@ name = "Svenska" "Push" = "Skjut" "Push changes to main component" = "Skicka ändringar till huvudkomponenten" "Put text on path" = "Placera text på bana"+"Quit {app}" = "Avsluta {app}" "R" = "R" "Radial" = "Radiell" "Radius" = "Radie"@@ -1198,11 +1204,13 @@ name = "Svenska" "Server address (ws://host:47900/)" = "Serveradress (ws://host:47900/)" "Server address" = "Serveradress" "Server for online sharing" = "Server för delning online"+"Services" = "Tjänster" "Set as default" = "Ange som standard" "Set variable" = "Ange variabel" "Set variable mode" = "Ange variabelläge" "Set variable {name}" = "Ange variabel {name}" "Set {name} to {value}" = "Ange {name} till {value}"+"Settings…" = "Inställningar…" "Shadows" = "Skuggor" "Shape" = "Form" "Shape builder" = "Formbyggare"@@ -1219,6 +1227,7 @@ name = "Svenska" "Sharing online · {people}" = "Delar online · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Delningen stoppades: filen ligger kvar på servern och är bara öppen för dig" "Show" = "Visa"+"Show All" = "Visa alla" "Show all templates" = "Visa alla mallar" "Show as grid" = "Visa som rutnät" "Show as list" = "Visa som lista"@@ -1494,6 +1503,7 @@ name = "Svenska" "Width" = "Bredd" "Width point" = "Breddpunkt" "Width profile" = "Breddprofil"+"Window" = "Fönster" "Wireframe kit" = "Trådramspaket" "With the layers' export settings" = "Med lagrens exportinställningar" "Wrap" = "Radbryt"@@ -26,6 +26,7 @@ name = "Türkçe" "About" = "Hakkında" "About FigCraft" = "FigCraft hakkında" "About this server" = "Bu sunucu hakkında"+"About {app}" = "{app} hakkında" "Action" = "Eylem" "Actions" = "Eylemler" "Actions for {name}" = "{name} için eylemler"@@ -182,6 +183,7 @@ name = "Türkçe" "Branch {name}" = "Dal: {name}" "Branches" = "Dallar" "Bound to {name}. Click to detach the variable." = "{name} değişkenine bağlı. Değişkeni ayırmak için tıklayın."+"Bring All to Front" = "Tümünü öne getir" "Bring forward" = "Öne getir" "Bring the main file's changes into the branch" = "Ana dosyanın değişikliklerini dala getir" "Bring to front" = "En öne getir"@@ -199,6 +201,7 @@ name = "Türkçe" "Can view" = "Görüntüleyebilir" "Checking the server…" = "Sunucu denetleniyor…" "Clear from this list" = "Bu listeden kaldır"+"Close Window" = "Pencereyi kapat" "Code or invite link" = "Kod veya davet bağlantısı" "Color profile: {name}" = "Renk profili: {name}" "Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Renk ve metin stilleri, Açık ve Koyu değişkenler, düğmeler, giriş alanları ve kartlar"@@ -571,11 +574,13 @@ name = "Türkçe" "Help page" = "Yardım sayfası" "Here: {names}" = "Burada: {names}" "Hidden" = "Gizli"+"Hide Others" = "Diğerlerini gizle" "Hide contents" = "İçeriği gizle" "Hide from publishing" = "Yayımlamadan gizle" "Hide section contents" = "Bölüm içeriğini gizle" "Hide these tips (Getting started in the Help menu shows them again)" = "Bu ipuçlarını gizle (Yardım menüsündeki Başlarken onları yeniden gösterir)" "Hide when publishing" = "Yayımlarken gizle"+"Hide {app}" = "{app} uygulamasını gizle" "Highlight layers on hover" = "Üzerine gelince katmanları vurgula" "Highlights" = "Açık tonlar" "Hold Alt" = "Alt tuşunu basılı tutun"@@ -1025,6 +1030,7 @@ name = "Türkçe" "Push" = "İt" "Push changes to main component" = "Değişiklikleri ana bileşene gönder" "Put text on path" = "Metni yola yerleştir"+"Quit {app}" = "{app} uygulamasından çık" "R" = "R" "Radial" = "Dairesel" "Radius" = "Yarıçap"@@ -1187,11 +1193,13 @@ name = "Türkçe" "Server address (ws://host:47900/)" = "Sunucu adresi (ws://host:47900/)" "Server address" = "Sunucu adresi" "Server for online sharing" = "Çevrimiçi paylaşım sunucusu"+"Services" = "Hizmetler" "Set as default" = "Varsayılan yap" "Set variable" = "Değişken ayarla" "Set variable mode" = "Değişken modunu ayarla" "Set variable {name}" = "{name} değişkenini ayarla" "Set {name} to {value}" = "{name} değerini {value} yap"+"Settings…" = "Ayarlar…" "Shadows" = "Gölgeler" "Shape" = "Şekil" "Shape builder" = "Şekil oluşturucu"@@ -1208,6 +1216,7 @@ name = "Türkçe" "Sharing online · {people}" = "Çevrimiçi paylaşılıyor · {people}" "Sharing stopped: the file stays on the server, open only to you" = "Paylaşım durduruldu: dosya sunucuda kalır, yalnızca size açık" "Show" = "Göster"+"Show All" = "Tümünü göster" "Show all templates" = "Tüm şablonları göster" "Show as grid" = "Izgara olarak göster" "Show as list" = "Liste olarak göster"@@ -1477,6 +1486,7 @@ name = "Türkçe" "Width" = "Genişlik" "Width point" = "Genişlik noktası" "Width profile" = "Genişlik profili"+"Window" = "Pencere" "Wireframe kit" = "Tel kafes seti" "With the layers' export settings" = "Katmanların dışa aktarma ayarlarıyla" "Wrap" = "Kaydır"@@ -25,6 +25,7 @@ status = "draft — needs native review" "About" = "关于" "About FigCraft" = "关于 FigCraft" "About this server" = "关于此服务器"+"About {app}" = "关于 {app}" "Action" = "操作" "Actions" = "操作" "Actions for {name}" = "{name} 的操作"@@ -175,6 +176,7 @@ status = "draft — needs native review" "Branch updated from main" = "已从主文件更新分支" "Branch {name}" = "分支 {name}" "Branches" = "分支"+"Bring All to Front" = "前置全部窗口" "Bring forward" = "上移一层" "Bring the main file's changes into the branch" = "将主文件的更改合入分支" "Bring to front" = "置于顶层"@@ -192,6 +194,7 @@ status = "draft — needs native review" "Can view" = "可查看" "Checking the server…" = "正在检查服务器…" "Clear from this list" = "从此列表中清除"+"Close Window" = "关闭窗口" "Code or invite link" = "代码或邀请链接" "Color profile: {name}" = "颜色配置文件:{name}" "CSS custom properties…" = "CSS 自定义属性…"@@ -552,9 +555,11 @@ status = "draft — needs native review" "Help page" = "帮助页面" "Here: {names}" = "在场:{names}" "Hidden" = "已隐藏"+"Hide Others" = "隐藏其他" "Hide contents" = "隐藏内容" "Hide from publishing" = "发布时隐藏" "Hide section contents" = "隐藏分区内容"+"Hide {app}" = "隐藏 {app}" "Highlight layers on hover" = "悬停时高亮图层" "Highlights" = "高光" "Hold Alt" = "按住 Alt"@@ -982,6 +987,7 @@ status = "draft — needs native review" "Push" = "推入" "Push changes to main component" = "将更改推送到主组件" "Put text on path" = "将文本放到路径上"+"Quit {app}" = "退出 {app}" "R" = "R" "Radial" = "径向" "Radius" = "半径"@@ -1141,11 +1147,13 @@ status = "draft — needs native review" "Server address" = "服务器地址" "Server address (ws://host:47900/)" = "服务器地址(ws://host:47900/)" "Server for online sharing" = "在线分享所用的服务器"+"Services" = "服务" "Set as default" = "设为默认" "Set variable" = "设置变量" "Set variable mode" = "设置变量模式" "Set variable {name}" = "设置变量 {name}" "Set {name} to {value}" = "将 {name} 设为 {value}"+"Settings…" = "设置…" "Shadows" = "阴影" "Shape" = "形状" "Shape builder" = "形状生成器"@@ -1161,6 +1169,7 @@ status = "draft — needs native review" "Sharing online · {people}" = "正在在线分享 · {people}" "Sharing stopped: the file stays on the server, open only to you" = "已停止分享:文件保留在服务器上,只有你能打开" "Show" = "显示"+"Show All" = "全部显示" "Show as grid" = "以网格显示" "Show as list" = "以列表显示" "Show autosaves" = "显示自动保存"@@ -1417,6 +1426,7 @@ status = "draft — needs native review" "Width" = "宽度" "Width point" = "宽度点" "Width profile" = "宽度配置"+"Window" = "窗口" "With the layers' export settings" = "使用图层的导出设置" "Wrap" = "换行" "Wrap in new section" = "包裹到新分区"@@ -41,6 +41,9 @@ pub mod live_text; pub mod measure_overlay; pub mod menus; pub mod missing_fonts;+pub mod native_menu;+#[cfg(test)]+mod native_menu_tests; pub mod online; pub mod pending_open; pub mod plugins_ui;@@ -719,7 +719,8 @@ fn over_menu(ctx: &egui::Context) -> bool { ctx.input(|i| i.pointer.interact_pos()).and_then(|p| ctx.layer_id_at(p)).is_some_and(|l| l.order == egui::Order::Foreground) } -const TOP_MENUS: &[&str] = &[+/// The main menu's menus, in order (the in-window menu and the native menu bar).+pub const TOP_MENUS: &[&str] = &[ tr_noop("File"), tr_noop("Edit"), tr_noop("View"),new file mode 100644@@ -0,0 +1,204 @@+//! The main menu as a native menu bar (macOS, through `craft-menu`): the in-window menu's items,+//! labels, shortcuts and enabled and checked state, built from the same catalog, with Open recent+//! and the installed plugins. With several windows it acts on the one in front.++use egui::ViewportId;+use figcraft_engine::catalog::{MENU, SEP};+use figcraft_engine::keymap::{self, Platform};+use serde_json::json;++use crate::FigApp;+use crate::i18n::{self, tr, tr_noop};+use crate::menus::TOP_MENUS;+use crate::plugins_ui::{self, MenuEntry};+use crate::windows;++/// Item ids that carry a parameter: a recent file (`file.recent:<path>`), Clear recent, and a+/// plugin's command (`plugin:<plugin id>:<command id>`). A native menu reports only the id.+pub const RECENT: &str = "file.recent:";+pub const CLEAR_RECENT: &str = "file.recent.clear";+pub const PLUGIN: &str = "plugin:";+/// A plugin's widget (`widget:<plugin id>`, Plugins ▸ Widgets).+pub const WIDGET: &str = "widget:";++/// The labels the Mac layout writes itself (the app menu, the Window menu), for+/// [`craft_menu::LayoutOptions::label`]; `{app}` is the app's name.+pub const MAC_LABELS: &[&str] = &[+ tr_noop("About {app}"),+ tr_noop("Settings…"),+ tr_noop("Services"),+ tr_noop("Hide {app}"),+ tr_noop("Hide Others"),+ tr_noop("Show All"),+ tr_noop("Quit {app}"),+ tr_noop("Window"),+ tr_noop("Minimize"),+ tr_noop("Zoom"),+ tr_noop("Bring All to Front"),+ tr_noop("Close Window"),+];++/// A label the Mac layout writes ([`MAC_LABELS`]), translated.+pub fn mac_label(english: &'static str) -> String {+ tr(english).to_string()+}++/// The main menu as a menu bar, with the Mac's shortcuts (a native menu bar is the Mac's).+/// `windows`: whether the app can open more windows (New window).+pub fn menu_bar(app: &FigApp, windows: bool) -> craft_menu::MenuBar {+ let mut rows = Vec::new();+ let path = |menu: &str| menu.split('/').map(|p| tr(p).to_string()).collect::<Vec<String>>();+ let row = |path: Vec<String>, id: String, label: String, shortcut: Option<&str>, enabled: bool, checked: Option<bool>| craft_menu::Row {+ path,+ id,+ label,+ shortcut: shortcut.map(str::to_string),+ enabled,+ checked,+ };+ let sep = |path: Vec<String>| row(path, String::new(), "---".into(), None, false, None);+ plugin_rows(app, &mut rows);+ for m in MENU {+ if m.label == SEP {+ rows.push(sep(path(m.menu)));+ continue;+ }+ if m.id == "file.openRecent" {+ recent_rows(app, path(&format!("{}/{}", m.menu, i18n::menu_item(m.id, m.label))), &mut rows);+ continue;+ }+ let enabled = crate::menus::available(m.id) && (m.id != "window.new" || windows);+ let checked = figcraft_engine::cmd::view::checked(&app.session, m.id);+ rows.push(row(+ path(m.menu),+ m.id.into(),+ i18n::menu_item(m.id, m.label).into_owned(),+ keymap::primary(m.id, Platform::Mac),+ enabled,+ checked,+ ));+ }+ let tops: Vec<&str> = TOP_MENUS.iter().map(|t| tr(t)).collect();+ let mut bar = craft_menu::from_items(&tops, &rows);+ bar.tag_item("help.about", craft_menu::ItemRole::About);+ bar.tag_item("app.preferences", craft_menu::ItemRole::Settings);+ bar.tag_menu(tr("File"), craft_menu::MenuRole::File);+ bar.tag_menu(tr("Help"), craft_menu::MenuRole::Help);+ bar+}++/// File ▸ Open recent: the recent files (greyed when the file is gone), then Clear recent.+fn recent_rows(app: &FigApp, path: Vec<String>, rows: &mut Vec<craft_menu::Row>) {+ let recents: Vec<&String> = crate::home_recents::listed(&app.session.prefs.recent_files).collect();+ for p in &recents {+ let file = std::path::Path::new(p.as_str());+ let name = file.file_name().map_or_else(|| p.to_string(), |f| f.to_string_lossy().into_owned());+ rows.push(craft_menu::Row {+ path: path.clone(),+ id: format!("{RECENT}{p}"),+ label: name,+ shortcut: None,+ enabled: file.exists(),+ checked: None,+ });+ }+ if !recents.is_empty() {+ rows.push(craft_menu::Row { path: path.clone(), id: String::new(), label: "---".into(), shortcut: None, enabled: false, checked: None });+ }+ rows.push(craft_menu::Row {+ path,+ id: CLEAR_RECENT.into(),+ label: tr("Clear recent").into(),+ shortcut: None,+ enabled: !recents.is_empty(),+ checked: None,+ });+}++/// The installed plugins' commands and widgets, heading the Plugins menu as in the in-window menu+/// ([`plugins_ui::menu_entries`]).+fn plugin_rows(app: &FigApp, rows: &mut Vec<craft_menu::Row>) {+ let plugins = &app.session.prefs.plugins;+ if plugins.is_empty() {+ return;+ }+ let top = tr("Plugins").to_string();+ let item = |path: Vec<String>, id: String, label: &str| craft_menu::Row {+ path,+ id,+ label: label.to_string(),+ shortcut: None,+ enabled: true,+ checked: None,+ };+ let (entries, widgets) = plugins_ui::menu_entries(plugins);+ for e in &entries {+ match e {+ MenuEntry::Command { plugin, command, label } => rows.push(item(vec![top.clone()], format!("{PLUGIN}{plugin}:{command}"), label)),+ MenuEntry::Submenu { plugin, name, commands } => {+ for (command, label) in commands {+ rows.push(item(vec![top.clone(), name.to_string()], format!("{PLUGIN}{plugin}:{command}"), label));+ }+ }+ }+ }+ for (plugin, name) in &widgets {+ rows.push(item(vec![top.clone(), tr("Widgets").to_string()], format!("{WIDGET}{plugin}"), name));+ }+ rows.push(craft_menu::Row { path: vec![top], id: String::new(), label: "---".into(), shortcut: None, enabled: false, checked: None });+}++/// Run the item `id` chosen in the native menu bar, as the in-window menu would.+pub fn run(app: &mut FigApp, id: &str) {+ if id == CLEAR_RECENT {+ app.run_user("file.openRecent", json!({"clear": true}));+ } else if let Some(path) = id.strip_prefix(RECENT) {+ // Through the user path, so an import report shows; `file.openRecent` also drops a file that is gone.+ if app.run_user("file.openRecent", json!({"path": path})).is_some_and(|r| r.get("index").is_some()) {+ app.ui.home = false;+ }+ } else if let Some((plugin, command)) = id.strip_prefix(PLUGIN).and_then(|rest| rest.split_once(':')) {+ plugins_ui::start(app, plugin, command);+ } else if let Some(plugin) = id.strip_prefix(WIDGET) {+ plugins_ui::insert_widget(app, plugin);+ } else if crate::menus::available(id) {+ crate::menus::invoke(app, id);+ }+}++/// The viewport of the window in front: the menu bar's key equivalents go to it.+pub fn front(app: &FigApp) -> ViewportId {+ windows::viewport(app.windows.focused)+}++/// The menu bar for the window in front (its file, selection and panels).+pub fn menu_bar_in_front(app: &mut FigApp, windows: bool) -> craft_menu::MenuBar {+ let id = app.windows.focused;+ crate::windows::as_window(app, id, |app| menu_bar(app, windows)).unwrap_or_else(|| menu_bar(app, windows))+}++/// Run the item `id` chosen in the menu bar in the window in front.+pub fn run_in_front(app: &mut FigApp, id: &str) {+ let front = app.windows.focused;+ if windows::as_window(app, front, |app| run(app, id)).is_none() {+ run(app, id);+ }+}++/// Key presses from the menu bar's key equivalents that arrived while another window drew: the+/// window in front takes them in its next frame.+pub fn send_keys(app: &mut FigApp, ctx: &egui::Context, events: Vec<egui::Event>) {+ let id = app.windows.focused;+ if windows::as_window(app, id, |app| app.control_input.extend(events)).is_some() {+ ctx.request_repaint_of(windows::viewport(id));+ }+}++/// The pastes in the main window's input while another window is in front: the menu bar's ⌘V+/// asks the main window for the clipboard ([`craft_menu::NativeMenu::take_clicks`]), so its+/// answer goes on to the window in front ([`send_keys`]).+pub fn take_pastes(events: &mut Vec<egui::Event>) -> Vec<egui::Event> {+ let (pastes, rest) = std::mem::take(events).into_iter().partition(|e| matches!(e, egui::Event::Paste(_)));+ *events = rest;+ pastes+}new file mode 100644@@ -0,0 +1,196 @@+//! The native menu bar's tree: built from the catalog like the in-window menu, with its shortcuts,+//! enabled and checked state, Open recent and the plugins, and its items run as the in-window+//! menu's do, in the window in front.++use std::cell::RefCell;++use craft_menu::{ItemRole, LayoutOptions, MenuBar, MenuRole, Node};+use egui::{RawInput, ViewportId};+use figcraft_engine::Session;+use figcraft_engine::plugins::{InstalledPlugin, Manifest};+use serde_json::json;++use crate::native_menu::{CLEAR_RECENT, MAC_LABELS, RECENT, front, menu_bar, menu_bar_in_front, run, run_in_front, send_keys, take_pastes};+use crate::{FigApp, NoServices};++fn app() -> FigApp {+ FigApp::new(Session::new(), Box::new(NoServices))+}++fn item<'a>(bar: &'a MenuBar, id: &str) -> &'a craft_menu::Item {+ bar.find(id).unwrap_or_else(|| panic!("no item {id}"))+}++#[test]+fn the_bar_has_the_in_window_menus_in_order() {+ let bar = menu_bar(&app(), true);+ let titles: Vec<&str> = bar.menus.iter().map(|m| m.title.as_str()).collect();+ assert_eq!(titles, ["File", "Edit", "View", "Object", "Text", "Arrange", "Vector", "Plugins", "Preferences", "Libraries", "Help"]);+ // Submenus keep their place: Edit ▸ Copy as.+ let edit = &bar.menus[1];+ assert!(edit.children.iter().any(|n| matches!(n, Node::Submenu { label, .. } if label == "Copy as")), "{:?}", edit.children);+}++#[test]+fn items_carry_their_shortcuts_and_state() {+ let mut a = app();+ let bar = menu_bar(&a, true);+ let save = item(&bar, "file.save");+ assert_eq!(save.label, "Save");+ assert_eq!(save.shortcut.as_deref(), Some("Cmd+S"));+ assert!(save.enabled);+ assert_eq!(item(&bar, "edit.redo").shortcut.as_deref(), Some("Shift+Cmd+Z"));+ // A View toggle shows its check, and follows it.+ assert_eq!(item(&bar, "view.rulers").checked, Some(false));+ a.run("view.rulers", json!({}));+ assert_eq!(item(&menu_bar(&a, true), "view.rulers").checked, Some(true));+ // New window only where the app can open windows.+ assert!(item(&bar, "window.new").enabled);+ assert!(!item(&menu_bar(&a, false), "window.new").enabled);+}++#[test]+fn open_recent_lists_the_files_and_clears_them() {+ let mut a = app();+ let dir = std::env::temp_dir().join(format!("figcraft-native-menu-{}", std::process::id()));+ std::fs::create_dir_all(&dir).unwrap();+ let here = dir.join("here.figcraft");+ std::fs::write(&here, b"x").unwrap();+ let gone = dir.join("gone.figcraft");+ a.session.prefs.recent_files = vec![here.to_string_lossy().into_owned(), gone.to_string_lossy().into_owned()];+ let bar = menu_bar(&a, true);+ let shown = item(&bar, &format!("{RECENT}{}", here.display()));+ assert_eq!(shown.label, "here.figcraft");+ assert!(shown.enabled);+ assert!(!item(&bar, &format!("{RECENT}{}", gone.display())).enabled, "a file that is gone is greyed");+ assert!(item(&bar, CLEAR_RECENT).enabled);+ run(&mut a, CLEAR_RECENT);+ assert!(a.session.prefs.recent_files.is_empty());+ assert!(!item(&menu_bar(&a, true), CLEAR_RECENT).enabled);+ let _ = std::fs::remove_dir_all(&dir);+}++#[test]+fn chosen_items_run_their_commands() {+ let mut a = app();+ assert!(!a.session.ds().unwrap().view.rulers);+ run(&mut a, "view.rulers");+ assert!(a.session.ds().unwrap().view.rulers);+ // Unknown ids and greyed parameters do nothing.+ run(&mut a, "no.such.command");+ run(&mut a, "plugin:missing");+}++#[test]+fn the_mac_layout_moves_about_and_settings_to_the_app_menu() {+ let bar = menu_bar(&app(), true);+ let layout = craft_menu::mac_layout(&bar, &LayoutOptions::new("FigCraft"));+ let first = &layout.bar.menus[0];+ assert_eq!(first.role, MenuRole::App);+ let roles: Vec<Option<ItemRole>> = first+ .children+ .iter()+ .filter_map(|n| match n {+ Node::Item(i) => Some(i.role),+ _ => None,+ })+ .collect();+ assert!(roles.contains(&Some(ItemRole::About)) && roles.contains(&Some(ItemRole::Settings)), "{:?}", first.children);+ assert!(layout.bar.find("help.about").is_some_and(|i| i.role == Some(ItemRole::About)));+ // A Window menu is added; Help stays last.+ let titles: Vec<&str> = layout.bar.menus.iter().map(|m| m.title.as_str()).collect();+ assert!(titles.contains(&"Window"), "{titles:?}");+ assert_eq!(titles.last(), Some(&"Help"));+}++#[test]+fn every_label_the_mac_layout_writes_is_translated() {+ let asked = RefCell::new(Vec::new());+ let label = |english: &'static str| {+ asked.borrow_mut().push(english);+ english.to_string()+ };+ craft_menu::mac_layout(&menu_bar(&app(), true), &LayoutOptions { label: &label, ..LayoutOptions::new("FigCraft") });+ let asked = asked.into_inner();+ assert!(asked.contains(&"About {app}") && asked.contains(&"Quit {app}"), "{asked:?}");+ for english in &asked {+ assert!(MAC_LABELS.contains(english), "{english:?} is missing from MAC_LABELS");+ }+ // Every full catalog (not a regional overlay) has them: About doesn't fall back to English.+ for lang in crate::i18n::LANGS.iter().filter(|l| l.parent == 0) {+ let missing: Vec<_> = MAC_LABELS.iter().filter(|k| lang.ui.binary_search_by(|(key, _)| key.cmp(k)).is_err()).collect();+ assert!(missing.is_empty(), "{}: untranslated menu bar labels {missing:?}", lang.code);+ }+ let de = crate::i18n::LANGS.iter().find(|l| l.code == "de").unwrap();+ let about = de.ui.iter().find(|(k, _)| *k == "About {app}").map(|(_, t)| t);+ assert!(matches!(about, Some(crate::i18n::Text::Plain("Über {app}"))));+}++fn plugin(id: &str, name: &str, commands: &[&str], widget: bool) -> InstalledPlugin {+ let commands: Vec<_> = commands.iter().map(|c| json!({"id": c, "label": format!("Run {c}")})).collect();+ let manifest: Manifest =+ serde_json::from_value(json!({"id": id, "name": name, "version": "1.0.0", "main": "main.js", "commands": commands, "widget": widget}))+ .unwrap();+ InstalledPlugin { manifest, path: None, source: None }+}++#[test]+fn the_plugins_menu_lists_commands_and_widgets_as_in_the_window() {+ let mut a = app();+ a.session.prefs.plugins = vec![plugin("one", "One", &["go"], false), plugin("two", "Two", &["a", "b"], false), plugin("pad", "Pad", &[], true)];+ let bar = menu_bar(&a, true);+ let plugins = bar.menus.iter().find(|m| m.title == "Plugins").unwrap();+ assert!(matches!(&plugins.children[0], Node::Item(i) if i.id == "plugin:one:go" && i.label == "One"));+ assert!(matches!(&plugins.children[1], Node::Submenu { label, children, .. } if label == "Two" && children.len() == 2));+ assert!(+ matches!(&plugins.children[2], Node::Submenu { label, children, .. } if label == "Widgets" && matches!(&children[0], Node::Item(i) if i.id == "widget:pad"))+ );+ assert!(matches!(plugins.children[3], Node::Separator));+}++/// Files A and B, A in a second window in front, B in the main window.+fn two_windows() -> (egui::Context, FigApp, u64) {+ let mut s = Session::empty();+ for n in ["A", "B"] {+ s.execute("file.new", &json!({"name": n})).unwrap();+ }+ let ctx = egui::Context::default();+ ctx.set_embed_viewports(false);+ let mut a = FigApp::new(s, Box::new(NoServices));+ let w = crate::control::handle(&mut a, &ctx, "window.new", &json!({"index": 0})).unwrap()["window"].as_u64().unwrap();+ a.windows.focused = w;+ (ctx, a, w)+}++fn rulers(a: &FigApp, name: &str) -> bool {+ a.session.docs.iter().find(|d| d.doc.name == name).unwrap().view.rulers+}++#[test]+fn with_two_windows_the_menu_acts_on_the_one_in_front() {+ let (_ctx, mut a, w) = two_windows();+ assert_eq!(front(&a), crate::windows::viewport(w));+ run_in_front(&mut a, "view.rulers");+ assert!(rulers(&a, "A") && !rulers(&a, "B"), "in the front window's file");+ assert_eq!(a.session.ds().unwrap().doc.name, "B", "the main window keeps its own file");+ assert_eq!(item(&menu_bar_in_front(&mut a, true), "view.rulers").checked, Some(true));+ assert_eq!(item(&menu_bar(&a, true), "view.rulers").checked, Some(false), "the main window's state");+ // With the main window in front, the main window's.+ a.windows.focused = 0;+ assert_eq!(front(&a), ViewportId::ROOT);+ run_in_front(&mut a, "view.rulers");+ assert!(rulers(&a, "B"));+}++#[test]+fn key_equivalents_and_pastes_go_to_the_window_in_front() {+ let (ctx, mut a, w) = two_windows();+ let key = egui::Event::Key { key: egui::Key::S, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND };+ send_keys(&mut a, &ctx, vec![key.clone()]);+ assert!(crate::windows::take_input(&mut a, ViewportId::ROOT).is_empty(), "not the main window's");+ assert_eq!(crate::windows::take_input(&mut a, crate::windows::viewport(w)), vec![key.clone()]);+ // The main window's input loses the paste it was asked for, and keeps the rest.+ let mut raw = RawInput { events: vec![key.clone(), egui::Event::Paste("text".into())], ..Default::default() };+ assert_eq!(take_pastes(&mut raw.events), vec![egui::Event::Paste("text".into())]);+ assert_eq!(raw.events, vec![key]);+}@@ -131,37 +131,68 @@ pub fn import_dialog(app: &mut FigApp) { }); } +/// One entry at the top of the Plugins menu.+pub enum MenuEntry<'a> {+ /// A plugin with one command: an item, with an ellipsis when it asks for parameters first.+ Command { plugin: &'a str, command: &'a str, label: String },+ /// A plugin with several commands: a submenu of them, as (command id, label).+ Submenu { plugin: &'a str, name: &'a str, commands: Vec<(&'a str, &'a str)> },+}++/// The top of the Plugins menu, for the in-window menu and the native menu bar: the installed+/// plugins' commands, then their widgets as (plugin id, name). Empty without plugins.+pub fn menu_entries(plugins: &[InstalledPlugin]) -> (Vec<MenuEntry<'_>>, Vec<(&str, &str)>) {+ let mut entries = Vec::new();+ for p in plugins {+ let m = &p.manifest;+ match m.commands.as_slice() {+ // A widget may have no commands: it's in Widgets.+ [] => {}+ [only] => {+ let label = if only.params.is_empty() { m.name.clone() } else { format!("{}…", m.name) };+ entries.push(MenuEntry::Command { plugin: &m.id, command: &only.id, label });+ }+ commands => entries.push(MenuEntry::Submenu {+ plugin: &m.id,+ name: &m.name,+ commands: commands.iter().map(|c| (c.id.as_str(), c.label.as_str())).collect(),+ }),+ }+ }+ let widgets = plugins.iter().filter(|p| p.manifest.widget).map(|p| (p.manifest.id.as_str(), p.manifest.name.as_str())).collect();+ (entries, widgets)+}++/// Insert plugin `id`'s widget (Plugins ▸ Widgets).+pub fn insert_widget(app: &mut FigApp, id: &str) {+ app.run("widget.insert", json!({"plugin": id}));+}+ /// The installed plugins at the top of the Plugins menu: a plugin with one command is an item, one /// with several a submenu. True when one was clicked. pub fn menu(app: &mut FigApp, ui: &mut Ui, t: &Tokens) -> bool { if app.session.prefs.plugins.is_empty() { return false; }+ let (entries, widgets) = menu_entries(&app.session.prefs.plugins); let mut chosen: Option<(String, String)> = None;- for p in &app.session.prefs.plugins {- let m = &p.manifest;- match m.commands.as_slice() {- // A widget may have no commands: it's in Widgets.- [] => {}- [only] => {- // An ellipsis: it asks for parameters first.- let label = if only.params.is_empty() { m.name.clone() } else { format!("{}…", m.name) };- if crate::menus::item(ui, &label, "", true, false, t) {- chosen = Some((m.id.clone(), only.id.clone()));+ for e in &entries {+ match e {+ MenuEntry::Command { plugin, command, label } => {+ if crate::menus::item(ui, label, "", true, false, t) {+ chosen = Some((plugin.to_string(), command.to_string())); } }- commands => {- let picked = crate::menus::submenu(ui, &format!("Plugins/plugin:{}", m.id), &m.name, t, |ui| {- commands.iter().find(|c| crate::menus::item(ui, &c.label, "", true, false, t)).map(|c| c.id.clone())+ MenuEntry::Submenu { plugin, name, commands } => {+ let picked = crate::menus::submenu(ui, &format!("Plugins/plugin:{plugin}"), name, t, |ui| {+ commands.iter().find(|(_, label)| crate::menus::item(ui, label, "", true, false, t)).map(|(id, _)| id.to_string()) }); if let Some(c) = picked {- chosen = Some((m.id.clone(), c));+ chosen = Some((plugin.to_string(), c)); } } } }- let widgets: Vec<(&str, &str)> =- app.session.prefs.plugins.iter().filter(|p| p.manifest.widget).map(|p| (p.manifest.id.as_str(), p.manifest.name.as_str())).collect(); let widget = (!widgets.is_empty()) .then(|| { crate::menus::submenu(ui, "Plugins/widgets", tr("Widgets"), t, |ui| {@@ -171,7 +202,7 @@ pub fn menu(app: &mut FigApp, ui: &mut Ui, t: &Tokens) -> bool { .flatten(); crate::menus::separator(ui); if let Some(plugin) = widget {- app.run("widget.insert", json!({"plugin": plugin}));+ insert_widget(app, &plugin); return true; } let Some((id, command)) = chosen else { return false };