From cef6e1a8c8b8e01ffb70960c9e5443cfdcfee9f0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 07:04:09 -0400 Subject: [PATCH] Render: table borders draw their dashes, compound bands and diagonals Explicit cell borders were stroked as plain solid lines (no dash, compound or cap), and diagonal borders (lnTlToBr, lnBlToTr) were read but never drawn. Both now go through the shape line stroker; diagonals are drawn under the cell edges. Wild synthetic set: border styles slide 5.67 -> 2.26, thick merged-cell borders 1.20 -> 0.69. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/render/src/lib.rs | 2 +- crates/render/src/table.rs | 26 +++++++++++++++++++++----- crates/render/src/tests_wild.rs | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 09d6ab1..4e84051 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1376,7 +1376,7 @@ fn blur(sigma: f64) -> Filter { /// Stroke `path` with `line`. A gradient line is laid out over `extent` (the untrimmed outline the /// line belongs to; `path` itself when `None`). #[allow(clippy::too_many_arguments)] -fn stroke( +pub(crate) fn stroke( ctx: &mut RenderContext, rctx: &Ctx, line: &Line, diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index 685df6f..c84e117 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -348,6 +348,17 @@ pub fn draw( }, } } + // Diagonal borders, under the edges: drawn like any line (dashes, compound, caps). + for (r, c, rect) in &rects { + let Some(cell) = t.cell(*r, *c) else { continue }; + for (line, a, b) in [(&cell.diag_down, (rect.x0, rect.y0), (rect.x1, rect.y1)), (&cell.diag_up, (rect.x0, rect.y1), (rect.x1, rect.y0))] { + let Some(l) = line.as_ref().filter(|l| l.fill.as_ref().is_some_and(|f| !f.is_none())) else { continue }; + let mut p = BezPath::new(); + p.move_to(a); + p.line_to(b); + crate::stroke(ctx, rctx, l, None, &p, None, m, l.width.unwrap_or(1.0).max(0.0), 1.0); + } + } // Borders: explicit cell borders, else the style's grid. for (r, c, rect) in &rects { let Some(cell) = t.cell(*r, *c) else { continue }; @@ -361,11 +372,16 @@ pub fn draw( for (i, e) in edges.iter().enumerate() { let line = cell.borders.get(i).and_then(|b| b.as_ref()); let (col, wd) = match line { - Some(l) => match &l.fill { - Some(Fill::Solid { color: cc }) => (rctx.color(cc, None), l.width.unwrap_or(1.0)), - Some(Fill::None) => continue, - _ => continue, - }, + // An explicit border is drawn like any line: dashed, compound, gradient. + Some(l) => { + if l.fill.as_ref().is_some_and(|f| !f.is_none()) { + let mut p = BezPath::new(); + p.move_to((e.0, e.1)); + p.line_to((e.2, e.3)); + crate::stroke(ctx, rctx, l, None, &p, None, m, l.width.unwrap_or(1.0).max(0.25), 1.0); + } + continue; + } None => match lk.sides.get(i).copied().flatten().or(lk.border) { Some(b) if b.0.a > 0 && b.1 > 0.0 => b, _ => continue, diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index 0581fb2..9a885c9 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -580,3 +580,21 @@ fn a_bevelled_shape_is_lit_from_the_upper_left() { let img = render(vec![bevelled(r#""#)]); assert!(close(img.pixel(165, 104), base, 2) && close(img.pixel(165, 165), base, 2)); } + +#[test] +fn table_borders_take_their_dashes_and_diagonals_are_drawn() { + let mut t = deckcraft_model::Table::new(1, 1, 200.0, 100.0); + t.style = "none".into(); + let mut dashed = Line::solid(ColorRef::rgb(RED), 4.0); + dashed.dash = Some(deckcraft_model::style::Dash::Preset { name: "dash".into() }); + if let Some(c) = t.rows.first_mut().and_then(|r| r.cells.first_mut()) { + c.borders[2] = Some(dashed); + c.diag_down = Some(Line::solid(ColorRef::rgb(Rgba::rgb(0, 0, 255)), 4.0)); + } + let sh = Shape { id: ShapeId(27), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), kind: ShapeKind::Table(t), ..Default::default() }; + let img = render(vec![sh]); + // Dashes of 4 widths (16 pt) on, 3 (12 pt) off along the top edge, from its start. + assert!(close(img.pixel(108, 100), RED, 40) && close(img.pixel(122, 100), WHITE, 30) && close(img.pixel(135, 100), RED, 40)); + // The diagonal from top-left to bottom-right passes through the middle. + assert!(close(img.pixel(200, 150), Rgba::rgb(0, 0, 255), 40), "{:?}", img.pixel(200, 150)); +}