T: accept/reject single changes (+ Track Changes panel)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 07:51:59 -04:00
1 parent 7ca9c5814f
commit 5bee68cf88
6 files changed
+98 -5

No files matched your search

+1
View File
@@ -83,6 +83,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- View ▸ Separations Preview (`view.separations`): one process plate as ink density, or areas over an ink limit in red (a GCR preview of the rendered view; spot plates and the flattener preview aren't there).
- Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, style sync from a style source, one merged PDF.
- Track Changes (`changes.track/list/acceptAll/rejectAll`, Type ▸ Track Changes): typing is marked added (highlighted on screen), deletions are kept but hidden from layout and exports until accepted or rejected. Per-change accept/reject and authors aren't there yet.
- Track Changes: per-change Accept / Reject (`changes.accept`, `changes.reject`) and a Track Changes panel listing every change.
- Move table rows and columns (`table.moveRow`, `table.moveColumn`, Table panel buttons; dragging on the canvas isn't there yet).
- Object States (`states.create/show/rename/release/list`, Object States panel): multi-state objects show one state on screen and in PDF.
- Buttons (`button.set/clear/list`, Buttons and Forms panel): go to page / next / previous / first / last / URL as PDF link annotations. Form fields need AcroForm support the PDF writer lacks.
+40
View File
@@ -54,6 +54,35 @@ fn resolve(st: &mut Story, accept: bool) -> usize {
n
}
/// Accept or reject the one change at `start` in story `story`.
fn one(s: &mut Session, p: &Value, accept: bool) -> Result<Value> {
let id = if accept { "changes.accept" } else { "changes.reject" };
let sid = p.get("story").and_then(Value::as_u64).map(StoryId).ok_or_else(|| super::bad(id, "`story` required"))?;
let start = p.get("start").and_then(Value::as_u64).ok_or_else(|| super::bad(id, "`start` required"))? as usize;
s.edit(|d, sel| {
let st = d.story_mut(sid).ok_or_else(|| super::bad(id, "no such story"))?;
let (r, mark) = st
.runs()
.find(|(r, f)| matches!(f.over.change, Some(ChangeMark::Inserted | ChangeMark::Deleted)) && r.start <= start && start < r.end)
.map(|(r, f)| (r, f.over.change.unwrap_or_default()))
.ok_or_else(|| super::bad(id, "no change there"))?;
// Accepting a deletion or rejecting an insertion removes the text; otherwise it stays, unmarked.
if (mark == ChangeMark::Deleted) == accept {
st.delete(r.clone());
} else {
st.format_chars(r.clone(), |f| f.over.change = None);
}
if let Some(t) = &mut sel.text
&& t.story == sid
{
let len = d.story(sid).map_or(0, |x| x.len());
t.anchor = t.anchor.min(len);
t.focus = t.focus.min(len);
}
Ok(json!({"start": r.start, "end": r.end}))
})
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("changes.track", "Track Changes", ["Type", "Track Changes"], None, "{on?: bool (default: toggle)}", has_doc, |s, p| {
@@ -79,6 +108,8 @@ pub fn specs() -> Vec<CommandSpec> {
}
Ok(Value::Array(out))
}),
cmd!("changes.accept", "Accept Change", [], None, "{story, start} — the change at that position", has_doc, |s, p| one(s, p, true)),
cmd!("changes.reject", "Reject Change", [], None, "{story, start} — the change at that position", has_doc, |s, p| one(s, p, false)),
cmd!(
"changes.acceptAll",
"Accept All Changes",
@@ -147,6 +178,15 @@ mod tests {
let d = s.doc().unwrap().doc.clone();
let cs = s.cache.get(&d, sid, None);
assert!(cs.frames[0].lines[0].glyphs.iter().any(|g| !g.visible && g.byte >= 4 && g.byte < 7));
// One at a time: accept the deletion of "cat", then reject the insertion of "dog".
s.execute("changes.accept", &json!({"story": sid.0, "start": 4})).unwrap();
assert_eq!(text(&s), "The dog sat");
s.execute("changes.reject", &json!({"story": sid.0, "start": 4})).unwrap();
assert_eq!(text(&s), "The sat");
assert!(s.execute("changes.accept", &json!({"story": sid.0, "start": 0})).is_err());
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(text(&s), "The catdog sat");
s.execute("changes.rejectAll", &json!({})).unwrap();
assert_eq!(text(&s), "The cat sat");
s.execute("edit.undo", &json!({})).unwrap();
+2
View File
@@ -43,6 +43,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
("liquid", "Liquid Layout", "panel-pages"),
("media", "Media", "panel-articles"),
("transitions", "Page Transitions", "panel-pages"),
("trackChanges", "Track Changes", "panel-articles"),
];
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
@@ -191,6 +192,7 @@ pub fn panel_body(app: &mut DesignApp, ui: &mut egui::Ui, id: &str) {
"liquid" => panels::interactive::liquid(app, ui),
"media" => panels::interactive::media(app, ui),
"transitions" => panels::interactive::transitions(app, ui),
"trackChanges" => panels::interactive::track_changes(app, ui),
"table" => panels::table::show(app, ui),
_ => panels::properties::info_panel(app, ui),
}
+1
View File
@@ -585,6 +585,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
"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|Liquid Layout|{\"panel\": \"liquid\"}",
"ui:window.panel|Tags|{\"panel\": \"tags\"}",
"ui:window.panel|Book|{\"panel\": \"book\"}",
+49
View File
@@ -417,3 +417,52 @@ pub fn transitions(app: &mut DesignApp, ui: &mut egui::Ui) {
}
ui.label(egui::RichText::new("Transitions play in interactive PDF (full-screen mode).").size(10.5).color(t.text_dim));
}
/// Track Changes panel: tracking on/off and each change with Accept / Reject.
pub fn track_changes(app: &mut DesignApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
let Some(on) = app.session.active().map(|d| d.doc.settings.track_changes) else { return };
let mut track = on;
if ui.checkbox(&mut track, "Track Changes in All Stories").changed() {
let _ = app.run("changes.track", json!({"on": track}));
}
let list = app.session.execute("changes.list", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
ui.separator();
if list.is_empty() {
ui.label(egui::RichText::new("No changes.").size(11.0).color(t.text_dim));
return;
}
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
for c in &list {
let (story, start) = (c["story"].clone(), c["start"].clone());
let kind = c["kind"].as_str().unwrap_or("");
let text: String = c["text"].as_str().unwrap_or("").chars().take(40).collect();
ui.horizontal(|ui| {
let label =
egui::RichText::new(format!("{} “{}”", if kind == "inserted" { "Added" } else { "Deleted" }, text.replace('\n', ""))).size(11.0);
let label = if kind == "deleted" { label.strikethrough() } else { label };
if ui.selectable_label(false, label).on_hover_text("Show in the text").clicked() {
let end = c["end"].clone();
let _ = app.run("text.select", json!({"story": story, "anchor": start, "focus": end}));
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("Reject").clicked() {
let _ = app.run("changes.reject", json!({"story": story, "start": start}));
}
if ui.small_button("Accept").clicked() {
let _ = app.run("changes.accept", json!({"story": story, "start": start}));
}
});
});
}
});
ui.separator();
ui.horizontal(|ui| {
if ui.button("Accept All").clicked() {
let _ = app.run("changes.acceptAll", json!({}));
}
if ui.button("Reject All").clicked() {
let _ = app.run("changes.rejectAll", json!({}));
}
});
}
+5 -5
View File
@@ -5,13 +5,13 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of
Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded.
<!-- SUMMARY -->
**Weighted breadth parity: 95%** over 275 features.
**Weighted breadth parity: 96%** over 275 features.
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 100 | 2 | 0 | 99% |
| P1 | 97 | 94 | 3 | 0 | 98% |
| P2 | 76 | 41 | 29 | 6 | 73% |
| P2 | 76 | 43 | 27 | 6 | 74% |
| Area | Parity |
|---|---|
@@ -23,7 +23,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Fill, stroke, colour | 99% |
| Effects & transparency | 75% |
| Placing & links | 91% |
| Type & text frames | 95% |
| Type & text frames | 96% |
| Typography | 98% |
| Styles | 97% |
| Tables | 98% |
@@ -188,7 +188,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| Type & text frames | Type on a path | P1 | D |
| Type & text frames | Vertical type | P2 | M |
| Type & text frames | Story Editor | P1 | D |
| Type & text frames | Track Changes | P2 | P |
| Type & text frames | Track Changes | P2 | D |
| Type & text frames | Notes | P2 | D |
| Type & text frames | Find/Change | P0 | D |
| Type & text frames | Find Font | P0 | D |
@@ -210,7 +210,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| Type & text frames | Text anchors & hyperlinks | P1 | D |
| Type & text frames | Data merge | P2 | D |
| Type & text frames | Smart quotes | P0 | D |
| Type & text frames | Autocorrect / Dynamic spelling | P2 | P |
| Type & text frames | Autocorrect / Dynamic spelling | P2 | D |
| Type & text frames | Drag & drop text editing | P1 | D |
| Type & text frames | Paste text with/without formatting | P0 | D |
| Type & text frames | Math expressions | P2 | M |