mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 12:16:53 -06:00
Merge PR #832
This commit is contained in:
@@ -154,6 +154,7 @@ Newest first.
|
||||
- **2026-10-13** · Links rows show their source paths; selecting a link shows its full path wrapped inside the panel, so same-named files from different folders can be distinguished. No readiness percentage change.
|
||||
- **2026-10-13** · The Text Wrap panel is laid out as InDesign's: the wrap buttons with Invert, and the four offsets (Top, Left, Bottom, Right) showing the object's values, with the link button that sets them all the same. It used to show a single offset, always 0. No readiness percentage change.
|
||||
- **2026-10-13** · Radial gradients stretch with the object's box, as InDesign draws them: by default an ellipse reaching the frame's sides (it was a circle reaching only the longer side), and a Gradient Swatch tool drag keeps the box's proportions; on screen and in PDF. No readiness percentage change.
|
||||
- **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** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -191,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", "新規ウィンドウ", "新建窗口"]),
|
||||
(
|
||||
@@ -3071,7 +3075,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Hide Rulers", "إخفاء المساطر"),
|
||||
("Extras", "إضافات"),
|
||||
("Workspace", "مساحة العمل"),
|
||||
("Editorial", "التحرير"),
|
||||
("Object & Layout", "الكائن والتخطيط"),
|
||||
("Styles", "الأنماط"),
|
||||
("Essentials Classic", "الأساسيات الكلاسيكية"),
|
||||
("New Workspace", "مساحة عمل جديدة"),
|
||||
("Properties", "خصائص"),
|
||||
("Layers", "الطبقات"),
|
||||
|
||||
@@ -112,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"),
|
||||
|
||||
@@ -114,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", "すべてのメニュー項目を表示"),
|
||||
|
||||
@@ -126,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"),
|
||||
|
||||
@@ -115,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", "Показати всі пункти меню"),
|
||||
|
||||
@@ -109,6 +109,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.
|
||||
@@ -244,6 +247,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.
|
||||
@@ -349,6 +356,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
@@ -202,7 +202,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}"),
|
||||
@@ -695,50 +695,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",
|
||||
],
|
||||
),
|
||||
@@ -759,6 +786,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))
|
||||
}
|
||||
@@ -1283,7 +1326,9 @@ fn run_ui_inner(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;
|
||||
@@ -1474,6 +1519,7 @@ fn run_ui_inner(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);
|
||||
@@ -1512,6 +1558,7 @@ fn run_ui_inner(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));
|
||||
}
|
||||
@@ -1521,7 +1568,12 @@ fn run_ui_inner(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"),
|
||||
@@ -1790,6 +1842,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()
|
||||
@@ -1829,6 +1891,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,
|
||||
@@ -2141,7 +2204,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}"),
|
||||
@@ -3612,6 +3680,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());
|
||||
|
||||
@@ -331,7 +331,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
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user