-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Merging cells: Rowspans [More Flexible Tables Pt.3b] #3501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 tasks
- For now, they are only formed when an unbreakable rowspan is detected - Groups rows together so they are laid out in a single region
We keep track of all rowspans in the table, and later backtrack to draw them over the final frames once all of their rows' heights have been resolved.
Even across regions.
Consider already covered height of rows which weren't resolved yet.
A rowspan's first few rows could be absent because they were fully empty auto rows. Hline splitting now detects that - hlines between rows in a rowspan will now be displayed if the previous rows don't actually exist (weren't rendered), thus avoiding a problem when using something like table.cell(colspan: all columns, rowspan: 10)[aaaa] with auto rows, since then the first 9 rows would be empty, and thus the hline above it would only appear above the last row, meaning it wouldn't appear at all.
There's still some work to do
No idea why subtracting `height_in_this_region` on multi-region rowspans breaks some examples and fixes others...
equivalent to previous approach, but simpler
seems like it was related to first frame expansion in 'layout_auto_row' after measurement, and now that we're removing entire frames from previous regions, there's no risk of some "stray frame" remaining at the start of 'sizes', but not sure here.
- Make variables a bit less messy (still missing 'started_in_this_region' -> 'frames_in_previous_regions') - Make breakable rowspan across unbreakable auto row make a bit more sense (keep the backlog) - Join the rowspan's current partial backlog with the current region's backlog to form its predicted backlog for measuring
very buggy right now
When the simulation is cancelled, we need to push it back. Or delay popping it (might explore that approach)
Should fix problems with content overflow, and properly allow auto rows to bypass the height of an unbreakable container when needed (they don't "glitch out" at least, which wouldn't be very useful)
- A bit more readable perhaps - Plus better comments
Merged
Fix extraneous spacing when things are sent to the next page
Makes a difference when the region base is different.
Yay! :) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Allows specifying a
rowspan
togrid.cell
andtable.cell
to have a cell span more than one row.Second part of the third task in #3001. Closes #131.
This PR does not contain major changes to the docs. It is expected that those will be done later.
DRAFT: Doing some final touches still.User-facing API
grid.cell(rowspan: amount, ...)
andtable.cell(rowspan: amount, ...)
to have a cell spanamount
rows, as seen below.breakable: true/false/auto
to cells, as in#table.cell(rowspan: ..., breakable: ...)[a]
, to either enforce that all rows spanned by that cell stay in the same page (true
) or to allow the cell (and, thus, its rows) to span multiple pages (false
). The default isauto
- the cell will default to being unbreakable if it only spans fixed-size rows, or breakable if it spans at least oneauto
row.Implementation details
The changes in this PR are divided as follows.
API and CellGrid changes
rowspan
andbreakable
fields. The former indicates how many rows it spans (default 1), the latter indicates whether or not the contents of the cell can be broken across pages (in particular, when it's not breakable, all rows spanned by the cell are forced to be displayed in the same page).CellGrid
was changed to properly mark rowspans' spanned positions as merged positions.Basic rowspan layout
rowspans.rs
file for functions and types exclusive to rowspans.Rowspan
struct has all data needed to layout a rowspan. It includes at which row it starts, how many rows it spans, and so on. One remarkable field in that struct isheights
, which indicates the height the rowspan will have available in each page. The heights are increased by each spanned row's final height duringfinish_region
. Thus, we will only know the heights a rowspan will have after all of its rows have been laid out.check_for_rowspans
runs, which checks if any cells within it are rowspans; if so, creates aRowspan
and appends it to the vector. That's how we're aware of rowspans duringfinish_region
.layout_single_row
orlayout_multi_row
because auto rows might not call either of those functions (if they have 0 measured frames, which is rather common for rowspans - this will always happen when the auto row is 100% composed of rowspans which don't end at it).layout_rowspan
function takes a singleRowspan
and lays out its cell's contents over the spanned pages, with the appropriate heights as part of thebacklog
. To do this, it modifies the frames for each region inself.finished
, inserting the cell's contents.current_region
frame, because the last region spanned by the row might not yet be finished when it is laid out. This is becauselayout_rowspan
is triggered as soon as the last row - or any row after the last row - of the rowspan is finished and laid out withinfinish_region
. However, it may also be triggered for remaining rowspans after all regions were finished (if their last rows weren't laid out for some reason, which can happen in some edge cases).finish_region
multiple times if it spans multiple pages. Therefore, I added a new parameter toRow::Frame
, a boolean, which istrue
when this is the last region which this row occupies, triggering the layout of all rowspans ending in it.check_for_rowspans
appendsRowspan
s to theself.rowspans
vector, before each row is laid out infn layout
.finish_region
, we keep expanding the rowspan's height for each spanned row.layout_rowspan
.Regarding fills and strokes
Fills were adapted such that a cell's fill is placed once for each spanned region. This means that a gradient or pattern would be repeated for each spanned region, not "stretched" at once across regions. This is done by checking the "local parent Y" of each cell, that is, the first row spanned by the cell in the current region (which will be its parent Y if the cell hasn't been broken across pages so far). If we're at the local parent Y at this moment, we draw the fill again.
Not sure if we should perhaps switch to drawing a single rectangle across multiple frames or something. Frankly, I don't know if that's even possible.
Hlines were adapted so that they aren't drawn when they are inside a rowspan. Cool.
But they were also adapted in another way. Usually, hlines fold the stroke of the bottom cell with the stroke of the top cell (usually giving priority to the bottom cell) together with the user-provided line's stroke to determine its final stroke. However, especially with rowspans thrown into the mix, the "top cell" might not actually have been laid out at all. This can happen if the hline is at the top of the region (which we already used to detect by checking if its index is 0, since hlines of index 0 are considered part of the top border and repeated across all regions), but also if the row immediately above the hline is an auto row which was fully empty and thus removed (can happen if the only things in the auto row are rowspans crossing through it). Now, hlines check the "top cell" in the "local top Y", that is, the last row above the hline in the current region. To achieve this, it receives a
local_top_y
parameter which is calculated once. It also receives the vector of rows in the current region as a parameter as it uses similar logic to determine whether or not it is inside a rowspan and thus shouldn't be drawn (in principle, it's fine to be "inside" a rowspan if none of the rowspan's rows above the hline were laid out in the current region).As a consequence of that change, and looking to produce better results when using table borders in general, I made it so cells' strokes now fold with the table top and bottom borders. This avoids some visual problems when the table borders are produced only by cell strokes, e.g. when there's gutter, you'd see some small holes (column gutters) at the bottom and top of each page, even when there's a colspan at the bottom of the page which would request a continuous line under it.
Unbreakable rowspans
They are pretty simple, in theory. Here's how they work:
unbreakable_rows_left
. While that field's value is positive,finish_region
should not be called.check_for_unbreakable_rows
.unbreakable_rows_left
is positive, but if it's zero, it will runsimulate_unbreakable_row_group
.check_for_unbreakable_cells
to check each cell of the current row to see if it is the parent of an unbreakable cell/rowspan. If so, it begins a simulation to find out how many more rows will have to be laid out together, in the same region. This is done by checking each upcoming row and checking if they themselves have unbreakable cells; if so, the simulation will run for at leastmax(cell.rowspan for cell in row)
more rows. The simulation also calculates the total height of the row group, and returns its collected data in anUnbreakableRowGroup
struct instance.check_for_unbreakable_rows
, we skip to the first region with enough height to fit the new unbreakable row group and updateself.unbreakable_rows_left
. That way, all upcoming rows within the unbreakable row group are laid out together in the same region, asfinish_region
won't be called at all while those rows are being laid out.As a consequence, the rowspan spanning those rows will be in a single region, because all that matters for a rowspan is the rows it spans.
measure_auto_row
when it finds an auto row. This can only happen, by default, when the user explicitly writesgrid.cell(breakable: true)[this cell spans an auto row]
; otherwise, we define that, without an explicit override, a rowspan cell is breakable iff it does not span any auto rows.measure_auto_row
was adapted to work with unbreakable row groups - it will useRegions::one
to measure cells whenunbreakable_rows_left
is positive (or we're simulating an unbreakable row group).Breakable rowspans
In principle, rowspans are breakable by default, as rowspans are defined by their rows, so the mere process of laying out rows and skipping pages when they don't fit, as is currently done, will generate breakable rowspans, as long as they span those rows.
The main problem is that rowspan cells might have content which requires more vertical space than what's made available by their fixed-size rows. Therefore, auto rows should expand just enough so that the rowspans can (in theory) fully display their content. As such, the problem being described only applies to rowspans spanning auto rows - by default, all such rowspans are breakable (although that can be overridden).
For unbreakable rowspans or rowspans within a single page, that problem is pretty simple to solve (ignoring fractional rows) - just sum all of the fixed-size rows and subtract from the height demanded by the content inside the rowspan. If the result, let's say
h
is positive, this means that we need to expand an auto row - which will always be the last spanned auto row - by at leasth
units, such that the content will be fully laid out.However, for breakable rowspans spanning at least two pages, that isn't enough, for a few reasons:
50%
), as different pages might have different heights. I do not tackle this specific case in my code, in principle, unless the grid has gutters (which leads us to the more complex case below).spanned height - content height
becomes inaccurate, asspanned height
might change depending on which gutter rows are removed or not.To tackle specifically 3 (and 2 along with it, but only when 3 is a problem, to simplify things for now), I had to adapt
measure_auto_rows
as follows.pending_rowspans
instead of doing anything with its remaining sizes. Rowspans in that vector will be simulated, together, later, to try to figure out which gutters will be removed later on.simulate_and_measure_rowspans_in_auto_row
inrowspans.rs
). Here, we first join all vectors of sizes of pending rowspans into a single vector of sizes (takingmax(rowspan.heights.at(region) for rowspan in pending_rowspans)
for each entry in the vector), as if they all became a single rowspan, so we can work with a single "height needed" number (by just adding them up). We also figure out the max spanned row by a rowspan such that our "unified" rowspan will go up to that row.total_rowspan_height - total_spanned_height
units. But, just to confirm, we run the simulation again, caching that value.total_rowspan_height - predicted_future_height
wherepredicted_future_height
is the sum of all upcoming fixed-size rows (ignoring gutter rows). This necessarily means the auto row will expand more than it should, but it's the best we can do in such a situation, at least for now.And, with that, we have the full process run by
measure_auto_rows
. Of note, the simulation described above does consider unbreakable row groups and whatnot, it tries to replicate the real layout algorithm as much as possible, while attempting to repeat the least code possible from it. Also, for now, the simulation ignores fractional-sized rows, so these will cause the auto row to expand more than it should for now.TODO
TODO:Open draft PRTODO:Perform any further necessary rebasesTODO:Cleanup, review again, check for mistakesTODO:Perhaps add a few more tests?TODO:Undraft PR