#Tables
1 messages Β· Page 4 of 1
though it wasnt hard to add rowspan support for that
well, had to think a bit to get the best algorithms and stuff but yeah
:p
im really glad i did colspans first
i cant imagine debugging both at once
lol
and the colspan code gave a strong foundation for the more basic rowspan stuff
I could imagine sometime in the future people would use typst to generate HTML, and have longer tables, and others may want to print those to pages and PDFs and such. Of course, table sizes would be arbitrary in some formats and have to break in others. Itβs probably a worst case scenario for this.
well yea
i think all of this work would be basically useless when exporting to HTML
lol
though HTML itself supports rowspans and colspans
and headers
so whatever
and regardless
the pre-grid work would still be useful
i.e. table.cell and the ability to specify arbitrary positions and stuff
that'd translate into HTML with cells in the same positions as in PDF
in theory
that'll depend on how the "layout rework" / layout IR thing pans out
but it's definitely possible to reuse like 100% of that particular code lol
Tables
I'll write down some thoughts about this later, not sure whether still today.
Ok!
Here's an example, which I will use for my question: There are two columns. In the first column there is cell A, which is a rowspan going from row i to k. In the second column, there is cell B, which has no span. Row i is auto-sized and a few non-auto rows follow. If I understand your approach correctly, to determine the proper size for B, you measure A and then subtract the fixed row sizings from i+1 to k plus the gutters in between and this will be the height of B.
Now the question: Do you measure A in an infinite vertical space or do you measure it precisely with the regions it will be layouted into later? If it it's latter, couldn't you simulate how the rows i+1 to k will be layouted (since they are all fixed sized, let's ignore fr for now) and by that also know which gutters will be present?
Do you measure A in an infinite vertical space or do you measure it precisely with the regions it will be layouted into later?
Sort of the latter I think? I mimic its backlog (at least the heights we know from previous regions up to the last spanned auto row), but thats about it. However, now that I'm thinking about it, it probably would make sense to simulate that even more precisely, by adding the current region's size to the backlog, and then appending whateverself.regions.backloghas.
If it it's latter, couldn't you simulate how the rows i+1 to k will be layouted (since they are all fixed sized, let's ignore fr for now) and by that also know which gutters will be present?
Hmmm... I didn't consider that. But that also sounds a bit annoying to implement π€
I'll experiment with the idea though, so thanks for sharing
By the way, regarding the stroke thing you wrote about a while ago. I think we should not let the strokes affect layout like I attempted in my branch. It would be less predictable and way too complex.
ok, though what I was thinking of doing earlier was just increase the cell inset based on the stroke thickness
it'd happen at grid resolution stage, so it'd be pretty straightforward
though at the same time I think it could be undesirable
I would at least leave it for now. Especially since then it is consistent with how other containers behave at the moment.
Configurable inside/center/outside strokes can be a separate work package across the whole layout engine.
got it
also btw, i was thinking about this and this could be a bit more complicated than i thought
for two main reasons
the first one being that (in my view) it's still possible for the rowspan to receive less space than it needs, since some other cell in the auto row could make it expand enough to change the configuration of gutter rows, which could theoretically make our predictions fail
the second one being that we'd have to simulate layout with quite some precision, so in particular we'd have to consider that there might be unbreakable rowspans contained within rows spanned later, so we'd have to take that into account too
cuz that could cause a group of rows to move to the next page
yeah, it would need to simulate the behavior for all columns
(just to finish the thought) i mean that's not that hard to simulate, but in the future we'll also have header rows in the mix and stuff, so the implementation can get a bit complex (perhaps there'd be a way to deduplicate stuff here though)
my first idea was to actually do the layout rather than simulating it. and then backtrack when layouting a rowspan.
It would be slower but maybe not that bad due to memoization.
At least if we ensure that it doesn't degrade to exponential time in edge cases.
hmmm
so you mean we'd "unlayout" all rows after that last auto row, (after all rowspan rows were laid out) measure again and repeat layout for the remaining rows?
i mean
"unlayout" after measuring
not the opposite
lol
yeah more or less
though maybe if we do that, we aren't necessarily converging to a good solution, just creating a new configuration
idk
in the second pass we would know the sizes for sure
at least for the first row we backtracked back to
at least I think so
hmmm
i think i'll need to think further about this, rn my brain is kinda melting π
in principle this would lead to some rather significant changes in the implementation
cuz rn rowspans are only laid out at the end
after all other cells
but well, if we can get that to work, then perhaps it's better, in the name of correctness
is a rowspan ending in row k laid out at row k or after everything else?
right now the latter (although it's possible to change that)
interesting
one thing I'd like to note is that it is important in which order things are pushed into frames
as that decides the numbering order
but it can also be faked by inserting things at an index (into a layer) after the fact
right
my main concern is that some rows can be omitted from layout
for example, if row k is fully empty by the time the rowspan gets there, and we depend on row k to layout, then the rowspan isnt laid out
assuming row k is an auto row
cuz empty auto rows are omitted
what is the definition of "empty"?
measure returned empty frames
(im referring to existing code )
i guess we could use some mechanism to detect if theres some non-empty rowspan which stops at that auto row, if so push it even if as an empty frame
I think that this code is only there to prevent empty frames at the end of a page in case of a multi page auto row.
sorry
i meant returned zero frames
this basically
(ignore the unbreakable stuff, that's probably gonna change :p)
I am not sure whether that happens in practice tbh
at this point it will basically only happen for rowspans
:p
cuz measure skips all frames from previous regions (for the last spanned auto row)
and ignores rowspans (when it's not the last)
so for example , if a cell spans all columns and has a rowspan across 5 auto rows, at least the first 4 are omitted
which caused some problems with fill and stroke, but I managed to work around that
I for sure don't want to force an approach. You know the problem better than I do. I'm just speaking my thoughts.
oh dont worry
consider that to be me thinking out loud
π
I also wonder about fractional rows
In some experiments with tablex, fr + rowspan failed to fill the whole page.
just to confirm, the first page? or subsequent pages?
Just one page
hmmm that's interesting
I can send the code when I'm home
yea sure
i'll check the tablex implementation later but in principle it's likely distinct from the one we have now
cuz tablex doesnt layout per region but per row
so row heights are all calculated in advance
is fr best effort then?
well, it depends on measure basically
which isnt always fully accurate
there could be multiple variables at play there, e.g. layout convergence
It did converge
i see
i can take a look later then
that could be insightful
but either way fractional rows are indeed a bit of a problem
in principle it could be simpler to just admit that we can't fully predict how stuff will be laid out and provide tablex's fit-spans option to users so they can control which rowspans affect auto rows
but i wanna take a stab at the other approach of backtracking at the last row
let's see how that turns out
also btw
a bit unrelated to this discussion but
ive been thinking of starting work on line customization in parallel, in case we spend too much time on rowspans
im still evaluating whether or not that could interfere somehow but in principle i think it should be pretty self-contained
im not sure yet though so just thinking out loud again
π
This demonstrates what I was referring to earlier:
#import "@preview/tablex:0.0.8": *
#show: columns.with(2)
#tablex(
columns: (80pt, 80pt),
rows: (auto, 1fr, 20pt),
rowspanx(3, lorem(10)),
[A],
[B],
[C],
)
#colbreak()
#table(
columns: (80pt, 80pt),
rows: (auto, 1fr, 20pt),
lorem(10),
[A],
lorem(10),
[B],
lorem(1),
[C],
)
Thanks, I'll take a look
I'm not really sure why it's doing that lol
measure went wrong along the way or something
Anyway
I'm starting to prototype the line stuff cuz yeah
I'll share in a sec
so, what do we think about the following:
- global (Grid-wide) stroke is a dictionary with 3 keys:
(inside: stroke or none or (x, y) => (left, right, top, bottom: cell stroke),
outside: stroke or (left, right, top, bottom: border stroke) or none or auto (default, inherits 'inside'),
gutter: stroke or none or (x, y) => same as cell stroke but for gutter cells or auto (default, inherits 'inside') )
^not 100% sure about the gutter one yet
writing just stroke: red for example is the same as stroke: (inside: red)
-
per-cell stroke: table.cell(stroke: stroke or none or (left: right: ...) or auto (Default))
-
explicit lines:
grid.hline,grid.vline, ... => in principle they'd be types, not elements, but not 100% sure- they can cross gutters by default (though we could add a parameter like
skip-guttersto override that, however not strictly necessary)
- they can cross gutters by default (though we could add a parameter like
-
priority order is: per-cell stroke and explicit hline / vline > anything defined by global stroke (in principle i was thinking of having global stroke just "disappear" on specific points when you override it somewhere)
what's the benefit of the extra layer in which inside/outside live, as opposed to having them alongside left/right/top/bottom?
and could I also write a left/right/top/bottom dict directly without returning it through a (x, y) => function?
hmmm. it felt a bit more organized
though we dont have to maintain this layout, i guess the more important question here is regarding the functionality
in principle i think im fine with removing the outside layer
on inside? I didnt consider that (mostly cuz it could be ambiguous with outside ) but I think it should be fine when nested
right, i guess theres also the fact that with outside I could just slap a Sides<...> and be done π
so thats probably just the first thing i had thought of
but it's fine, we'll create a new dict type either way so
you can create a thin wrapper around Sides just with the extra fields
for that, you might actually be able to salvage something from my branch
fair enough
which?
oh right
the strokes branch
got it
from a user perspective, i always liked this booktabs style with explicit lines (your third point), so i suggest that this kind of thing should at least be simple to typeset (without the user having to count rows first)
sure thing
im not rlly sure regarding vlines cuz I have to admit the way tablex does it is a bit hacky
like the
#tablex(
vlinex(), (), (), vlinex(),
)
deal
but I think I can implement something similar but without the (), () stuff
you'd just have to table.vline(stroke: none)
Β―_(γ)_/Β―
anyway im writing stuff down
and i think i have a clearer idea of my initial plan for strokes now
basically we'll have the following priorities:
cell stroke > explicit lines > automatic lines
and when a cell's stroke is none that's equivalent to not changing much
also I think I will use the tablex approach of disabling the automatic line when you draw an explicit line over it
otherwise it would be too clunky to deal with
however the automatic line won't be disabled if you just change cell stroke
after all you can completely override the automatic line using explicit lines, but not with cell stroke (which cant cross gutters since gutters - in theory - aren't cells)
oh god
this will multiply the size of table cells tenfold
sorry people
π
Maybe it is time to but a box somewhere in there
well, it doesnt help much with RAM usage (since they are already in the heap) but I guess it could help for traversal
well, if an Option is none we waste 480 bytes right now. if there was a box in the Option we would save that
if the strokes are uniform strokes it should probably rather be Arc though
and after type rework that Arc could even be shared with what Value will then use to store the Stroke!
right
I guess for per-cell override we can just create a new Arc anyway
though we would have to Arc on the Sides right?
ah
though
I think this could not be very helpful if they use the (x, y) => (sides of stroke here) form
only if they use (sides of stroke here) directly
which is probably gonna be rare
and automatic lines (i.e. specifying red + 2pt instead of either option above) dont affect the cell stroke at all (at least in my current mental model)
but they price for the field size is paid always
since there is no other Option in the type that's the highest layer where we can save anything
or through Arc sharing of course, but that seems harder
i think what i was thinking of is that Arc sharing would be very rare (only in the (sides of stroke here) case)
for 99% of other cases it would be just a Box
I was thinking of the case where Sides casts from a stroke and ends up calling Sides::uniform
hmmm
I think I see what you mean
so you mean that , when they write (x, y) => black for example
we reduce the amount of space used by 4 times
yes
we could also implement a more general optimization within Sides
I hope it doesnt affect performance too much π , but a small price to pay for 10000 tables with 5000 cells each...
where it chooses between uniform and non-uniform boxed/arced repr internally
but that's out of scope for the PR I would say
i agree
but sounds like a cool idea in principle
we'd have to study whether or not it'd be worth it for other cases though
but that's for another day
also btw @robust viper I can finally implement Fold on Celled thanks to your PR on that
so that's pretty cool π
nice
right now I think im only gonna implement it for Value, Value, im not sure if it'd make sense to implement for others
I guess it could perhaps make sense for inner = Array, outer = Value which isnt too hard to implement
but then inner = Func, outer = Value would require creating another Func π€
so for now i chose to be consistent :p
ok so, do you think it's fine to implement IntoValue / FromValue / Reflect for Arc<T> in order to achieve this? Or should I make a newtype for this to reduce the impact?
well, the plan is to generalize it in the future IIUC so maybe the former isnt that bad of a workaround for now, but still could be seen as too much for the PR ig
it seems okay at first glance
ok
i think it wouldnt be too hard to change later anyway
so i'll go with that for now
right so
@robust viper regarding the Arc optimization, I found a small problem :p
I believe the optimization is considerably hindered by Resolve
because to implement Resolve on Arc<T> we have to create a new Arc<T::Output>
so basically every side is always a separate Arc
rn strokes are always resolved so im not sure how we'd tackle that
even if they arent resolved immediately, i.e. you specified (x, y) => stroke, that just delays the resolve call to when the function is effectively called, i.e. when generating the grid (I had to make a separate ResolvedCelled<T> type to make that possible)
an alternative would be to check for equal strokes somehow, but that sounds like the amount of bookkeeping required wouldnt be worth it
unless we use a HashMap or something I guess
well , thinking further, I guess there isnt much we can do regarding delayed resolve calls either way
while non-delayed ones should be fine
we just wont be able to convert to FixedStroke right away
(otherwise we'd have to create new Arcs to convert Arc<Stroke<Abs>> - that's the output of resolving Arc<Stroke> - to Arc<FixedStroke>)
uhhh actually idk
theres a problem which is that pushing the stroke for GridCell requires converting back from Arc<Stroke<Abs>> to Arc<Stroke> (aka Arc<Stroke<Length>> )
maybe i can like fold twice or something to avoid that lol
sending this link here for reference https://drafts.csswg.org/css-tables/#table-layout-algorithm
that's how browsers do tables apparently
and seems like we have been nailing it so far
π
step 5 is different though but thats the only "big" difference
I don't see a step 5
3A.5?
this
right
in tablex they all roughly match
in typst the current algorithm measures rows on the fly
so it's a bit different
Is one approach better than the other?
well
measuring on the fly is probably better for the simplest cases (i.e. no rowspans)
but we will have rowspans now so we have to backtrack at some point or another
considering we'll have html output at some point it might be good to align with css tables anyway
yeah for sure
though i wouldnt tie our hands too much in that sense
but still not bad to know that we arent that far off
of note, my current algorithm just delays layout of rowspans to the end of table layout
but there are a few problems with that (as Laurenz said, the order in which things are placed matters for counter numbering and stuff)
so in theory i'd make it happen whenever the last row is reached instead
but the problem is that the last row is currently not guaranteed to exist
π
though thats pretty easy to fix in general
it's just that theres an optimization for when no space is being used in an auto row , then it's just yeetus deletus
anyway
so
one possible solution to the problems i was facing could in theory be to take an approach similar to the one suggested by the CSS spec
but with no rowspans it could be a bit wasteful
maybe we can have a flag like "needs relayout" idk
but im still considering it, cuz laurenz did propose an alternate algorithm , so im currently thinking about it
an update regarding this
from what im seeing, using #[resolve] basically means resolve is called on the spot
meaning the Arc optimization is basically useless :p
(as it is right now)
cuz each resolve has to create a new Arc so u get one Arc per stroke field in the end
one possible way around this would be to make a SharedSides or smth as an enum with Uniform(Arc<T>) vs Sides(Sides<Arc<T>>) or something
then it would at least allow some gains when using black instead of (left: black, right: black, top: black, bottom: black) for stroke
but anyway
im gonna work with what I have for now, we can change later
fucking awesome
What am I looking at?
some random test failure
among like 30 others
and theres a veeeeery tiny difference
lol
of two pixels!
but with png it's hard to understand anything.
lol
yea
but it was enough for me to figure it out
i screwed up the thickness of horizontal lines
lol
cuz im implementing per-cell stroke on vertical lines first
well, with that said...
yey
it looks like for small files pdf files are literally 5 times smaller
so maybe pdf is the way.
i mean... depends a lot on the content lol
but anyway
ok so
one thing i was wondering about
if a cell has something like stroke: none, should that cause it to disable the automatic lines around it? π€
and what about explicit lines ?
right now im just considering stroke: none as "i dont care"
so lines just keep going like usual
i guess thats also how stuff behaves with a block or smth
how then you make it "I don't want stokes"?
none is void, nothingness
that'd be a global stroke: none
hmm
basically it'd encourage using set union rather than set difference to build your set of table lines, if you get what i mean
:p
well, I think it's the same as CSS priority selectors. the more specific it is, the more priority
so if global-only set, then that's that. if also stroke none set for a cell, then it should override the global setting
so it should be "no stroke" around a/the cell
yea but consider like
consider two adjacent cells AB
what if A.stroke.right is none and B.stroke.left is red?
right now im prioritizing B either way
but i think it'd be more natural if it were red
and this is what I don't understand in spreadsheets. idk what calc does in this situation
afaik it seems that it prints whichever's stroke is the thickest.
so rn what im doing is a three-way fold
im folding the left stroke with the right stroke with the automatic line's stroke
so like if red stroke is thicker than blue one, then the stroke will be red and no blue at all.
so if the left stroke is 5pt, the right stroke is red and the automatic line is dashed, you'll get a dashed red 5pt stroke
so "some" stroke by this logic should be prioritized over "none" stroke.
hmm, though interesting, idk if it is the most desired thing. after all, the user can set all the styling, so they can set it themselves, if needed. seems too complicated for a default behavior.
well
i was planning on doing that for explicit lines
as in explicit lines dont fold with per-cell strokes
but maybe that doesnt make much sense
or at least could be seen as inconsistent. idk
maybe it should be just a two-way fold
between the two cell strokes
at least that would be more consistent
but why fold?
fold combines properties in a useful way
one cell just wants a 5pt border, the other just wants a red border
so you get red + 5pt
but I think this is very impractical
but if one wants blue and the other wants red then one necessarily wins
so i prioritized the right cell in those cases
let's see what calc has to say about that
if you write black + 5pt instead of just 5pt then you will properly ask for a color
and then you will engage in the epic duel with your right partner
another alternative is to just be lazy and overlap the two
but idk
i think per-cell strokes are "in the same level" of abstraction
so i think overlapping would be bad cuz if everyone asked for the same stroke you'd have a bunch of equal strokes overlapped
unnecessarily
meanwhile it could make more sense to overlap explicit lines
well, I think calc agrees
cuz then that's on you
ig makes sense if u consider LTR
for RTL it'd be the left one automatically
and here is if red is slightly thicker:
hmm
interesting
in this case it'd probably be thicker blue
unless blue explicitly specified a thickness
then blue would still win
but be thin
i think it's more consistent to always prioritize the right cell
maybe right + bottom in general
for hlines
but there is no rlt/ltr in spreadsheets
i think this is completely biased thing. it just uses some kind of priority algorithm. maybe for that case it is "right wins".
i mean, maybe it depends on your system language
you could test with some RTL language
maybe, but we don't know
ok so
also how do you save that in the spreadsheet that supposed to look the same on any machine?
our options are basically
if left cell has red + 5pt stroke , right cell has blue stroke and the automatic line (i.e. the line coming from #table(stroke: ...stroke here...)) is just dashed (no other stroke properties overridden), then the resulting stroke for the vertical line between the two cells would be:
a) three-way fold between the three strokes: blue + 5pt + dashed (prioritizing right)
b) two-way fold between the cells (completely overriding the vertical line): blue + 5pt stroke (prioritizing right)
c) same as above but resulting in red + 5pt (prioritize left)
d) no folding (prioritize right): blue
e) no folding (prioritize left): red + 5pt
it can't depend on system's things. only if it's fonts or something.
it'd just be a visual thing
much like typst does it
what does "visual thing" even mean? it's supposed to be setting-driven. not "env var" driven.
i mean that when you open the spreadsheet in the spreadsheet app, the horizontal order of cells is reversed, and the spreadsheet grows to the left
but when saved to a text file it's in LTR order
you mean the order changes when the spreadsheet is being saved to a file?
?r
#set text(dir: rtl)
#table(
columns: 3,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
thats what i mean
but that's typst, not spreadsheet/calc
so
what im hypothesizing is that it does the same
i dont know if it does, but i didnt test it either
anyway whatever
let's set that aside
π we need opinions on this
:p
and a related question is
if the user specified table.vline(x: 1, stroke: (dashed properties here)) , the cell at position (0, 5) has a right stroke of red + 5pt, and the cell at position (1, 5) has a left stroke of blue, what would be the effective stroke of the vertical line between the two cells?
(same options)
is "automatic line" a #set table.cell(stoke: (dash: "dashed"))?
yes
wait
no
misread
it'd be #set table(stroke: ...)
not #set table.cell
#set table.cell behaves the same as overriding each cell's stroke manually (and, of note, it does fold)
but then it's not "automatic", it's the "global default" a.k.a. "table-wide default"
well
to be more specific, automatic is #set table(stroke: dashed) for example, in contrast with #set table(stroke: (x, y) => dashed)
the former generates lines on between every column and every row (what I call "automatic lines")
the latter doesnt
basically that
but with filtering
yeah
you forgot the "thickness" comparison
no it's intentional
the comparison is missing vs present
i mean i get what you mean (there are two identical options as a result)
no, I mean the "prioritizing right" thing
i would need a fourth property to separate them though
maybe miter_limit i guess lol
but at that point the question would be too complex idk
what you didn't say is if folding mixes colors
well, if you "want to sell folding", I would including the color mixing. otherwise it's not a good folding.
that's true
?r
#set table(stroke: red + 5pt)
#set table(stroke: blue)
#table[stuff]
brain malfunction, amiright
ye
what does that show?
that it doesnt mix colors
but you don't say to mix them, you just overriding one color with another
exactly
thats how it works
stroke folding atm is between missing and present properties
so if one stroke specifies some properties and the other specifies some others, the resulting stroke has all of them
and I also implied half-transparent colors, that mixes better, logically, because there is transparency involved.
but if they have the same properties then one of the strokes just wins
in this case it's the innermost set rule
in our case we dont know yet (im proposing the one from the right cell)
ok, then maybe I thought of a magical fold function that combines the two.
sounds like a new package idea. mixer or folder or something.
lol
anyway yeah so basically
right now we need to define what happens when there are conflicts
of all sorts
at one particular line position (between two cells), there can be:
- automatic line
- stroke from left cell (or top, for hlines)
- stroke from right cell (or bottom)
- explicit line
i think im gonna add those questions to the table planning document (been a while since I last updated it :p)
I think I'll go with the d (i really really want to insert the joke here)
i forgot it exists
right now my mental model is:
- explicit line causes automatic line to be completely disabled (so there's never a conflict between the two);
- for automatic lines (defined e.g. as
#set table(stroke: red)), the two cell's strokes are folded together with the automatic line's stroke - explicit lines always lose to the (folded) cell strokes
- two conflicting explicit lines are drawn on top of each other
i guess it's not the most consistent thing
but there are too many variables lol
it could be made more consistent with "fold all the things" mentality and have explicit lines fold between each other and with cell strokes as well
we could also error for conflicting explicit lines
lol
thats boring though
whenever there are a lot of variables, you have to lock some of them out. also try to keep practicality in mind, something that everyone will use, not some very niche edge case. that can be addressed last, if at all. that's my take/advise.
yea
i was thinking of just completely disabling fold for explicit lines
hence my last two choices
cuz i think they're just supposed to be overrides for what you expect of the table lines in a sense
while the rest (automatic stuff) would use folding where possible
but honestly idk
this could be too arbitrary
i need more opinions
we also can just "see what will happen" and adjust the behavior in next typst version. I mean the default behavior should be good enough for most people at first, and then we can tinker more. a lot of people can post a lot of useful feedback that can substantially simplify the process of coming up with default behavior.
Ok sΓ³
I had an idea of how we could make it more consistent
I'm just not 100% sure if it's what the user would expect in general
But basically we could make explicit lines fold between each other , with priority to the one specified last
And in particular they'd fold with cell strokes and all that
Basically all lines would be drawn in a single run
And it would be a fold all the things kind of thing
Notably I'd need the lists of lines to be sorted for the algorithm to be simple enough
But then I'd have to use an integer for priority or something , unless i folded before inserting them into the list
Tbh maybe doing it before is wiser
Actually , well, it'd still split your lines into many tiny pieces so whatever
it seems i may have done an oopsie
ok well it was mostly just me assigning a really high value to y in grid.hline(y: ...)
dont mind me
π
Is this in memory? How much ram does your machine posses?
Also that's one fat opsie
- yes
- 64 GB
ig thats what happens when you try to place a line at row 23498293849
i should have expected that
Ah okay.
ok
it werk
code:
#grid(
columns: 3,
inset: 3pt,
stroke: (x, _) => (right: (5pt, (dash: "dotted")).at(calc.rem(x, 2))),
grid.vline(x: 0, stroke: red),
grid.vline(x: 1, stroke: red),
grid.vline(x: 2, stroke: red),
grid.vline(x: 3, stroke: red),
grid.cell[a],
[b], [c]
)
horizontal lines: soon β’οΈ ...
easy border customization: soon β’οΈ ...
automatic hline / vline positioning: soon β’οΈ ...
basic error checking i really need to do this: soon β’οΈ ...
btw i made it so you cant do that oopsie now
or at least, it would be slightly harder
cuz now it errors if you try to place the line at an invalid index
instead of trying really hard to do it
and eating all of your RAM
but i mean, grid.cell(y: 293182498)[i hate RAM] would basically cause the same thing so whatever
but then that's your fault!
I actually hate that, currently, recursion and lots of calls to small methods causes use ram consumption π¦
grid.cell(y: 293182498)[i ate RAM] *
tail call optimization when
gotta let those functional typsters go brrrrr
Should I? π
i mean is it even possible? lol
if it is then... wouldnt be a bad idea
but i think it could require Rust support for it in the first place
which is currently in nightly
(the become keyword)
the what?
Someone sent me a link to code once that achieve tail call optimizations by.... Using loops?
Because tail call optimisation is converting calls to loop (jmp), if I remember correctly
Well i guess it depends on how low level the language is but yeah
With the VM compiler it could become even easier to just repeat evaluation for some bytecode
Though you'd have to make sure the arguments are in the correct registers and whatnot
And still , without Rust support it could be a bit useless lol
But idk
Tailcall is a library that adds safe, zero-cost tail recursion to stable Rust. Eventually, it will be superseded by the become keyword.
That can be used now for tail call optimization on stable. It allows many kinds of useful recursion cases.
AFAIK, It's not clear that rust will get TCO due to its impact in debugging.
ladies and gentlemen...
i am proud to announce...
horizontal lines π
huzzah
that's basically all of the "hard part" - per-cell strokes and manually-specified strokes are working
next up on the list are a few tiny things, including easy border customization and automatic hline / vline positioning
and also general cleanup ig lol
wait, what is the order of overlapping?
yes
because...?
ok
someone in tablex had proposed adding a z-index thing for lines
it could be interesting to have something like that but we'd need a bit of design discussion
mhm
So, has the rowspan been lowered in priority or is it done? or you just taking a break from it?
well
it's paused for now
it'll probably need a rather substantial rewrite to accommodate the new algorithm, whichever it is
but I still dont know exactly where to go with that
plus just fixing a ton of small rowspan things over time was really mentally draining
:p
so by working on the next PR im at least moving things forward
and for some line customization is much more important than rowspans anyway
well yes
for some
:p
and well
i hope to get that rowspan work done soon
but if it takes too long and release approaches, we can also just release a reduced version of rowspans
unbreakable-only
and then add breakable later
that'd be better than nothing ig
lol
I still don't understand this whole business with breakable rowspans (I just never had to deal with it, so idk if I will ever use it), so some rowspan capability is definitely better than none.
unbreakable rowspans are the tablex ones
they are rather simple to implement
you just gotta make sure all spanned rows are in the same page
and then just layout the cell normally
breakable rowspans are much harder
this is technically my first time implementing them since tablex doesnt have breakable rowspans
(it doesnt have breakable cells at all as a matter of fact)
but the idea is that for example you could have a rowspan going through all 100 rows of your multi-page table
so that can be interesting depending on your needs
but it's hard to implement
requires some amount of backtracking and stuff
i.e. laying out all spanned rows and then going back and laying out the rowspan
hmm, maybe I saw a breakable rowspans, but I probably never did them myself.
hold on, is the unbreakable version already implemented?
yeah
i mean the breakable version is implemented too
the problem is that it's not ideal
theres the gutter problem i spoke about before
beta version shouldn't be ideal
of course yea
but it has to be fixed before PR'ing
and the fix involves changing the algorithm so yea
will take a bit β’οΈ
I'm interested if people would prefer non-ideal breakable version over none.
well yea that could be made to work
but it would be a breaking change if we fixed it later
ig
:p
well not rlly breaking but it could change your document layout considerably
ok, we have border overrides now
(you can also just use hlines and vlines with index 0 / len() )
code:
#set page(height: 5em)
#table(
columns: 3,
inset: 3pt,
stroke: (left: 3pt + green.darken(20%), right: 3pt + yellow.darken(20%), top: 3pt + blue.darken(20%), bottom: 3pt + red.darken(20%)),
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
[a], [b], [c],
)
will radius come later?
can't it be copied from a block?
have you tested this with gutter?
not yet
In fact it probably won't work with gutter yet
But it should be easy to fix (just roughly a matter of dividing indices by 2)
I don't understand: if you can customize the outer table stroke, why can't you use a radius on it? is it (radius property) tightly coupled with another elements? isn't the table a block/rect already?
isn't the table a block/rect already?
no
why can't you use a radius on it?
they are table lines like any others
this is necessary so that cell strokes have priority
and so you can override them with manual hlines / vlines
a block/rect requires uniform stroke
so you cant override it at certain points
with that said, we could just say that border strokes have maximum priority and forbid any kinds of overrides on them
but idk if that'd be desirable
can't table use a custom radius implementation?
or will it be too hard to do?
like rounding 2 lines so that they make a round corner.
If you want you want rounded corners it sounds better to put a rect inside the cell
Or put the table inside a rect for the outside
Thats not true. You can specify separate strokes for each side https://typst.app/docs/reference/visualize/rect/#parameters-stroke
sorry
i mean uniform strokes on each side
Okay, I misunderstood then
but yeah i think you can use a block for now
with table
i guess you can "kind of" override strokes here
if the block is breakable, will the table break correctly? so that 2 corners are rounded one 1 and 2 page.
hmm
i mean, right now blocks preserve radius on all pages
so tables would (hypothetically) do the same
There's an issue tracking that I believe
there could be some parameter to block to control that
Ok so
I'm thinking here that stroke: none should get some love
Cuz right now it's just completely useless
Setting stroke: none for a cell isn't very useful for example
I guess it makes sense if you want to override some set rule or something though
But e.g. for hlines or vlines , why would you use a stroke: none when that's the same as not adding anything at all
So i could make it so explicit stroke: none wins where appropriate
Anyway
I think , with that aside, that pretty much all functionality is implemented
Idk if there's something I forgot
Oh right, gutter
Looking into it
for per-cell stuff it already works so thats nice
Why is the black line above the green and yellow?
hlines are always drawn on top of vlines
if we reversed that then there would be a black vertical line on the top border instead
I guess I donβt think about this much. I think it would look prettier if the middle hline was behind the others. But I really donβt have a clue how typst handles zorder.
well
right now there are just two for loops in the "render_fills_strokes" function
one draws hlines and one draws vlines
there isnt that much to it really :p
we could in theory make a single for loop by joining the lists of hlines and vlines and sorting them by "priority"
Hlines is the second loop?
im just not sure what would be the most expected thing here
it's the first, but lines are prepended to the frame so hlines appear on top
that is, they are added below other things
I have to admit I like the simplicity of it. I had a tablex the other day. And I had overlapping hlines but I couldnβt figure out which lines would be drawn first. Itβs more tricky when the x y cell placement is used.
Itβs one of those things that most people donβt realize or care about until they see something that they donβt expect.
yea
i was thinking that we could somehow make it so that vlines specified after hlines would appear on top
when manually specified
but
im not sure how easy that is to make
at least in an efficient way
or maybe we should just not care and have a layer parameter and thats it
with that said, this isn't an essential thing :p
we can definitely ship without this as needed
this difference is only observed if your stroke has differing thickness
Could you just provide a drawing order offset. It gets delayed prepending
Ehh now that I read it. It sounds like a hack.
Shouldnβt the stroke have a zorder attribute?
this is what I was referring to
So smart π
at least it's been requested before for tablex
but there is some overhead to that approach
in that we will have to sort the lines in every page
maybe it's not that bad though
especially if we use a stable sort
How does cetz do this?
it's worth noting though
that frames themselves have a layer mechanism
but i wouldnt just expose that directly to the user cuz there'd be the risk of messing with stuff that isnt a line
well
im not fully sure how borders would interact with this thoug
like, should we give borders a higher priority
but then what about other kinds of lines
should they have the same priority as borders
What about repeated header of the table? π Is it possible to implement this in upcoming typst release?
yeah
i mean
my current plan is to go up to part 5 (repeated headers) for the next release
im hoping it will be possible, though it will depend on the speed of previous PRs
typst 0.11 or 0.12?
π
As far as the right way to order lines . Iβd be tempted to see how html/css does it. I thought i saw a snippet above somewhere.
Not that itβs really the right way.
im having a hard time figuring that out but, from this page https://developer.mozilla.org/en-US/docs/Web/CSS/border-collapse, it seems that vlines come on top
and i guess there is also this
?r ```
#rect(
stroke: (left: red + 10pt, top: blue + 5pt),
width: 50pt,
height: 50pt,
)
though i dont think it'd be practical to implement something like that in tables
and css doesnt do that, only for borders
Also donβt overwork yourself.
yea thanks, I'm trying π
I canβt tell which line is above
neither is
Tricky
theres like a blending thing in the middle
okay
i think i see
in html borders occupy space
this is something we dont do (at least yet)
so they dont overlap , rather one goes above the other
The vlines for html?
oh
nvm
i mean it's true that they occupy space
but what they're doing here is
largest thickness goes on top
on my browser (firefox) at least
yeah
i just wonder if this is generally desirable though
but it seems to produce the most pleasing output
im just worried cuz this could be a bit inconsistent with the rest of typst
blocks and rects do the blending thing, here we dont
but theres also not much we can do ig
Well itβs one way.
Iβm trying to think when you would want thin strokes above thick strokes. But idk it seems like a pretty good general thing. Iβm sure Iβll find something later
The thin ones can also be manually placed later.
Iβm realizing how rare it is to have h/v lines of different colors.
A w3c example.
Latex doesnβt follow the same rules.
But it is more of an example table in both cases.
im trying to figure out how tabularray handles this
Itβs truly a gem among latex table libraries.
This example is from tabular ray. It has thin dashes over the thick boarder.
So far two camps.
- big borders on top
- hlines on top
i cant deny the html one looks a bit nicer
but at the same time its stroke affects layout so it's not really the same
Anyway Iβm sleepy. Hope you find the best solution.
opinions needed!
atm hlines are always drawn on top of vlines, which can look a bit weird as shown above. So:
-
should we display lines with larger stroke on top of others with smaller stroke, as HTML/CSS (or at least Firefox) seems to do? That avoids the problem shown above, but is there any situation in which the current behavior could be desirable?
- β οΈ In HTML/CSS, stroke thickness affects layout, which isn't the case in Typst, which could impact the answer.
- Tabularray (LaTeX) seems to do the same as Typst (hlines on top).
-
should we add smth like a
layer(orz-indexbut that's ugly) param to(grid/table).(hline/vline)to let users customize the order in which lines they specify are drawn?- Or should we maybe, instead, have lines always be drawn in the specification order (so specifying a
vlineafter anhlinein the table params causes it to be drawn above that hline in particular, but not on top of hlines coming after it)?
- This other approach could perhaps not play so well with automatic positioning of lines. - Or we can just not care/postpone.
- Or should we maybe, instead, have lines always be drawn in the specification order (so specifying a
also, some other things we have to decide:
3. What about gutter? Rn you can customize just a gutter line by placing a line between 2 cells and covering with 2 lines on top of both ends. But I thought about a gutter parameter to the stroke option, which would accept a stroke type, an array of columns, or (x, y) => stroke where x, y are the gutter's position relative to other gutters (so like (0, 0) would be the 1st gutter cell, (1, 0) would be the 2nd gutter cell in that row..). Thoughts?
- Should a cell with
stroke: noneor(left: none)or smth (not unspecified stroke) interrupt lines around it? (Rnstroke: noneis useless on cells, so I thought of maybe making it useful.)- What about explicit lines with
stroke: none? Maybe that could be used to remove some default strokes, making that behavior opt-in instead of forcing it.
- What about explicit lines with
cc @robust viper @keen crescent @upbeat pine @grand haven (and others who could want to join in)
On 4, I don't think it should disrupt any lines around it. If I specify stroke as none, then that just means I don't want a stroke for that line, which has no bearing on whether I want a stroke for another line, so interrupting those other strokes could be a bit awkward. I would think that behavior would be produced when stroke color is set to the page color
I'll probably need to catch up on the rest of the stuff to give a better opinion; haven't been keeping up due to some personal issues
fair enough
i was thinking that some way to disable one default stroke but not all could be very useful tho
but maybe this is like "backwards thinking" a bit
rn tablex just completely removes a default stroke if you specify a line over it
What do you mean by 'default stroke?'
those strokes
if you specify your own stroke at the second column (between the two cells), tablex removes the black line in the second column
Ah, I misread slightly, I thought you were talking about strokes on lines
of course that makes sense if your line covers the entire thing. but what if your line is restricted to a small part
hm
actually
i mean if it's restricted to a small part then you probably just want to affect that part anyway
so
I think it could be awkward if you just remove stroke on all boundaries of a single cell but keep other cells with parts of their strokes
oh that wouldnt happen
but still
uhm
i subtly kinda changed the topic which may be causing a bit of confusion
but still I agree with you
more generally, how about we add something like
stroke: (horizontal: none, vertical: x => (red, blue).at(calc.rem(x, 2)))
does that sound useful?
cuz i mean technically you can do the same with cell strokes
so it could be "redundant" in a sense
so remove the stroke on the horizontal, but keep it on the vertical?
I see that kind of as a hacky way of creating a row/colspan
here i mean table(stroke: ...)
yea sorry
although that will be possible anyway
lol
but
i mean like table-wide
like tablex's auto-vlines: true/false, auto-hlines: true/false
Table wide I definitely can see that being useful, if redundant with auto-h/vlines
Very useful
Is there an implementation of that feature from tablex in base yet? I think I remember that being in discussion
in base
wdym?
You mentioned auto-h/vlines in tablex, but not in base typst (I don't remember if it's already there or not, I might be being dumb)
oh
yeah base typst barely has any stroke customization
rn
what im doing rn is adding all the stuff related to that
Ah ok
note that in the future you'll be able to achieve more or less the same with
table(stroke: (x, y) => (left: (red, blue).at(calc.rem(x, 2)), right: (red, blue).at(calc.rem(x + 1, 2))))
I wonder if disabling the lines at the stroke level versus the line level is the better way to go though
Stroke level brings up bad memories of tables in ms-word
lol
yeah i was thinking about making the API as flexible as possible for both usecases
i want to use lines vs i want to use per-cell stroke
Understood, I think that's reasonable
i mean , if u think about it, this is basically map-hlines / map-vlines from tablex
except that it doesnt affect your own explicit lines (they always override the table-wide thing)
which i think would be consistent
either way
i wont add this for now, just bringing it up
we can very well have a PR without everything and later just add more stuff as needed
next thing ill implement will be a pos parameter to explicit lines
akin to gutter-pos in tablex
cuz you can be either to the left or right of gutter
also btw dont worry about it
i only pinged you cuz you seem to be interested in general, but please dont feel forced to respond, we all have our own stuff so it's fine π
Hi @wary knot - in the new Table api, can the row height be specified?
Yeah, just like in the current one
You'd write rows: (1em, 2em, 3em) for instance
- idk if this would improve output of all use cases, so maybe no.
- this would be very cool as it looks like it provides a very granular settings which is good for typst
- order of specification could be nice as an alternative option, this way your tables will be less verbose, but for complex output a
layerthing should be used
- but gutter is an empty space between cells, not a stoke line
- probably. I think the priority is obvious:
table<vhline<cell. so if cell doesn't want a stroke around it, it won't have it. but idk about adjacent cells with different stroke settings. probably a cell with visible stroke should win.
I think 2 is overkill for now, I would not care/postpone. I don't really understand the relative part in the idea of 3. Overall, I'm not sure how often gutter will be used with table and how much work we should invest in it. It mostly exists because it is useful for grids. 4 sort of depends on how we view strokes and lines: Are they the different interfaces that configure the same thing or are they separate things?
I'm basically of the same opinion. The only thing is probably better to have a layer than to adhere to the input order. The input order can become unreadable very quickly, especially with complex tables.
I like being able to fiddle with the layers. But I also like a smart default I would prefer thick lines on top for most situations. I donβt think inputs order is valid because you could use xy placements. And that could cause overlapping artifacts between adjacent cells.
the input order would work for simple tables and it will make tables less verbose.
thanks for the feedback everyone!
regarding 3: i meant lines going over that empty space (lines over gutter)
regarding 4: that's basically what im doing right now, though ignoring stroke: none
regarding 2: ok π
i was thinking of adding it just as a way to circumvent the stroke thickness problem, if we dont implement largest thickness on top
but it's not strictly necessary
regarding 3: i meant basically having a separate coordinate system for gutters, i.e. a "gutter cell" at (x, y) in that system would correspond to real internal cell (2*x - 1, 2*y - 1)
but yeah i mean i just thought of having a separate way to configure gutter lines, but it's already possible to do that in a way so it's not that bad
regarding 4: right now they are basically the same thing
ive made everything fold, in order of priority, so cell strokes fold with lines at the same position and then fold with global stroke at the same position
but the fold ignores none, so it's only none at a position if all of those are none
I'm fine with any automatic solution, I just wouldn't want an extra argument.
same with gutter. It shouldn't have extra API for now imo because the use cases are still (at least to me) rather unclear.
gotcha
do you have an example? this is something I would never have thought of. and probably have never seen.
?r t=l
#table(gutter: 5pt, columns: 3, ..([a], [b], [c],) *3)
ahhh, I thought something like this:
so something like "lines that intersect/overlap gutter"
#1182377294682128445 message:
Rn you can customize just a gutter line by placing a line between 2 cells and covering with 2 lines on top of both ends.
so "a gutter line" is that part of a line that overlaps gutter? what is the "line" in "by placing a line between 2 cells"? and which "lines" are in "covering with 2 lines on top of both ends"?
is this "a gutter line"?
Yep
The idea is that you'd draw a line between the top b and the bottom b
And then place a line on the top b only, and on the bottom b only
That way you'll have changed the gutter line
This
And then add right stroke to both b's
What's left is the gutter line
so you don't actually draw a full-length vline? you can do \cline{1-2}?
so it's a 2 step trick?
Yeah
To customize just the gutter line
But as Laurenz said, it would probably be a very rare use case
so what about the "gutter cell" thing? I don't understand how can you navigate the indices here...
I don't like that there is something that overlaps the gutter in general. it makes it less of a gap and more of an additional cell. so it's not 3 by 3 table, but a 5 by 5 table where some cells are smaller than others. and you can't put anything directly into the small cells.
so making lines that overlap gutter customizable is overkill, at least for now for sure.
It would be (0,0) - the thing between 1 and 3; (1, 0) - between 2 and 4; (0, 1) ; between 5 and 6; (1, 1), between 6 and 7 ; and so on
But yeah. I agree it'd be a bit convoluted
So I'll leave it out
Yeah that's pretty much how it works internally
but you are iterating over 2 "lines that overlap gutter" at a time. I specifically marked each such line, because you have to be able to access each 24 of them separately. if not (possible), then there is 100% no point.
Yeah it'd need further design discussion to be viable
So I don't think it's worth it
Ok so
Regarding thickness
I'm thinking of implementing the sorting based on thickness solution
Cuz then there would rarely be any complaints about layering
Maybe if you use a pattern stroke
But that's about it
ok
here's a pro tip for you guys
one of the best ways to have ideas is called taking a shower
π
ok so i think i've figured out what we should do regarding stroke: none
and this comment by Laurenz on number 4 was very helpful
basically here's what im thinking
table.hline(stroke: none)(likewise forvline) will completely delete lines under it. But not cell strokes (mainly cuz they have priority).
cuz the idea is to override the default lines.
But
table.cell(stroke: none)will not delete anything
it just will make sure that particular cell has no stroke override
but it wont affect the lines
i think this makes more sense
because then it's easier to say "I want to delete this line" by just adding a table.hline(stroke: none)
otherwise hlines and vlines with stroke: none would just be 100% useless lol.
i think this would be consistent with being able to disable lines at the border with stroke: (top: none) for e.g.
they'd all just be "lines"
i also advanced a bit in my mental model of the new rowspans algorithm
but the work is still being done
all i can say (without confusing everyone) is that I've thought of one possible way to start work on it
but i havent thought of it to its completion yet
perhaps another conversation with Laurenz might be in order , as things become clearer π
and finally
I had some ideas regarding headers and footers
just to share with you guys, right now im thinking of something like this
simplest case:
#table(
columns: 2,
table.header(
table.cell(colspan: 2)[*Super Header*],
[*A*], [*B*],
[bye],
repeat: true
),
[data], [data],
[data], [data],
[data], [data],
table.footer(
[*C*], [*D*],
table.cell(colspan: 2)[*Super Footer*],
repeat: true
)
)
that would be the basic API at least
but I had the idea of allowing headers and footers in arbitrary positions
so
simplest case:
#table(
columns: 2,
table.header(
table.cell(colspan: 2)[*Super Header*],
[*A*], [*B*],
[bye],
repeat: true
),
[data], [data],
[data], [data],
[data], [data],
table.header(
table.cell(colspan: 2)[*New Header Just Dropped*],
[*A*], [*B*],
repeat: true
),
[data], [data],
[data], [data],
table.footer(
[*C*], [*D*],
table.cell(colspan: 2)[*First Footer*],
repeat: true
)
[data], [data],
[data], [data],
table.footer(
[*C*], [*D*],
table.cell(colspan: 2)[*Second Footer*],
repeat: true
)
)
the idea is that the first header is "sticky" (repeated) until the second one, which is sticky until the end
while footers work a bit differently - the first one that appears is "sticky" from the start until when it appears, and the second one is sticky from there until (coincidentally, as that's where it appears) the end
hell yeah it werk
now you can specify hlines on top of or below a row (useful when you have gutter)
So i could make a weave pattern with the lines? I cause that would be a very rare use case.
huh
no it's just so u can actually place a line there
(see the green arrow)
by default lines always go on top of rows / to hte left of columns
so the bottom of a row / right of a column were impossible* to customize with gutter
*you can use cell stroke but then you cant make it cross gutters or use it to override the default lines
is that the cell line/ cells lines
what code?
#table(
columns: 3,
gutter: 3pt,
stroke: blue,
table.hline(end: 2, stroke: red),
table.hline(end: 2, stroke: aqua, position: bottom),
table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), [b], table.vline(end: 2, stroke: aqua, position: right), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
table.hline(end: 2, stroke: red),
[d], [e], [f],
table.hline(end: 2, stroke: red),
[g], [h], [i],
table.hline(end: 2, stroke: red),
)
well
i didnt realize that an unintended consequence of automatic positioning is that you need to specify position: bottom to place the bottom line when you use gutter
I thought that was interstesting.
I almost thought it should go at the last row of the table.
uhhh
actually thats weird
the last line just disappeared
i set it to green and nothing changed
lol
yeah no it's just a bug
gotta figure that out
if i place an xy cell before the below does it go below that cell.
it goes below the below β’οΈ
π
I kept automatic positioning very simple
the index of the line will be right after the cell placed last
so
to its right / on top of the row below it
so if u give it position: bottom it will go below the row below
and position: right would go after the column after
so the default positions are left and top. for vlines and hlines respectivly
yes
Its an acceptable choice.
although the position is more like "next" than below/right.
yea sort of
but it makes more sense when you dont rely on automatic positioning
when you explicitly specify the index
then it truly becomes before the index you specified / after it
does the right/below work with col/row/spans