mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 03:56:50 -06:00
Table: inserting and deleting rows and columns keeps merges consistent (#408)
Row and column insert/delete ignored merged cells, leaving covered cells with no anchor or new cells overlapping an existing span. These edits now read the merged areas first, shift or resize each one (a row or column inserted inside a merge widens it, one deleted from it narrows it), move the anchor and its text to the next covered cell when the anchor row or column is deleted, and rebuild spans and merge flags from the result. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -106,39 +106,120 @@ impl Table {
|
||||
pub fn height(&self) -> f64 {
|
||||
self.rows.iter().map(|r| r.height).sum()
|
||||
}
|
||||
/// Insert an empty row before `at`. A merge the new row falls inside grows to cover it.
|
||||
pub fn insert_row(&mut self, at: usize) {
|
||||
let at = at.min(self.rows.len());
|
||||
let merges = self.merges();
|
||||
let h = self.rows.get(at.saturating_sub(1)).map(|r| r.height).unwrap_or(30.0);
|
||||
self.rows.insert(at, Row { height: h, cells: (0..self.cols.len()).map(|_| Cell::new("")).collect() });
|
||||
self.set_merges(merges.into_iter().map(|m| m.insert_row(at)));
|
||||
}
|
||||
/// Insert an empty column before `at`. A merge the new column falls inside grows to cover it.
|
||||
pub fn insert_col(&mut self, at: usize) {
|
||||
let at = at.min(self.cols.len());
|
||||
let merges = self.merges();
|
||||
let w = self.cols.get(at.saturating_sub(1)).copied().unwrap_or(80.0);
|
||||
self.cols.insert(at, w);
|
||||
for r in &mut self.rows {
|
||||
let i = at.min(r.cells.len());
|
||||
r.cells.insert(i, Cell::new(""));
|
||||
}
|
||||
self.set_merges(merges.into_iter().map(|m| m.insert_col(at)));
|
||||
}
|
||||
/// Delete row `at`. Merges through it shrink; a merge anchored on it moves its anchor (and
|
||||
/// text) to the next row it covers.
|
||||
pub fn delete_row(&mut self, at: usize) -> bool {
|
||||
if self.rows.len() <= 1 || at >= self.rows.len() {
|
||||
return false;
|
||||
}
|
||||
let merges = self.merges();
|
||||
for m in &merges {
|
||||
if m.r == at && m.rs > 1 {
|
||||
self.move_anchor((m.r, m.c), (m.r + 1, m.c));
|
||||
}
|
||||
}
|
||||
self.rows.remove(at);
|
||||
self.set_merges(merges.into_iter().filter_map(|m| m.delete_row(at)));
|
||||
true
|
||||
}
|
||||
/// Delete column `at`. Merges through it shrink; a merge anchored on it moves its anchor (and
|
||||
/// text) to the next column it covers.
|
||||
pub fn delete_col(&mut self, at: usize) -> bool {
|
||||
if self.cols.len() <= 1 || at >= self.cols.len() {
|
||||
return false;
|
||||
}
|
||||
let merges = self.merges();
|
||||
for m in &merges {
|
||||
if m.c == at && m.gs > 1 {
|
||||
self.move_anchor((m.r, m.c), (m.r, m.c + 1));
|
||||
}
|
||||
}
|
||||
self.cols.remove(at);
|
||||
for r in &mut self.rows {
|
||||
if at < r.cells.len() {
|
||||
r.cells.remove(at);
|
||||
}
|
||||
}
|
||||
self.set_merges(merges.into_iter().filter_map(|m| m.delete_col(at)));
|
||||
true
|
||||
}
|
||||
/// The merged areas: every uncovered cell spanning more than one cell, clipped to the grid.
|
||||
fn merges(&self) -> Vec<Merge> {
|
||||
let (nr, nc) = (self.rows.len(), self.cols.len());
|
||||
let mut out = vec![];
|
||||
for (r, row) in self.rows.iter().enumerate() {
|
||||
for (c, cell) in row.cells.iter().enumerate().take(nc) {
|
||||
if cell.is_covered() {
|
||||
continue;
|
||||
}
|
||||
let rs = (cell.row_span.max(1) as usize).min(nr - r);
|
||||
let gs = (cell.grid_span.max(1) as usize).min(nc - c);
|
||||
if rs > 1 || gs > 1 {
|
||||
out.push(Merge { r, c, rs, gs });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Rebuild every cell's spans and merge flags from `merges`.
|
||||
fn set_merges(&mut self, merges: impl IntoIterator<Item = Merge>) {
|
||||
for row in &mut self.rows {
|
||||
for cell in &mut row.cells {
|
||||
cell.grid_span = 1;
|
||||
cell.row_span = 1;
|
||||
cell.h_merge = false;
|
||||
cell.v_merge = false;
|
||||
}
|
||||
}
|
||||
for m in merges {
|
||||
if m.rs <= 1 && m.gs <= 1 {
|
||||
continue;
|
||||
}
|
||||
for r in m.r..m.r.saturating_add(m.rs) {
|
||||
for c in m.c..m.c.saturating_add(m.gs) {
|
||||
if let Some(cell) = self.cell_mut(r, c) {
|
||||
cell.h_merge = c > m.c;
|
||||
cell.v_merge = r > m.r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(cell) = self.cell_mut(m.r, m.c) {
|
||||
cell.grid_span = u32::try_from(m.gs).unwrap_or(u32::MAX);
|
||||
cell.row_span = u32::try_from(m.rs).unwrap_or(u32::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Swap the anchor cell's contents into the cell that takes over as anchor.
|
||||
fn move_anchor(&mut self, from: (usize, usize), to: (usize, usize)) {
|
||||
let Some(a) = self.cell(from.0, from.1).cloned() else { return };
|
||||
let Some(b) = self.cell(to.0, to.1).cloned() else { return };
|
||||
if let Some(x) = self.cell_mut(to.0, to.1) {
|
||||
*x = a;
|
||||
}
|
||||
if let Some(x) = self.cell_mut(from.0, from.1) {
|
||||
*x = b;
|
||||
}
|
||||
}
|
||||
/// Merge the rectangle (r0,c0)–(r1,c1) inclusive into its top-left cell.
|
||||
pub fn merge(&mut self, r0: usize, c0: usize, r1: usize, c1: usize) -> bool {
|
||||
let (r0, r1) = (r0.min(r1), r0.max(r1));
|
||||
@@ -203,6 +284,56 @@ impl Table {
|
||||
}
|
||||
}
|
||||
|
||||
/// A merged area: anchor (r, c) spanning `rs` rows and `gs` columns.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Merge {
|
||||
r: usize,
|
||||
c: usize,
|
||||
rs: usize,
|
||||
gs: usize,
|
||||
}
|
||||
|
||||
impl Merge {
|
||||
fn insert_row(self, at: usize) -> Self {
|
||||
if at <= self.r {
|
||||
Merge { r: self.r + 1, ..self }
|
||||
} else if at < self.r + self.rs {
|
||||
Merge { rs: self.rs + 1, ..self }
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
fn insert_col(self, at: usize) -> Self {
|
||||
if at <= self.c {
|
||||
Merge { c: self.c + 1, ..self }
|
||||
} else if at < self.c + self.gs {
|
||||
Merge { gs: self.gs + 1, ..self }
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
fn delete_row(self, at: usize) -> Option<Self> {
|
||||
let m = if at < self.r {
|
||||
Merge { r: self.r - 1, ..self }
|
||||
} else if at < self.r + self.rs {
|
||||
Merge { rs: self.rs - 1, ..self }
|
||||
} else {
|
||||
self
|
||||
};
|
||||
(m.rs > 1 || m.gs > 1).then_some(m)
|
||||
}
|
||||
fn delete_col(self, at: usize) -> Option<Self> {
|
||||
let m = if at < self.c {
|
||||
Merge { c: self.c - 1, ..self }
|
||||
} else if at < self.c + self.gs {
|
||||
Merge { gs: self.gs - 1, ..self }
|
||||
} else {
|
||||
self
|
||||
};
|
||||
(m.rs > 1 || m.gs > 1).then_some(m)
|
||||
}
|
||||
}
|
||||
|
||||
/// A table style defined in the file (`ppt/tableStyles.xml`), as PowerPoint writes the built-in
|
||||
/// styles a deck uses: how each part of a table (whole table, banded rows and columns, first and
|
||||
/// last rows and columns, corner cells) is filled, bordered and lettered.
|
||||
@@ -329,6 +460,97 @@ pub fn table_styles() -> Vec<(String, String)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every cell is owned by exactly one anchor (itself or a span over it), and no span leaves the grid.
|
||||
fn assert_topology(t: &Table) {
|
||||
let (nr, nc) = (t.n_rows(), t.n_cols());
|
||||
let mut owner = vec![vec![0u32; nc]; nr];
|
||||
for (r, row) in t.rows.iter().enumerate() {
|
||||
assert_eq!(row.cells.len(), nc);
|
||||
for (c, cell) in row.cells.iter().enumerate() {
|
||||
if cell.is_covered() {
|
||||
assert_eq!((cell.grid_span, cell.row_span), (1, 1), "covered ({r},{c}) has spans");
|
||||
continue;
|
||||
}
|
||||
let (rs, gs) = (cell.row_span as usize, cell.grid_span as usize);
|
||||
assert!(r + rs <= nr && c + gs <= nc, "span at ({r},{c}) leaves the grid");
|
||||
for o in owner.iter_mut().skip(r).take(rs) {
|
||||
for x in o.iter_mut().skip(c).take(gs) {
|
||||
*x += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (r, o) in owner.iter().enumerate() {
|
||||
for (c, n) in o.iter().enumerate() {
|
||||
assert_eq!(*n, 1, "cell ({r},{c}) owned {n} times");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merged() -> Table {
|
||||
// 4x4 with a 2x2 merge at (1,1) holding "M".
|
||||
let mut t = Table::new(4, 4, 400.0, 30.0);
|
||||
if let Some(c) = t.cell_mut(1, 1) {
|
||||
c.text = TextBody::from_text("M");
|
||||
}
|
||||
assert!(t.merge(1, 1, 2, 2));
|
||||
assert_topology(&t);
|
||||
t
|
||||
}
|
||||
|
||||
fn anchor_text(t: &Table, r: usize, c: usize) -> String {
|
||||
t.cell(r, c).unwrap().text.text()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_and_column_edits_keep_merges_consistent() {
|
||||
for at in 0..=4 {
|
||||
let mut t = merged();
|
||||
t.insert_row(at);
|
||||
assert_topology(&t);
|
||||
let mut t = merged();
|
||||
t.insert_col(at);
|
||||
assert_topology(&t);
|
||||
}
|
||||
for at in 0..4 {
|
||||
let mut t = merged();
|
||||
assert!(t.delete_row(at));
|
||||
assert_topology(&t);
|
||||
let mut t = merged();
|
||||
assert!(t.delete_col(at));
|
||||
assert_topology(&t);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserting_inside_a_merge_grows_it() {
|
||||
let mut t = merged();
|
||||
t.insert_row(2);
|
||||
let a = t.cell(1, 1).unwrap();
|
||||
assert_eq!((a.row_span, a.grid_span), (3, 2));
|
||||
assert!(t.cell(2, 2).unwrap().h_merge && t.cell(2, 2).unwrap().v_merge);
|
||||
t.insert_col(0);
|
||||
assert_eq!(t.cell(1, 2).unwrap().grid_span, 2);
|
||||
assert_eq!(anchor_text(&t, 1, 2), "M");
|
||||
assert_topology(&t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_the_anchor_row_or_column_keeps_the_text() {
|
||||
let mut t = merged();
|
||||
assert!(t.delete_row(1));
|
||||
let a = t.cell(1, 1).unwrap();
|
||||
assert!(!a.is_covered());
|
||||
assert_eq!((a.row_span, a.grid_span), (1, 2));
|
||||
assert_eq!(anchor_text(&t, 1, 1), "M");
|
||||
assert!(t.delete_col(1));
|
||||
let a = t.cell(1, 1).unwrap();
|
||||
assert!(!a.is_covered());
|
||||
assert_eq!((a.row_span, a.grid_span), (1, 1));
|
||||
assert_eq!(anchor_text(&t, 1, 1), "M");
|
||||
assert_topology(&t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_merge_split() {
|
||||
let mut t = Table::new(3, 3, 300.0, 30.0);
|
||||
|
||||
Reference in New Issue
Block a user