Initial table per-cell customization [More Flexible Tables Pt.2a]#3037
Conversation
|
|
||
| self.finish_region(engine)?; | ||
|
|
||
| self.render_fills_strokes()?; |
There was a problem hiding this comment.
This will be called (and the for loop in render_fills_strokes will run) for all users of GridLayouter, including list and enum, which don't use fills and strokes at all. Before, I avoided that by checking if the global fill was None, but now I can't do that due to per-cell overrides.
Should I perhaps add some toggle in this layout method to opt into rendering fills and strokes, as to optimize for those use cases? Or perhaps have some associated constant in trait Cell, something like const USES_STROKES_OR_FILLS: bool;, to indicate whether this is necessary?
There was a problem hiding this comment.
I wouldn't worry about it for now. The overhead is probably relatively low.
Enable show rules
table.typ and grid-styling.typ were getting cluttered
| ); | ||
| )?; | ||
|
|
||
| // Prepare grid layout by unifying content and gutter tracks. |
There was a problem hiding this comment.
I think this comment is outdated.
| /// // Creating a temporary #{ ... } scope to avoid affecting other grids | ||
| /// // with this show rule. | ||
| /// // Here, we will italicize all cells. | ||
| /// show grid.cell: emph |
There was a problem hiding this comment.
I think this example isn't ideal because emphasizing the whole grid would be the same. Maybe we can come up with something that's really best done via show grid.cell.
There was a problem hiding this comment.
I'm not sure, I think with the PR at the current state we'd have to add a fairly non-standard example here (e.g. apply emph based on alignment or something). With the x, y fields PR, however, we'd probably have more to show, since we could e.g. make only the first row bold.
There was a problem hiding this comment.
Then maybe let's remove the example until we have something idiomatic to show. Or I guess keep and change it later. I don't mind either way.
There was a problem hiding this comment.
Sure, removing sounds fair for now.
There was a problem hiding this comment.
especially the third and fourth test are a bit hard to understand because of the colors. maybe blue should be replaced by aqua on the tests? it is better suited for a background color.
Co-authored-by: Laurenz <laurmaedje@gmail.com>
| /// ``` | ||
| #[fold] | ||
| #[default(Sides::splat(Abs::pt(0.0).into()))] | ||
| pub inset: Sides<Option<Rel<Length>>>, |
There was a problem hiding this comment.
Should we make inset Celled in this PR? Or leave this for later maybe (perhaps to make it faster to review this PR, since this isn't urgent)?
There was a problem hiding this comment.
If it's a small diff with no further complication, I'm fine with having it in this PR.
There was a problem hiding this comment.
Right... I'm having some trouble because of #[fold]. Celled<T> doesn't implement Fold, so I tried to implement Fold for it using something like
impl<T: Fold<Output = T>> Fold for Celled<T> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
match (self, outer) {
(Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
(inner, _) => inner
}
}
}Note the T: Fold<Output = T> bound there; that was necessary for Output to be Self. But that doesn't work, because Sides<Option<T>>::Output is Sides<T>, so <Celled<T> as Fold>::Output must be Celled<T::Output> instead. So I tried something like
impl<T: Fold> Fold for Celled<T> {
type Output = Celled<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
match (self, outer) {
(Self::Value(inner), Celled::Value(outer)) => Celled::Value(inner.fold(outer)),
(inner, _) => inner,
}
}
}but that doesn't work because inner is Celled<T>, not Celled<T::Output>.
So I think we'd have to give up on fold for inset if we want this to work properly (or special-case the Fold implementation for Sides<Option<T>>)... though it would be nice to have fold for the (Value, Value) case at least... 🤔
Anyway... maybe we can just leave this to another PR or something so we don't have to think about this now :p
There was a problem hiding this comment.
Maybe that's the reason I didn't add it in the first place!
| pub struct Cell { | ||
| /// The cell's body. | ||
| pub body: Content, | ||
|
|
|
|
||
| /// Creates a cell with empty content. | ||
| /// Needed to fill incomplete rows. | ||
| fn new_empty_cell() -> Self; |
There was a problem hiding this comment.
This could maybe also be realized as a Default bound on the trait.
| let cell_count = cells.len(); | ||
| if cell_count % c != 0 { | ||
| let cells_remaining = c - (cell_count % c); | ||
| cells.reserve_exact(cells_remaining); |
There was a problem hiding this comment.
Instead of this reserve, we could also allocate the full cell vector with_capacity upfront and then use a for loop to populate it above. This way we can also skip the if condition here, I think.
There was a problem hiding this comment.
Do you mean calculating how many cells will be needed with something like let capacity = cells.len() + cells.len() % c, and then doing a for i in 0..capacity instead of looping on cells itself, taking cells.get(i).unwrap_or_else(T::default), resolving it and pushing it to the final vector on each iteration?
That could perhaps work. I'll have to allocate a new vector with with_capacity anyway in my x,y fields PR (since cells will be potentially moved around), though I think I'll have to keep this if with reserve in the next PR, since we can't predict how long the final cells vector will be when cells can have arbitrary positions (we can only predict its minimum length). For this PR, though, this can probably work. (Not sure how it behaves in terms of performance, but hopefully shouldn't be too bad?)
There was a problem hiding this comment.
I meant the capacity calculation like you said. I didn't necessarily mean putting everything into a single loop, but that might work as well! I was just thinking that if we reserve everything up-front, then the cells_remaining loop doesn't need to be in an if because it would just run zero times. I'll leave up to you whether a single loop or two loops work better.
There was a problem hiding this comment.
Oooh!
Actually, initially the if wasn't there, but a lot of tests started failing... Turns out that, if cell_count % c == 0 (the last row is already filled), cells_remaining would be equal to c - 0, a.k.a. c, so it would add more cells than necessary. I couldn't find a better alternative at the time... though, using early allocation as you suggest, we could probably just use let cells_remaining = cells.capacity() - cells.len() instead :p
There was a problem hiding this comment.
From https://doc.rust-lang.org/std/vec/struct.Vec.html#method.with_capacity:
The vector will be able to hold at least capacity elements without reallocating.
I don't think we can rely on calling cells.capacity(). But we can use the variable we called with_capacity with.
There was a problem hiding this comment.
I've simplified the overall structure of the code. I couldn't manage to remove the Edit: I've managed to remove the if, unfortunately.if; I just had to recall a fundamental lesson in modular arithmetic: c - cell_count % c might not be within 0..c, but (c - cell_count % c) % c will be :)
I tried to use Vec::with_capacity, but I would necessarily have to change things to use a for loop with push(next()) or something - I thought of using .extend() to avoid doing that, but turns out the usage of SourceResult inside iterators is a pain (I'd have to collect and end up creating yet another vector!). I will have to eventually create that for loop and stuff, but I thought the new code is simpler for now; let me know what you think.
There was a problem hiding this comment.
Regarding possible capacity optimizations, I believe the iterator holds the correct size hint all throughout the iterator "pipeline" (see here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b565d7bb84801927b1029389d1e305de ), so we should probably be fine I think, assuming collect is smart enough.
|
Thank you! |
Creates the elements
grid.cellandtable.cellwith the propertiesfill,alignandinset, besidesbody.Fields
xandywill be added in a future PR.Part of the second task in #3001
grid-cell.typandtable-cell.typ.gridandtabledocs (we can probably adjust them better after this PR if needed).Summary of changes
User-facing API
You can now override grid and table properties for particular cells through
grid.cellandtable.cell. You can also apply show rules on them. Show rules allow overriding align and inset, but not fill.The following examples now work:
The outputs of the examples with
tableare:Implementation details
GridLayouterand related types (includingCelled<T>) are now in a new file:layout/gridlayouter.rs.Cellstruct which (for now) holds a body (Content) and a fill. (Contains only properties useful for the GridLayouter.) It implementsFrom<Content>so you can create aCellwith just a body and no property overrides (used inlistandenum's implementations).GridCellandTableCell, the elements corresponding togrid.cellandtable.cell, respectively. Moved the application of per-cell alignment and inset to theShowimplementation of those elements.ResolvableCelltrait with a method.resolve_celltaking its final position in the grid (x,y), the global grid options (fill,alignandinset), and returning aCell(with only body and fill). This is implemented byGridCellandTableCell.Synthesize": the cell's fields are adjusted to their final values, so that, in show rules,it.fill,it.alignandit.insetwill have their real values instead ofautoeverywhere, which isn't very useful.Content, and becomes thebodyin the returnedCell, which also holds its final fill.CellGridstruct which holds a vector of cells (they are owned); the tracks;is_rtlandhas_gutter. Those fields were removed fromGridLayouter(except forhas_gutteras it's just abool) and replaced by agrid: &'a CellGridfield.CellGridis responsible for positioning the cells in the grid according to the tracks. (In this PR, for now, it just converts cells to a vector ofCelland ensures that vector has the correct length.) It does not, however, measure the tracks, or otherwise do anything related toLayout- that's entirely done byGridLayouter.GridLayouter::cellmethod, which retrieves the cell at a certain position, was moved toCellGrid::cell. This is so theGridLayoutercan callself.grid.cell(x, y)without prompting a full immutable borrow ofself, which caused borrow check problems (when usingself.cell(x, y)) insiderender_fills_strokes, asselfwas already mutably borrowed, but through a different field.CellGridcan be constructed either throughCellGrid::new, which accepts an array of already finalCellinstances (used bylistandenum), or throughCellGrid::new_resolve::<T: ResolvableCell>(used bygridandtable), which takes an array of resolvable cells and resolves them into finalCellinstances, before callingCellGrid::newat the end.CellGridis created by the caller ofGridLayouter, and must be passed by reference as a parameter upon creating a newGridLayouter.GridLayouterwas adapted to use per-cell fill instead of a global fill (assumes each cell was already resolved before throughCellGrid,if needed, and is aware of its final fill property). It does not handle alignment and inset at all.Additionally, created
Smart::or_elsesince it was convenient for the implementation ofresolve_cellsbyGridCellandTableCell. Fixed some weird docs onSmart::and_thenas a drive-by.Alternative design: Generics
Before dc99528,
Cellwas a trait implemented byGridCellandTableCellwithfillas a function (instead of a struct withbodyandfillas fields). This causedCellGridto be generic overT: Celland hold aVec<T>instead of aVec<Cell>. As a result,GridLayouterwas also generic overT: Cell. There were two main problems related to this:GridCell, forTableCelland forContent(list / enum). This could cause some bloat, although it could be reduced by refactoring a bit the implementation. Still, that was something to consider.GridCellandTableCellhad to (somewhat redundantly) implementLayoutto satisfy being aCell, but they already implementedShow. In theLayoutcode, additionally, each cell was being cloned so it could bepack()ed before laying out. Thus, each cell was being cloned on each call tomeasure()orlayout()by theGridLayouter. With the new design, we onlypack()once (without cloning!): on theresolve_cellcall, before theGridLayouterinstance is even created.Possible problems and regressions
Cell.bodyfield) will hold a copy of their own fill, alignment and inset data. Thus, cells are now larger than just a simple Content body field. Not sure if there's much we can do about this if we want to make "mini-Synthesize" possible. (Would be convenient with some Cow-like thing, but not sure how we could achieve that?)[ABC]cells each 3-4% slower to cold compile (5.2 -> 5.4ms) on my computer (AMD Ryzen 7 5700G CPU). Increasing to 5000[ABC]cells renders a 10% cold compile time increase (32ms -> 35ms) For a document with 50 tables and 50 grids each with 5000[ABC]cells, it becomes about a 8% cold compile time increase (645 -> 700ms). It seems to remain that way for 5000 tables and 5000 grids with 5000[ABC]cells each (34s -> 37s), though that's very extreme already 😂