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 09d6ab1e..4e840515 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 685df6f9..c84e1172 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 0581fb27..9a885c9e 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)); +}