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 <current>, 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 <noreply@anthropic.com>
This commit is contained in:
moppeeer
2026-10-13 20:16:51 +02:00
co-authored by Claude Opus 5.5
parent e862e0a613
commit 0a34316b82
13 changed files with 284 additions and 73 deletions
+1
View File
@@ -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.
+1 -13
View File
@@ -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<String> = 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()
+66 -19
View File
@@ -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())
+12 -1
View File
@@ -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,
}
}
+4
View File
@@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "قص"),
("Copy", "نسخ"),
("Paste", "لصق"),
("Editorial", "التحرير"),
("Object & Layout", "الكائن والتخطيط"),
("Styles", "الأنماط"),
("Essentials Classic", "الأساسيات الكلاسيكية"),
("Reverse Path", "عكس المسار"),
("Set Anchor", "تعيين نقطة الارتساء"),
("Clear", "مسح"),
+4
View File
@@ -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"),
+4
View File
@@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "カット"),
("Copy", "コピー"),
("Paste", "ペースト"),
("Editorial", "エディトリアル"),
("Object & Layout", "オブジェクト&レイアウト"),
("Styles", "スタイル"),
("Essentials Classic", "基本(クラシック)"),
("Reverse Path", "パスの方向反転"),
("Set Anchor", "アンカーポイントを設定"),
("Clear", "消去"),
+4
View File
@@ -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"),
+4
View File
@@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "Вирізати"),
("Copy", "Копіювати"),
("Paste", "Вставити"),
("Editorial", "Редакція"),
("Object & Layout", "Об’єкт і макет"),
("Styles", "Стилі"),
("Essentials Classic", "Основне (класичне)"),
("Reverse Path", "Розвернути контур"),
("Set Anchor", "Задати прив’язку"),
("Clear", "Очистити"),
+8
View File
@@ -104,6 +104,9 @@ pub struct SavedWorkspace {
pub docking: Option<craft_ui::docking::Layout<String>>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
/// The workspace's own icon column (see [`UiState::icon_groups`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon_groups: Option<Vec<Vec<String>>>,
}
/// 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<String>,
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<Vec<Vec<String>>>,
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(),
+174 -38
View File
@@ -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<Result<Value,
}
"window.panel" => {
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<Result<Value,
floating: u.floating.clone(),
docking: u.docking.clone(),
docking_hidden: u.docking_hidden.clone(),
icon_groups: u.icon_groups.clone(),
};
app.ui.custom_workspaces.retain(|x| x.name != name);
app.ui.custom_workspaces.push(w);
@@ -1475,6 +1521,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
app.ui.floating = w.floating;
app.ui.docking = w.docking;
app.ui.docking_hidden = w.docking_hidden;
app.ui.icon_groups = w.icon_groups;
app.ui.workspace = w.name;
return Some(Ok(Value::Null));
}
@@ -1484,7 +1531,12 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
}
app.ui.docking = None;
app.ui.docking_hidden.clear();
app.ui.control_bar = matches!(name, "Advanced" | "Typography" | "Printing and Proofing" | "Book");
app.ui.control_bar = matches!(name, "Advanced" | "Typography" | "Printing and Proofing" | "Book" | "Essentials Classic");
// Essentials Classic: the Control panel and an icon column of grouped panels, no
// Properties stack.
let classic = name == "Essentials Classic";
app.ui.icon_groups = classic.then(|| CLASSIC_GROUPS.iter().map(|g| g.iter().map(|p| p.to_string()).collect()).collect());
app.ui.dock_expanded = !classic;
let dock_tab = match name {
"Typography" => Some("properties"),
"Interactive for PDF" | "Digital Publishing" => Some("pages"),
@@ -1753,6 +1805,16 @@ fn parse_entries(entries: &[&str]) -> Vec<Item> {
.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<Item> = 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<Item>)> {
/// Check state of a toggle command (None = not a toggle).
pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
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<String> =
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<String>) {
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());
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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) |