we are NOT looking for a general presentation about Grid
but about one that explains how you actually implemented the specification
Our audience already knows Grid
but what it doesn’t know is how easy or hard it is to actually implement Grid in a browser
Igalia Web Platform Team
Web engines hacker working on Chromium/Blink and Safari/WebKit
Member of CSS Working Group since 2017
Initial spec by Microsoft 2011
IE10 released a prefixed version of the initial spec in 2012
Google started to work on it by the end of 2011
Igalia sponsored by Bloomberg started to work on Blink and WebKit implementation by summer 2013
Mozilla started Firefox implementation around 2014
In 2017 Chrome, Firefox and Safari shipped CSS Grid Layout together
"Proposal to CSSWG, Sept 2016" by Jen Simmons
Explain the process to implement a new CSS property for Grid Layout:
GitHub repository:
https://github.com/w3c/csswg-drafts/
grid-busy-cells
grid-emtpy-cells
grid-taken-areas
grid-skip-areas
grid-busy-cells
grid-empty-cells
grid-taken-areas
grid-skip-areas
<grid-area> =
<grid-line> [ / <grid-line> ]{0,3}
grid-skip-areas: 2/2, 3/3, 4/4, 5/5
Can fixed positioned items use a skipped area?
How does it interact with repeat(auto-fit)
?
Is there a way to specify skipped areas in grid-template-areas
?
...
<div class="grid">
<div class="dummy"></div>
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
<div>E</div>
<div>F</div>
<div>G</div>
<div>H</div>
</div>
<div class="grid">
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
<div>E</div>
<div>F</div>
<div>G</div>
<div>H</div>
</div>
Content: description, motivation, design, specification status, interoperability and compatibility risks, etc.
Project mailing list:
blink-dev@chromium.org
dev-platform@lists.mozilla.org
webkit-dev@lists.webkit.org
../platform/runtime_enabled_features.json5
{
name: "CSSDayGridSkipAreas",
status: "experimental",
},
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[721/721] STAMP obj/blink_tests.stamp
real 10m27.546s
user 55m40.446s
sys 1m54.131s
<grid-skip-area> = none | <grid-area>
css/properties/shorthands/shorthands_custom.cc
: GridArea::ParseShorthand()
bool GridArea::ParseShorthand(
bool important,
CSSParserTokenRange& range,
const CSSParserContext& context,
const CSSParserLocalContext&,
HeapVector<CSSPropertyValue, 256>& properties) const {
DCHECK_EQ(gridAreaShorthand().length(), 4u);
CSSValue* row_start_value =
css_parsing_utils::ConsumeGridLine(range, context);
if (!row_start_value)
return false;
GridArea::ParseShorthand()
CSSValue* column_start_value = nullptr;
CSSValue* row_end_value = nullptr;
CSSValue* column_end_value = nullptr;
if (css_property_parser_helpers::ConsumeSlashIncludingWhitespace(range)) {
column_start_value = css_parsing_utils::ConsumeGridLine(range, context);
if (!column_start_value)
return false;
if (css_property_parser_helpers::ConsumeSlashIncludingWhitespace(range)) {
row_end_value = css_parsing_utils::ConsumeGridLine(range, context);
if (!row_end_value)
return false;
if (css_property_parser_helpers::ConsumeSlashIncludingWhitespace(range)) {
column_end_value = css_parsing_utils::ConsumeGridLine(range, context);
if (!column_end_value)
return false;
}
}
}
if (!range.AtEnd())
return false;
css/properties/css_parsing_utils.cc
: css_parsing_utils::ConsumeGridArea()
GridArea::ParseShorthand()
bool GridArea::ParseShorthand(
bool important,
CSSParserTokenRange& range,
const CSSParserContext& context,
const CSSParserLocalContext&,
HeapVector<CSSPropertyValue, 256>& properties) const {
DCHECK_EQ(gridAreaShorthand().length(), 4u);
CSSValueList* grid_area = css_parsing_utils::ConsumeGridArea(range, context);
if (!grid_area)
return false;
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[12/12] STAMP obj/blink_tests.stamp
real 0m28.963s
user 1m25.154s
sys 0m4.390s
{
name: "grid-skip-area",
property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"],
runtime_flag: "CSSDayGridSkipAreas",
layout_dependent: true,
field_group: "*",
field_template: "external",
include_paths: ["third_party/blink/renderer/core/style/grid_area_positions.h"],
default_value: "GridAreaPositions()",
type_name: "GridAreaPositions",
keywords: ["none"],
typedom_types: ["Keyword"],
converter: "ConvertGridAreaPositions",
},
style/grid_area_positions.h
class GridAreaPositions {
...
private:
GridPosition row_start_;
GridPosition row_end_;
GridPosition column_start_;
GridPosition column_end_;
};
int usecounterhelper::mapcsspropertyidtocsssampleidforhistogram(
CSSPropertyID unresolved_property) {
switch (unresolved_property) {
...
case CSSPropertyID::kGridSkipArea:
return 641;
css/properties/longhands/longhands_custom.cc
const CSSValue* GridSkipArea::ParseSingleValue(
CSSParserTokenRange& range,
const CSSParserContext& context,
const CSSParserLocalContext&) const {
if (range.Peek().Id() == CSSValueID::kNone)
return css_property_parser_helpers::ConsumeIdent(range);
return css_parsing_utils::ConsumeGridArea(range, context);
}
css/resolver/style_builder_converter.cc
GridAreaPositions StyleBuilderConverter::ConvertGridAreaPositions(
StyleResolverState& state,
const CSSValue& value) {
const auto& values = To<CSSValueList>(value);
DCHECK_EQ(values.length(), 4u);
GridPosition row_start = ConvertGridPosition(state, values.Item(0));
GridPosition column_start = ConvertGridPosition(state, values.Item(1));
GridPosition row_end = ConvertGridPosition(state, values.Item(2));
GridPosition column_end = ConvertGridPosition(state, values.Item(3));
return GridAreaPositions(row_start, row_end, column_start, column_end);
}
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[789/789] STAMP obj/blink_tests.stamp
real 13m33.084s
user 94m28.150s
sys 3m5.134s
Now the browser accepts:
grid-skip-area: 3 / 5;
grid-skip-area:
1 / 1 / span 2 / span 2;
grid-skip-area: none;
But it doesn't do anything with that property
style/grid_positions_resolver.cc
GridSpan GridPositionsResolver::ResolveGridAreaPositionsFromStyle(
const ComputedStyle& grid_container_style,
GridTrackSizingDirection direction,
size_t auto_repeat_tracks_count) {
GridPosition initial_position =
direction == kForColumns
? grid_container_style.GridSkipArea().ColumnStart()
: grid_container_style.GridSkipArea().RowStart();
GridPosition final_position =
direction == kForColumns ? grid_container_style.GridSkipArea().ColumnEnd()
: grid_container_style.GridSkipArea().RowEnd();
return ResolveInitialAndFinalPositionsFromStyle(
grid_container_style, direction, auto_repeat_tracks_count,
initial_position, final_position);
}
void LayoutGrid::PlaceItemsOnGrid(
...
GridSpan grid_skip_rows =
GridPositionsResolver::ResolveGridAreaPositionsFromStyle(
*Style(), kForRows, AutoRepeatCountForDirection(kForRows));
GridSpan grid_skip_columns =
GridPositionsResolver::ResolveGridAreaPositionsFromStyle(
*Style(), kForColumns, AutoRepeatCountForDirection(kForColumns));
const GridArea* grid_skip_area = nullptr;
if (!grid_skip_rows.IsIndefinite() && !grid_skip_columns.IsIndefinite()) {
grid_skip_rows.Translate(abs(grid.SmallestTrackStart(kForRows)));
grid_skip_columns.Translate(abs(grid.SmallestTrackStart(kForColumns)));
grid_skip_area = new GridArea(grid_skip_rows, grid_skip_columns);
}
...
Chromium implementation used to have a matrix (vector of vectors), each grid cell was created even if empty
Now it uses a doubly linked lists, only occupied cells are created
std::unique_ptr<GridArea> ListGridIterator::NextEmptyGridArea(
size_t fixed_track_span,
size_t varying_track_span,
const GridArea* grid_skip_area) {
...
auto OverlapsGridSkipArea = [this, fixed_track_span, varying_track_span,
grid_skip_area]() {
bool is_row_axis = direction_ == kForColumns;
size_t row_span = is_row_axis ? varying_track_span : fixed_track_span;
size_t column_span = is_row_axis ? fixed_track_span : varying_track_span;
return grid_skip_area->Overlaps(GridArea(
GridSpan::TranslatedDefiniteGridSpan(row_index_, row_index_ + row_span),
GridSpan::TranslatedDefiniteGridSpan(column_index_,
column_index_ + column_span)));
};
...
std::unique_ptr<GridArea> ListGridIterator::NextEmptyGridArea(
...
while (OverlapsGridSkipArea())
varying_index++;
...
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[138/138] STAMP obj/blink_tests.stamp
real 5m42.757s
user 40m18.312s
sys 1m13.215s
grid-skip-area
is already working, but it only accepts one area
@@ -1872,11 +1872,11 @@
field_group: "*",
field_template: "external",
include_paths: ["third_party/blink/renderer/core/style/grid_area_positions.h"],
- default_value: "GridAreaPositions()",
- type_name: "GridAreaPositions",
+ default_value: "Vector<GridAreaPositions>()",
+ type_name: "Vector<GridAreaPositions>",
keywords: ["none"],
typedom_types: ["Keyword"],
- converter: "ConvertGridAreaPositions",
+ converter: "ConvertGridAreaPositionsList",
},
css/properties/longhands/longhands_custom.cc
const CSSValue* GridSkipArea::ParseSingleValue(
CSSParserTokenRange& range,
const CSSParserContext& context,
const CSSParserLocalContext&) const {
if (range.Peek().Id() == CSSValueID::kNone)
return css_property_parser_helpers::ConsumeIdent(range);
CSSValueList* grid_areas = CSSValueList::CreateCommaSeparated();
do {
CSSValue* grid_area =
css_parsing_utils::ConsumeGridArea(range, context, true);
if (!grid_area)
return nullptr;
grid_areas->Append(*grid_area);
} while (css_property_parser_helpers::ConsumeCommaIncludingWhitespace(range));
if (!range.AtEnd())
return nullptr;
return grid_areas;
}
css/resolver/style_builder_converter.cc
Vector<GridAreaPositions> StyleBuilderConverter::ConvertGridAreaPositionsList(
StyleResolverState& state,
const CSSValue& value) {
Vector<GridAreaPositions> list;
for (auto& curr_value : To<CSSValueList>(value)) {
list.push_back(ConvertGridAreaPositions(state, *curr_value));
}
return list;
}
std::unique_ptr<GridArea> ListGridIterator::NextEmptyGridArea(
size_t fixed_track_span,
size_t varying_track_span,
const Vector<GridArea*>& grid_skip_areas) {
...
auto OverlapsGridSkipAreas = [this, fixed_track_span, varying_track_span,
grid_skip_areas]() {
bool is_row_axis = direction_ == kForColumns;
size_t row_span = is_row_axis ? varying_track_span : fixed_track_span;
size_t column_span = is_row_axis ? fixed_track_span : varying_track_span;
for (auto* grid_skip_area : grid_skip_areas) {
if (grid_skip_area->Overlaps(
GridArea(GridSpan::TranslatedDefiniteGridSpan(
row_index_, row_index_ + row_span),
GridSpan::TranslatedDefiniteGridSpan(
column_index_, column_index_ + column_span))))
return true;
}
return false;
};
...
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[373/373] STAMP obj/blink_tests.stamp
real 5m54.719s
user 40m34.151s
sys 1m16.931s
grid-skip-area
now accepts a list of grid areas and skip them as expected
grid-skip-area:
1 / 1, 2 / 2, 3 / 3, 4 / 4;
grid-skip-area:
3 / 5 / span 2 / span 3, 9 / 12;
grid-skip-areas
$ git grep -l grid-skip-area | xargs sed -i 's/grid-skip-area/grid-skip-areas/g'
$ git grep -l GridSkipArea | xargs sed -i 's/GridSkipArea/GridSkipAreas/g'
$ time ninja -C src/out/Default blink_tests
ninja: Entering directory `src/out/Default'
[550/550] STAMP obj/blink_tests.stamp
real 9m45.056s
user 69m15.646s
sys 2m19.425s
<div class="grid">
<div></div>
<div></div>
<div></div>
...
<div></div>
</div>
grid-skip-areas: 1/4/-1, 1/8/-1, 2/2/span 3/span 2, 2/6/auto/span 2, 4/5/auto/span 2, 2/10/auto/span 2, 4/9/auto/span 2;
$ cd chromium/src/third_party/blink/
$ tools/run_web_tests.py -t Default/
Found 86793 tests; running 81273, skipping 5520.
$ tools/run_web_tests.py -t Default/ web_tests/external/wpt/css/css-grid/ web_tests/fast/css-grid-layout/
Found 916 tests; running 916, skipping 0.
All 916 tests ran as expected (908 passed, 8 didn't).
GitHub repository:
https://github.com/web-platform-tests/wpt/
Shared tests repository for all browser vendors
Helps to improve interoperability between different implementations and also specs
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Grid Layout Test: parsing grid-skip-areas with valid values</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-grid-1/#propdef-grid-skip-areas">
<meta name="assert" content="grid-skip-areas supports the full grammar 'none | <grid-area>#'.">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/parsing-testcommon.js"></script>
<script>
// none
test_valid_value("grid-skip-areas", "none", "none");
// <grid-areas>
test_valid_value("grid-skip-areas", "1", "1 / auto / auto / auto");
test_valid_value("grid-skip-areas", "1 / 2", "1 / 2 / auto / auto");
test_valid_value("grid-skip-areas", "1 / 2 / 3 / 4", "1 / 2 / 3 / 4");
test_valid_value("grid-skip-areas", "3 / 1 / span 2 / span 3", "3 / 1 / span 2 / span 3");
// <grid-areas>#
test_valid_value("grid-skip-areas", "1, 2", "1 / auto / auto / auto, 2 / auto / auto / auto");
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Grid Layout Test: grid-skip-areas</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="http://www.w3.org/TR/css-grid-1/#grid-skip-areas">
<meta name="assert" content="This test checks that grid-skip-areas avoids auto-placed items to use them.">
<link href="support/grid.css" rel="stylesheet">
<style>
.grid {
position: relative;
grid: repeat(3, 50px) / repeat(4, 100px);
}
.grid > div {
background: magenta;
}
</style>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/check-layout-th.js"></script>
<body onload="checkLayout('.grid');">
<div id="log"></div>
<div class="grid" style="grid-skip-areas: 1 / 1;">
<div data-offset-x="100" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="200" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="300" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="0" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="100" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="200" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="300" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
</div>
<div class="grid" style="grid-skip-areas: 1 / 1, 2 / 2;">
<div data-offset-x="100" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="200" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="300" data-offset-y="0" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="0" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="200" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
<div data-offset-x="300" data-offset-y="50" data-expected-width="100" data-expected-height="50"></div>
</div>
</body>
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Grid Layout Test: grid-skip-areas</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<link rel="help" href="http://www.w3.org/TR/css-grid-1/#grid-skip-areas">
<link rel="match" href="grid-skip-areas-002-ref.html">
<meta name="assert" content="This test checks that grid-skip-areas avoids auto-placed items to use them.">
<style>
#grid {
display: grid;
grid: repeat(4, 50px) / repeat(4, 50px);
grid-skip-areas: 2 / 2 / span 2 / span 2;
}
#grid > div {
background: green;
}
</style>
<p>The test passes if you see an empty 100x100px box with a 50px green border.</p>
<div id="grid">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<!DOCTYPE html>
<html lang=en>
<meta charset="utf-8">
<title>CSS Grid Layout Test: grid-skip-areas reference file</title>
<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
<style>
div {
width: 100px;
height: 100px;
border: 50px solid green;
}
</style>
<p>The test passes if you see an empty 100x100px box with a 50px green border.</p>
<div></div>
Intent to ship mail
Change runtime flag to stable
../platform/runtime_enabled_features.json5
{
name: "CSSDayGridSkipAreas",
status: "stable",
},
After a few cycles the runtime flag will be removed
All changes need to be approved by an owner/reviewer
Tests should pass in all platforms
E.g. https://chromium-review.googlesource.com/c/chromium/src/+/1659448/
Open process, all browser engines now are open source
Shipping some feature has an huge impact (interoperability and backwards compatibility)
Externals individuals and/or companies (other than browser vendors) can contribute