From 0a34316b82ea3f66b624eaa8a11648ec3d2c70c7 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 13 Oct 2026 20:16:51 +0200 Subject: [PATCH 1/2] feat(ui): Window menu in InDesign's order; Window > Workspace with Essentials Classic LYH Studio episode 6 starts with "Window > Workspace > Essential Classic" and opens panels with "Window > Color > Gradient & Swatches". DesignCraft's Window menu had no Workspace or Color submenu (panels sat in one "Panels" list) and no Essentials Classic workspace. - Window menu: Arrange, Workspace, then the panels alphabetically with InDesign's groups (Color: Color, Gradient, Swatches; Editorial; Interactive; Object & Layout; Output; Styles; Type & Tables; Utilities). Every panel stays reachable; Next/Previous Document moved into Arrange. - Window > Workspace: the built-in workspaces as "[Essentials]", the current one checked, Reset , New Workspace. The app bar's switcher uses the same list and order. - Essentials Classic: the Control panel, no Properties stack, and an icon column of named panel groups ([Pages, Layers, Links] [Stroke, Color] [Swatches] [Effects] [Paragraph] [Text Wrap] [Character Styles, Paragraph Styles] [Gradient]); Window > Pages opens Pages from its group. Saved workspaces keep the groups. - Flyouts and floating panels name Pages, Layers and Properties. - Translations for Editorial, Object & Layout, Styles and Essentials Classic. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/chrome.rs | 14 +- crates/ui-egui/src/dock.rs | 85 ++++++++++--- crates/ui-egui/src/i18n.rs | 13 +- crates/ui-egui/src/i18n/ar.rs | 4 + crates/ui-egui/src/i18n/it.rs | 4 + crates/ui-egui/src/i18n/ja.rs | 4 + crates/ui-egui/src/i18n/pt_br.rs | 4 + crates/ui-egui/src/i18n/uk.rs | 4 + crates/ui-egui/src/lib.rs | 8 ++ crates/ui-egui/src/menus.rs | 212 +++++++++++++++++++++++++------ docs/target-app-parity.md | 2 +- docs/ui-parity.md | 2 +- 13 files changed, 284 insertions(+), 73 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8a0f82071..495aadc61 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-13** · The Window menu follows InDesign's order (Arrange, Workspace, then the panels alphabetically with Color, Editorial, Object & Layout, Styles… submenus), and Window › Workspace lists the workspaces as InDesign does, now with Essentials Classic: the Control panel and an icon column of named panel groups. No readiness percentage change. - **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change. - **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change. - **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change. diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index c2f1cb48e..5e02276a7 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -63,19 +63,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) { crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)), |ui| { let customs: Vec = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect(); - for w in [ - "Essentials", - "Advanced", - "Book", - "Digital Publishing", - "Interactive for PDF", - "Printing and Proofing", - "Typography", - ] - .into_iter() - .map(str::to_string) - .chain(customs.iter().cloned()) - { + for w in crate::menus::WORKSPACES.iter().map(|w| w.to_string()).chain(customs.iter().cloned()) { if ui .selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w))) .clicked() diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 0d581e992..46cb606e4 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -113,30 +113,77 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { }, ); } - // Collapsed icon strip (37 pt) to the left of the panel stack. - egui::Panel::right("icon_column").exact_size(37.0).resizable(false).frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| { + // Collapsed icon strip (37 pt) to the left of the panel stack; a workspace's iconic groups + // (Essentials Classic) show their names. + let groups = app.ui.icon_groups.clone(); + egui::Panel::right("icon_column").exact_size(icon_column_width(app)).resizable(false).frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| { dock_header(ui, &t, "«"); ui.add_space(4.0); - ui.vertical_centered(|ui| { - for (id, label, icon) in ICON_PANELS { - let open = app.ui.open_panel.as_deref() == Some(*id); - let floats = app.ui.floating.iter().any(|(p, _)| p == id); - let response = - icons::button(ui, icon, 28.0, open || floats, crate::i18n::tr(&app.ui.language, label)).interact(Sense::click_and_drag()); - if response.clicked() { - if floats { - ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", *id)))); - } else { - app.ui.open_panel = if open { None } else { Some(id.to_string()) }; + match &groups { + Some(groups) => { + for (i, group) in groups.iter().enumerate() { + if i > 0 { + let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 7.0), Sense::hover()); + ui.painter().line_segment([r.left_center(), r.right_center()], Stroke::new(1.0, t.border)); + } + for (id, label, icon) in group.iter().filter_map(|id| panel_entry(id)) { + let on = app.ui.open_panel.as_deref() == Some(id) || app.ui.floating.iter().any(|(p, _)| p == id); + let (r, response) = ui.allocate_exact_size(vec2(ui.available_width(), 26.0), Sense::click_and_drag()); + if on { + ui.painter().rect_filled(r.shrink2(vec2(3.0, 1.0)), 3.0, t.tool_active); + } else if response.hovered() { + ui.painter().rect_filled(r.shrink2(vec2(3.0, 1.0)), 3.0, t.hover); + } + icons::paint(ui.painter(), egui::Rect::from_min_size(r.min + vec2(8.0, 4.0), vec2(18.0, 18.0)), icon, t.icon); + crate::rtl::paint( + ui.painter(), + r.left_center() + vec2(32.0, 0.0), + egui::Align2::LEFT_CENTER, + crate::i18n::tr(&app.ui.language, label), + semibold(11.5), + t.text, + ); + icon_clicked(app, ui, id, &response); } } - crate::panel_docking::legacy_tab(app, ui, id, &response); - ui.add_space(1.0); } - }); + None => { + ui.vertical_centered(|ui| { + for (id, label, icon) in ICON_PANELS { + let on = app.ui.open_panel.as_deref() == Some(*id) || app.ui.floating.iter().any(|(p, _)| p == id); + let response = icons::button(ui, icon, 28.0, on, crate::i18n::tr(&app.ui.language, label)).interact(Sense::click_and_drag()); + icon_clicked(app, ui, id, &response); + ui.add_space(1.0); + } + }); + } + } }); } +/// Width of the icon column: icons only, or icons with names for a workspace's groups. +fn icon_column_width(app: &DesignApp) -> f32 { + if app.ui.icon_groups.is_some() { 150.0 } else { 37.0 } +} + +/// (id, label, icon) of any dock panel. +fn panel_entry(id: &str) -> Option<(&'static str, &'static str, &'static str)> { + DOCK_TABS.iter().chain(ICON_PANELS).find(|p| p.0 == id).copied() +} + +/// A click on a panel's icon: raise it when it floats, else open or close its flyout. +fn icon_clicked(app: &mut DesignApp, ui: &mut egui::Ui, id: &str, response: &egui::Response) { + if response.clicked() { + if app.ui.floating.iter().any(|(p, _)| p == id) { + ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", id)))); + } else { + let open = app.ui.open_panel.as_deref() == Some(id); + app.ui.open_panel = if open { None } else { Some(id.to_string()) }; + } + } + crate::panel_docking::legacy_tab(app, ui, id, response); +} + /// 11 pt dock header strip with a collapse chevron. fn dock_header(ui: &mut egui::Ui, t: &Tokens, chevron: &str) { let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 11.0), Sense::hover()); @@ -152,10 +199,10 @@ pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) { } let Some(id) = app.ui.open_panel.clone() else { return }; let t = Tokens::get(ctx); - let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned(); + let label = crate::i18n::tr(&app.ui.language, panel_entry(&id).map(|p| p.1).unwrap_or("Panel")).to_owned(); let screen = ctx.content_rect(); let dock_w = if app.ui.dock_expanded { ctx.memory(|m| m.area_rect(egui::Id::new("dock")).map(|r| r.width())).unwrap_or(280.0) } else { 0.0 }; - let pos = egui::pos2(screen.max.x - 37.0 - dock_w - 262.0, 120.0); + let pos = egui::pos2(screen.max.x - icon_column_width(app) - dock_w - 262.0, 120.0); let mut open = true; egui::Area::new(egui::Id::new("panel_flyout")).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| { egui::Frame::popup(ui.style()).fill(t.panel).inner_margin(egui::Margin::same(0)).show(ui, |ui| { @@ -262,7 +309,7 @@ pub fn floating(app: &mut DesignApp, ctx: &egui::Context) { } let t = Tokens::get(ctx); for (id, at) in app.ui.floating.clone() { - let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned(); + let label = crate::i18n::tr(&app.ui.language, panel_entry(&id).map(|p| p.1).unwrap_or("Panel")).to_owned(); let mut open = true; let mut dock = false; let r = egui::Window::new(label.clone()) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 5252c3881..ffa50c108 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -55,6 +55,10 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), + ("Editorial", ["Redaktion", "Éditorial", "Editorial", "エディトリアル", "编辑"]), + ("Object & Layout", ["Objekt und Layout", "Objet et mise en page", "Objeto y diseño", "オブジェクト&レイアウト", "对象和版面"]), + ("Styles", ["Formate", "Styles", "Estilos", "スタイル", "样式"]), + ("Essentials Classic", ["Essentials Classic", "Essentials Classic", "Essentials Classic", "Essentials Classic", "基本功能(经典)"]), ("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]), ("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]), ("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]), @@ -2921,7 +2925,14 @@ pub fn style_name<'a>(lang: &str, name: &'a str) -> &'a str { /// Built-in workspace captions; custom workspace names remain unchanged. pub fn workspace_name<'a>(lang: &str, name: &'a str) -> &'a str { match name { - "Essentials" | "Advanced" | "Book" | "Digital Publishing" | "Interactive for PDF" | "Printing and Proofing" | "Typography" => tr(lang, name), + "Essentials" + | "Essentials Classic" + | "Advanced" + | "Book" + | "Digital Publishing" + | "Interactive for PDF" + | "Printing and Proofing" + | "Typography" => tr(lang, name), _ => name, } } diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index a182d09ec..f17794248 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Editorial", "التحرير"), + ("Object & Layout", "الكائن والتخطيط"), + ("Styles", "الأنماط"), + ("Essentials Classic", "الأساسيات الكلاسيكية"), ("Reverse Path", "عكس المسار"), ("Set Anchor", "تعيين نقطة الارتساء"), ("Clear", "مسح"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 75ece03f6..e389a0390 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Editorial", "Editoriale"), + ("Object & Layout", "Oggetto e layout"), + ("Styles", "Stili"), + ("Essentials Classic", "Essenziali classico"), ("Reverse Path", "Inverti tracciato"), ("Set Anchor", "Imposta ancoraggio"), ("Clear", "Cancella"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 642b4a4c0..68a093c4d 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Editorial", "エディトリアル"), + ("Object & Layout", "オブジェクト&レイアウト"), + ("Styles", "スタイル"), + ("Essentials Classic", "基本(クラシック)"), ("Reverse Path", "パスの方向反転"), ("Set Anchor", "アンカーポイントを設定"), ("Clear", "消去"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 982dfb20d..5474ceb19 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Editorial", "Editorial"), + ("Object & Layout", "Objeto e layout"), + ("Styles", "Estilos"), + ("Essentials Classic", "Essencial clássico"), ("Reverse Path", "Inverter demarcador"), ("Set Anchor", "Definir ponto de ancoragem"), ("Clear", "Limpar"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index f3bc8fb35..91feec029 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Editorial", "Редакція"), + ("Object & Layout", "Об’єкт і макет"), + ("Styles", "Стилі"), + ("Essentials Classic", "Основне (класичне)"), ("Reverse Path", "Розвернути контур"), ("Set Anchor", "Задати прив’язку"), ("Clear", "Очистити"), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fc7a3e634..46863f007 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -104,6 +104,9 @@ pub struct SavedWorkspace { pub docking: Option>, #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] pub docking_hidden: std::collections::BTreeMap>, + /// The workspace's own icon column (see [`UiState::icon_groups`]). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon_groups: Option>>, } /// A missing on-by-default bool stays on. `bool::default` is false. @@ -239,6 +242,10 @@ pub struct UiState { #[serde(skip)] pub docking_opened: Option, pub dock_expanded: bool, + /// A workspace's iconic panel groups (Essentials Classic), shown with their names in the icon + /// column instead of every panel; `None` is the full icon column. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon_groups: Option>>, pub units: Unit, pub workspace: String, /// Window › Workspace › New Workspace: saved panel arrangements. @@ -341,6 +348,7 @@ impl Default for UiState { docking_raise: None, docking_opened: None, dock_expanded: true, + icon_groups: None, units: Unit::Picas, workspace: "Essentials".into(), custom_workspaces: Vec::new(), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 872cb81d0..b87848930 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -190,7 +190,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ "window.workspace", "Workspace", None, - "{name: Essentials|Advanced|Book|Digital Publishing|Interactive for PDF|Printing and Proofing|Typography}", + "{name: Advanced|Book|Digital Publishing|Essentials|Essentials Classic|Interactive for PDF|Printing and Proofing|Typography or a saved workspace}", ), ("window.newWorkspace", "New Workspace…", None, "{name} — saves the current bars and panel arrangement"), ("window.deleteWorkspace", "Delete Workspace…", None, "{name}"), @@ -682,50 +682,77 @@ pub const MENUS: &[(&str, &[&str])] = &[ ), ( "Window", + // InDesign's order: Arrange, Workspace, then the panels alphabetically with their groups. &[ ">Arrange", "ui:window.newWindow", "ui:window.split", + "-", + "ui:window.nextDocument", + "ui:window.previousDocument", + "<", + "ui:window.workspace", + "-", + "ui:window.panel|Articles|{\"panel\": \"articles\"}", + ">Color", + "ui:window.panel|Color|{\"panel\": \"color\"}", + "ui:window.panel|Gradient|{\"panel\": \"gradient\"}", + "ui:window.panel|Swatches|{\"panel\": \"swatches\"}", + "<", + "ui:window.taskBar", + "ui:window.controlBar", + ">Editorial", + "ui:window.panel|Notes|{\"panel\": \"notes\"}", + "ui:window.panel|Track Changes|{\"panel\": \"trackChanges\"}", + "<", + "ui:window.panel|Effects|{\"panel\": \"effects\"}", + "ui:window.panel|Info|{\"panel\": \"info\"}", + ">Interactive", + "ui:window.panel|Bookmarks|{\"panel\": \"bookmarks\"}", + "ui:window.panel|Buttons and Forms|{\"panel\": \"buttons\"}", + "ui:window.panel|Hyperlinks|{\"panel\": \"hyperlinks\"}", + "ui:window.panel|Liquid Layout|{\"panel\": \"liquid\"}", + "ui:window.panel|Media|{\"panel\": \"media\"}", + "ui:window.panel|Object States|{\"panel\": \"states\"}", + "ui:window.panel|Page Transitions|{\"panel\": \"transitions\"}", + "<", + "ui:window.panel|Layers|{\"panel\": \"layers\"}", + "ui:window.panel|Links|{\"panel\": \"links\"}", + ">Object & Layout", + "ui:window.panel|Align|{\"panel\": \"align\"}", + "ui:window.panel|Pathfinder|{\"panel\": \"pathfinder\"}", + "<", + ">Output", + "ui:window.panel|Attributes|{\"panel\": \"attributes\"}", + "ui:window.panel|Preflight|{\"panel\": \"preflight\"}", + "<", + "ui:window.panel|Pages|{\"panel\": \"pages\"}", + "ui:window.panel|Properties|{\"panel\": \"properties\"}", + "ui:window.panel|Stroke|{\"panel\": \"stroke\"}", + ">Styles", + "ui:window.panel|Character Styles|{\"panel\": \"characterStyles\"}", + "ui:window.panel|Paragraph Styles|{\"panel\": \"paragraphStyles\"}", + "<", + "ui:window.panel|Text Wrap|{\"panel\": \"textWrap\"}", + ">Type & Tables", + "ui:window.panel|Character|{\"panel\": \"character\"}", + "ui:window.panel|Conditional Text|{\"panel\": \"conditions\"}", + "ui:window.panel|Glyphs|{\"panel\": \"glyphs\"}", + "ui:window.panel|Paragraph|{\"panel\": \"paragraph\"}", + "ui:window.panel|Table|{\"panel\": \"table\"}", + "<", + ">Utilities", + "ui:window.panel|Book|{\"panel\": \"book\"}", + "ui:window.panel|Data Merge|{\"panel\": \"dataMerge\"}", + "ui:window.panel|Library|{\"panel\": \"library\"}", + "ui:window.panel|Scripts|{\"panel\": \"scripts\"}", + "ui:window.panel|Tags|{\"panel\": \"tags\"}", "<", "-", "ui:window.hidePanels", "ui:window.hidePanelsExceptTools", - "ui:window.nextDocument", - "ui:window.previousDocument", - "-", - "ui:window.controlBar", - "ui:window.taskBar", "ui:window.toolsDoubleColumn", "-", - "ui:window.panel", - "-", - ">Type & Tables", - "ui:window.panel|Conditional Text|{\"panel\": \"conditions\"}", - "ui:window.panel|Notes|{\"panel\": \"notes\"}", - "ui:window.panel|Glyphs|{\"panel\": \"glyphs\"}", - "<", - ">Interactive", - "ui:window.panel|Hyperlinks|{\"panel\": \"hyperlinks\"}", - "ui:window.panel|Bookmarks|{\"panel\": \"bookmarks\"}", - "ui:window.panel|Articles|{\"panel\": \"articles\"}", - "ui:window.panel|Object States|{\"panel\": \"states\"}", - "ui:window.panel|Buttons and Forms|{\"panel\": \"buttons\"}", - "ui:window.panel|Media|{\"panel\": \"media\"}", - "ui:window.panel|Page Transitions|{\"panel\": \"transitions\"}", - "ui:window.panel|Track Changes|{\"panel\": \"trackChanges\"}", - "ui:window.panel|Scripts|{\"panel\": \"scripts\"}", - "ui:window.panel|Liquid Layout|{\"panel\": \"liquid\"}", - "ui:window.panel|Tags|{\"panel\": \"tags\"}", - "ui:window.panel|Book|{\"panel\": \"book\"}", - "<", - ">Utilities", - "ui:window.panel|Data Merge|{\"panel\":\"dataMerge\"}", - "<", - ">Output", - "ui:window.panel|Attributes|{\"panel\": \"attributes\"}", - "cmd:preflight.run", - "<", - "-", "ui:window.brightness", ], ), @@ -746,6 +773,22 @@ pub const MENUS: &[(&str, &[&str])] = &[ ), ]; +/// The built-in workspaces, in InDesign's order. +pub const WORKSPACES: &[&str] = + &["Advanced", "Book", "Digital Publishing", "Essentials", "Essentials Classic", "Interactive for PDF", "Printing and Proofing", "Typography"]; + +/// Essentials Classic's icon column: InDesign's iconic panel groups, top to bottom. +pub const CLASSIC_GROUPS: &[&[&str]] = &[ + &["pages", "layers", "links"], + &["stroke", "color"], + &["swatches"], + &["effects"], + &["paragraph"], + &["textWrap"], + &["characterStyles", "paragraphStyles"], + &["gradient"], +]; + pub fn ui_label(id: &str) -> Option<(&'static str, Option<&'static str>)> { UI_COMMANDS.iter().find(|c| c.0 == id).map(|c| (c.1, c.2)) } @@ -1260,7 +1303,9 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option { let panel = p.get("panel").and_then(Value::as_str).unwrap_or("properties").to_string(); - if crate::dock::DOCK_TABS.iter().any(|(id, _, _)| *id == panel) { + // A panel in the workspace's icon groups opens from there (Essentials Classic's Pages). + let iconic = app.ui.icon_groups.as_ref().is_some_and(|g| g.iter().flatten().any(|p| *p == panel)); + if !iconic && crate::dock::DOCK_TABS.iter().any(|(id, _, _)| *id == panel) { app.ui.dock_tab = panel; app.ui.dock_expanded = true; app.ui.open_panel = None; @@ -1437,6 +1482,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option Option Option Some("properties"), "Interactive for PDF" | "Digital Publishing" => Some("pages"), @@ -1753,6 +1805,16 @@ fn parse_entries(entries: &[&str]) -> Vec { .map(|(id, label, _)| Item::Cmd { label: label.to_string(), id: "window.panel".into(), params: json!({"panel": id}), shortcut: None }) .collect(); out.push(Item::Sub("Panels".into(), items)); + } else if e == "ui:window.workspace" { + // Shown as InDesign's "[Essentials]"; Reset names the current workspace (see `menu_items`). + let mut items: Vec = WORKSPACES + .iter() + .map(|w| Item::Cmd { label: w.to_string(), id: "window.workspace".into(), params: json!({"name": w}), shortcut: None }) + .collect(); + items.push(Item::Sep); + items.push(cmd_item("window.resetWorkspace", Value::Null)); + items.push(cmd_item("window.newWorkspace", Value::Null)); + out.push(Item::Sub("Workspace".into(), items)); } else if e == "ui:window.brightness" { let items = crate::theme::Brightness::ALL .iter() @@ -1792,6 +1854,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { /// Check state of a toggle command (None = not a toggle). pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option { Some(match id { + "window.workspace" => params.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()), "view.rulers" => app.ui.rulers, "view.frameEdges" => app.ui.frame_edges, "view.overprintPreview" => app.ui.overprint_preview, @@ -2089,7 +2152,12 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str } Item::Cmd { label, .. } if !app.ui.show_full_menus && app.ui.hidden_menu_items.contains(&menu_key(path, label)) => hidden += 1, Item::Cmd { label, id, params, shortcut: _ } => { - let label = crate::i18n::tr(&app.ui.language, label); + let lang = &app.ui.language; + let label = match id.as_str() { + "window.workspace" => format!("[{}]", crate::i18n::workspace_name(lang, label)), + "window.resetWorkspace" => format!("{} {}", crate::i18n::tr(lang, "Reset"), crate::i18n::workspace_name(lang, &app.ui.workspace)), + _ => crate::i18n::tr(lang, label).to_owned(), + }; let text = match checked(app, id, params) { Some(true) => format!("✓ {label}"), Some(false) => format!(" {label}"), @@ -3379,6 +3447,74 @@ mod tests { assert_eq!(app.ui.workspace, "Essentials"); } + #[test] + fn essentials_classic_is_the_control_panel_and_iconic_groups() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + run_ui(&mut app, "window.workspace", &json!({"name": "Essentials Classic"})).unwrap().unwrap(); + assert!(app.ui.control_bar && !app.ui.dock_expanded); + let groups = app.ui.icon_groups.clone().unwrap(); + assert_eq!(groups[0], ["pages", "layers", "links"]); + assert_eq!(groups.last().unwrap(), &["gradient"]); + assert_eq!(checked(&app, "window.workspace", &json!({"name": "Essentials Classic"})), Some(true)); + assert_eq!(checked(&app, "window.workspace", &json!({"name": "Essentials"})), Some(false)); + // Window › Pages opens Pages from its icon group, not the Properties stack. + run_ui(&mut app, "window.panel", &json!({"panel": "pages"})).unwrap().unwrap(); + assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_expanded), (Some("pages"), false)); + // A workspace saved from it keeps the groups. + run_ui(&mut app, "window.newWorkspace", &json!({"name": "Mine"})).unwrap().unwrap(); + run_ui(&mut app, "window.workspace", &json!({"name": "Essentials"})).unwrap().unwrap(); + assert_eq!((app.ui.icon_groups.as_ref(), app.ui.dock_expanded, app.ui.control_bar), (None, true, false)); + run_ui(&mut app, "window.workspace", &json!({"name": "Mine"})).unwrap().unwrap(); + assert_eq!(app.ui.icon_groups, Some(groups)); + } + + #[test] + fn the_window_menu_is_in_indesigns_order_and_reaches_every_panel() { + let window = menu_tree().into_iter().find(|(m, _)| *m == "Window").unwrap().1; + let names: Vec<&str> = window + .iter() + .filter_map(|i| match i { + Item::Sub(n, _) => Some(n.as_str()), + Item::Cmd { label, .. } => Some(label.as_str()), + Item::Sep => None, + }) + .collect(); + assert_eq!(&names[..4], ["Arrange", "Workspace", "Articles", "Color"]); + let sub = |name: &str| { + window + .iter() + .find_map(|i| match i { + Item::Sub(n, c) if n == name => Some(c.clone()), + _ => None, + }) + .unwrap() + }; + // Window › Workspace › Essentials Classic, Window › Color › Gradient & Swatches. + assert!( + sub("Workspace") + .iter() + .any(|i| matches!(i, Item::Cmd { id, params, .. } if id == "window.workspace" && params["name"] == "Essentials Classic")) + ); + let color: Vec = + sub("Color").iter().filter_map(|i| if let Item::Cmd { label, .. } = i { Some(label.clone()) } else { None }).collect(); + assert_eq!(color, ["Color", "Gradient", "Swatches"]); + // Every panel is in the menu. + fn panels(items: &[Item], out: &mut Vec) { + for i in items { + match i { + Item::Sub(_, c) => panels(c, out), + Item::Cmd { id, params, .. } if id == "window.panel" => out.extend(params["panel"].as_str().map(str::to_string)), + _ => {} + } + } + } + let mut found = Vec::new(); + panels(&window, &mut found); + for (id, _, _) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) { + assert!(found.iter().any(|p| p == id), "Window menu misses the {id} panel"); + } + } + #[test] fn interactive_workspaces_show_their_panels() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index eb7d7e5c1..d2ab2922e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -329,7 +329,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Font menu: search, favourites (stars, Show Favorites Only; `favoriteFonts` preference) and each family previewed in its own face. Western (and other-script) families come first, then Japanese, Simplified Chinese, Traditional Chinese and Korean ones, each group alphabetical and set off by a separator; the group comes from the font's own data (`meta` languages, family names in a CJK language, OS/2 code pages, else what it covers), read once (installed fonts by the font scan, without loading them). CJK families show their native names, the English name on hover; Preferences › Type › Show Font Names in English (`showFontNamesInEnglish`) shows English names throughout. Search matches either name; documents store the English name. Every family list (Character panel, Control bar, Properties, style dialogs, Find Font, Glyphs) follows this order. - Application preferences (engine `Prefs`) now persist across launches (desktop: prefs.json beside ui.json). - View ▸ Fit Selection in Window (⌥⌘=) and Find/Change ▸ Object (`find.objects`, `find.changeObjects`: by fill, stroke, weight, opacity, kind, layer, label). -- Custom workspaces (`window.newWorkspace/deleteWorkspace/resetWorkspace`; workspace switcher shows the current one): bars and panel arrangement saved by name. +- Custom workspaces (`window.newWorkspace/deleteWorkspace/resetWorkspace`; workspace switcher shows the current one): bars and panel arrangement saved by name. Essentials Classic shows the Control panel and InDesign's iconic panel groups with names. The Window menu follows InDesign's: Arrange, Workspace, then the panels alphabetically with their Color, Editorial, Interactive, Object & Layout, Output, Styles, Type & Tables and Utilities groups. - Word/RTF Import Options (File ▸ Place with Import Options…, `place.styles`, `file.place {styleMap, styleConflicts}`): map imported styles onto the document's, use existing / redefine / auto-rename on conflicts, or remove formatting. - Custom anchored objects (`anchored.insert/options {position: custom}`): placed relative to the anchor, frame, column, page margins or page edge with reference points and offsets, no space in the text, keep within column; IDML Anchored position. - Variable font axes: Character panel sliders; any axis values (`fontStyle: "Bold {wght:650}"`) make that instance for layout, screen and PDF. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 8805defce..f9da7383b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -47,7 +47,7 @@ basics of editing fail for too many users on Windows and Linux. | Colour fields: hex, picker from any swatch field | ✓ | ✓ | ~ | hex entry and picker issues (#323, #190) | | Panels: Pages, Layers, Swatches, Color, Stroke, Character, Paragraph, Styles, Links, Effects, Text Wrap, Align, Pathfinder, Table, Index, TOC, Hyperlinks, Bookmarks, Articles, Tags, Notes, Track Changes, Object States, Buttons and Forms, Preflight, Data Merge, Library, Scripts, Glyphs, Info | ✓ | all present | ~ | **Tabs panel missing** (#326); Navigator, Trap Presets, Animation, Timing missing; pull-down panels can't scroll on Linux (#267) | | Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | generated command dialogs fill the long tail; depth varies | -| Workspaces | Essentials, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Typography, Touch | ✓ named workspaces + custom | ~ | tab bar overlaps menus in Book/Advanced (#182) | +| Workspaces | Essentials, Essentials Classic, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Review, Typography, Touch | ✓ named workspaces (Essentials Classic: Control panel and iconic panel groups) + custom; Window › Workspace; no Review or Touch | ~ | tab bar overlaps menus in Book/Advanced (#182) | | Story Editor | ✓ | ✓ | ~ | style-name column, depth ruler missing | | Undo granularity (a drag = one step) | ✓ | ✓ (Begin/Preview/Commit) | ✓ | | | Closing with unsaved changes asks | ✓ | ✓ File › Close and window/menu quit (#146, #545) | ✓ | macOS Dock Quit still bypasses the prompt (#544) | From d49d7c4ea006eeaac2c98a6e0c4e4016a6f7df8e Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 13 Oct 2026 20:18:14 +0200 Subject: [PATCH 2/2] i18n: keep the new Window menu strings beside Workspace Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n.rs | 8 ++++---- crates/ui-egui/src/i18n/ar.rs | 8 ++++---- crates/ui-egui/src/i18n/it.rs | 8 ++++---- crates/ui-egui/src/i18n/ja.rs | 8 ++++---- crates/ui-egui/src/i18n/pt_br.rs | 8 ++++---- crates/ui-egui/src/i18n/uk.rs | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index ffa50c108..07d294c56 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -55,10 +55,6 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), - ("Editorial", ["Redaktion", "Éditorial", "Editorial", "エディトリアル", "编辑"]), - ("Object & Layout", ["Objekt und Layout", "Objet et mise en page", "Objeto y diseño", "オブジェクト&レイアウト", "对象和版面"]), - ("Styles", ["Formate", "Styles", "Estilos", "スタイル", "样式"]), - ("Essentials Classic", ["Essentials Classic", "Essentials Classic", "Essentials Classic", "Essentials Classic", "基本功能(经典)"]), ("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]), ("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]), ("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]), @@ -195,6 +191,10 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Buttons and Forms", ["Schaltflächen und Formulare", "Boutons et formulaires", "Botones y formularios", "ボタンとフォーム", "按钮和表单"]), ("Liquid Layout", ["Flüssiges Layout", "Mise en page liquide", "Maquetación líquida", "リキッドレイアウト", "自适应版面"]), ("Workspace", ["Arbeitsbereich", "Espace de travail", "Espacio de trabajo", "ワークスペース", "工作区"]), + ("Editorial", ["Redaktion", "Éditorial", "Editorial", "エディトリアル", "编辑"]), + ("Object & Layout", ["Objekt und Layout", "Objet et mise en page", "Objeto y diseño", "オブジェクト&レイアウト", "对象和版面"]), + ("Styles", ["Formate", "Styles", "Estilos", "スタイル", "样式"]), + ("Essentials Classic", ["Essentials Classic", "Essentials Classic", "Essentials Classic", "Essentials Classic", "基本功能(经典)"]), ("Split Window", ["Fenster teilen", "Fractionner la fenêtre", "Dividir ventana", "ウィンドウを分割", "拆分窗口"]), ("New Window", ["Neues Fenster", "Nouvelle fenêtre", "Nueva ventana", "新規ウィンドウ", "新建窗口"]), ( diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index f17794248..6a282b764 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,10 +30,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), - ("Editorial", "التحرير"), - ("Object & Layout", "الكائن والتخطيط"), - ("Styles", "الأنماط"), - ("Essentials Classic", "الأساسيات الكلاسيكية"), ("Reverse Path", "عكس المسار"), ("Set Anchor", "تعيين نقطة الارتساء"), ("Clear", "مسح"), @@ -130,6 +126,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Hide Rulers", "إخفاء المساطر"), ("Extras", "إضافات"), ("Workspace", "مساحة العمل"), + ("Editorial", "التحرير"), + ("Object & Layout", "الكائن والتخطيط"), + ("Styles", "الأنماط"), + ("Essentials Classic", "الأساسيات الكلاسيكية"), ("New Workspace", "مساحة عمل جديدة"), ("Properties", "خصائص"), ("Layers", "الطبقات"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index e389a0390..44902773f 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,10 +28,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), - ("Editorial", "Editoriale"), - ("Object & Layout", "Oggetto e layout"), - ("Styles", "Stili"), - ("Essentials Classic", "Essenziali classico"), ("Reverse Path", "Inverti tracciato"), ("Set Anchor", "Imposta ancoraggio"), ("Clear", "Cancella"), @@ -116,6 +112,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Buttons and Forms", "Pulsanti e moduli"), ("Liquid Layout", "Layout liquido"), ("Workspace", "Area di lavoro"), + ("Editorial", "Editoriale"), + ("Object & Layout", "Oggetto e layout"), + ("Styles", "Stili"), + ("Essentials Classic", "Essenziali classico"), ("Split Window", "Dividi finestra"), ("New Window", "Nuova finestra"), ("Show All Menu Items", "Mostra tutte le voci di menu"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 68a093c4d..cb3dcdcda 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,10 +30,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), - ("Editorial", "エディトリアル"), - ("Object & Layout", "オブジェクト&レイアウト"), - ("Styles", "スタイル"), - ("Essentials Classic", "基本(クラシック)"), ("Reverse Path", "パスの方向反転"), ("Set Anchor", "アンカーポイントを設定"), ("Clear", "消去"), @@ -118,6 +114,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Buttons and Forms", "ボタンとフォーム"), ("Liquid Layout", "リキッドレイアウト"), ("Workspace", "ワークスペース"), + ("Editorial", "エディトリアル"), + ("Object & Layout", "オブジェクト&レイアウト"), + ("Styles", "スタイル"), + ("Essentials Classic", "基本(クラシック)"), ("Split Window", "ウィンドウを分割"), ("New Window", "新規ウィンドウ"), ("Show All Menu Items", "すべてのメニュー項目を表示"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 5474ceb19..210ee8d45 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,10 +31,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), - ("Editorial", "Editorial"), - ("Object & Layout", "Objeto e layout"), - ("Styles", "Estilos"), - ("Essentials Classic", "Essencial clássico"), ("Reverse Path", "Inverter demarcador"), ("Set Anchor", "Definir ponto de ancoragem"), ("Clear", "Limpar"), @@ -130,6 +126,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Hide Rulers", "Ocultar réguas"), ("Extras", "Extras"), ("Workspace", "Área de trabalho"), + ("Editorial", "Editorial"), + ("Object & Layout", "Objeto e layout"), + ("Styles", "Estilos"), + ("Essentials Classic", "Essencial clássico"), ("New Workspace", "Nova área de trabalho"), ("Properties", "Propriedades"), ("Layers", "Camadas"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 91feec029..a90e9b607 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -31,10 +31,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), - ("Editorial", "Редакція"), - ("Object & Layout", "Об’єкт і макет"), - ("Styles", "Стилі"), - ("Essentials Classic", "Основне (класичне)"), ("Reverse Path", "Розвернути контур"), ("Set Anchor", "Задати прив’язку"), ("Clear", "Очистити"), @@ -119,6 +115,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Buttons and Forms", "Кнопки та форми"), ("Liquid Layout", "Гнучкий макет"), ("Workspace", "Робоче середовище"), + ("Editorial", "Редакція"), + ("Object & Layout", "Об’єкт і макет"), + ("Styles", "Стилі"), + ("Essentials Classic", "Основне (класичне)"), ("Split Window", "Розділити вікно"), ("New Window", "Нове вікно"), ("Show All Menu Items", "Показати всі пункти меню"),