Skip to content

Conversation

PgBiel
Copy link
Contributor

@PgBiel PgBiel commented Mar 3, 2024

Allows specifying a grid.header and table.header to have some rows repeat across a grid's or table's pages.

NOTE: This PR depends on #3501, so the PR diff is larger than it really is; as such, here is the link to the real diff: PgBiel/typst@rowspans...PgBiel:typst:repeatable-headers (EDIT: PR rebased!)

First part of the fifth task in #3001. Closes #400 (as the initially desired feature set is available), although more header features are intended to be added over time.

For now, a header may only start at the top of the table, and end at its last page.

User-facing API

  • You may now use table.header and grid.header to place a repeatable header in your grid or table. It will repeat across pages.
    • You can also specify table.header(repeat: false, ...) to not repeat it, though that's generally not very useful (other than for organizational purposes), so the default is repeat: true.
    • When row gutter is enabled, the row gutter immediately below the header is repeated as well. Looks better that way.
#set page(width: auto, height: 15em)
#table(
  columns: 5,
  align: center + horizon,
  table.header(
    table.cell(colspan: 5)[*Cool Zone*],
    table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
    table.hline(start: 2, end: 3, stroke: yellow)
  ),
  ..range(0, 15).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
)

// With gutter
#set page(width: auto, height: 15em)
#table(
  columns: 5,
  align: center + horizon,
  gutter: 3pt,
  table.header(
    table.cell(colspan: 5)[*Cool Zone*],
    table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
    table.hline(start: 2, end: 3, stroke: yellow),
  ),
  ..range(0, 15).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
)

Without gutter:

output

With gutter:

output

Implementation details

The implementation is actually rather simple (compared to other PRs, such as rowspans - that one was very complex). Here are the most important tricks I used for layout:

  1. Header information is kept in a (for now, minimal) Header struct in CellGrid. It currently only contains where the header stops, since the header always starts at y == 0.
  2. At the end of finish_region, we call layout_header, which will layout, for the next region, each individual header row as an unbreakable row group, and thus calculate the header height for the current region.
  3. layout_header will use simulate_header (which just calls simulate_unbreakable_row_group from the rowspans PR) to calculate the header height and skip to the first fitting region.
  4. To prevent header orphaning, we skip regions which only contain the header but no more rows (at the top of finish_region).
  5. (Core detail) Unbreakable row groups and fixed-size rows may skip regions until they fit, but they stop if self.regions.in_last() is true, currently. In this PR, however, I created a new in_last_with_offset function. It's very similar to the former, but instead of checking if we're in the regions.last region and all the full height is remaining, we check if full height - header height is remaining, since skipping a region will always add a header to the top. So, if we're at the last region and there's just the header above us, we stop skipping.
  6. (Core detail) Regarding auto rows, the changes were in two places.
    1. In prepare_auto_row_cell_measurement, where we prepare the information needed to construct the pod, we now use self.regions.map to modify the backlog and the last region by subtracting the current header height from both. This makes sure cells in breakable auto rows won't overlap with the header.
      • Alternatively, we could have changed Regions::next to automatically subtract an automatically-calculated header height from the new region size and not change the value of full, which is probably more technically appropriate, but I didn't take this approach not only to keep the implementation simple, but also because full is already reduced when we're effectively laying out the rowspan, so it wouldn't make much of a difference in practice, other than through possibly very unusual setups. We can always change this in a future update as users give their feedback.
    2. In the rowspan simulation procedures, I basically replicated all of the behavior described above, subtracting header height from each new simulated region, and re-simulating the header when we would otherwise "place" it.

And that's it!

Other details

  • Regarding the API: I created the Grid/TableHeader structs; changed Grid/TableChild to accept both headers and Grid/TableItem (the old children); headers can contain items too (but not other headers, of course - there are helpful errors for that). In CellGrid::resolve, there are now two for loops - one for children and one for items inside the children (if the child is a header, then one child iteration will correspond to N item iterations (inside the header); if the child is a single item, then there's only a single item iteration, and everything behaves as usual).
    • We perform some extra logic here to ensure the header starts in the first row, and also to ensure all rowspans are fully contained within the header, causing it to expand.
    • If the header is too large, we don't care, it will just look bad. Blame the user here.
    • Added some "FIXME" comments for when we update headers to be able to appear in other places than the first row, but for now they aren't of concern.
  • Regarding lines: Header lines have priority when repeated, in general, but they lose against explicit lines unless they are explicit lines themselves (basically works like top border stroke).

@PgBiel PgBiel force-pushed the repeatable-headers branch from 482c11c to a2ebe32 Compare March 3, 2024 19:38
@PgBiel PgBiel mentioned this pull request Mar 3, 2024
18 tasks
PgBiel added 2 commits March 5, 2024 01:01
Relevant for rowspans with full colspan spanning multiple auto rows (the
first N - 1 will be empty and removed)
@PgBiel
Copy link
Contributor Author

PgBiel commented Mar 5, 2024

I've added a fix for a small bug I found while, well, pondering about things. :P

Turns out lines above empty auto rows were being ignored since the auto rows were removed. Now they are still considered, but have less priority than lines above whatever is below.

@laurmaedje
Copy link
Member

Needs merge/rebase for status checks to be okay again.

PgBiel added 15 commits March 5, 2024 14:18
Doesn't matter much right now since headers always start at the first
row, but doing this mostly for correctness
- Delay gutter adjustment for header end index
- Cells always check if they are in a header, if so expand it
- Ensure rowspans aren't laid out through the same rows twice
- Ensure multi-page auto rows cause previous rowspans to be laid out
  even at their first regions
- It only checked if there were '(amount of header rows)' rows or less
  in the region to determine if the header was an orphan. The problem is
that empty auto rows are omitted, so the header can have less rows than
'header.end'. Now, we just check if the last row is a header. If so, no
other rows were laid out.
- We were enumerating since the start of all regions, not since the
  first region spanned by the rowspan. Therefore, the rowspan wouldn't
apply its `dy` correctly, only in the first page of the entire table.
- Line position filtering has been moved to 'render_fills_strokes'
@PgBiel
Copy link
Contributor Author

PgBiel commented Mar 6, 2024

Alright, turns out there were several little logic bugs I could think of. There could perhaps be more, but in principle the most relevant ones should be gone. We can probably figure the rest out during testing.

Safer alternative in case a header row is removed
@PgBiel
Copy link
Contributor Author

PgBiel commented Mar 6, 2024

Ready. 👍

@laurmaedje laurmaedje added this pull request to the merge queue Mar 6, 2024
@laurmaedje
Copy link
Member

Great, thank you!

Merged via the queue into typst:main with commit 898367f Mar 6, 2024
@PgBiel PgBiel deleted the repeatable-headers branch March 7, 2024 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Table headers that repeat on every page
2 participants