From 12f8a77a00b6d37b691df251073a4ef1e352a6e3 Mon Sep 17 00:00:00 2001 From: rafi612 Date: Sat, 10 Oct 2026 14:07:33 +0200 Subject: [PATCH 1/4] =?UTF-8?q?User=20Interface=20=E2=80=BA=20System=20Tit?= =?UTF-8?q?le=20Bar:=20native=20decorations=20on=20Windows/Linux,=20file?= =?UTF-8?q?=20in=20OS=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vectorcraft/src/main.rs | 67 ++++++++++++---- crates/engine/src/cmd/prefscmds.rs | 1 + crates/engine/src/lib.rs | 5 ++ crates/ui-egui/src/brand.rs | 9 +++ crates/ui-egui/src/chrome.rs | 119 +++++++++++++++++++++++++++- crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/lib.rs | 7 ++ crates/ui-egui/src/prefs_dialog.rs | 36 +++++++++ 17 files changed, 243 insertions(+), 21 deletions(-) diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 915daadd..0ce3d74f 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -400,6 +400,32 @@ fn adapter_summary(info: &eframe::wgpu::AdapterInfo) -> String { /// macOS keeps its traffic lights over the integrated title strip. const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos"); +/// Whether this start draws its own title bar: Windows and Linux do, unless Preferences › +/// User Interface › System Title Bar asks for the system's. Read from the saved preferences +/// before the window opens; a missing or unreadable value keeps the default. +fn custom_titlebar(saved: Option<&vectorcraft_ui_egui::UiState>) -> bool { + CUSTOM_TITLEBAR && !saved.map(|ui| ui.engine_prefs.get("systemTitleBar").and_then(serde_json::Value::as_bool).unwrap_or(false)).unwrap_or(false) +} + +fn native_options(custom_titlebar: bool) -> eframe::NativeOptions { + eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("VectorCraft") + .with_inner_size(window::DEFAULT_SIZE) + .with_min_inner_size(window::MIN_SIZE) + .with_drag_and_drop(true) + .with_decorations(!custom_titlebar) + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false) + .with_icon(app_icon()) + .with_app_id("ai.storyteller.vectorcraft"), + #[cfg(feature = "windows7")] + renderer: eframe::Renderer::Glow, + ..Default::default() + } +} + fn main() -> std::process::ExitCode { // First, so every start-up warning is recorded (`logging`). let logger = logging::install(); @@ -486,22 +512,8 @@ fn main() -> std::process::ExitCode { } #[cfg(feature = "wgpu")] let startup = std::sync::Arc::new(gpu::Startup::default()); - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_title("VectorCraft") - .with_inner_size(window::DEFAULT_SIZE) - .with_min_inner_size(window::MIN_SIZE) - .with_drag_and_drop(true) - .with_decorations(!CUSTOM_TITLEBAR) - .with_fullsize_content_view(true) - .with_titlebar_shown(false) - .with_title_shown(false) - .with_icon(app_icon()) - .with_app_id("ai.storyteller.vectorcraft"), - #[cfg(feature = "windows7")] - renderer: eframe::Renderer::Glow, - ..Default::default() - }; + let custom_titlebar = custom_titlebar(saved.as_ref()); + let options = native_options(custom_titlebar); #[cfg(feature = "wgpu")] let options = { let mut options = options; @@ -575,7 +587,7 @@ fn main() -> std::process::ExitCode { { app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into()); } - app.custom_titlebar = CUSTOM_TITLEBAR; + app.custom_titlebar = custom_titlebar; if let Some(port) = control_port { let rx = control_server::start(port, cc.egui_ctx.clone()); app = app.with_control(rx).with_automation_roots(roots.clone()); @@ -618,6 +630,27 @@ fn main() -> std::process::ExitCode { mod tests { use super::*; + fn ui_state_with_prefs(prefs: serde_json::Value) -> vectorcraft_ui_egui::UiState { + let mut ui = vectorcraft_ui_egui::UiState::default(); + ui.engine_prefs = prefs; + ui + } + + #[test] + fn the_system_title_bar_preference_keeps_the_window_decorations() { + // Preferences › User Interface › System Title Bar gives the window back its system + // decorations on Windows and Linux; macOS always has them. + assert_eq!(super::custom_titlebar(None), super::CUSTOM_TITLEBAR, "no preferences: the default"); + let off = ui_state_with_prefs(serde_json::json!({"systemTitleBar": false})); + assert_eq!(super::custom_titlebar(Some(&off)), super::CUSTOM_TITLEBAR); + let on = ui_state_with_prefs(serde_json::json!({"systemTitleBar": true})); + assert!(!super::custom_titlebar(Some(&on))); + assert_eq!(super::native_options(false).viewport.decorations, Some(true)); + if super::CUSTOM_TITLEBAR { + assert_eq!(super::native_options(true).viewport.decorations, Some(false)); + } + } + /// The file extensions the macOS bundle declares: its document types and its own exported type. fn plist_extensions(plist: &str) -> Vec<&str> { ["CFBundleTypeExtensions", "public.filename-extension"] diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index d1562c5e..0c764faa 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -282,6 +282,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool), // `auto` or a language code the shell registers (`zh-hant`); the shell shows it as a dropdown. p!("interfaceLanguage", "User Interface", "Language", "Language", text), + p!("systemTitleBar", "User Interface", "", "System Title Bar", bool), // Performance p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool), p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index ec72f1ee..8a964610 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -512,6 +512,10 @@ pub struct Prefs { /// UI language: `auto` (follow the system locale) or a language code such as `en`, `zh-hant`. /// The list of languages belongs to the shell (`ui-egui` i18n); an unknown code reads as `auto`. pub interface_language: String, + /// Windows and Linux: use the system's title bar and window buttons instead of the app bar + /// acting as the title bar (tiling window managers, desktops that draw their own decorations). + /// Read when the app starts. macOS always uses the system's. + pub system_title_bar: bool, // Performance pub gpu_performance: bool, pub animated_zoom: bool, @@ -726,6 +730,7 @@ impl Default for Prefs { ui_scaling: 1.0, scale_cursor_with_ui: false, interface_language: s("auto"), + system_title_bar: false, gpu_performance: true, animated_zoom: true, gpu_preference: s("automatic"), diff --git a/crates/ui-egui/src/brand.rs b/crates/ui-egui/src/brand.rs index 1d88dfb2..0a528569 100644 --- a/crates/ui-egui/src/brand.rs +++ b/crates/ui-egui/src/brand.rs @@ -36,6 +36,12 @@ pub fn paint_mark(ui: &Ui, r: Rect) { ui.painter().image(texture(ui.ctx()).id(), r, uv, Color32::WHITE); } +/// The mark's texture id (tests: find the painted marks among the shapes). +#[cfg(test)] +pub fn texture_id(ctx: &Context) -> egui::TextureId { + texture(ctx).id() +} + #[cfg(test)] mod tests { use super::*; @@ -55,6 +61,9 @@ mod tests { let ctx = Context::default(); crate::theme::install_fonts(&ctx); let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + // The system title bar hides the in-app mark (the OS draws its own icon), so paint the + // app-drawn bar here. + app.custom_titlebar = true; let mut frame = || { let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::chrome::app_bar(&mut app, ui)); let uploads = out.textures_delta.set.values().flat_map(|d| d.iter()).filter(|d| d.image.size() == [128, 128]).count(); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index be7d3f3c..653f24fb 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -18,9 +18,12 @@ pub const APP_BAR_HEIGHT: f32 = 44.0; /// The application bar: brand mark, Home, menus, then Discord, search and the workspace switcher /// at the right. With [`VectorcraftApp::custom_titlebar`] it is also the window's title bar /// ([`titlebar`]): the caption buttons take the right end and the rest of the bar drags the window. +/// With the system title bar (Windows and Linux, `system_title_bar`) the OS draws its own icon and +/// title, so the in-app bar shows Home, menus and workspace controls only: no brand mark. pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let custom = app.custom_titlebar; + let system = system_title_bar(app); let left = app.titlebar_inset.clamp(8.0, f32::from(i8::MAX)) as i8; let frame = egui::Frame::NONE.fill(t.app_bar).inner_margin(egui::Margin { left, right: if custom { 0 } else { 14 }, top: 0, bottom: 0 }); let bar = egui::Panel::top("app_bar").exact_size(APP_BAR_HEIGHT).frame(frame.stroke(Stroke::new(1.0, t.border))).show(ui, |ui| { @@ -28,10 +31,12 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { titlebar::drag_area(ui, ui.max_rect()); } ui.horizontal_centered(|ui| { - // Brand mark: the app icon. - let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); - crate::brand::paint_mark(ui, r); - ui.add_space(4.0); + // With the system title bar the OS draws its own icon: no in-app brand mark. + if !system { + let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); + crate::brand::paint_mark(ui, r); + ui.add_space(4.0); + } let on_home = menus::home_showing(app); if widgets::icon_button(ui, "house", tl!("Home"), on_home, 24.0).clicked() { app.run("app.home", json!({})).ok(); @@ -109,6 +114,35 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { } } +/// Whether the window shows the system's title bar instead of the app drawing its own: Windows +/// and Linux with Preferences › User Interface › System Title Bar. macOS always has system +/// decorations, but keeps the in-app brand mark, so it never counts as system mode here; nor +/// does the web build (the browser tab has no document title), nor an embedder that never +/// turned the preference on. +pub fn system_title_bar(app: &VectorcraftApp) -> bool { + !app.custom_titlebar && app.session.prefs.system_title_bar && !cfg!(any(target_os = "macos", target_arch = "wasm32")) +} + +/// The OS window title: the active document's name suffixed with the app name (a `*` prefix +/// marks unsaved changes, like the `*` in the document tabs), or just the app name with no +/// document open. +pub fn window_title(app: &VectorcraftApp) -> String { + app.session + .active() + .map(|d| format!("{}{} \u{2014} VectorCraft", if d.is_dirty() { "*" } else { "" }, d.title())) + .unwrap_or_else(|| "VectorCraft".into()) +} + +/// Keep the OS window title (and the taskbar / Alt-Tab entry) on the active file. Sends +/// `ViewportCommand::Title` only when the title changed since the last frame. +pub fn sync_window_title(app: &mut VectorcraftApp, ctx: &egui::Context) { + let want = window_title(app); + if app.last_window_title != want { + app.last_window_title = want.clone(); + ctx.send_viewport_cmd(egui::ViewportCommand::Title(want)); + } +} + /// An anchor button: icon, tooltip, command and the convert command's `to`. type AnchorButton<'a> = (&'a str, &'a str, &'a str, Option<&'a str>); @@ -965,4 +999,81 @@ mod tests { let off = hint(&mut app); assert!(off.contains(" to zoom into an area") && !off.contains("Hold"), "{off}"); } + + fn title_test_app(custom_titlebar: bool) -> crate::VectorcraftApp { + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.custom_titlebar = custom_titlebar; + // The preference that makes the shell launch without its own title bar. + app.run("prefs.set", json!({"key": "systemTitleBar", "value": !custom_titlebar})).unwrap(); + app + } + + #[test] + fn without_the_preference_the_mark_stays() { + // The default (e.g. the web build): no custom bar, no preference. + let app = crate::VectorcraftApp::new(Session::new(), Default::default()); + assert!(!super::system_title_bar(&app)); + } + + #[test] + fn window_title_with_no_document_is_the_app_name() { + assert_eq!(super::window_title(&title_test_app(true)), "VectorCraft"); + } + + #[test] + fn window_title_follows_the_active_file() { + let mut app = title_test_app(true); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + assert_eq!(super::window_title(&app), "foo.svg \u{2014} VectorCraft"); + // Unsaved changes gain a `*` prefix, like the `*` in the document tabs. + app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + assert_eq!(super::window_title(&app), "*foo.svg \u{2014} VectorCraft"); + } + + #[test] + fn system_mode_hides_the_mark() { + for custom in [true, false] { + let mut app = title_test_app(custom); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| super::app_bar(&mut app, ui)); + let uploads = out.textures_delta.set.values().flat_map(|d| d.iter()).filter(|d| d.image.size() == [128, 128]).count(); + out.textures_delta.clear(); + let marks: Vec = out + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Mesh(m) if m.texture_id == crate::brand::texture_id(&ctx) => Some(m.calc_bounds()), + _ => None, + }) + .collect(); + let system = super::system_title_bar(&app); + assert_eq!(system, !custom && !cfg!(target_os = "macos")); + assert_eq!(marks.is_empty(), system, "mark in system mode (custom={custom})"); + assert_eq!(uploads, if system { 0 } else { 1 }, "mark texture in system mode (custom={custom})"); + } + } + + #[test] + fn sync_window_title_sends_only_on_change() { + let mut app = title_test_app(true); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + super::sync_window_title(&mut app, &ctx); + assert_eq!(app.last_window_title, "VectorCraft"); + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + let cmds = out.viewport_output.remove(&egui::ViewportId::ROOT).map(|v| v.commands).unwrap_or_default(); + assert!(cmds.iter().any(|c| matches!(c, egui::ViewportCommand::Title(t) if t == "VectorCraft")), "{cmds:?}"); + // Same document state again: the cached title stops a repeat command. + super::sync_window_title(&mut app, &ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + let cmds = out.viewport_output.remove(&egui::ViewportId::ROOT).map(|v| v.commands).unwrap_or_default(); + assert!(!cmds.iter().any(|c| matches!(c, egui::ViewportCommand::Title(_))), "repeat Title: {cmds:?}"); + app.run("file.new", json!({"width": 100, "height": 100, "name": "foo.svg"})).unwrap(); + super::sync_window_title(&mut app, &ctx); + assert_eq!(app.last_window_title, "foo.svg \u{2014} VectorCraft"); + } } diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 06aa8084..065bda2a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -673,3 +673,5 @@ Patchwork… Patchwork… Stained Glass… Vitráž… Texturizer… Texturovač… + System Title Bar Systémové záhlaví okna + Applies at next launch. Projeví se při příštím spuštění. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f703db8b..f4927b5e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3975,3 +3975,5 @@ axis Both Ambos Top Right Arriba a la derecha Bottom Right Abajo a la derecha Edge Borde + System Title Bar Barra de título del sistema + Applies at next launch. Se aplicará en el próximo inicio. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 04728393..6dc963a7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3987,3 +3987,5 @@ axis Both Les deux Bottom Right En bas à droite effect Soft Doux Edge Bord + System Title Bar Barre de titre du système + Applies at next launch. Prend effet au prochain démarrage. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 975c1a03..bbb37c63 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3977,3 +3977,5 @@ axis Both Entrambi effect Soft Morbido effect Regular Normale Edge Bordo + System Title Bar Barra del titolo di sistema + Applies at next launch. Si applica al prossimo avvio. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 93697179..40a9fe66 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3970,3 +3970,5 @@ effect Soft ソフト effect Horizontal 水平 effect Vertical 垂直 Edge エッジ + System Title Bar システムのタイトルバー + Applies at next launch. 次回の起動時に適用されます。 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6b1ef85a..182b9d5e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2733,3 +2733,5 @@ Patchwork… Colcha de retalhos… Stained Glass… Vitral… Texturizer… Texturizador… + System Title Bar Barra de título do sistema + Applies at next launch. Aplica-se na próxima inicialização. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 83b26c87..3c04c962 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3973,3 +3973,5 @@ axis Both Оба Top Right Сверху справа Bottom Right Снизу справа Edge Край + System Title Bar Системная строка заголовка + Applies at next launch. Применится при следующем запуске. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5dab363e..34afec8f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3958,3 +3958,5 @@ axis Both Обидві Top Right Вгорі праворуч Bottom Right Знизу праворуч Edge Край + System Title Bar Системний рядок заголовка + Applies at next launch. Набуде чинності після перезапуску. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b4ec898e..1dd56e99 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3606,3 +3606,5 @@ effect Soft 柔和 effect Horizontal 水平 effect Vertical 垂直 Edge 边缘 + System Title Bar 系统标题栏 + Applies at next launch. 下次启动时生效。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 5101d723..70696085 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3387,3 +3387,5 @@ axis Both 兩者 Bottom Right 右下 effect Soft 柔和 Edge 邊緣 + System Title Bar 系統標題列 + Applies at next launch. 下次啟動時套用。 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 6a4c67e6..d82fbbde 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -353,6 +353,9 @@ pub struct VectorcraftApp { /// Windows and Linux: the window has no OS decorations, so the app bar is the title bar (drag, /// double-click to maximize, caption buttons) and invisible edge zones resize the window. pub custom_titlebar: bool, + /// Last window title sent to the OS (`ViewportCommand::Title`, see `chrome::sync_window_title`): + /// sent again only when it changes, so idle frames don't spam the backend. + pub(crate) last_window_title: String, /// The graphics adapter the window renders with ("name (backend)"), as the host reports it: /// shown in Help › About and `ui.inspect` for GPU bug reports. `None` when unknown. pub graphics_adapter: Option, @@ -462,6 +465,7 @@ impl VectorcraftApp { canvas_rect: None, hover_doc: None, custom_titlebar: false, + last_window_title: String::new(), graphics_adapter: None, place: Default::default(), system_paste: false, @@ -958,6 +962,9 @@ impl VectorcraftApp { } shortcut_editor::sync(&self.ui); prefs_dialog::apply_runtime(self, ctx); + // The OS title bar (and the taskbar / Alt-Tab entry) follows the active file; with the + // system title bar this is where the document name lives, as the in-app mark is hidden. + crate::chrome::sync_window_title(self, ctx); self.drain_control(ctx); if !self.synthetic.is_empty() { ctx.request_repaint(); diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index da7595de..72b649a5 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -261,6 +261,11 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { PrefKind::Bool if sp.key == "animatedZoom" => { ui.add_enabled_ui(d.bool("gpuPerformance"), |ui| bool_row(ui, d, sp.key, sp.label)); } + PrefKind::Bool if sp.key == "systemTitleBar" && cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) => { + bool_row(ui, d, sp.key, sp.label); + // The window's decorations are chosen once, when the app starts. + ui.label(egui::RichText::new(tl!("Applies at next launch.")).color(t.text_dim).size(11.0)); + } PrefKind::Bool => bool_row(ui, d, sp.key, sp.label), PrefKind::Num { min, max, unit } => { labeled(ui, sp.label, |ui| { @@ -530,6 +535,37 @@ mod tests { assert_eq!(saved["engine_prefs"]["gpuPreference"], json!("highPerformance")); } + /// User Interface › System Title Bar: shown with its next-launch note, applied by OK and + /// saved with the UI state under the key the desktop app reads before the window opens. + #[test] + fn system_title_bar_preference_shows_and_persists() { + fn texts(s: &egui::Shape, out: &mut Vec) { + 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 a = app(); + open(&mut a, Some("User Interface")); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + // The window sizes itself on the first frame and draws on the second. + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx())); + out.textures_delta.clear(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx())); + out.textures_delta.clear(); + let mut shown = vec![]; + out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown)); + assert!(shown.iter().any(|t| t == "System Title Bar"), "{shown:?}"); + assert!(shown.iter().any(|t| t == "Applies at next launch."), "{shown:?}"); + a.ui.dialog.as_mut().unwrap().fields.insert("systemTitleBar".into(), json!(true)); + confirm(&mut a).unwrap(); + assert!(a.session.prefs.system_title_bar); + let saved: Value = serde_json::from_slice(&serde_json::to_vec(&a.ui).unwrap()).unwrap(); + assert_eq!(saved["engine_prefs"]["systemTitleBar"], json!(true)); + } + /// Units › Numbers Without Units Are Points (#394): the preference reaches the fields when it /// changes, and its checkbox is dimmed unless a unit is Picas. #[test] From bd895a1de718605d3f4126ada59a6027810d3c23 Mon Sep 17 00:00:00 2001 From: rafi612 Date: Sat, 10 Oct 2026 14:46:33 +0200 Subject: [PATCH 2/4] Remove the in-window VectorCraft menu (File first, Exit quits, Mac unchanged) --- crates/ui-egui/src/i18n/cs.tsv | 2 +- crates/ui-egui/src/i18n/pt-br.tsv | 2 +- crates/ui-egui/src/i18n/tests.rs | 22 +++++-- crates/ui-egui/src/menus.rs | 91 ++++++++++++++++++-------- crates/ui-egui/src/native_menu.rs | 59 +++++++++++++++-- crates/ui-egui/src/tests_nativemenu.rs | 8 ++- crates/ui-egui/src/titlebar.rs | 2 +- docs/development.md | 2 +- 8 files changed, 140 insertions(+), 48 deletions(-) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 065bda2a..dc1cafa7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 182b9d5e..b409f420 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 9283d1ed..2679903c 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -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 = - 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) { + 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()); } diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 89f3abff..486dfa7b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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)> { menu_tree_named(true) } @@ -1936,24 +1938,13 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { 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)> { 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)> = 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) { + 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| { 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] diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs index aeb12f3c..cb2c1330 100644 --- a/crates/ui-egui/src/native_menu.rs +++ b/crates/ui-egui/src/native_menu.rs @@ -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 { @@ -413,7 +458,7 @@ fn take_submenu(bar: &mut MenuBar, command: &str) -> Option { /// 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 diff --git a/crates/ui-egui/src/tests_nativemenu.rs b/crates/ui-egui/src/tests_nativemenu.rs index afcca1ad..d2e6546c 100644 --- a/crates/ui-egui/src/tests_nativemenu.rs +++ b/crates/ui-egui/src/tests_nativemenu.rs @@ -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); diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs index 13e70dfe..d9b9f11c 100644 --- a/crates/ui-egui/src/titlebar.rs +++ b/crates/ui-egui/src/titlebar.rs @@ -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..] { diff --git a/docs/development.md b/docs/development.md index 09abdcc1..45db7d6b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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`. From 2e643b0e7601419279ebb1175b41f40cb941fb3f Mon Sep 17 00:00:00 2001 From: rafi612 Date: Sat, 10 Oct 2026 15:26:27 +0200 Subject: [PATCH 3/4] Make the menu bar more compact --- crates/ui-egui/src/brand.rs | 4 +-- crates/ui-egui/src/chrome.rs | 66 +++++++++++++++------------------- crates/ui-egui/src/titlebar.rs | 12 ++++--- 3 files changed, 37 insertions(+), 45 deletions(-) diff --git a/crates/ui-egui/src/brand.rs b/crates/ui-egui/src/brand.rs index 0a528569..461af137 100644 --- a/crates/ui-egui/src/brand.rs +++ b/crates/ui-egui/src/brand.rs @@ -1,6 +1,6 @@ //! The VectorCraft brand mark: the app icon (the engraved dragon, `assets/app-icon/`, see its //! README). The PNG carries the icon's rounded corners; it is decoded once per context into a -//! mipmapped texture, so it stays crisp from the 22 pt app-bar mark to the About box. +//! mipmapped texture, so it stays crisp from the 18 pt app-bar mark to the About box. use egui::{Color32, Context, Id, Rect, TextureHandle, TextureOptions, Ui, pos2}; @@ -81,7 +81,7 @@ mod tests { let (uploads, marks) = frame(); assert_eq!(uploads, 1); assert_eq!(marks.len(), 1); - assert_eq!(marks[0].size(), egui::vec2(22.0, 22.0)); + assert_eq!(marks[0].size(), egui::vec2(18.0, 18.0)); // Later frames reuse the texture. let (uploads, marks) = frame(); assert_eq!((uploads, marks.len()), (0, 1)); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 653f24fb..a8c8b15e 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; -use egui::{CornerRadius, Sense, Stroke, StrokeKind, Ui, vec2}; +use egui::{CornerRadius, Sense, Stroke, Ui, vec2}; use serde_json::json; use vectorcraft_doc::NodeKind; @@ -12,8 +12,12 @@ use crate::theme::{self, Tokens}; use crate::widgets; use crate::{VectorcraftApp, icons, menus, titlebar}; -/// The application bar's height, in points. -pub const APP_BAR_HEIGHT: f32 = 44.0; +/// The application bar's height, in points (compact, as in PhotoCraft). +pub const APP_BAR_HEIGHT: f32 = 32.0; +/// The brand mark's side. +const MARK: f32 = 18.0; +/// Height of the workspace switcher. +const WIDGET_H: f32 = 22.0; /// The application bar: brand mark, Home, menus, then Discord, search and the workspace switcher /// at the right. With [`VectorcraftApp::custom_titlebar`] it is also the window's title bar @@ -33,12 +37,12 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal_centered(|ui| { // With the system title bar the OS draws its own icon: no in-app brand mark. if !system { - let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); + let (r, _) = ui.allocate_exact_size(vec2(MARK, MARK), Sense::hover()); crate::brand::paint_mark(ui, r); - ui.add_space(4.0); + ui.add_space(6.0); } let on_home = menus::home_showing(app); - if widgets::icon_button(ui, "house", tl!("Home"), on_home, 24.0).clicked() { + if widgets::icon_button(ui, "house", tl!("Home"), on_home, 20.0).clicked() { app.run("app.home", json!({})).ok(); } ui.add_space(2.0); @@ -51,8 +55,8 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { menus::menu_bar(app, ui) }; // The right-side group fills the space after the menus from the right; when it runs - // short, Discord goes first (it is also under Help), then the search box becomes an - // icon, then the workspace switcher narrows. + // short, Discord goes first (it is also under Help), then the search icon (the palette + // stays under its shortcut), then the workspace switcher narrows. let full = ui.max_rect(); let right_edge = if custom { full.right() - titlebar::WIDTH - 10.0 } else { full.right() }; let right = egui::Rect::from_min_max(egui::pos2(menus_end + 8.0, full.top()), egui::pos2(right_edge, full.bottom())); @@ -63,45 +67,31 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let ws = ui.painter().layout_no_wrap(ws_name, egui::FontId::proportional(12.0), t.text); let ws_w = (ws.size().x + 36.0).clamp(112.0, 190.0); let gap = ui.spacing().item_spacing.x; - let with_search = ws_w + 8.0 + gap + 200.0; - let search_full = room >= with_search; - let discord = room >= with_search + 10.0 + gap + crate::community::discord_width(ui, false); - let ws_w = if search_full { ws_w } else { ws_w.min(room - 8.0 - gap - 24.0).max(64.0) }; + // The search icon, as in PhotoCraft: shown while a minimal switcher fits beside it. + let icon = 28.0; + let search = room >= 64.0 + 8.0 + gap + icon; + let ws_w = ws_w.min(room - 8.0 - gap - if search { icon } else { 0.0 }).max(64.0); + let discord = search && room >= ws_w + 8.0 + gap + icon + 10.0 + gap + crate::community::discord_width(ui, false); let mut rui = ui.new_child(egui::UiBuilder::new().max_rect(right).layout(egui::Layout::right_to_left(egui::Align::Center))); let ui = &mut rui; // Workspace switcher: shows the current workspace, opens Window → Workspace. - let (wr, wresp) = ui.allocate_exact_size(vec2(ws_w, 24.0), Sense::click()); + let (wr, wresp) = ui.allocate_exact_size(vec2(ws_w, WIDGET_H), Sense::click()); ui.painter().rect_filled(wr, CornerRadius::same(4), if wresp.hovered() { t.hover } else { t.panel }); ui.painter().with_clip_rect(wr.shrink2(vec2(4.0, 0.0))).galley(wr.left_center() + vec2(10.0, -ws.size().y / 2.0), ws, t.text); icons::paint(ui, "chevron-down", egui::Rect::from_center_size(wr.right_center() - vec2(12.0, 0.0), vec2(12.0, 12.0)), t.text_dim); let wresp = wresp.on_hover_text(tl!("Switch workspace")); egui::Popup::menu(&wresp).show(|ui| crate::workspaces::popup(app, ui)); ui.add_space(8.0); - // Search box → command palette. - let open_palette = if search_full { - let (r, resp) = ui.allocate_exact_size(vec2(200.0, 24.0), Sense::click()); - ui.painter().rect_filled(r, CornerRadius::same(12), t.input); - ui.painter().rect_stroke( - r, - CornerRadius::same(12), - Stroke::new(1.0, if resp.hovered() { t.input_border } else { t.divider }), - StrokeKind::Inside, - ); - icons::paint(ui, "search", egui::Rect::from_center_size(r.left_center() + vec2(14.0, 0.0), vec2(13.0, 13.0)), t.text_dim); - ui.painter().text( - r.left_center() + vec2(26.0, 0.0), - egui::Align2::LEFT_CENTER, - tl!("Search commands and tools"), - egui::FontId::proportional(11.5), - t.text_dim, - ); - resp.clicked() - } else { - widgets::icon_button(ui, "search", tl!("Search commands and tools"), false, 24.0).clicked() - }; - if open_palette { - app.ui.palette_open = true; - app.ui.palette_query.clear(); + // Search → command palette (its shortcut opens it too). + if search { + let tip = match menus::shortcut_of("help.commandPalette") { + Some(sc) => format!("{} ({})", tl!("Search commands and tools"), menus::pretty_shortcut(sc)), + None => tl!("Search commands and tools").to_string(), + }; + if widgets::icon_button(ui, "search", &tip, app.ui.palette_open, icon).clicked() { + app.ui.palette_open = !app.ui.palette_open; + app.ui.palette_query.clear(); + } } if discord { ui.add_space(10.0); diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs index d9b9f11c..79b00c53 100644 --- a/crates/ui-egui/src/titlebar.rs +++ b/crates/ui-egui/src/titlebar.rs @@ -248,7 +248,7 @@ mod tests { .layers() .filter(|(l, _)| l.order != Order::Foreground) .flat_map(|(_, ws)| ws.iter()) - .filter(|w| w.sense.senses_click() && w.rect.top() < 44.0 && !skip.contains(&w.id)) + .filter(|w| w.sense.senses_click() && w.rect.top() < 32.0 && !skip.contains(&w.id)) .map(|w| w.rect) .collect() }) @@ -263,7 +263,7 @@ mod tests { let out = w.frame(vec![]); let [min, max, close] = CAPTIONS.map(|c| w.caption(c)); assert_eq!((close.right(), close.top()), (width, 0.0), "Close sits in the window's corner at {width}"); - assert!(close.height() >= 42.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH)); + assert!(close.height() >= 30.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() >= 11, "home, eight menus, workspace switcher, search: {widgets:?}"); @@ -273,10 +273,12 @@ mod tests { assert!(!a.shrink(0.5).intersects(b.shrink(0.5)), "{a:?} overlaps {b:?} at {width}"); } } - // Wide windows show the whole group; narrow ones drop Discord and shorten the search. + // Wide windows show the whole group; narrow ones drop Discord, then the search icon. let texts: Vec = texts(&out).into_iter().map(|(t, _)| t).collect(); let has = |s: &str| texts.iter().any(|t| t == s); - assert_eq!((has("Discord"), has("Search commands and tools")), (width > 1000.0, width > 1000.0), "{width}: {texts:?}"); + assert_eq!(has("Discord"), width > 1000.0, "{width}: {texts:?}"); + // Search is an icon only (as in PhotoCraft): its label lives in the tooltip. + assert!(!has("Search commands and tools"), "{width}: {texts:?}"); } } @@ -314,7 +316,7 @@ mod tests { fn empty_bar_space_drags_and_double_click_maximizes() { let mut w = Win::new(1440.0); w.frame(vec![]); - let empty = pos2(700.0, 22.0); + let empty = pos2(700.0, 16.0); assert!(w.bar_widgets().iter().all(|r| !r.contains(empty)), "the probe point must be empty bar space"); let cmds = w.gesture(&[empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)]); assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}"); From 6315086f96dd37292055fae8d9d76d0da40510c2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 21:01:01 +0700 Subject: [PATCH 4/4] Review: System Title Bar only where it applies, docs, title repaint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Preferences hides System Title Bar on macOS and the web, where it did nothing (it fell through to the plain checkbox row), and lists it with the window options instead of under Language. - The next-launch note reuses "Applies the next time VectorCraft starts." instead of a second wording in every catalog. - A new window title asks for one more frame: AppKit lays the title bar out again for it, and the next frame centres the window buttons (#968). - The menu bar drops the bold branch for the VectorCraft menu it no longer shows; theme is imported where the tests use it. - Docs: Language is Preferences › User Interface › Language (VectorCraft › Language on macOS); the control protocol describes System Title Bar; ROADMAP Last updated. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 6 +++--- apps/vectorcraft/src/main.rs | 6 ++---- crates/engine/src/cmd/prefscmds.rs | 2 +- crates/ui-egui/src/chrome.rs | 3 +++ crates/ui-egui/src/i18n/cs.tsv | 1 - crates/ui-egui/src/i18n/es.tsv | 1 - crates/ui-egui/src/i18n/fr.tsv | 1 - crates/ui-egui/src/i18n/it.tsv | 1 - crates/ui-egui/src/i18n/ja.tsv | 1 - crates/ui-egui/src/i18n/pt-br.tsv | 1 - crates/ui-egui/src/i18n/ru.tsv | 1 - crates/ui-egui/src/i18n/uk.tsv | 1 - crates/ui-egui/src/i18n/zh-hans.tsv | 1 - crates/ui-egui/src/i18n/zh-hant.tsv | 1 - crates/ui-egui/src/menus.rs | 9 +++------ crates/ui-egui/src/prefs_dialog.rs | 13 ++++++++----- docs/control-protocol.md | 6 ++++-- docs/development.md | 6 +++--- docs/mcp.md | 2 +- 19 files changed, 28 insertions(+), 35 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 730af72c..87f14118 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372), file dialogs on Linux shown off the UI thread so the compositor no longer finds the window not responding, and parented to the window on Linux and Windows (#592), anchors and handle ends picked from 2 px past their drawn size before the segments through them (#593), an image copied in a browser pasting as the picture, not its address (#597), the Free Transform widget with Constrain, Free Transform, Perspective Distort and Free Distort (#598), touch gestures: a two-finger tap undoes and a three-finger tap redoes (#585), the font menus' live preview staying on the text instead of flickering on and off every frame (#563), Edit Artboards showing the active artboard in the Properties panel with New Artboard, Delete Artboard and Exit (#530), the same DirectWrite fonts in `vectorcraft-cli` and its MCP server (#579), SVG layers keeping their names when wrapped and sublayers round-tripping through SVG (#372), a Layers panel that lays out only the rows in view and keeps a document's open rows in its file (#638), a panel dropped on the dock lights up the icon column it goes back to and pops out of its icon (#649), Image Trace's View modes and an Advanced section that stays as it was left (part of #641), Image Trace's Palette: Limited, Full Tone, Automatic and Document Library (part of #641), Image Trace's Create Strokes: line art traced as stroked centre lines (part of #641), a dual-GPU Mac falling back to its other GPU when one fails to start (#651), two fingers dragged on a touch screen panning the canvas (part of #449), the Home screen's Recent Files and a double-clicked New Document preset creating the document (part of #663), quitting no longer crashing on Macs with a Touch Bar (#661), Animated Zoom, Large Tabs, Scale Cursor Proportional to UI and anti-aliased bitmaps in Pixel Preview acting (part of #394), Smart Guides › Spacing Guides (part of #394), Edit Artboards' Properties choosing an artboard's size preset and flipping it between portrait and landscape (#671), Preferences › Type › Additional Fonts Folder loading the fonts of a folder you choose (#683), Canvas Color's light, medium and dark grey (part of #663), a document tab's close box after its title on Windows and Linux, before it on macOS (part of #673), Rearrange All Artboards' layouts (a grid by row or by column, one row, one column) and right-to-left order, one click away in the Properties and Artboards panels (#681), User Interface › Show Tool Group Labels and Cmd+, opening Settings on the Mac (part of #663), the Windows taskbar and Alt+Tab showing VectorCraft's icon, a click with a selection tool making its artboard the active one, and Paste in Place, in Front and in Back pasting onto the active artboard (#693), Photoshop-style raster effects on a pixel-filter pipeline: Effect › Blur › Radial Blur and Smart Blur, Effect › Sharpen › Unsharp Mask (part of gap 2), the Properties panel's number fields all one width (#696), the rotate cursor drawn as a curved double arrow (#701), the Templates folder in the desktop's own documents folder, made when missing (#703), PDF and .ai glyphs whose ToUnicode names no character (U+FFFD) taking their character from the embedded font or keeping their outlines, with a warning (#708), Illustrator EPS and `.ai` files opening from their editing data with their colours, gradients, transparency, images, names and every artboard (part of #637), `.ai` files saved without PDF compatibility opening from their editing data (part of #637), guides left out of raster exports and Fit to Artwork Bounds, hidden art left out of Fit to Artwork Bounds, Illustrator files compared with their page object by object, and `editingData: false` opening only what the page prints (#735), art reaching past a PDF's page clipped to its crop box (#675), Package collecting what placed documents link to (#777), area type, threaded type and type on a path made whole from Illustrator files' text documents (part of #637), the missing-fonts dialog with Find Fonts and a folder search (#792), Effect › Stylize › Glowing Edges (#765), Offset Path's Joins and Zig Zag's Points as dropdowns and Relative/Absolute as radio buttons in the effect dialogs (#828), the control channel's `app.quit` reporting a pending save prompt (#830), blurred type that stays put while the view moves (#755), Windows starting on Direct3D 12 and loading no Vulkan driver unless asked (#806), the Artboard tool naming its artboard once (#824), Save for Web taking typed pixel sizes (#839), rulers ticked over their whole length in a rotated view (#826), the gradient stop popover opening beside the art (#862) and recent colours recolouring the selected stop (#835), every align button following Align To (#789), the Properties panel's Paragraph section with all seven alignments and Hyphenate (#775), isolation mode dimming and locking the art around the isolated group (#833), color books (`.acb`) opening as swatch libraries (#831), files that can't be opened reported in a dialog (#861), `run` and `command_batch` steps using earlier steps' results (#785), ruler guides pulling drawn points with Smart Guides on and Smart Guides labelling centres and anchors on hover (#812), shadows and glows keeping their blend modes in PDF (#827) and `.ai` import skipping layer checks that can't succeed (#758), the Curvature tool editing any selected path (#798), raster exports composited in floating point so translucent edges over opaque art stay opaque (#787), Calligraphic and Bristle Brush Options with pen-pressure Calligraphic brushes (part of #852), a polygon's Polygon Properties in the Properties and Transform panels, Flare Tool Options that last and more tools opening their options on a double-click (part of #812), MCP file access confined with --automation-read-root / --automation-write-root (#832), the Transform effect's dialog with the reference point, Random, sliders and an angle dial (part of #885), artboard X/Y edits in the Properties and Transform panels and Artboard Options taking their art along with Move Artwork with Artboard (#922), Effect › Pixelate's Color Halftone, Crystallize, Mezzotint and Pointillize (part of gap 2), linked files changed while the app was in the background following Update Links when its window comes back to the front (#925), the Tabs panel's Leader and Align On fields keeping what is typed until Enter (#924), the Properties panel's Gradient section with the gradient's type and a freeform gradient's Draw (Points or Lines) while the Gradient tool is active (#921), PSD and PSB files opened and placed, linked or embedded, as their merged image (#918), Direct Selection and Group Selection marquees selecting the type, images and other objects without anchors that they touch (#927), PDF and `.ai` glyphs that no mapping names found by their outline in the installed font, so their text stays one type object (#811), Effect › Video's De-Interlace and NTSC Colors (part of gap 2), the Libraries panel's search, groups and library export/import (#926), Live Corner widgets on every selected path, dragged together (#938), Scatter, Art and Pattern Brush Options with Colorization and Scatter values on pen pressure (part of gap 5), artboard names fitting their artboard's width on screen when zoomed out (#949), Effect › Texture's Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer (part of gap 2), Inner Glow's Center/Edge Source as a dropdown (#962), the macOS window buttons centred on the app bar (#968); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372), file dialogs on Linux shown off the UI thread so the compositor no longer finds the window not responding, and parented to the window on Linux and Windows (#592), anchors and handle ends picked from 2 px past their drawn size before the segments through them (#593), an image copied in a browser pasting as the picture, not its address (#597), the Free Transform widget with Constrain, Free Transform, Perspective Distort and Free Distort (#598), touch gestures: a two-finger tap undoes and a three-finger tap redoes (#585), the font menus' live preview staying on the text instead of flickering on and off every frame (#563), Edit Artboards showing the active artboard in the Properties panel with New Artboard, Delete Artboard and Exit (#530), the same DirectWrite fonts in `vectorcraft-cli` and its MCP server (#579), SVG layers keeping their names when wrapped and sublayers round-tripping through SVG (#372), a Layers panel that lays out only the rows in view and keeps a document's open rows in its file (#638), a panel dropped on the dock lights up the icon column it goes back to and pops out of its icon (#649), Image Trace's View modes and an Advanced section that stays as it was left (part of #641), Image Trace's Palette: Limited, Full Tone, Automatic and Document Library (part of #641), Image Trace's Create Strokes: line art traced as stroked centre lines (part of #641), a dual-GPU Mac falling back to its other GPU when one fails to start (#651), two fingers dragged on a touch screen panning the canvas (part of #449), the Home screen's Recent Files and a double-clicked New Document preset creating the document (part of #663), quitting no longer crashing on Macs with a Touch Bar (#661), Animated Zoom, Large Tabs, Scale Cursor Proportional to UI and anti-aliased bitmaps in Pixel Preview acting (part of #394), Smart Guides › Spacing Guides (part of #394), Edit Artboards' Properties choosing an artboard's size preset and flipping it between portrait and landscape (#671), Preferences › Type › Additional Fonts Folder loading the fonts of a folder you choose (#683), Canvas Color's light, medium and dark grey (part of #663), a document tab's close box after its title on Windows and Linux, before it on macOS (part of #673), Rearrange All Artboards' layouts (a grid by row or by column, one row, one column) and right-to-left order, one click away in the Properties and Artboards panels (#681), User Interface › Show Tool Group Labels and Cmd+, opening Settings on the Mac (part of #663), the Windows taskbar and Alt+Tab showing VectorCraft's icon, a click with a selection tool making its artboard the active one, and Paste in Place, in Front and in Back pasting onto the active artboard (#693), Photoshop-style raster effects on a pixel-filter pipeline: Effect › Blur › Radial Blur and Smart Blur, Effect › Sharpen › Unsharp Mask (part of gap 2), the Properties panel's number fields all one width (#696), the rotate cursor drawn as a curved double arrow (#701), the Templates folder in the desktop's own documents folder, made when missing (#703), PDF and .ai glyphs whose ToUnicode names no character (U+FFFD) taking their character from the embedded font or keeping their outlines, with a warning (#708), Illustrator EPS and `.ai` files opening from their editing data with their colours, gradients, transparency, images, names and every artboard (part of #637), `.ai` files saved without PDF compatibility opening from their editing data (part of #637), guides left out of raster exports and Fit to Artwork Bounds, hidden art left out of Fit to Artwork Bounds, Illustrator files compared with their page object by object, and `editingData: false` opening only what the page prints (#735), art reaching past a PDF's page clipped to its crop box (#675), Package collecting what placed documents link to (#777), area type, threaded type and type on a path made whole from Illustrator files' text documents (part of #637), the missing-fonts dialog with Find Fonts and a folder search (#792), Effect › Stylize › Glowing Edges (#765), Offset Path's Joins and Zig Zag's Points as dropdowns and Relative/Absolute as radio buttons in the effect dialogs (#828), the control channel's `app.quit` reporting a pending save prompt (#830), blurred type that stays put while the view moves (#755), Windows starting on Direct3D 12 and loading no Vulkan driver unless asked (#806), the Artboard tool naming its artboard once (#824), Save for Web taking typed pixel sizes (#839), rulers ticked over their whole length in a rotated view (#826), the gradient stop popover opening beside the art (#862) and recent colours recolouring the selected stop (#835), every align button following Align To (#789), the Properties panel's Paragraph section with all seven alignments and Hyphenate (#775), isolation mode dimming and locking the art around the isolated group (#833), color books (`.acb`) opening as swatch libraries (#831), files that can't be opened reported in a dialog (#861), `run` and `command_batch` steps using earlier steps' results (#785), ruler guides pulling drawn points with Smart Guides on and Smart Guides labelling centres and anchors on hover (#812), shadows and glows keeping their blend modes in PDF (#827) and `.ai` import skipping layer checks that can't succeed (#758), the Curvature tool editing any selected path (#798), raster exports composited in floating point so translucent edges over opaque art stay opaque (#787), Calligraphic and Bristle Brush Options with pen-pressure Calligraphic brushes (part of #852), a polygon's Polygon Properties in the Properties and Transform panels, Flare Tool Options that last and more tools opening their options on a double-click (part of #812), MCP file access confined with --automation-read-root / --automation-write-root (#832), the Transform effect's dialog with the reference point, Random, sliders and an angle dial (part of #885), artboard X/Y edits in the Properties and Transform panels and Artboard Options taking their art along with Move Artwork with Artboard (#922), Effect › Pixelate's Color Halftone, Crystallize, Mezzotint and Pointillize (part of gap 2), linked files changed while the app was in the background following Update Links when its window comes back to the front (#925), the Tabs panel's Leader and Align On fields keeping what is typed until Enter (#924), the Properties panel's Gradient section with the gradient's type and a freeform gradient's Draw (Points or Lines) while the Gradient tool is active (#921), PSD and PSB files opened and placed, linked or embedded, as their merged image (#918), Direct Selection and Group Selection marquees selecting the type, images and other objects without anchors that they touch (#927), PDF and `.ai` glyphs that no mapping names found by their outline in the installed font, so their text stays one type object (#811), Effect › Video's De-Interlace and NTSC Colors (part of gap 2), the Libraries panel's search, groups and library export/import (#926), Live Corner widgets on every selected path, dragged together (#938), Scatter, Art and Pattern Brush Options with Colorization and Scatter values on pen pressure (part of gap 5), artboard names fitting their artboard's width on screen when zoomed out (#949), Effect › Texture's Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer (part of gap 2), Inner Glow's Center/Edge Source as a dropdown (#962), the macOS window buttons centred on the app bar (#968), a compact 32-point app bar starting at File (File › Exit quits) with a search icon, and User Interface › System Title Bar on Windows and Linux (#969); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are @@ -114,7 +114,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it. - Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on the dock, which lights up under the pointer, its panels go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating panels and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, docking a panel into a dock group it doesn't belong to, reordering tabs by dragging, collapsing a floating group to icons, resizing floating panels. - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done. - - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Spanish, French, Italian, Russian and Ukrainian ship complete, and Czech and Brazilian Portuguese cover every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a VectorCraft ▸ Language menu and a Language preference. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). + - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Spanish, French, Italian, Russian and Ukrainian ship complete, and Czech and Brazilian Portuguese cover every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). - Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8). - The macOS menu bar is built from the in-window menus (one model, `native_menu`) and laid out as a Mac app's: the VectorCraft menu holds About, Settings ▸ (each Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays Hide Edges), Hide Others, Show All and Quit; Window gains Minimize ⌃⌘M, Zoom and Bring All to Front, and Help the system's search field. Its key equivalents go back to egui as key presses, so user shortcuts, focused text fields and the Type tool's input method behave as in the window, and nothing fires twice; enabled, check marks and labels update in place. `--in-window-menus` or `VECTORCRAFT_IN_WINDOW_MENUS=1` keeps the menus in the window; `ui.inspect` reports `nativeMenuBar`. - macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats. The DMG opens on a designed Finder window, VectorCraft.app beside the `Applications` link over a background with the app icon (#493). @@ -155,7 +155,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart. - Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). - **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). -- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish, French, Italian, Japanese, Russian and Ukrainian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. +- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish, French, Italian, Japanese, Russian and Ukrainian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. - **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `` and PDF soft masks. - **Image Trace Flat Logo mode:** a fourth mode beside Black and White, Grayscale and Color, for flat-colour logos. Each pixel is read as a blend of the logo's colours (found automatically or named), the blend is interpolated smoothly and every outline is placed where two colours are equally present, so edges are accurate to a fraction of a pixel. Outlines are then cleaned: corners the blur rounded are put back, near-straight runs and near-circular arcs become exact lines and arcs, the rest is fitted with as few Béziers as the tolerance allows, touching colours overlap by a hair so no gap opens, and an outline that strays from the raw contour is fitted plainly instead. Gradients and photos are refused with advice to use Color. *Measured* (a local experiment, not run in CI): a flat-colour logo's vector original was rendered down to a soft 199 × 100 px transparent PNG, traced, and the result compared with the original at 1600 px wide. The built-in colour presets (6 Colors, 16 Colors, High Fidelity Photo, Low Fidelity Photo) had a mean edge error of 2.5 to 3.4 px and region overlap of 0.88 to 0.91; Flat Logo had 0.78 px and 0.966 (0.48 px and 0.975 from a crisp source), in about 0.1 s. Limits: flat-colour art only; needle-thin sharp tips are shortened by about 1.7 source pixels because the blur erases them; it works at no more than 1024 px on the long side (larger images are averaged down first); only one logo family has been scored against a vector original. - **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own; filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points or an image over 64 megapixels is refused with advice rather than running out of memory, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 0ce3d74f..4589a2e2 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -630,10 +630,8 @@ fn main() -> std::process::ExitCode { mod tests { use super::*; - fn ui_state_with_prefs(prefs: serde_json::Value) -> vectorcraft_ui_egui::UiState { - let mut ui = vectorcraft_ui_egui::UiState::default(); - ui.engine_prefs = prefs; - ui + fn ui_state_with_prefs(engine_prefs: serde_json::Value) -> vectorcraft_ui_egui::UiState { + vectorcraft_ui_egui::UiState { engine_prefs, ..Default::default() } } #[test] diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 0c764faa..a6ca884f 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -278,11 +278,11 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("toolGroupLabels", "User Interface", "", "Show Tool Group Labels", bool), p!("openDocumentsAsTabs", "User Interface", "", "Open Documents As Tabs", bool), p!("largeTabs", "User Interface", "", "Large Tabs", bool), + p!("systemTitleBar", "User Interface", "", "System Title Bar", bool), p!("uiScaling", "User Interface", "UI Scaling", "Scale", num(0.75, 2.0, "×")), p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool), // `auto` or a language code the shell registers (`zh-hant`); the shell shows it as a dropdown. p!("interfaceLanguage", "User Interface", "Language", "Language", text), - p!("systemTitleBar", "User Interface", "", "System Title Bar", bool), // Performance p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool), p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool), diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index a8c8b15e..2d029eef 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -130,6 +130,9 @@ pub fn sync_window_title(app: &mut VectorcraftApp, ctx: &egui::Context) { if app.last_window_title != want { app.last_window_title = want.clone(); ctx.send_viewport_cmd(egui::ViewportCommand::Title(want)); + // One more frame: a new title makes AppKit lay its title bar out again, putting the window + // buttons back where it keeps them until the next frame centres them on the bar (#968). + ctx.request_repaint(); } } diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index dc1cafa7..13d0ae90 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -673,5 +673,4 @@ Stained Glass… Vitráž… 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 diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f4927b5e..eb8eabfd 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3976,4 +3976,3 @@ axis Both Ambos Bottom Right Abajo a la derecha Edge Borde System Title Bar Barra de título del sistema - Applies at next launch. Se aplicará en el próximo inicio. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 6dc963a7..c44f7863 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3988,4 +3988,3 @@ axis Both Les deux effect Soft Doux Edge Bord System Title Bar Barre de titre du système - Applies at next launch. Prend effet au prochain démarrage. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index bbb37c63..5a93088d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3978,4 +3978,3 @@ effect Soft Morbido effect Regular Normale Edge Bordo System Title Bar Barra del titolo di sistema - Applies at next launch. Si applica al prossimo avvio. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 40a9fe66..2df63e5a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3971,4 +3971,3 @@ effect Horizontal 水平 effect Vertical 垂直 Edge エッジ System Title Bar システムのタイトルバー - Applies at next launch. 次回の起動時に適用されます。 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b409f420..13e53b3c 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2733,5 +2733,4 @@ Stained Glass… Vitral… Texturizer… Texturizador… System Title Bar Barra de título do sistema - Applies at next launch. Aplica-se na próxima inicialização. Exit Sair diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3c04c962..75401a79 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3974,4 +3974,3 @@ axis Both Оба Bottom Right Снизу справа Edge Край System Title Bar Системная строка заголовка - Applies at next launch. Применится при следующем запуске. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 34afec8f..4209612b 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3959,4 +3959,3 @@ axis Both Обидві Bottom Right Знизу праворуч Edge Край System Title Bar Системний рядок заголовка - Applies at next launch. Набуде чинності після перезапуску. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 1dd56e99..6cae9b22 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3607,4 +3607,3 @@ effect Horizontal 水平 effect Vertical 垂直 Edge 边缘 System Title Bar 系统标题栏 - Applies at next launch. 下次启动时生效。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 70696085..fe6676ae 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3388,4 +3388,3 @@ axis Both 兩者 effect Soft 柔和 Edge 邊緣 System Title Bar 系統標題列 - Applies at next launch. 下次啟動時套用。 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 486dfa7b..a5d7f7a1 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -13,7 +13,7 @@ use crate::VectorcraftApp; use crate::io; use crate::panels::character::Face; use crate::state::{DockTab, next_zoom}; -use crate::theme::{self, Brightness, Tokens}; +use crate::theme::{Brightness, Tokens}; use crate::widgets; #[derive(Clone, Debug)] @@ -2754,11 +2754,7 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 { .ui(ui, |ui| { let mut titles = Vec::with_capacity(tree.len()); 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) - }; + let text = egui::RichText::new(tl!(title)).size(13.0).color(t.text); titles.push(ui.menu_button(text, |ui| menu_body(app, ui, items, &mut clicked)).response); } (ui.cursor().min.x, switch_on_hover(ui.ctx(), &titles)) @@ -3520,6 +3516,7 @@ pub fn menu_strings() -> std::collections::BTreeSet { #[cfg(test)] mod tests { use super::*; + use crate::theme; /// One headless frame of the in-window menu bar; returns its titles (left to right) as /// (rect, id of the title's popup). diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 72b649a5..a38e8977 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -261,10 +261,13 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { PrefKind::Bool if sp.key == "animatedZoom" => { ui.add_enabled_ui(d.bool("gpuPerformance"), |ui| bool_row(ui, d, sp.key, sp.label)); } - PrefKind::Bool if sp.key == "systemTitleBar" && cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) => { - bool_row(ui, d, sp.key, sp.label); - // The window's decorations are chosen once, when the app starts. - ui.label(egui::RichText::new(tl!("Applies at next launch.")).color(t.text_dim).size(11.0)); + // Windows and Linux only: macOS always has the system's title bar, the web has none. + PrefKind::Bool if sp.key == "systemTitleBar" => { + if cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))) { + bool_row(ui, d, sp.key, sp.label); + // The window's decorations are chosen once, when the app starts. + ui.label(egui::RichText::new(tl!("Applies the next time VectorCraft starts.")).color(t.text_dim).size(11.0)); + } } PrefKind::Bool => bool_row(ui, d, sp.key, sp.label), PrefKind::Num { min, max, unit } => { @@ -558,7 +561,7 @@ mod tests { let mut shown = vec![]; out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown)); assert!(shown.iter().any(|t| t == "System Title Bar"), "{shown:?}"); - assert!(shown.iter().any(|t| t == "Applies at next launch."), "{shown:?}"); + assert!(shown.iter().any(|t| t == "Applies the next time VectorCraft starts."), "{shown:?}"); a.ui.dialog.as_mut().unwrap().fields.insert("systemTitleBar".into(), json!(true)); confirm(&mut a).unwrap(); assert!(a.session.prefs.system_title_bar); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 6492976b..549048af 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -208,8 +208,10 @@ Tile Edge Color: Object → Pattern → Tile Edge Color… (`ui.tileEdgeColor`) Window title bar: on Windows and Linux the window has no OS decorations and the app bar is the title bar. Its caption buttons (Minimize, Maximize/Restore, Close) sit at the bar's right end, 46 pt each; they are window chrome, not commands, so drive them with `ui.click` if needed. Close runs `app.quit` (the same `saveChanges` questions for -modified documents), and empty bar space and the 5 pt window edges move and resize the window. macOS and the web -build are unchanged. +modified documents), and empty bar space and the 5 pt window edges move and resize the window. Preferences › User +Interface › System Title Bar (`systemTitleBar`, read when the app starts) gives the window its OS decorations back +instead: the OS title then shows the active document (`*name — VectorCraft` with unsaved changes) and the app bar +drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged. Fill/Stroke chips and panel shortcuts: the Control bar's and Properties' Fill and Stroke chips bring their proxy forward (`paint.toggleActive {fill}`) and open a popover with the Swatches panel (Shift-click: the Color panel's mixer); diff --git a/docs/development.md b/docs/development.md index 45db7d6b..9d5a0111 100644 --- a/docs/development.md +++ b/docs/development.md @@ -86,7 +86,7 @@ By default VectorCraft's own crates log at `info` and everything else at `warn`. ## macOS: the menu bar -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`. +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 icon and the workspace switcher next to the traffic lights, centred on the bar by `apps/vectorcraft/src/mac_window.rs`). `--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. 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. @@ -182,8 +182,8 @@ locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, com - The shared widgets (`widgets::check`, `dropdown`, `menu_item`, the buttons, `label_row`, tooltips of `icon_button`…), the menus, the dock, the toolbar, the dialog frame and `panels::empty_state` translate the text they are given, so a panel mostly needs its literals wrapped in `tl!` to be covered by the tests. -- The language is VectorCraft › Language (the `app.language` UI command, `{lang: auto|}`) or Edit › - Preferences › User Interface › Language; both set the `interfaceLanguage` preference (`auto` or a language +- The language is Edit › Preferences › User Interface › Language or, on macOS, VectorCraft › Language (the + `app.language` UI command, `{lang: auto|}`); both set the `interfaceLanguage` preference (`auto` or a language code; `auto` follows the system locale: `VECTORCRAFT_LOCALE`, then `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE`, the macOS preferred languages, the Windows user locale; on the web, `?lang=` in the address, then the browser's `navigator.languages`). The Preferences dialog previews the chosen language before OK. diff --git a/docs/mcp.md b/docs/mcp.md index d2d92393..e5582c8f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2601,7 +2601,7 @@ without ideographs uses its em box. One undo step. ## New type in a Japanese interface -While the interface is in Japanese (VectorCraft › Language, or `auto` on a Japanese system), new type starts with em +While the interface is in Japanese (Preferences › User Interface › Language, or `auto` on a Japanese system), new type starts with em box top-to-top leading (`leadingModel: "emBoxTop"`) and em box centre character alignment (`charAlign: "emBoxCenter"`); in other languages it starts on the Roman baseline (#432). This covers the Type tools, `text.create` and `text.createInPath`, which also take `leadingModel` and `charAlign` to choose, and `paraStyle.new` /