mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 21:37:41 -06:00
Merge PR #1760: Tables: rows with gridBefore/gridAfter keep their grid columns
This commit is contained in:
+26
-3
@@ -43,6 +43,13 @@ pub struct Row {
|
||||
pub controls: ControlWrap,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
/// Grid column where the row's first cell starts: the columns `w:gridBefore` skips.
|
||||
pub fn first_grid(&self) -> usize {
|
||||
(self.props.grid_before as usize).min(MAX_COLS)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct Table {
|
||||
#[serde(default)]
|
||||
@@ -86,18 +93,22 @@ impl Table {
|
||||
}
|
||||
|
||||
pub fn cols(&self) -> usize {
|
||||
self.grid.len().max(self.rows.iter().map(|r| r.cells.iter().map(Cell::span).sum::<usize>()).max().unwrap_or(0))
|
||||
let row_cols = |r: &Row| r.first_grid() + r.cells.iter().map(Cell::span).sum::<usize>() + r.props.grid_after as usize;
|
||||
self.grid.len().max(self.rows.iter().map(row_cols).max().unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Grid column where cell `c` of row `r` starts.
|
||||
pub fn grid_col(&self, r: usize, c: usize) -> usize {
|
||||
self.rows.get(r).map(|row| row.cells.iter().take(c).map(Cell::span).sum()).unwrap_or(0)
|
||||
self.rows.get(r).map(|row| row.first_grid() + row.cells.iter().take(c).map(Cell::span).sum::<usize>()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Cell index in row `r` that covers grid column `g`.
|
||||
pub fn cell_at_grid(&self, r: usize, g: usize) -> Option<usize> {
|
||||
let row = self.rows.get(r)?;
|
||||
let mut x = 0;
|
||||
let mut x = row.first_grid();
|
||||
if g < x {
|
||||
return None;
|
||||
}
|
||||
for (i, c) in row.cells.iter().enumerate() {
|
||||
if g < x + c.span() {
|
||||
return Some(i);
|
||||
@@ -495,6 +506,18 @@ impl Table {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn grid_before_shifts_the_row() {
|
||||
let mut t = Table::new(2, 3, 300.0);
|
||||
t.rows[1].cells.remove(0);
|
||||
t.rows[1].props.grid_before = 1;
|
||||
assert_eq!(t.grid_col(1, 0), 1);
|
||||
assert_eq!(t.grid_col(1, 1), 2);
|
||||
assert_eq!(t.cell_at_grid(1, 0), None);
|
||||
assert_eq!(t.cell_at_grid(1, 2), Some(1));
|
||||
assert_eq!(t.cols(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_table_shape() {
|
||||
let t = Table::new(3, 4, 468.0);
|
||||
|
||||
@@ -374,7 +374,7 @@ fn select_at(s: &mut Session, v: &Value) -> CmdResult {
|
||||
/// before it) or, at the table's right edge, a row's last cell (after it). (row, cell, after)
|
||||
fn beside_line(t: &Table, line: usize) -> Option<(usize, usize, bool)> {
|
||||
t.rows.iter().enumerate().find_map(|(r, row)| {
|
||||
let mut g = 0usize;
|
||||
let mut g = row.first_grid();
|
||||
for (c, cell) in row.cells.iter().enumerate() {
|
||||
if g == line {
|
||||
return Some((r, c, false));
|
||||
|
||||
@@ -91,7 +91,7 @@ impl CellRange {
|
||||
let ((r0, r1), (g0, g1)) = (self.rows, self.cols);
|
||||
let mut out = Vec::new();
|
||||
for (r, row) in t.rows.iter().enumerate().take(r1.saturating_add(1)).skip(r0) {
|
||||
let mut g = 0usize;
|
||||
let mut g = row.first_grid();
|
||||
for (c, cell) in row.cells.iter().enumerate() {
|
||||
let end = g.saturating_add(cell.span());
|
||||
if g <= g1 && end > g0 {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use wordcraft_doc::numbering::Counters;
|
||||
use wordcraft_doc::props::{Align, Border, Borders, HeightRule, Rgb, TextDirection, VAlign, VMerge};
|
||||
use wordcraft_doc::styles::{PartSet, TablePart, TableStyleProps};
|
||||
use wordcraft_doc::{Block, Blocks, Paragraph, StoryRef, Table};
|
||||
use wordcraft_doc::{Block, Blocks, Paragraph, Row, StoryRef, Table};
|
||||
use wordcraft_geom::Rect;
|
||||
|
||||
use crate::para::{CellText, ClKind, ParaLayout};
|
||||
@@ -181,7 +181,7 @@ pub fn measure_table_columns(doc: &wordcraft_doc::Document, t: &Table) -> Vec<(f
|
||||
let mut spanned: Vec<(usize, usize, f32, f32)> = Vec::new();
|
||||
let mut budget = MEASURE_BUDGET;
|
||||
'rows: for (ri, row) in t.rows.iter().enumerate() {
|
||||
let mut g = 0usize;
|
||||
let mut g = row.first_grid();
|
||||
for cell in &row.cells {
|
||||
let span = cell.span();
|
||||
let margins = cell.props.resolved_margins(margins_def);
|
||||
@@ -240,6 +240,7 @@ pub fn measure_table_columns(doc: &wordcraft_doc::Document, t: &Table) -> Vec<(f
|
||||
}
|
||||
g += span;
|
||||
}
|
||||
spanned.extend(row_gaps(row, g).map(|(g, span, w)| (g, span, w, w)));
|
||||
}
|
||||
for (g, span, min, max) in spanned {
|
||||
let Some(cs) = cols.get_mut(g..(g + span).min(ncols)) else { continue };
|
||||
@@ -473,7 +474,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
let mut heights: Vec<f32> = Vec::with_capacity(nrows);
|
||||
let mut bottoms: Vec<f32> = Vec::with_capacity(nrows);
|
||||
for (ri, row) in t.rows.iter().enumerate() {
|
||||
let mut g = 0usize;
|
||||
let mut g = row.first_grid();
|
||||
let mut cells = Vec::with_capacity(row.cells.len());
|
||||
let mut rh = 0.0f32;
|
||||
// The tallest cell margins plus border bands in the row.
|
||||
@@ -520,7 +521,9 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
let cb = cell.props.borders.unwrap_or_default();
|
||||
// A row's table property exceptions (`w:tblPrEx`) override the table's borders for its cells.
|
||||
let row_borders = row.props.exceptions.as_ref().and_then(|e| e.borders).unwrap_or_default();
|
||||
let (first_g, last_g) = (g == 0, g + span >= ncols);
|
||||
// A row that skips grid columns (`w:gridBefore`/`w:gridAfter`) has its edge at its own
|
||||
// first and last cell.
|
||||
let (first_g, last_g) = (g == row.first_grid(), g + span >= ncols.saturating_sub(row.props.grid_after as usize));
|
||||
let rb = look.map(|l| region_borders(&l.borders, (ri, nrows), (first_g, last_g))).unwrap_or_default();
|
||||
let mut borders = Borders {
|
||||
top: edge(cb.top.or(if ri == 0 { row_borders.top } else { row_borders.between }).or(rb.top), ri == 0, tb.top, tb.between),
|
||||
@@ -671,7 +674,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
||||
.rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut gx = 0;
|
||||
let mut gx = r.first_grid();
|
||||
r.cells
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
@@ -1105,7 +1108,7 @@ fn column_content(ctx: &mut Ctx, t: &Table, style: Option<&TableStyleProps>, dep
|
||||
let mut spanning = Vec::new();
|
||||
let mut any = false;
|
||||
for (ri, row) in t.rows.iter().enumerate() {
|
||||
let mut g = 0usize;
|
||||
let mut g = row.first_grid();
|
||||
for cell in &row.cells {
|
||||
let span = cell.span();
|
||||
if cell.props.vmerge != VMerge::Continue {
|
||||
@@ -1121,6 +1124,7 @@ fn column_content(ctx: &mut Ctx, t: &Table, style: Option<&TableStyleProps>, dep
|
||||
}
|
||||
g += span;
|
||||
}
|
||||
spanning.extend(row_gaps(row, g).map(|(g, span, w)| (g, span, (w, w))));
|
||||
}
|
||||
for (g, span, need) in spanning {
|
||||
let Some(cs) = cols.get_mut(g..(g + span).min(t.cols())) else { continue };
|
||||
@@ -1134,6 +1138,17 @@ fn column_content(ctx: &mut Ctx, t: &Table, style: Option<&TableStyleProps>, dep
|
||||
any.then_some(cols)
|
||||
}
|
||||
|
||||
/// The grid columns row `row` leaves empty before its first cell and after its last one (which
|
||||
/// ends at grid column `end`) when it gives their width (`w:wBefore`, `w:wAfter`): (first column,
|
||||
/// columns, width in points). Column widths make room for them like for an empty cell.
|
||||
fn row_gaps(row: &Row, end: usize) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||
let p = &row.props;
|
||||
[(0, row.first_grid(), p.w_before), (end, p.grid_after as usize, p.w_after)]
|
||||
.into_iter()
|
||||
.filter(|&(_, n, _)| n > 0)
|
||||
.filter_map(|(g, n, w)| w.filter(|w| w.is_finite() && *w > 0.0).map(|w| (g, n, w.min(1584.0))))
|
||||
}
|
||||
|
||||
/// The narrowest and widest a block list can be laid out.
|
||||
fn blocks_content(ctx: &mut Ctx, blocks: &Blocks, text: Option<&CellText>, depth: usize) -> (f32, f32) {
|
||||
let (mut lo, mut hi) = (0.0f32, 0.0f32);
|
||||
|
||||
@@ -1474,6 +1474,147 @@ fn table_border_corners_close() {
|
||||
assert!((h_left - (v_left - 1.5)).abs() < 0.01, "horizontal starts at {h_left}, vertical centre line at {v_left}");
|
||||
}
|
||||
|
||||
/// A row with `w:gridBefore` starts at the grid column after the skipped ones, not at the first.
|
||||
#[test]
|
||||
fn grid_before_row_starts_after_skipped_columns() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let mut t = Table::new(2, 3, 468.0);
|
||||
t.rows[1].cells.remove(0);
|
||||
t.rows[1].props.grid_before = 1;
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let x_of = |r: usize, c: usize| {
|
||||
l.pages[0].items.iter().find_map(|i| match i {
|
||||
Placed::Cell { rect, row, cell, .. } if *row == r && *cell == c => Some(rect.x),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let (shifted, second) = (x_of(1, 0).unwrap(), x_of(0, 1).unwrap());
|
||||
assert!((shifted - second).abs() < 0.01, "row 1 starts at {shifted}, grid column 1 at {second}");
|
||||
}
|
||||
|
||||
/// The area of cell `c` of row `r` of the table on page `page`.
|
||||
fn cell_rect(l: &DocLayout, page: usize, r: usize, c: usize) -> Option<wordcraft_geom::Rect> {
|
||||
l.pages.get(page)?.items.iter().find_map(|i| match i {
|
||||
Placed::Cell { rect, row, cell, .. } if *row == r && *cell == c => Some(*rect),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Rows that skip grid columns at the start and at the end (`w:gridBefore`, `w:gridAfter`, as in
|
||||
/// #1174): the cells sit on their own grid columns, the middle one keeps its width, and a merge
|
||||
/// down the last column matches by the shifted column.
|
||||
#[test]
|
||||
fn grid_before_and_after_rows_keep_their_columns_and_merges() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let grid = [239.0f32, 679.0, 101.0, 3790.0, 2312.0, 1189.0].map(|w| w / 20.0);
|
||||
let mut t = Table::new(3, 4, 468.0);
|
||||
t.props.fixed = true;
|
||||
t.grid = grid.to_vec();
|
||||
for (ri, row) in t.rows.iter_mut().enumerate() {
|
||||
row.cells.truncate(3);
|
||||
row.props.grid_before = 1;
|
||||
row.props.w_before = Some(grid[0]);
|
||||
for (c, span) in row.cells.iter_mut().zip([2, 1, 2]) {
|
||||
c.props.span = span;
|
||||
c.props.width = None;
|
||||
}
|
||||
row.cells[1] = wordcraft_doc::Cell::with_text("Middle cell, wide enough for a few words on a line.");
|
||||
row.cells[2].props.span = 2;
|
||||
row.cells[2].props.vmerge = if ri == 0 { wordcraft_doc::props::VMerge::Restart } else { wordcraft_doc::props::VMerge::Continue };
|
||||
}
|
||||
// A row without the gap, and one skipping the last column instead.
|
||||
t.rows[1].props = Default::default();
|
||||
t.rows[1].cells.insert(0, wordcraft_doc::Cell::empty());
|
||||
t.rows[2].props.grid_before = 0;
|
||||
t.rows[2].props.grid_after = 1;
|
||||
t.rows[2].cells[0].props.span = 3;
|
||||
t.rows[2].cells[2].props.span = 1;
|
||||
let colx = |g: usize| grid[..g].iter().sum::<f32>();
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let rect = |r, c| cell_rect(&l, 0, r, c).unwrap();
|
||||
let x0 = rect(1, 0).x;
|
||||
let middle = rect(0, 1);
|
||||
assert!((rect(0, 0).x - x0 - colx(1)).abs() < 0.01, "row 0 starts at {}", rect(0, 0).x - x0);
|
||||
assert!((middle.x - x0 - colx(3)).abs() < 0.01 && (middle.w - grid[3]).abs() < 0.01, "middle cell {middle:?}");
|
||||
assert!((rect(2, 2).x - x0 - colx(4)).abs() < 0.01 && (rect(2, 2).w - grid[4]).abs() < 0.01, "row 2's last cell {:?}", rect(2, 2));
|
||||
// The merge runs down all three rows.
|
||||
let merged = rect(0, 2);
|
||||
let rows_h = rect(0, 0).h + rect(1, 0).h + rect(2, 0).h;
|
||||
assert!((merged.h - rows_h).abs() < 0.01, "merged cell {} tall, rows {rows_h}", merged.h);
|
||||
}
|
||||
|
||||
/// A row's own first and last cell carry the table's outer side borders when it skips the grid
|
||||
/// columns beyond them.
|
||||
#[test]
|
||||
fn skipped_columns_put_the_side_borders_on_the_rows_cells() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let mut t = Table::new(3, 3, 468.0);
|
||||
t.props.style = None;
|
||||
let thick = Some(Border::single(3.0));
|
||||
t.props.borders = Some(Borders { left: thick, right: thick, ..Borders::default() });
|
||||
t.rows[1].cells.remove(0);
|
||||
t.rows[1].props.grid_before = 1;
|
||||
t.rows[2].cells.pop();
|
||||
t.rows[2].props.grid_after = 1;
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let rect = |r, c| cell_rect(&l, 0, r, c).unwrap();
|
||||
let vertical_at = |x: f32, y: f32| {
|
||||
l.pages[0].items.iter().any(|i| matches!(i, Placed::Rule { x0, y0, x1, y1, .. } if x0 == x1 && (x0 - x).abs() < 0.01 && *y0 <= y && *y1 >= y))
|
||||
};
|
||||
let (r1, r2) = (rect(1, 0), rect(2, 1));
|
||||
assert!(vertical_at(r1.x, r1.y + r1.h / 2.0), "no left border at row 1's first cell {r1:?}");
|
||||
assert!(vertical_at(r2.x + r2.w, r2.y + r2.h / 2.0), "no right border at row 2's last cell {r2:?}");
|
||||
}
|
||||
|
||||
/// AutoFit makes the skipped columns as wide as the row says (`w:wBefore`).
|
||||
#[test]
|
||||
fn autofit_gives_skipped_columns_their_width() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let mut t = Table::new(2, 2, 468.0);
|
||||
t.props.style = None;
|
||||
for row in &mut t.rows {
|
||||
for c in &mut row.cells {
|
||||
*c = wordcraft_doc::Cell::with_text("A");
|
||||
}
|
||||
}
|
||||
t.rows[1].cells.remove(0);
|
||||
t.rows[1].props.grid_before = 1;
|
||||
t.rows[1].props.w_before = Some(150.0);
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let gap = cell_rect(&l, 0, 1, 0).unwrap().x - cell_rect(&l, 0, 0, 0).unwrap().x;
|
||||
assert!((gap - 150.0).abs() < 0.01, "skipped column {gap} wide");
|
||||
}
|
||||
|
||||
/// Rows skipping a grid column keep it skipped on every page, under the repeated header row.
|
||||
#[test]
|
||||
fn grid_before_rows_stay_shifted_across_pages() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
let mut t = Table::new(80, 3, 468.0);
|
||||
t.rows[0].props.header = true;
|
||||
for row in t.rows.iter_mut().skip(1) {
|
||||
row.cells.remove(0);
|
||||
row.props.grid_before = 1;
|
||||
}
|
||||
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
assert!(l.pages.len() >= 2, "pages {}", l.pages.len());
|
||||
let column1 = cell_rect(&l, 0, 0, 1).unwrap().x;
|
||||
for (pi, p) in l.pages.iter().enumerate() {
|
||||
assert!(cell_rect(&l, pi, 0, 1).is_some_and(|r| (r.x - column1).abs() < 0.01), "no header on page {pi}");
|
||||
for i in &p.items {
|
||||
if let Placed::Cell { rect, row, cell: 0, .. } = i
|
||||
&& *row > 0
|
||||
{
|
||||
assert!((rect.x - column1).abs() < 0.01, "row {row} on page {pi} starts at {}", rect.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_own_border_overrides_only_that_side() {
|
||||
let mut d = Document::from_text("before\nafter");
|
||||
|
||||
Reference in New Issue
Block a user