diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index 354e6814d..772c107e8 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -1731,21 +1731,44 @@ impl<'a> GridLayouter<'a> { }) .unwrap_or(LinePosition::Before); + // FIXME: In the future, directly specify in 'self.rrows' when + // we place a repeated header rather than its original rows. + // That would let us remove most of those verbose checks, both + // in 'lines.rs' and here. Those checks also aren't fully + // accurate either, since they will also trigger when some rows + // have been removed between the header and what's below it. + let is_under_repeated_header = self + .grid + .header + .as_ref() + .and_then(Repeatable::as_repeated) + .zip(prev_y) + .is_some_and(|(header, prev_y)| { + // Note: 'y == header.end' would mean we're right below + // the NON-REPEATED header, so that case should return + // false. + prev_y < header.end && y > header.end + }); + // If some grid rows were omitted between the previous resolved // row and the current one, we ensure lines below the previous // row don't "disappear" and are considered, albeit with less // priority. However, don't do this when we're below a header, // as it must have more priority instead of less, so it is - // chained later instead of before. + // chained later instead of before. The exception is when the + // last row in the header is removed, in which case we append + // both the lines under the row above us and also (later) the + // lines under the header's (removed) last row. let prev_lines = prev_y .filter(|prev_y| { prev_y + 1 != y - && !self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| prev_y + 1 == header.end) + && (!is_under_repeated_header + || self + .grid + .header + .as_ref() + .and_then(Repeatable::as_repeated) + .is_some_and(|header| prev_y + 1 != header.end)) }) .map(|prev_y| get_hlines_at(prev_y + 1)) .unwrap_or(&[]); @@ -1765,15 +1788,16 @@ impl<'a> GridLayouter<'a> { &[] }; - // The header lines, if any, will correspond to the lines under - // the previous row, so they function similarly to 'prev_lines'. - let expected_header_line_position = expected_prev_line_position; + let mut expected_header_line_position = LinePosition::Before; let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = self.grid.header.as_ref().zip(prev_y) { - if prev_y + 1 != y - && prev_y + 1 == header.end - && !self.grid.has_gutter + if is_under_repeated_header + && (!self.grid.has_gutter + || matches!( + self.grid.rows[prev_y], + Sizing::Rel(length) if length.is_zero() + )) { // For lines below a header, give priority to the // lines originally below the header rather than @@ -1783,10 +1807,18 @@ impl<'a> GridLayouter<'a> { // lines being normally laid out then will be // precisely the lines below the header. // - // Additionally, we don't append header lines when - // gutter is enabled, since, in that case, there will - // be a gutter row between header and content, so no - // lines should overlap. + // Additionally, we don't repeat lines above the row + // below the header when gutter is enabled, since, in + // that case, there will be a gutter row between header + // and content, so no lines should overlap. The + // exception is when the gutter at the end of the + // header has a size of zero, which happens when only + // column-gutter is specified, for example. In that + // case, we still repeat the line under the gutter. + expected_header_line_position = expected_line_position( + header.end, + header.end == self.grid.rows.len(), + ); get_hlines_at(header.end) } else { &[] diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst/src/layout/grid/lines.rs index 1249c3e95..c976da2d7 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst/src/layout/grid/lines.rs @@ -547,7 +547,7 @@ pub(super) fn hline_stroke_at_column( // Ensure the row above us is a repeated header. // FIXME: Make this check more robust when headers at arbitrary // positions are added. - local_top_y + 1 == header.end && y != header.end + local_top_y < header.end && y > header.end }); // Prioritize the footer's top stroke as well where applicable. @@ -559,7 +559,7 @@ pub(super) fn hline_stroke_at_column( // Ensure the row below us is a repeated footer. // FIXME: Make this check more robust when footers at arbitrary // positions are added. - local_top_y.unwrap_or(0) + 1 != footer.start && y == footer.start + local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start }); let (prioritized_cell_stroke, deprioritized_cell_stroke) = diff --git a/tests/ref/layout/grid-footers-5.png b/tests/ref/layout/grid-footers-5.png index b58ed2669..0cfd2d668 100644 Binary files a/tests/ref/layout/grid-footers-5.png and b/tests/ref/layout/grid-footers-5.png differ diff --git a/tests/ref/layout/grid-headers-4.png b/tests/ref/layout/grid-headers-4.png index e60877d83..be6080f74 100644 Binary files a/tests/ref/layout/grid-headers-4.png and b/tests/ref/layout/grid-headers-4.png differ diff --git a/tests/typ/layout/grid-footers-5.typ b/tests/typ/layout/grid-footers-5.typ index db2489fd9..874fcd2e4 100644 --- a/tests/typ/layout/grid-footers-5.typ +++ b/tests/typ/layout/grid-footers-5.typ @@ -26,3 +26,16 @@ gutter: 3pt, table.footer[a][b][c] ) + +--- +// Test footer stroke priority edge case +#set page(height: 10em) +#table( + columns: 2, + stroke: black, + ..(table.cell(stroke: aqua)[d],) * 8, + table.footer( + table.cell(rowspan: 2, colspan: 2)[a], + [c], [d] + ) +) diff --git a/tests/typ/layout/grid-headers-4.typ b/tests/typ/layout/grid-headers-4.typ index 13fd41dd1..6ede601c2 100644 --- a/tests/typ/layout/grid-headers-4.typ +++ b/tests/typ/layout/grid-headers-4.typ @@ -56,3 +56,44 @@ ), [a\ b] ) + +--- +// Test header stroke priority edge case (last header row removed) +#set page(height: 8em) +#table( + columns: 2, + stroke: black, + gutter: (auto, 3pt), + table.header( + [c], [d], + ), + ..(table.cell(stroke: aqua)[d],) * 8, +) + +--- +// Yellow line should be kept here +#set text(6pt) +#table( + column-gutter: 3pt, + inset: 1pt, + table.header( + [a], + table.hline(stroke: yellow), + ), + table.cell(rowspan: 2)[b] +) + +--- +// Red line should be kept here +#set page(height: 6em) +#set text(6pt) +#table( + column-gutter: 3pt, + inset: 1pt, + table.header( + table.hline(stroke: red, position: bottom), + [a], + ), + [a], + table.cell(stroke: aqua)[b] +)