CSS Grid Layout Implementation Details

Manuel Rego Casasnovas (@regocas)

CSS Day / 14 June 2019 (Amsterdam)
Creative Commons Attribution-ShareAlike

About this talk

CSS Day logo
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

About me

Igalia Web Platform Team

Web engines hacker working on Chromium/Blink and Safari/WebKit

Member of CSS Working Group since 2017

Igalia logo

About me

CSS Grid Layout History

Recent History

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

Idea

"Proposal to CSSWG, Sept 2016" by Jen Simmons

Slide from Jen Simmons presentation about skipping some cells in grid-auto-flow

Idea

Explain the process to implement a new CSS property for Grid Layout:

  • CSS Working Group
  • Intent to implement
  • Implementation behind a runtime flag
  • Test suite in Web Platform Tests repository
  • Ship feature!

CSS Working Group (CSSWG)

GitHub repository:
https://github.com/w3c/csswg-drafts/

New CSS property

Screenshot of a GitHub issue on CSSWG repository

Name bikeshedding

grid-busy-cells

grid-emtpy-cells

grid-taken-areas

grid-skip-areas

Name bikeshedding

grid-busy-cells

grid-empty-cells

grid-taken-areas

grid-skip-areas

Grammar

<grid-area> =
<grid-line> [ / <grid-line> ]{0,3}

<grid-skip-areas> =
none | <grid-area>#

Example

grid-skip-areas: 2/2, 3/3, 4/4, 5/5

Slide from Jen Simmons presentation about skipping some cells in grid-auto-flow

More questions

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?

...

Now


  <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>
              
A
B
C
D
E
F
G
H

Future?


  <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>
              
A
B
C
D
E
F
G
H

Intent to implement

Intent to implement

Content: description, motivation, design, specification status, interoperability and compatibility risks, etc.

Mail templates: Chromium, Firefox

W3C Technical Architecture Group (TAG) review

Intent to implement

Project mailing list:

  • blink-dev@chromium.org
  • dev-platform@lists.mozilla.org
  • webkit-dev@lists.webkit.org

Implementation

Setup

Get the Code: Checkout, Build, & Run Chromium

... some hours later ...

Source code

chromium/src/third_party/blink/renderer/core/

Runtime flag

../platform/runtime_enabled_features.json5

    {
      name: "CSSDayGridSkipAreas",
      status: "experimental",
    },

Building patch

$ 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

Parser

<grid-skip-areas> =
none | <grid-area>#

Parser

<grid-skip-area> = none | <grid-area>

Refactoring

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;

New method

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;

Building patch

$ 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

New property

css/css_properties.json5

{
      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",
    },

New class

style/grid_area_positions.h

class GridAreaPositions {
  ...
 private:
  GridPosition row_start_;
  GridPosition row_end_;
  GridPosition column_start_;
  GridPosition column_end_;
};

Use counters

frame/use_counter_helper.cc

int usecounterhelper::mapcsspropertyidtocsssampleidforhistogram(
    CSSPropertyID unresolved_property) {
  switch (unresolved_property) {
    ...
    case CSSPropertyID::kGridSkipArea:
      return 641;

Chrome Platform Status

Screenshot of Chrome Platform Status metrics for grid-auto-flow property

Parsing

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);
}

Style builder

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);
}

Building patch

$ 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

Status

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 resolver refactoring

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);
}

Layout code

layout/layout_grid.cc

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);
  }
  ...

Memory structure

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

Memory structure

Image of a sparse grid of 3x3 with only 4 items

Memory structure

Image of a matrix memory structure storing the sparse grid and saving memory for 5 cells that are not used

Memory structure

Image of a doubly linked lists memory structure storing the sparse grid and saving memory only for the 4 cells that are occupied

Auto-placement algorithm

layout/grid.cc

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)));
  };
  ...

Auto-placement algorithm

layout/grid.cc

std::unique_ptr<GridArea> ListGridIterator::NextEmptyGridArea(
  ...
  while (OverlapsGridSkipArea())
    varying_index++;
  ...

Building patch

$ 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

Status

grid-skip-area is already working, but it only accepts one area

Screenshot of example with grid-skip-area working

Multiple areas

css/css_properties.json5

@@ -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",
     },

Parsing

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;
}

Style builder

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;
}

Auto-placement algorithm

layout/grid.cc

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;
  };
  ...

Building patch

$ 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

Status

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;
Screenshot of example with grid-skip-area working for multiple areas

Rename to
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'
  

Building patch

$ 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

Example


  <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;

Tests

Running "layout" tests

$ 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).

Web Platform Tests (WPT)

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

src/third_party/blink/web_tests/external/wpt/

Parsing test

<!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>

JavaScript test

<!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>

Reference test

<!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>

Reference file

<!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>

Reference test

Output of the reference test with patches and without patches

wpt.fyi

Screenshot of wpt.fyi website showing tests results for css-grid on the different browsers

Ship it!

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

Review process

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/

Contributing to Web Platform

Open process, all browser engines now are open source

Shipping some feature has an huge impact (interoperability and backwards compatibility)

Open Web Platform

Externals individuals and/or companies (other than browser vendors) can contribute

  • Report/Star/Vote bugs
  • Use new features and provide feedback
  • Write/Tweet/Speak about the new features and missing use cases
  • Participate in CSS WG discussions
  • Implement the feature yourself or sponsoring someone else

Acknowledgements

Igalia logo Bloomberg logo

Igalia and Bloomberg working together
to build a better web

Thanks