mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 12:07:02 -06:00
Merge PR #473 from NikitkaRa: Table: inserting and deleting rows and columns keeps merges consistent (#408)
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));
|
||||
@@ -204,6 +285,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.
|
||||
@@ -330,6 +461,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