diff --git a/ROADMAP.md b/ROADMAP.md index 853cb4f..9ff0a22 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. +- Move table rows and columns (`table.moveRow`, `table.moveColumn`, Table panel buttons; dragging on the canvas isn't there yet). - 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. diff --git a/crates/doc/src/table.rs b/crates/doc/src/table.rs index c282c5a..0531e61 100644 --- a/crates/doc/src/table.rs +++ b/crates/doc/src/table.rs @@ -691,6 +691,44 @@ impl Table { } } + /// Move row `from` to position `to` (indices before the move). Refused when merged cells span + /// rows (the move would cut them). + pub fn move_row(&mut self, from: usize, to: usize) -> Result<(), String> { + let (nr, nc) = (self.nrows(), self.ncols()); + if from >= nr || to >= nr { + return Err("no such row".into()); + } + if self.regions().iter().any(|r| r.2 > 1) { + return Err("unmerge cells that span rows first".into()); + } + let row = self.rows.remove(from); + self.rows.insert(to, row); + let cells: Vec = self.cells.drain(from * nc..(from + 1) * nc).collect(); + let at = to * nc; + for (k, c) in cells.into_iter().enumerate() { + self.cells.insert(at + k, c); + } + Ok(()) + } + + /// Move column `from` to position `to`. Refused when merged cells span columns. + pub fn move_col(&mut self, from: usize, to: usize) -> Result<(), String> { + let (nr, nc) = (self.nrows(), self.ncols()); + if from >= nc || to >= nc { + return Err("no such column".into()); + } + if self.regions().iter().any(|r| r.3 > 1) { + return Err("unmerge cells that span columns first".into()); + } + let col = self.columns.remove(from); + self.columns.insert(to, col); + for r in 0..nr { + let c = self.cells.remove(r * nc + from); + self.cells.insert(r * nc + to, c); + } + Ok(()) + } + /// Mark the first `header` rows as header rows and the last `footer` rows as footer rows. pub fn set_header_footer(&mut self, header: usize, footer: usize) { let n = self.nrows(); diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index f92b383..6b57ccf 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -112,6 +112,18 @@ fn table_specs() -> Vec { cmd!("table.deleteColumn", "Delete Column", ["Table", "Delete"], None, "{} — the columns of the target cells", in_table, delete_cols), cmd!("table.delete", "Delete Table", ["Table", "Delete"], None, "{}", in_table, delete_table), cmd!("table.merge", "Merge Cells", ["Table"], None, "{} — merge the target cell range", in_table, merge), + cmd!( + "table.moveRow", + "Move Row", + [], + None, + "{from?, to} (0-based; from defaults to the target row) — like dragging a row", + in_table, + |s, p| move_rc(s, p, true) + ), + cmd!("table.moveColumn", "Move Column", [], None, "{from?, to} (0-based; from defaults to the target column)", in_table, |s, p| move_rc( + s, p, false + )), cmd!( "table.sortRows", "Sort", @@ -1090,6 +1102,17 @@ mod style_tests { } } +fn move_rc(s: &mut Session, p: &Value, rows: bool) -> Result { + const ID: &str = "table.move"; + let g = target(s, p, ID)?; + let from = p.get("from").and_then(Value::as_u64).map_or(if rows { g.range.r0 } else { g.range.c0 }, |v| v as usize); + let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(ID, "`to` required"))? as usize; + edit_table(s, &g, ID, |t| { + if rows { t.move_row(from, to) } else { t.move_col(from, to) }.map_err(|e| bad(ID, e))?; + Ok(json!({"from": from, "to": to})) + }) +} + fn sort_rows(s: &mut Session, p: &Value) -> Result { let g = target(s, p, "table.sortRows")?; let col = p.get("column").and_then(Value::as_u64).map_or(g.range.c0, |c| c as usize); @@ -1158,4 +1181,25 @@ mod sort_tests { let r = s.execute("table.splitHorizontally", &json!({"rows": [0, 0], "cols": [1, 1]})).unwrap(); assert_eq!((r["rows"].as_u64(), r["cols"].as_u64()), (Some(5), Some(3))); } + + #[test] + fn move_rows_and_columns() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let r = s.execute("frame.create", &json!({"rect": [72, 72, 500, 400], "content": "text", "text": ""})).unwrap(); + s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 0})).unwrap(); + s.execute("table.insert", &json!({"rows": 3, "cols": 2})).unwrap(); + for (r, c, t) in [(0, 0, "a"), (1, 0, "b"), (2, 0, "c"), (0, 1, "x")] { + s.execute("table.setCell", &json!({"rows": [r, r], "cols": [c, c], "text": t})).unwrap(); + } + let cell = |s: &Session, r: usize, c: usize| { + let d = &s.doc().unwrap().doc; + let t = d.stories.values().flat_map(|st| st.tables.values()).next().unwrap(); + t.cell(r, c).unwrap().text.text.clone() + }; + s.execute("table.moveRow", &json!({"from": 0, "to": 2})).unwrap(); + assert_eq!([cell(&s, 0, 0), cell(&s, 1, 0), cell(&s, 2, 0)], ["b", "c", "a"]); + s.execute("table.moveColumn", &json!({"from": 1, "to": 0})).unwrap(); + assert_eq!((cell(&s, 2, 0), cell(&s, 2, 1)), ("x".to_string(), "a".to_string())); + } } diff --git a/crates/ui-egui/src/panels/table.rs b/crates/ui-egui/src/panels/table.rs index e243aa7..f9e4e81 100644 --- a/crates/ui-egui/src/panels/table.rs +++ b/crates/ui-egui/src/panels/table.rs @@ -28,6 +28,23 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { let run = |app: &mut DesignApp, id: &str, p: Value| { let _ = app.run(id, p); }; + // Move the target row / column (InDesign drags them). + ui.horizontal(|ui| { + caption(ui, "Move"); + let (nr, nc) = (rows.len(), cols.len()); + for (label, id, from, to, ok) in [ + ("Row ↑", "table.moveRow", r0, r0.saturating_sub(1), r0 > 0), + ("Row ↓", "table.moveRow", r0, r0 + 1, r0 + 1 < nr), + ("Col ←", "table.moveColumn", c0, c0.saturating_sub(1), c0 > 0), + ("Col →", "table.moveColumn", c0, c0 + 1, c0 + 1 < nc), + ] { + if ui.add_enabled(ok, egui::Button::new(label).small()).clicked() + && let Err(e) = app.run(id, json!({"from": from, "to": to})) + { + app.status(format!("Table: {e}")); + } + } + }); // Table and cell styles: pick to apply, + to save the current look as a new style. if let Some(st) = app.session.active() { let styles = &st.doc.styles; diff --git a/docs/parity.md b/docs/parity.md index 15ade91..5c69baa 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia |---|---|---|---|---|---| | P0 | 102 | 99 | 3 | 0 | 99% | | P1 | 97 | 92 | 4 | 1 | 97% | -| P2 | 76 | 34 | 21 | 21 | 59% | +| P2 | 76 | 34 | 22 | 20 | 59% | | Area | Parity | |---|---| @@ -26,7 +26,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Type & text frames | 94% | | Typography | 98% | | Styles | 97% | -| Tables | 92% | +| Tables | 94% | | Long documents | 96% | | Interactivity & digital | 75% | | Output & production | 80% | @@ -259,7 +259,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t | Tables | Tables spanning frames | P1 | D | | Tables | Sort table rows | P2 | D | | Tables | Excel import | P2 | D | -| Tables | Drag to move rows/columns | P2 | M | +| Tables | Drag to move rows/columns | P2 | P | | Long documents | Table of contents | P1 | D | | Long documents | Index | P2 | D | | Long documents | Books | P2 | D |