-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Initial table per-cell customization [More Flexible Tables Pt.2a] #3037
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
Conversation
|
||
self.finish_region(engine)?; | ||
|
||
self.render_fills_strokes()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
crates/typst/src/layout/grid.rs
Outdated
); | ||
)?; | ||
|
||
// Prepare grid layout by unifying content and gutter tracks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment is outdated.
crates/typst/src/layout/grid.rs
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, removing sounds fair for now.
tests/ref/layout/grid-cell.png
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe that's the reason I didn't add it in the first place!
pub struct Cell { | ||
/// The cell's body. | ||
pub body: Content, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
/// Creates a cell with empty content. | ||
/// Needed to fill incomplete rows. | ||
fn new_empty_cell() -> Self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. That makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.cell
andtable.cell
with the propertiesfill
,align
andinset
, besidesbody
.Fields
x
andy
will be added in a future PR.Part of the second task in #3001
grid-cell.typ
andtable-cell.typ
.grid
andtable
docs (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.cell
andtable.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
table
are:Implementation details
GridLayouter
and related types (includingCelled<T>
) are now in a new file:layout/gridlayouter.rs
.Cell
struct which (for now) holds a body (Content
) and a fill. (Contains only properties useful for the GridLayouter.) It implementsFrom<Content>
so you can create aCell
with just a body and no property overrides (used inlist
andenum
's implementations).GridCell
andTableCell
, the elements corresponding togrid.cell
andtable.cell
, respectively. Moved the application of per-cell alignment and inset to theShow
implementation of those elements.ResolvableCell
trait with a method.resolve_cell
taking its final position in the grid (x
,y
), the global grid options (fill
,align
andinset
), and returning aCell
(with only body and fill). This is implemented byGridCell
andTableCell
.Synthesize
": the cell's fields are adjusted to their final values, so that, in show rules,it.fill
,it.align
andit.inset
will have their real values instead ofauto
everywhere, which isn't very useful.Content
, and becomes thebody
in the returnedCell
, which also holds its final fill.CellGrid
struct which holds a vector of cells (they are owned); the tracks;is_rtl
andhas_gutter
. Those fields were removed fromGridLayouter
(except forhas_gutter
as it's just abool
) and replaced by agrid: &'a CellGrid
field.CellGrid
is responsible for positioning the cells in the grid according to the tracks. (In this PR, for now, it just converts cells to a vector ofCell
and 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::cell
method, which retrieves the cell at a certain position, was moved toCellGrid::cell
. This is so theGridLayouter
can 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
, asself
was already mutably borrowed, but through a different field.CellGrid
can be constructed either throughCellGrid::new
, which accepts an array of already finalCell
instances (used bylist
andenum
), or throughCellGrid::new_resolve::<T: ResolvableCell>
(used bygrid
andtable
), which takes an array of resolvable cells and resolves them into finalCell
instances, before callingCellGrid::new
at the end.CellGrid
is created by the caller ofGridLayouter
, and must be passed by reference as a parameter upon creating a newGridLayouter
.GridLayouter
was 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_else
since it was convenient for the implementation ofresolve_cells
byGridCell
andTableCell
. Fixed some weird docs onSmart::and_then
as a drive-by.Alternative design: Generics
Before dc99528,
Cell
was a trait implemented byGridCell
andTableCell
withfill
as a function (instead of a struct withbody
andfill
as fields). This causedCellGrid
to be generic overT: Cell
and hold aVec<T>
instead of aVec<Cell>
. As a result,GridLayouter
was also generic overT: Cell
. There were two main problems related to this:GridCell
, forTableCell
and 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.GridCell
andTableCell
had to (somewhat redundantly) implementLayout
to satisfy being aCell
, but they already implementedShow
. In theLayout
code, 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_cell
call, before theGridLayouter
instance is even created.Possible problems and regressions
Cell.body
field) 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 😂