diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index c84e1172..40024d8e 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -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 = 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; } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 0de3d695..01d84ffe 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -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); +}