-
Notifications
You must be signed in to change notification settings - Fork 707
[css-grid] Can the sizing algo be made to deal with this #2356
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
Comments
Ok, we at Igalia have been taking a look to the example. I created a reduced one, so it's easier to check the problem: It has a grid container with 4
The algorithm process each of them:
Then it stays with the maximum for each column:
As you see the last 2 columns measure 200px in total, when it'll be enough if they measure 150px. The problem is that the algorithm is trying to fulfill some preconditions. We're not sure if there'll be a better way to do this and if it'd have any consequences in more complex examples with different items order and so on. |
definition: I think the "correct" to solve this is to
Depending on the specifics of the system, there may be one solution, or a range of solutions: your example has one degree of liberty, and can vary between 50px <= `d_1_2@ <= 200px, my example had a single solution. I'm a long way out of school, but maybe some of the more mathematically inclined among us known the algorithms that can be used to solve that, their complexity, whether they are parallelization friendly, and whether the way we pick one solution when there is a range gives us a different choice of algorithms? |
N.B.: my memory is fuzzy, but I think this is the sort of problem that can be solved by the Simplex algorithm |
Actually we don't need 3 items in my example, just 2 items and 3 columns are enough to reproduce the problem. <div style="display: inline-grid; border: solid thick;">
<img src="https://placehold.it/300x100" style="grid-column: 1 / 3;">
<img src="https://placehold.it/250x100" style="grid-column: 2 / 4;">
</div> Which will cause that the columns are: |
The Working Group just discussed The full IRC log of that discussion<dael> Topic: Can the sizing algo be made to deal with this<dael> github: https://github.com//issues/2356 <florian> https://florian.rivoal.net/csswg/grid/grid-span-auto-001-ref.html <dael> florian: I was laying out a page of manga using grid so each cell is a grid. It should layout like ^ <florian> https://florian.rivoal.net/csswg/grid/grid-span-auto-001.html <dael> florian: Defining a bunch of columns and rows and everything can fit, but it's manual. <dael> florian: Here's what you get ^ <dael> florian: It has unneeded empty spaces. <TabAtkins> Very simple example: https://github.com//issues/2356#issuecomment-379878492 <dael> florian: I think this boils down to current sizing algo not considering enough things. It's descried as a possibly improvable heuristic. If we agree this is a good thing to solve...it sounds desireable...I think this is linear programming solvable by constraints. I'm not good enough at math to put the steps but I think this is solvable. <dael> TabAtkins: Simple example ^ <dael> TabAtkins: [explains example] We take the largest of the planned increases so we're not loosly wrapping each item. Ideal would be the latter image. It's one possible version. <dael> florian: I believe an algo exists that we could solve it, or are we too locked into compat. <dael> fantasai: I think we should be able to make the adjustments. I don't think anyone depends on this slight amount of slack. As time goes on we might get more constrained but I think we're not at that state. IF we could solve it it would be great, but I don't know how to do it. <dael> florian: I might be able to research the thoroetical algo but I'm not righ to say if impl-able. <dael> fremy: I've been working on some layouts and you can do a post-process to reduce the sizing. <dael> TabAtkins: This example (bottom of link) it shows one possible switch. BUt there's several ways to solve it like making the column 300 and the last one goes to 0. <dael> florian: Some examples have a range of solutions, mine has one solution. <dael> florian: I think it's worth looking into if there's a general option that we're not blocked in. I'd appriciate a mathematically inclided person to help. Maybe do the current algo and squish would work. I <dael> fremy: browsers are impl grid and we don't want to change whole algo so that's easier. <dael> TabAtkins: And your site may be doing it for you. In the manga example your images have known widths, it's just easier if CSS does it for you. <dael> fantasai: TabAtkins and I can take an action to look at post-processing step to see if that works. Maybe send a email to bert with a link to this issue to see if he has some insight. <dael> ChrisL: We was talking to people from Monash (sp?) univeristy with expertese. <TabAtkins> s/making the column 300 and the last one goes to 0/making the columns 50,250,0, which is a big change from the current way they lay out/ <dael> fantasai: We also had Caesar (sp?) who was working on an impl of Bert's grid spec. <ChrisL> s/(sp?)// <fantasai> s/Caesar/César/ <Rossen> dael, here's the illustration that should go with the BFC discussion (issue 2452) https://lists.w3.org/Archives/Public/www-archive/2018Apr/0001.html <ChrisL> s/Monash (sp?)/Monash/ <dael> florian: I think the trickier part is that this is more complicated. If we have auto sizing it's simplier. If you have one min and one max you span. It's much harder to explain to people that don't know CSS but I'll try. <dael> astearns: Anything more? <dael> florian: Nope. Action on Tab and fantasai to look at post-processing step suggested by fremy. |
I know very little about grid, but the problem in #2356 (comment) looks like an LP (unless you want to consider the lengths to be an integral number of pixels, then it's ILP and much more hard). Effectively the simplex algorithm is the common approach. The linear inequalities define a polytope which is the feasible region among which you want to find the point where the objective function is optimal. This solution may not be unique, but (if the problem is feasible) a vertex of the polytope will be one of these solutions. Simplex starts at some vertex and then keeps iterating contiguous vertices, moving in the direction of the maximum slope of the objective function. This means that in common cases, the algorithm will be fast. The downside is that a polytope has an exponential number of vertices, so the worst-case is exponential. Instead of simplex, there are interior point algorithms which are guaranteed to be polynomial in the worst case, but they may be slower for common cases. At university I wasn't taught how these algorithms actually work so probably they are much more complex to implement than simplex, which is not difficult (as an exercise I implemented it in matlab in no more than 100 lines, I think), just a bit tricky because you want to avoid cycles and various other subtleties which can make it go wrong in edge cases. There are solvers specialized in this kind of problems, but usually they have commercial licenses. So yes, a post-processing step in the grid algorithm might be a better option. |
I took another look at this and designed an algorithm which works in polynomial time:
It doesn't respect this invariant:
but this wasn't either in the LP formulation above. In particular, the result doesn't look balanced, and the size distribution varies significantly depending on whether you iterate columns left-to-right or right-to-left. But this has an easy fix: first you calculate the column sizes iterating columns left-to-right, then right-to-left, and finally you average both values per each column. I wrote the algorithm in JS, you can see the result in Codewindow.addEventListener("load", function() {
document.querySelectorAll(".polyfill").forEach(polyfill);
});
function polyfill(grid) {
grid.style.display = "block";
let itemData = new Map();
let n = 0;
let m = 0;
for (let child of grid.children) {
let cs = getComputedStyle(child);
itemData.set(child, {
colStart: cs.gridColumnStart - 1,
colEnd: cs.gridColumnEnd - 1,
rowStart: cs.gridRowStart - 1,
rowEnd: cs.gridRowEnd - 1,
contentSize: child.offsetWidth,
});
n = Math.max(n, cs.gridRowEnd - 1);
m = Math.max(m, cs.gridColumnEnd - 1);
}
grid.style.display = "";
let gap = parseFloat(getComputedStyle(grid).columnGap) || 0;
let cells = Array(n).fill().map(_ => Array(m));
for (let data of itemData.values()) {
let {rowStart, rowEnd, colStart, colEnd} = data;
for (let i = rowStart; i < rowEnd; ++i) {
for (let j = colStart; j < colEnd; ++j) {
cells[i][j] = data;
}
}
}
function main(reversed) {
let columnSizes = Array(m);
let currentSizes = Array(n);
let col = reversed ? m - 1 : 0;
while (reversed ? col >= 0 : col < m) {
let max = 0;
let rowsWithCells = [];
for (let row = 0; row < n; ++row) {
let cell = cells[row][col];
if (!cell) continue;
let {colStart, colEnd} = cell;
if (reversed) [colStart, colEnd] = [colEnd-1, colStart+1];
rowsWithCells.push(row);
if (colStart === col) {
currentSizes[row] = cell.contentSize;
}
if (colEnd - 1 === col) {
max = Math.max(max, currentSizes[row]);
} else {
currentSizes[row] = Math.max(0, currentSizes[row] - gap);
}
}
columnSizes[col] = max;
for (let row of rowsWithCells) {
currentSizes[row] = Math.max(0, currentSizes[row] - max);
}
col += reversed ? -1 : 1;
}
return columnSizes;
}
let columnSizes1 = main(false);
let columnSizes2 = main(true);
let average = columnSizes1.map((n, i) => (n + columnSizes2[i])/2);
let result = average.map(n => n + "px").join(" ");
grid.style.gridTemplateColumns = result;
let pre = document.createElement("pre");
pre.textContent = "columns: " + result;
grid.parentElement.insertBefore(pre, grid.nextSibling);
} http://jsbin.com/qazuyucege/edit The output for Florian's example is exactly like the reference ( The output for Manuel's 1st example is a bit different ( The output for Manuel's 2nd example is maybe more unexpected ( |
So a few observations on the problem of computing intrinsic widths of columns in a table in the presence of column-spanning cells, which is a problem somewhat related to this one. (I feel like I've written this down before, but I can't find it, so I may as well write it down again.) The table case only has to deal with one set of widths at a time; I'm not sure whether that's the same for grid. The simplest solution to the problem, and the one that gives the mathematically minimal solution, is to progressively compute the widths of each from one side of the table to the other, assigning the column the smallest width needed to accomodate all of the cells that end (in the direction the columns are being traversed) in that column. This solution is not useful because, while it gives the smallest possible table, the assignment of widths to columns is often poor. For example, <table border>
<tr><th colspan=3>This is a long heading
<tr><td>1<td>2<td>3
</table> produces a table that looks like:
So in practice, the algorithm used is roughly the following (at least in Gecko since 2007 and Chromium since 2022); WebKit may differ in a few respects as described below although I haven't really tested any of this stuff since around 2007 or 2008:
Note that some implementations do not do all of these things. In particular, at least some of (Gecko pre-2007, Chromium pre-LayoutNG, WebKit, and EdgeHTML) did not do the temporary/permanent buffer setup, which leads to similarly (as above) bad distributions of widths when there are column-spanning cells whose spanned columns partially overlap. It's also possible that there may be some of those implementations that did the temporary/permanent buffer setup but didn't do the integer sort, although I don't think so (at least not in the long term). I think there were also some implementations whose distribution of the width of spanning cells may have followed rules more different from the regular table width distribution rules. The above rules tend to strike a good balance between producing smallest-possible results and producing results with a balanced distribution of widths between columns. |
We had some further discussion of this issue after the face-to-face yesterday. Part of the conclusion is that what grid already does is quite similar to the above. After some discussion, I suggested that the result from the above algorithm could be narrowed roughly as follows: Repeat the following until stable (I believe it will converge, but we definitely need to double-check):
|
Note that if you have <div style="display: inline-grid">
<div style="width: 100px; grid-column: 1 / 3"></div>
<div style="width: 100px; grid-column: 2 / 4"></div>
<div style="width: 100px; grid-column: 1 / 4"></div>
</div> then the current spec sizes the columns as The algorithm above considers that the items with a span of 2 have no excess (their grid area is 100px and they are 100px wide). The 3rd item has excess, but it doesn't matter because the column excess is the minimum. So it's no-op. However, we could eliminate the excess of the 3rd item by sizing the columns as |
Something that I was thinking is that #2356 (comment) minimizes the total size of the table, but that's actually stronger than what was being desired here (avoiding items with unnecessary excess). For example, <div style="display: inline-grid; border: solid; grid-template-columns: repeat(5, auto)">
<div style="grid-column: 1 / 3; width: 100px"></div>
<div style="grid-column: 2 / 5; width: 200px"></div>
<div style="grid-column: 4 / 6; width: 100px"></div>
</div> The current spec sizes the columns like So, dropping the requirement of a minimal sum of track sizes, I think we could come up with al algorithm that fits much better with how the spec currently behaves. I'm not convinced by iterative algorithms with dubious convergence that try to reduce excess after the fact. I think this needs to be handled in https://drafts.csswg.org/css-grid/#extra-space, doing something better than the "item-incurred increase" and "planned increase" thingies. |
I just ran into a case that grid sizing ought to be able to deal with, but apparently cannot.
test: https://florian.rivoal.net/csswg/grid/grid-span-auto-001.html
reference: https://florian.rivoal.net/csswg/grid/grid-span-auto-001-ref.html
This is grid with a few auto sized columns, and a bunch of elements spanning them in various ways. Given the size of the various things in it, it can be densely packed (as seen in the reference, with manually calculated sizes), but the current sizing algo leaves a bunch of unnecessary white space.
The motivating use case for this was trying to use grid to layout comics / manga (so it may become massively common if/when the Japanese Manga publishers starts using Grid). Since the browser has all the information it needs to calculate the "ideal" layout, grid should be able to handle that.
Finding the "correct" size doesn't seem to involve any particularly difficult math either. You cannot solve this by only resolving the (min/max) sizes of individual tracks, you also need to do the same for any pair of grid lines that items span between, but once you do that, you just have a simple system of linear equations to solve.
Now, if a grid item spans across several tracks which have different intrinsic sizing functions (like one column being max-content, and the other being min-content), I'm not 100% sure what the semantics are. But at least if all the columns it spans across have the same intrinsic sizing function, there should be no problem.
The text was updated successfully, but these errors were encountered: