Skip to content
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

layout: Add initial support for row height distribution #31421

Merged
merged 1 commit into from Feb 29, 2024

Conversation

mrobinson
Copy link
Member

@mrobinson mrobinson commented Feb 23, 2024

This change adds a version of row height distribution that follows the
distribution algorithm used for tables in Blink's LayoutNG. This is just
an intermediate step toward implementing a distribution algorithm for
both rows and columns more similar to Layout NG.

The CSS Table 3 specification is often wrong with regard to web
compatibility, which is why we have abandoned it in favor of the Layout
NG algorithm for row height distribution.

Co-authored-by: Oriol Brufau obrufau@igalia.com


  • ./mach build -d does not report any errors
  • ./mach test-tidy does not report any errors
  • There are tests for these changes

@mrobinson mrobinson added the T-linux-wpt-2020 Do a try run of the WPT label Feb 23, 2024
@github-actions github-actions bot removed the T-linux-wpt-2020 Do a try run of the WPT label Feb 23, 2024
Copy link

🔨 Triggering try run (#8024844911) for Linux WPT

Copy link

Test results for linux-wpt-layout-2020 from try job (#8024844911):

Flaky unexpected result (16)
  • FAIL [expected PASS] /_mozilla/css/dirty_viewport.html (#13731)
  • TIMEOUT [expected OK] /_webgl/conformance/glsl/misc/shader-with-non-reserved-words.html (#16216)
    • NOTRUN [expected PASS] subtest: Overall test
  • OK /css/css-fonts/variations/at-font-face-font-matching.html (#20684)
    • PASS [expected FAIL] subtest: Matching font-weight: '400' should prefer '501 550' over '502 560'
    • PASS [expected FAIL] subtest: Matching font-weight: '430' should prefer '420 440' over '450 460'
    • PASS [expected FAIL] subtest: Matching font-weight: '430' should prefer '501 550' over '502 560'
    • PASS [expected FAIL] subtest: Matching font-weight: '500' should prefer '400' over '350 399'
    • PASS [expected FAIL] subtest: Matching font-weight: '500' should prefer '350 399' over '351 398'
    • PASS [expected FAIL] subtest: Matching font-weight: '501' should prefer '501' over '502 510'
    • PASS [expected FAIL] subtest: Matching font-stretch: '110%' should prefer '110% 120%' over '115% 116%'
    • PASS [expected FAIL] subtest: Matching font-style: 'italic' should prefer 'italic' over 'oblique 20deg'
    • PASS [expected FAIL] subtest: Matching font-style: 'italic' should prefer 'oblique 20deg' over 'oblique 30deg 60deg'
    • PASS [expected FAIL] subtest: Matching font-style: 'oblique 20deg' should prefer 'oblique 10deg' over 'italic'
    • And 8 more unexpected results...
  • OK /css/cssom-view/MediaQueryList-addListener-handleEvent.html (#24571)
    • FAIL [expected PASS] subtest: looks up handleEvent method on every event dispatch assert_equals: expected 1 but got 0
  • TIMEOUT [expected PASS] /css/filter-effects/feimage-reference-foreign-object-crash.html
  • TIMEOUT [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.html (#29081)
    • TIMEOUT [expected PASS] subtest: no-referrer referrer policy used to create the starting page Test timed out
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • PASS [expected FAIL] subtest: Same-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
    • PASS [expected FAIL] subtest: aElement.click() before the load event must NOT replace
  • TIMEOUT [expected OK] /html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.html (#30970)
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/document-with-fragment-empty.html (#28259)
    • TIMEOUT [expected FAIL] subtest: Autofocus elements in top-level browsing context's documents with empty fragments should work. Test timed out
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/document-with-fragment-nonexistent.html (#28259)
    • TIMEOUT [expected FAIL] subtest: Autofocus elements in top-level browsing context's documents with non-existent fragments should work. Test timed out
  • OK [expected TIMEOUT] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html (#22667)
    • FAIL [expected TIMEOUT] subtest: Check that popups from a sandboxed iframe escape the sandbox if
      allow-popups-to-escape-sandbox is used assert_equals: It came from a sandboxed iframe expected "null" but got "http://web-platform.test:8000"
  • OK /html/semantics/forms/form-submission-0/urlencoded2.window.html (#28687)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: Basic File test (formdata event)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: non-ASCII in name and value (normal form)
  • TIMEOUT /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
  • TIMEOUT /resource-timing/test_resource_timing.https.html (#25216)
    • FAIL [expected PASS] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img) assert_equals: expected 13608704 but got 13608960
  • OK [expected ERROR] /workers/constructors/Worker/Worker-constructor.html (#22991)
Stable unexpected results that are known to be intermittent (13)
  • FAIL [expected PASS] /_mozilla/mozilla/iframe/resize_after_load.html (#13573)
  • TIMEOUT [expected OK] /_webgl/conformance/glsl/misc/shader-uniform-packing-restrictions.html (#28103)
    • NOTRUN [expected PASS] subtest: Overall test
  • TIMEOUT [expected OK] /_webgl/conformance/uniforms/out-of-bounds-uniform-array-access.html (#26225)
    • NOTRUN [expected PASS] subtest: Overall test
  • PASS [expected FAIL] /css/css-text/text-transform/text-transform-full-size-kana-001.html (#21949)
  • PASS [expected FAIL] /css/css-text/text-transform/text-transform-full-size-kana-002.html (#21949)
  • PASS [expected FAIL] /css/css-text/text-transform/text-transform-full-size-kana-003.html (#21949)
  • PASS [expected FAIL] /css/css-text/text-transform/text-transform-full-size-kana-004.html (#21949)
  • OK /html/browsers/history/the-history-interface/traverse_the_history_4.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected TIMEOUT] subtest: Host element with delegatesFocus including no focusable descendants should be skipped promise_test: Unhandled rejection with value: object "TypeError: host.attachShadow is not a function"
    • FAIL [expected NOTRUN] subtest: Area element should support autofocus promise_test: Unhandled rejection with value: object "TypeError: w.document.querySelector(...) is null"
  • CRASH [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html (#24057)
  • OK /html/semantics/forms/historical.html (#28568)
    • FAIL [expected PASS] subtest: <input name=isindex> should not be supported assert_regexp_match: expected object "/?isindex=x$/" but got "about:blank"
  • OK [expected TIMEOUT] /webmessaging/with-ports/017.html (#24486)
    • PASS [expected TIMEOUT] subtest: origin of the script that invoked the method, about:blank
  • TIMEOUT [expected OK] /webmessaging/without-ports/018.html (#24485)
    • TIMEOUT [expected PASS] subtest: origin of the script that invoked the method, javascript: Test timed out
Stable unexpected results (11)
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-001.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-002.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-003.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-004.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-005.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-006.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-007.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-013.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/clear-applies-to-014.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/float-applies-to-013.xht
  • PASS [expected FAIL] /css/CSS2/floats-clear/float-applies-to-014.xht

Copy link

⚠️ Try run (#8024844911) failed.

@mrobinson mrobinson marked this pull request as draft February 24, 2024 17:21
@mrobinson
Copy link
Member Author

I'm converting this one back into a draft. I think we should simply implement the LayoutNG distribution algorithm outright to avoid the code churn.

@mrobinson mrobinson added the T-linux-wpt-2020 Do a try run of the WPT label Feb 26, 2024
@github-actions github-actions bot removed the T-linux-wpt-2020 Do a try run of the WPT label Feb 26, 2024
Copy link

🔨 Triggering try run (#8050682843) for Linux WPT

Copy link

Test results for linux-wpt-layout-2020 from try job (#8050682843):

Flaky unexpected result (17)
  • TIMEOUT [expected OK] /_webgl/conformance/glsl/misc/shader-with-non-reserved-words.html (#16216)
    • NOTRUN [expected PASS] subtest: Overall test
  • TIMEOUT [expected OK] /_webgl/conformance/uniforms/out-of-bounds-uniform-array-access.html (#26225)
    • NOTRUN [expected PASS] subtest: Overall test
  • TIMEOUT [expected PASS] /css/css-fonts/font-size-adjust-reload.html (#30678)
  • TIMEOUT [expected PASS] /css/css-position/crashtests/scroll-tree-parent-construction.html
  • OK /css/cssom-view/MediaQueryList-addListener-handleEvent.html (#24571)
    • FAIL [expected PASS] subtest: looks up handleEvent method on every event dispatch assert_equals: expected 2 but got 1
  • TIMEOUT [expected PASS] /css/filter-effects/feimage-circular-reference-foreign-object-crash.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/empty-iframe-load-event.html (#29066)
    • FAIL [expected PASS] subtest: Check execution order from nested timeout assert_equals: Expected nested setTimeout to run second expected true but got false
    • FAIL [expected PASS] subtest: Check execution order on load handler assert_equals: Expected onload to run first expected false but got true
  • TIMEOUT /html/browsers/browsing-the-web/navigating-across-documents/javascript-url-no-beforeunload.window.html (#29055)
    • PASS [expected FAIL] subtest: Navigating an opened window with an iframe via location.href to a javascript: URL must not fire beforeunload on the iframe: undefined completion
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.html (#29056)
    • PASS [expected FAIL] subtest: Cross-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • PASS [expected FAIL] subtest: Same-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html (#29181)
    • PASS [expected FAIL] subtest: cross-document navigation then cross-document navigation
  • OK /html/browsers/history/the-history-interface/traverse_the_history_3.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • TIMEOUT [expected FAIL] subtest: Host element with delegatesFocus including no focusable descendants should be skipped Test timed out
    • NOTRUN [expected FAIL] subtest: Area element should support autofocus
  • TIMEOUT [expected PASS] /html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/fieldset-dynamic-oof-container-crash.html
  • OK [expected TIMEOUT] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html (#22667)
    • FAIL [expected TIMEOUT] subtest: Check that popups from a sandboxed iframe escape the sandbox if
      allow-popups-to-escape-sandbox is used assert_equals: It came from a sandboxed iframe expected "null" but got "http://web-platform.test:8000"
  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html (#24066)
    • NOTRUN [expected FAIL] subtest: Check that popups from a sandboxed iframe do not escape the sandbox
  • OK /html/semantics/forms/form-submission-0/urlencoded2.window.html (#28687)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: 0x00 in filename (formdata event)
Stable unexpected results that are known to be intermittent (11)
  • FAIL [expected PASS] /_mozilla/css/dirty_viewport.html (#13731)
  • TIMEOUT [expected OK] /_webgl/conformance/glsl/misc/shader-uniform-packing-restrictions.html (#28103)
    • NOTRUN [expected PASS] subtest: Overall test
  • OK /css/css-fonts/variations/at-font-face-font-matching.html (#20684)
    • PASS [expected FAIL] subtest: Matching font-weight: '399' should prefer '500 501' over '502 510'
    • PASS [expected FAIL] subtest: Matching font-stretch: '90%' should prefer '110% 140%' over '120% 130%'
    • PASS [expected FAIL] subtest: Matching font-style: 'oblique 20deg' should prefer 'oblique 20deg' over 'oblique 30deg 60deg'
    • PASS [expected FAIL] subtest: Matching font-style: 'oblique 21deg' should prefer 'oblique 0deg' over 'oblique -50deg -20deg'
    • PASS [expected FAIL] subtest: Matching font-style: 'oblique 10deg' should prefer 'oblique 0deg' over 'oblique -50deg -20deg'
  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
  • OK /html/browsers/history/the-history-interface/traverse_the_history_2.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT [expected OK] /html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.html (#30970)
  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/track/track-element/no-cuechange-before-play.html (#31014)
    • FAIL [expected TIMEOUT] subtest: Ensure that the 'cuechange' event is not fired before video playback has begun. assert_true: Not expecting event, but got canplaythrough event expected true got false
  • OK [expected TIMEOUT] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html (#24057)
    • FAIL [expected TIMEOUT] subtest: Check that popups from a sandboxed iframe escape the sandbox if
      allow-popups-to-escape-sandbox is used assert_equals: It came from a sandboxed iframe expected "null" but got "http://web-platform.test:8000"
  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html (#22154)
    • NOTRUN [expected FAIL] subtest: Check that popups from a sandboxed iframe do not escape the sandbox
  • OK [expected ERROR] /html/semantics/scripting-1/the-script-element/defer-script/async-script.html?reload (#29054)
  • TIMEOUT [expected OK] /webmessaging/without-ports/018.html (#24485)
    • TIMEOUT [expected PASS] subtest: origin of the script that invoked the method, javascript: Test timed out

Copy link

✨ Try run (#8050682843) succeeded.

@mrobinson mrobinson marked this pull request as ready for review February 27, 2024 11:54
if let Some(percentage_resolution_size) = percentage_resolution_size {
let get_percent_block_size_deficit = |row_index: usize, track_size: Au| {
let size_needed_for_percent = percentage_resolution_size
.scale_by(self.rows[row_index].percent.0 as f32 / 100.);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already a f32

Suggested change
.scale_by(self.rows[row_index].percent.0 as f32 / 100.);
.scale_by(self.rows[row_index].percent.0 / 100.);

Comment on lines 1275 to 1279
let non_empty_rows_all_constrained = non_empty_rows
.iter()
.filter(|index| is_unconstrained(*index))
.next()
.is_none();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let non_empty_rows_all_constrained = non_empty_rows
.iter()
.filter(|index| is_unconstrained(*index))
.next()
.is_none();
let non_empty_rows_all_constrained = !non_empty_rows.iter().any(is_unconstrained);

let mut rows_to_grow = &empty_rows;
let unconstrained_empty_rows: Vec<usize> = rows_to_grow
.iter()
.map(|track| *track)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(|track| *track)
.copied()

let get_percent_block_size_deficit = |row_index: usize, track_size: Au| {
let size_needed_for_percent = percentage_resolution_size
.scale_by(self.rows[row_index].percent.0 as f32 / 100.);
size_needed_for_percent - track_size.max(Au::zero())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be?

Suggested change
size_needed_for_percent - track_size.max(Au::zero())
(size_needed_for_percent - track_size).max(Au::zero())

Or possibly leave it to the callers, since one is already checking > Au::zero() and doesn't need .max(Au::zero())

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this one. I think we do need this in case the sum of the percentages of the rows is greater than 100%.

row_deficit.to_f32_px() / percent_block_size_deficit.to_f32_px();
let size = percent_distributable_block_size.scale_by(ratio);
track_sizes[track_index] += size;
excess_size -= size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this make excess_size negative?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. We calculate the ratio via (row_deficit / total_deficit), but this ratio is multiplied by total_deficit.min(excess_size), so we always take a ratio of excess size.

Comment on lines 1329 to 1338
.auto_is(Length::zero)
.block
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to resolve both dimensions:

Suggested change
.auto_is(Length::zero)
.block
.block
.auto_is(Length::zero)

Also this is not taking min/max-height into account, but we can leave it for the follow-up.

@@ -306,11 +399,11 @@ impl<'a> TableLayout<'a> {
for column_index in 0..self.table.size.width {
let column_measure = &mut column_measures[column_index];
let final_intrinsic_percentage_width = column_measure
.percentage_width
.percentage
.0
.min(100. - total_intrinsic_percentage_width);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that Percentage is actually 1-based, but we can fix them in a follow-up.

Copy link
Member Author

@mrobinson mrobinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @Loirooriol. I think I've addressed all the issues you raised.

row_deficit.to_f32_px() / percent_block_size_deficit.to_f32_px();
let size = percent_distributable_block_size.scale_by(ratio);
track_sizes[track_index] += size;
excess_size -= size;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. We calculate the ratio via (row_deficit / total_deficit), but this ratio is multiplied by total_deficit.min(excess_size), so we always take a ratio of excess size.

This change adds a version of row height distribution that follows the
distribtuion algorithm used for tables in Blink's LayoutNG. This is just
an intermediate step toward implementing a distribution algorithm for
both rows and columns more similar to Layout NG.

The CSS Table 3 specification is often wrong with regard to web
compatability, which is why we have abandoned it in favor of the Layout
NG algorithm for row height distribution.  this work.

Co-authored-by: Oriol Brufau <obrufau@igalia.com>
@mrobinson mrobinson added this pull request to the merge queue Feb 29, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 29, 2024
@mrobinson mrobinson added this pull request to the merge queue Feb 29, 2024
Merged via the queue into servo:main with commit 127aa65 Feb 29, 2024
9 checks passed
@mrobinson mrobinson deleted the table-height-determination branch February 29, 2024 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

None yet

2 participants