U: custom workspaces (new, delete, reset)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 00:39:40 -04:00
parent 614eee2e1d
commit e5ec65bed9
6 changed files with 125 additions and 5 deletions

View File

@ -63,6 +63,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Font menu: search, favourites (stars, Show Favorites Only; `favoriteFonts` preference) and each family previewed in its own face.
- 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.
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.

View File

@ -42,15 +42,39 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
icons::paint(ui.painter(), egui::Rect::from_min_size(r.min + vec2(3.0, 2.0), vec2(14.0, 14.0)), "search", t.icon);
ui.painter().text(r.min + vec2(20.0, 9.0), egui::Align2::LEFT_CENTER, "Search", egui::FontId::proportional(11.0), t.text_dim);
ui.add_space(8.0);
ui.menu_button(egui::RichText::new("Essentials ▾").font(semibold(11.5)).color(t.text), |ui| {
let current = app.ui.workspace.clone();
ui.menu_button(egui::RichText::new(format!("{current} ▾")).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())
{
if ui.button(w).clicked() {
if ui.selectable_label(w == current, &w).clicked() {
let _ = app.run("window.workspace", json!({"name": w}));
ui.close();
}
}
ui.separator();
if ui.button(format!("Reset {current}")).clicked() {
let _ = app.run("window.resetWorkspace", json!({}));
ui.close();
}
if ui.button("New Workspace…").clicked() {
let _ = app.run("window.newWorkspace", json!({}));
ui.close();
}
if !customs.is_empty() {
ui.menu_button("Delete Workspace", |ui| {
for w in &customs {
if ui.button(w).clicked() {
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
ui.close();
}
}
});
}
});
ui.add_space(6.0);
if icons::button(ui, "share", 22.0, false, "Share").clicked() {

View File

@ -641,6 +641,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"findFont" => "Find/Replace Font",
"polygonSettings" => "Polygon Settings",
"userDictionary" => "User Dictionary",
"newWorkspace" => "New Workspace",
"fittingOptions" => "Frame Fitting Options",
"qrCode" => "Generate QR Code",
"preferences" => "Preferences",
@ -850,6 +851,13 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
}
"keyboardShortcuts" => keyboard_shortcuts(app, ui, &mut d),
"userDictionary" => user_dictionary(app, ui, &mut d),
"newWorkspace" => {
ui.horizontal(|ui| {
ui.label("Name:");
text_field(ui, &mut d, "name", 220.0);
});
ui.label(egui::RichText::new("Saves which bars show and where the panels are.").size(10.5));
}
"fittingOptions" => {
check(ui, &mut d, "autoFit", "Auto-Fit");
ui.add_space(4.0);
@ -1191,6 +1199,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
doc["overprintBlack"] = json!(d.b("overprintBlack"));
app.run("document.preferences", doc)
}
"newWorkspace" => app.run("window.newWorkspace", json!({"name": d.s("name")})),
"fittingOptions" => {
let m = |k: &str| d.fields.get(k).and_then(Value::as_str).and_then(|s| parse_measure(s, Unit::Points).ok()).unwrap_or(0.0);
app.run(

View File

@ -70,6 +70,20 @@ pub enum ScreenMode {
Presentation,
}
/// A saved workspace: which bars show and where the panels are.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SavedWorkspace {
pub name: String,
pub control_bar: bool,
pub task_bar: bool,
pub tools_double_column: bool,
pub dock_tab: String,
pub dock_expanded: bool,
pub open_panel: Option<String>,
pub floating: Vec<(String, [f32; 2])>,
}
/// Persisted UI state.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
@ -104,6 +118,8 @@ pub struct UiState {
pub dock_expanded: bool,
pub units: Unit,
pub workspace: String,
/// Window › Workspace › New Workspace: saved panel arrangements.
pub custom_workspaces: Vec<SavedWorkspace>,
/// Transform reference point (0..8, row-major; 0 = top-left).
pub ref_point: u8,
/// Align To target (`selection`, `keyObject`, `margins`, `page`, `spread`).
@ -153,6 +169,7 @@ impl Default for UiState {
dock_expanded: true,
units: Unit::Picas,
workspace: "Essentials".into(),
custom_workspaces: Vec::new(),
ref_point: 0,
align_to: "selection".into(),
text_style_tab: 0,

View File

@ -87,6 +87,9 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
None,
"{name: Essentials|Advanced|Book|Digital Publishing|Interactive for PDF|Printing and Proofing|Typography}",
),
("window.newWorkspace", "New Workspace…", None, "{name} — saves the current bars and panel arrangement"),
("window.deleteWorkspace", "Delete Workspace…", None, "{name}"),
("window.resetWorkspace", "Reset Workspace", None, "{} — back to the current workspace as saved (or its defaults)"),
("window.brightness", "Interface Color Theme", None, "{brightness: dark|mediumDark|mediumLight|light|highContrast}"),
];
@ -907,8 +910,56 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
Ok(json!({"url": url}))
}
"window.toolsDoubleColumn" => flag(&mut app.ui.tools_double_column),
"window.newWorkspace" => {
let Some(name) = p.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()) else {
app.ui.dialog = Some(crate::dialogs::Dialog::new("newWorkspace", json!({"name": ""})));
return Some(Ok(Value::Null));
};
let u = &app.ui;
let w = crate::SavedWorkspace {
name: name.to_string(),
control_bar: u.control_bar,
task_bar: u.task_bar,
tools_double_column: u.tools_double_column,
dock_tab: u.dock_tab.clone(),
dock_expanded: u.dock_expanded,
open_panel: u.open_panel.clone(),
floating: u.floating.clone(),
};
app.ui.custom_workspaces.retain(|x| x.name != name);
app.ui.custom_workspaces.push(w);
app.ui.workspace = name.to_string();
Ok(Value::Null)
}
"window.deleteWorkspace" => {
let name = p.get("name").and_then(Value::as_str).unwrap_or("");
let n = app.ui.custom_workspaces.len();
app.ui.custom_workspaces.retain(|x| x.name != name);
if app.ui.custom_workspaces.len() == n {
return Some(Err(format!("no saved workspace `{name}`")));
}
if app.ui.workspace == name {
app.ui.workspace = "Essentials".into();
}
Ok(Value::Null)
}
"window.resetWorkspace" => {
let name = app.ui.workspace.clone();
return run_ui(app, "window.workspace", &json!({"name": name}));
}
"window.workspace" => {
let name = p.get("name").and_then(Value::as_str).unwrap_or("Essentials");
if let Some(w) = app.ui.custom_workspaces.iter().find(|w| w.name == name).cloned() {
app.ui.control_bar = w.control_bar;
app.ui.task_bar = w.task_bar;
app.ui.tools_double_column = w.tools_double_column;
app.ui.dock_tab = w.dock_tab;
app.ui.dock_expanded = w.dock_expanded;
app.ui.open_panel = w.open_panel;
app.ui.floating = w.floating;
app.ui.workspace = w.name;
return Some(Ok(Value::Null));
}
// Workspaces choose which bars and panels are visible.
app.ui.control_bar = matches!(name, "Advanced" | "Typography" | "Printing and Proofing" | "Book");
app.ui.dock_tab = if name == "Typography" { "properties".into() } else { app.ui.dock_tab.clone() };
@ -1559,4 +1610,22 @@ mod tests {
// Fixed-parameter variants keep their own labels.
assert!(all.iter().any(|(l, id, p)| l == "Fill Frame Proportionally" && id == "object.fit" && p["mode"] == "fillProportionally"));
}
#[test]
fn custom_workspaces_save_apply_and_delete() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.ui.dock_tab = "swatches".into();
app.ui.control_bar = true;
run_ui(&mut app, "window.newWorkspace", &json!({"name": "Mine"})).unwrap().unwrap();
run_ui(&mut app, "window.workspace", &json!({"name": "Essentials"})).unwrap().unwrap();
app.ui.dock_tab = "properties".into();
run_ui(&mut app, "window.workspace", &json!({"name": "Mine"})).unwrap().unwrap();
assert_eq!((app.ui.dock_tab.as_str(), app.ui.control_bar, app.ui.workspace.as_str()), ("swatches", true, "Mine"));
app.ui.dock_tab = "layers".into();
run_ui(&mut app, "window.resetWorkspace", &json!({})).unwrap().unwrap();
assert_eq!(app.ui.dock_tab, "swatches", "reset to the saved arrangement");
run_ui(&mut app, "window.deleteWorkspace", &json!({"name": "Mine"})).unwrap().unwrap();
assert!(app.ui.custom_workspaces.is_empty());
assert_eq!(app.ui.workspace, "Essentials");
}
}

View File

@ -10,12 +10,12 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 99 | 3 | 0 | 99% |
| P1 | 97 | 85 | 11 | 1 | 93% |
| P1 | 97 | 86 | 10 | 1 | 94% |
| P2 | 76 | 23 | 13 | 40 | 39% |
| Area | Parity |
|---|---|
| Application shell & workspace | 80% |
| Application shell & workspace | 82% |
| Documents, pages, spreads | 85% |
| Layers | 95% |
| Frames, shapes & paths | 95% |
@ -45,7 +45,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
| Application shell & workspace | Multiple documents in tabs | P0 | D |
| Application shell & workspace | New Window for same document | P1 | M |
| Application shell & workspace | Dockable panels | P0 | D |
| Application shell & workspace | Workspaces | P1 | P |
| Application shell & workspace | Workspaces | P1 | D |
| Application shell & workspace | Control panel | P0 | D |
| Application shell & workspace | Properties panel | P0 | D |
| Application shell & workspace | Contextual task bar | P2 | D |