mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
T: drag selected table rows and columns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d30285c542
commit
b23f19f0c9
+1
-1
@@ -86,7 +86,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- 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).
|
||||
- Move table rows and columns (`table.moveRow`, `table.moveColumn`, Table panel buttons; with whole rows or columns selected, drag them with the Type tool — `table.dropCells`).
|
||||
- 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 too (`form.set/clear/list`: text fields, check boxes, combo and list boxes, signature fields; required, multiline, defaults) exported as AcroForm widgets through an incremental update (not in PDF/X).
|
||||
- Power Zoom (Hand tool: press and hold still for half a second, or Alt-press): zoom out to the spread, aim the red view rectangle, release to zoom back in there.
|
||||
|
||||
@@ -112,6 +112,15 @@ fn table_specs() -> Vec<CommandSpec> {
|
||||
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.dropCells",
|
||||
"Drag Rows or Columns",
|
||||
[],
|
||||
None,
|
||||
"{frame, from: [x, y], to: [x, y]} — selected whole rows (or columns) dragged from `from` move to the row (column) at `to`; otherwise a text drag",
|
||||
has_doc,
|
||||
drop_cells
|
||||
),
|
||||
cmd!(
|
||||
"table.placeGraphic",
|
||||
"Convert Cell to Graphic Cell",
|
||||
@@ -1132,6 +1141,76 @@ mod style_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The table cell under spread point `pt` in text frame `frame`: (story, table, row, col).
|
||||
fn cell_at(s: &Session, frame: designcraft_doc::ItemId, pt: designcraft_geom::Point) -> Option<(StoryId, u64, usize, usize)> {
|
||||
let st = s.active()?;
|
||||
let loc = st.doc.find(frame)?;
|
||||
let it = st.doc.item_at(&loc)?;
|
||||
let sid = it.text_frame()?.story;
|
||||
let inner = (st.doc.parent_xf(&loc) * it.text_xf()).inverse() * pt;
|
||||
let cs = s.cache.get(&st.doc, sid, None);
|
||||
let fi = cs.frames.iter().position(|f| f.frame == frame)?;
|
||||
let (table, row, col, _) = designcraft_compose::hit_cell(&cs, fi, inner)?;
|
||||
Some((sid, table, row, col))
|
||||
}
|
||||
|
||||
fn drop_cells(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "table.dropCells";
|
||||
let frame = super::id_param(p, "frame").ok_or_else(|| bad(ID, "`frame` required"))?;
|
||||
let pt = |k: &str| -> Result<designcraft_geom::Point> {
|
||||
let a = p.get(k).and_then(Value::as_array).ok_or_else(|| bad(ID, format!("`{k}` required")))?;
|
||||
Ok(designcraft_geom::Point::new(a.first().and_then(Value::as_f64).unwrap_or(0.0), a.get(1).and_then(Value::as_f64).unwrap_or(0.0)))
|
||||
};
|
||||
let (from, to) = (pt("from")?, pt("to")?);
|
||||
let sel = s.doc()?.selection.cells;
|
||||
let hit_from = cell_at(s, frame, from);
|
||||
let hit_to = cell_at(s, frame, to);
|
||||
if let (Some(ts), Some((sid, tid, fr, fc)), Some((_, tid2, tr, tc))) = (sel, hit_from, hit_to)
|
||||
&& ts.story == sid
|
||||
&& ts.table == tid
|
||||
&& tid2 == tid
|
||||
&& ts.range.contains(fr, fc)
|
||||
{
|
||||
let (nr, nc) = {
|
||||
let t = s.doc()?.doc.story(sid).and_then(|st| st.tables.get(&tid)).ok_or_else(|| bad(ID, "no table"))?;
|
||||
(t.nrows(), t.ncols())
|
||||
};
|
||||
let r = ts.range;
|
||||
let rows = r.c0 == 0 && r.c1 + 1 == nc;
|
||||
let cols = r.r0 == 0 && r.r1 + 1 == nr;
|
||||
if rows || cols {
|
||||
let (lo, hi, target) = if rows { (r.r0, r.r1, tr) } else { (r.c0, r.c1, tc) };
|
||||
if target >= lo && target <= hi {
|
||||
return Ok(json!({"moved": 0}));
|
||||
}
|
||||
let n = hi - lo + 1;
|
||||
let r2 = s.edit(|d, selm| {
|
||||
let t = d.story_mut(sid).and_then(|st| st.table_mut(tid)).ok_or_else(|| bad(ID, "no table"))?;
|
||||
let mv = |t: &mut designcraft_doc::Table, a: usize, b: usize| if rows { t.move_row(a, b) } else { t.move_col(a, b) };
|
||||
let new_lo = if target > hi {
|
||||
for _ in 0..n {
|
||||
mv(t, lo, target).map_err(|e| bad(ID, e))?;
|
||||
}
|
||||
target + 1 - n
|
||||
} else {
|
||||
for k in 0..n {
|
||||
mv(t, lo + k, target + k).map_err(|e| bad(ID, e))?;
|
||||
}
|
||||
target
|
||||
};
|
||||
let range =
|
||||
if rows { CellRange::new(new_lo, r.c0, new_lo + n - 1, r.c1) } else { CellRange::new(r.r0, new_lo, r.r1, new_lo + n - 1) };
|
||||
selm.cells = Some(TableSel { range, ..ts });
|
||||
Ok(json!({"moved": n, "to": new_lo}))
|
||||
})?;
|
||||
return Ok(r2);
|
||||
}
|
||||
}
|
||||
// Not a row/column drag: select text from the press to the release.
|
||||
s.execute("text.placeCaret", &json!({"frame": frame.0, "point": [from.x, from.y]}))?;
|
||||
s.execute("text.extendTo", &json!({"frame": frame.0, "point": [to.x, to.y]}))
|
||||
}
|
||||
|
||||
fn place_graphic(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "table.placeGraphic";
|
||||
let g = target(s, p, ID)?;
|
||||
@@ -1306,6 +1385,23 @@ mod sort_tests {
|
||||
let t = d.stories.values().flat_map(|st| st.tables.values()).next().unwrap();
|
||||
t.cell(r, c).unwrap().text.text.clone()
|
||||
};
|
||||
// Dragging a selected row onto the last row moves it there.
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let (sid, tid) = d.stories.values().find_map(|st| st.tables.keys().next().map(|k| (st.id, *k))).unwrap();
|
||||
let fid = d.stories[&sid].frames[0];
|
||||
let cs = s.cache.get(&d, sid, None);
|
||||
let cells = &cs.frames[0].tables[0].cells;
|
||||
let centre = |r: usize| {
|
||||
let c = cells.iter().find(|c| c.row == r && c.col == 0).unwrap().rect.center();
|
||||
d.item(fid).unwrap().text_xf() * c
|
||||
};
|
||||
let (from, to) = (centre(0), centre(2));
|
||||
s.execute("table.select", &json!({"story": sid.0, "table": tid, "rows": [0, 0], "what": "row"})).unwrap();
|
||||
let r2 = s.execute("table.dropCells", &json!({"frame": fid.0, "from": [from.x, from.y], "to": [to.x, to.y]})).unwrap();
|
||||
assert_eq!(r2["moved"], 1);
|
||||
assert_eq!([cell(&s, 0, 0), cell(&s, 1, 0), cell(&s, 2, 0)], ["b", "c", "a"]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!([cell(&s, 0, 0), cell(&s, 1, 0), cell(&s, 2, 0)], ["a", "b", "c"]);
|
||||
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();
|
||||
|
||||
@@ -14,6 +14,17 @@ pub struct TypeTool {
|
||||
start: Option<Point>,
|
||||
selecting: Option<u64>,
|
||||
drawing: bool,
|
||||
/// A press on a table whose whole rows or columns are selected: (frame, press point in
|
||||
/// spread space) — a drag moves them, a click places the caret.
|
||||
cell_drag: Option<(u64, Point)>,
|
||||
}
|
||||
|
||||
/// Are whole rows or whole columns of a table selected?
|
||||
fn whole_rows_or_cols(cx: &ToolContext) -> bool {
|
||||
let Some(ts) = cx.selection.cells else { return false };
|
||||
let Some(t) = cx.doc.story(ts.story).and_then(|s| s.tables.get(&ts.table)) else { return false };
|
||||
let r = ts.range;
|
||||
(r.c0 == 0 && r.c1 + 1 == t.ncols()) || (r.r0 == 0 && r.r1 + 1 == t.nrows())
|
||||
}
|
||||
|
||||
impl TypeTool {
|
||||
@@ -33,11 +44,17 @@ impl Tool for TypeTool {
|
||||
self.start = Some(ev.pos);
|
||||
self.drawing = false;
|
||||
self.selecting = None;
|
||||
self.cell_drag = None;
|
||||
if let Some((_, id)) = cx.hit(ev.pos)
|
||||
&& let Some(it) = cx.doc.item(id)
|
||||
&& !matches!(it.content, designcraft_doc::Content::Graphic(_) | designcraft_doc::Content::Group { .. })
|
||||
{
|
||||
let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos);
|
||||
// Whole rows/columns selected: wait to see whether this is a drag.
|
||||
if ev.kind == PointerKind::Down && !ev.mods.shift && whole_rows_or_cols(cx) {
|
||||
self.cell_drag = Some((id.0, sp));
|
||||
return vec![];
|
||||
}
|
||||
self.selecting = Some(id.0);
|
||||
let cmd = if ev.kind == PointerKind::DoubleClick {
|
||||
"text.selectWord"
|
||||
@@ -52,6 +69,9 @@ impl Tool for TypeTool {
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some(a) = self.start else { return vec![] };
|
||||
if self.cell_drag.is_some() {
|
||||
return vec![];
|
||||
}
|
||||
if let Some(fid) = self.selecting {
|
||||
let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos);
|
||||
return vec![Action::Exec("text.extendTo".into(), json!({"frame": fid, "point": [sp.x, sp.y]}))];
|
||||
@@ -75,6 +95,15 @@ impl Tool for TypeTool {
|
||||
}
|
||||
PointerKind::Up => {
|
||||
let start = self.start.take();
|
||||
if let Some((fid, from)) = self.cell_drag.take() {
|
||||
let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos);
|
||||
let moved = start.is_some_and(|a| (ev.pos - a).hypot() >= cx.tol(3.0));
|
||||
return if moved {
|
||||
vec![Action::Exec("table.dropCells".into(), json!({"frame": fid, "from": [from.x, from.y], "to": [sp.x, sp.y]}))]
|
||||
} else {
|
||||
vec![Action::Exec("text.placeCaret".into(), json!({"frame": fid, "point": [from.x, from.y]}))]
|
||||
};
|
||||
}
|
||||
if std::mem::take(&mut self.drawing) {
|
||||
self.selecting = None;
|
||||
return vec![Action::Commit];
|
||||
|
||||
+4
-4
@@ -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: 98%** over 275 features.
|
||||
**Weighted breadth parity: 99%** over 275 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 96 | 1 | 0 | 99% |
|
||||
| P2 | 76 | 66 | 10 | 0 | 93% |
|
||||
| P2 | 76 | 67 | 9 | 0 | 94% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -26,7 +26,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Type & text frames | 99% |
|
||||
| Typography | 100% |
|
||||
| Styles | 100% |
|
||||
| Tables | 98% |
|
||||
| Tables | 100% |
|
||||
| Long documents | 100% |
|
||||
| Interactivity & digital | 100% |
|
||||
| Output & production | 92% |
|
||||
@@ -259,7 +259,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| Tables | Tables spanning frames | P1 | D |
|
||||
| Tables | Sort table rows | P2 | D |
|
||||
| Tables | Excel import | P2 | D |
|
||||
| Tables | Drag to move rows/columns | P2 | P |
|
||||
| Tables | Drag to move rows/columns | P2 | D |
|
||||
| Long documents | Table of contents | P1 | D |
|
||||
| Long documents | Index | P2 | D |
|
||||
| Long documents | Books | P2 | D |
|
||||
|
||||
Reference in New Issue
Block a user