Render: skip laying out table cell text that lands wholly off the target

This commit is contained in:
Nikita Rakitin
2026-10-14 04:22:37 +03:00
parent 457406a7a5
commit 8a5eec369f
2 changed files with 46 additions and 1 deletions
+22 -1
View File
@@ -297,6 +297,25 @@ pub fn cell_rects(t: &Table, heights: &[f64]) -> Vec<(usize, usize, Rect)> {
out
}
/// Can a cell's text be left undrawn because it lands wholly outside the `target` (pixels)? Only
/// for cells whose text stays inside the cell: its row grew to fit it (one row, horizontal text,
/// not at the height cap), so nothing overflows. A margin covers italic overhang and the like.
/// A table of thousands of rows otherwise lays out every cell's text off the slide's bottom.
fn text_off_target(cell: &Cell, rect: Rect, m: Affine, target: (u16, u16)) -> bool {
if cell.row_span > 1 || !matches!(cell.vertical, None | Some(TextDir::Horizontal)) || rect.height() >= 20_000.0 {
return false;
}
let b = m.transform_rect_bbox(rect).inflate(64.0, 64.0);
let view = Rect::new(0.0, 0.0, f64::from(target.0), f64::from(target.1));
b.is_finite() && (b.x1 < view.x0 || b.y1 < view.y0 || b.x0 > view.x1 || b.y0 > view.y1)
}
#[cfg(test)]
thread_local! {
/// Cells whose text `draw` laid out on this thread (tests check off-slide cells are skipped).
pub(crate) static TEXT_CELLS_DRAWN: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Draws the table; returns the box its cells cover (table space), which grows with their text.
#[allow(clippy::too_many_arguments)]
pub fn draw(
@@ -399,11 +418,13 @@ pub fn draw(
// Text.
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
if cell.text.is_empty() {
if cell.text.is_empty() || text_off_target(cell, *rect, m, (ctx.width(), ctx.height())) {
continue;
}
let body = cell_body(rctx, t, *r, *c, cell);
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
#[cfg(test)]
TEXT_CELLS_DRAWN.with(|n| n.set(n.get() + 1));
crate::draw_text(ctx, rctx, &tmp, &body, *rect, m, fields, None);
let _ = peniko::Fill::NonZero;
}
+24
View File
@@ -1711,3 +1711,27 @@ fn east_asian_vertical_text_stands_ideographs_upright() {
let (w, h) = ink(deckcraft_model::text::TextDir::Vertical);
assert!(h > w * 3, "vert: stroke on its side {w}x{h}");
}
#[test]
fn table_rows_below_the_slide_are_not_laid_out_and_the_slide_looks_the_same() {
// A fuzzed deck's 200 x 200 table of long text took 70 s to draw, most of it off the slide.
let table = |rows: usize| {
let mut t = Table::new(rows, 2, 400.0, 30.0);
for r in 0..rows {
for c in 0..2 {
t.cell_mut(r, c).unwrap().text = TextBody::from_text(&format!("Row {r} col {c}"));
}
}
Shape { id: ShapeId(611), xfrm: Some(Xfrm::new(50.0, 50.0, 400.0, 30.0 * rows as f64)), kind: ShapeKind::Table(t), ..Default::default() }
};
let opts = RenderOpts { scale: 0.5, ..Default::default() };
crate::table::TEXT_CELLS_DRAWN.with(|n| n.set(0));
let big = render_slide(&deck_with(vec![table(500)]), 0, &opts);
let drawn = crate::table::TEXT_CELLS_DRAWN.with(|n| n.get());
assert!(drawn > 2 * 10 && drawn < 2 * 40, "{drawn} cells drawn");
// 30 rows already run past the slide's bottom: the same picture.
let small = render_slide(&deck_with(vec![table(30)]), 0, &opts);
assert_eq!((big.width, big.height), (small.width, small.height));
let diff = (0..big.height).flat_map(|y| (0..big.width).map(move |x| (x, y))).filter(|&(x, y)| big.pixel(x, y) != small.pixel(x, y)).count();
assert_eq!(diff, 0);
}