Skip to content

layout: Implement block-axis centering for buttons - #46590

Merged
mrobinson merged 1 commit into
servo:mainfrom
Loirooriol:button-layout
Jul 19, 2026
Merged

layout: Implement block-axis centering for buttons#46590
mrobinson merged 1 commit into
servo:mainfrom
Loirooriol:button-layout

Conversation

@Loirooriol

@Loirooriol Loirooriol commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Mark <button> elements and <input> that lay out as buttons with a new FragmentFlags::IS_BUTTON flag. Then, if they contain a block formatting context, center their contents in the block axis.

This is done similarly as for vertical-align on table cells: we wrap the contents inside a PositioningFragment, which is then shifted to appear at the correct place. But unlike table cells, this is done before caching the layout results, not afterwards.

In the future, when we implement align-content for block containers, we should probably switch to that.

Testing: 1 tests passes, and adding 3 new tests
Fixes: #41432

@Loirooriol
Loirooriol requested a review from mrobinson as a code owner July 17, 2026 11:02
@Loirooriol Loirooriol added the A-layout/2020 https://github.com/servo/servo/wiki/Layout-2020 label Jul 17, 2026
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Jul 17, 2026
@Loirooriol Loirooriol added the T-linux-wpt Do a try run of the WPT label Jul 17, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Jul 17, 2026
@github-actions

Copy link
Copy Markdown

🔨 Triggering try run (#29577731042) for Linux (WPT)

@github-actions

Copy link
Copy Markdown

Test results for linux-wpt from try job (#29577731042):

Flaky unexpected result (35)
  • OK /IndexedDB/idbtransaction-oncomplete.any.sharedworker.html (#45911)
    • FAIL [expected PASS] subtest: IDBTransaction - complete event

      assert_array_equals: lengths differ, expected array ["upgradeneeded", "complete", "success", "opencursor"] length 4, got ["upgradeneeded", "complete", "success"] length 3
      

  • CRASH [expected OK] /_webgl/conformance/uniforms/no-over-optimization-on-uniform-array-16.html
  • OK /css/css-cascade/layer-cssom-order-reverse.html (#36094)
    • FAIL [expected PASS] subtest: Insert layer invalidates @font-face

      assert_equals: expected "220px" but got "122.8px"
      

    • FAIL [expected PASS] subtest: Delete layer invalidates @font-face

      assert_equals: expected "220px" but got "122.8px"
      

  • OK /css/css-fonts/variations/at-font-face-font-matching.html (#20684)
    • FAIL [expected PASS] subtest: Matching font-weight: '501' should prefer '503 520' over '500'

      assert_equals: Unexpected font on test element expected 487 but got 532
      

    • FAIL [expected PASS] subtest: Matching font-style: 'oblique 10deg' should prefer 'oblique 30deg 60deg' over 'oblique 40deg 50deg'

      assert_equals: Unexpected font on test element expected 487 but got 532
      

    • FAIL [expected PASS] subtest: Matching font-style: 'oblique -10deg' should prefer 'oblique 0deg 10deg' over 'oblique 40deg 50deg'

      assert_equals: Unexpected font on test element expected 487 but got 532
      

  • FAIL [expected PASS] /css/css-sizing/dynamic-available-size-iframe.html (#45763)
  • OK [expected TIMEOUT] /fetch/api/redirect/redirect-keepalive.https.any.html (#32153)
    • PASS [expected TIMEOUT] subtest: [keepalive][iframe][load] mixed content redirect; setting up
  • TIMEOUT [expected OK] /fetch/content-encoding/br/big-br-body.https.any.html
    • TIMEOUT [expected PASS] subtest: large br data should be decompressed successfully

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: large br data should be decompressed successfully with byte stream
  • TIMEOUT [expected OK] /fetch/content-encoding/br/big-br-body.https.any.worker.html
    • TIMEOUT [expected PASS] subtest: large br data should be decompressed successfully

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: large br data should be decompressed successfully with byte stream
  • TIMEOUT [expected OK] /fetch/content-encoding/gzip/big-gzip-body.https.any.worker.html
    • TIMEOUT [expected PASS] subtest: large gzip data should be decompressed successfully

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: large gzip data should be decompressed successfully with byte stream
  • TIMEOUT [expected OK] /fetch/content-encoding/zstd/big-zstd-body.https.any.worker.html
    • TIMEOUT [expected PASS] subtest: large zstd data should be decompressed successfully

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: large zstd data should be decompressed successfully with byte stream
  • ERROR [expected TIMEOUT] /html/browsers/browsing-the-web/history-traversal/pageswap/pageswap-initial-navigation.html (#40387)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
    • PASS [expected FAIL] subtest: Link with onclick navigation and href navigation
  • OK /html/browsers/browsing-the-web/navigating-across-documents/javascript-url-return-value-handling-dynamic.html
    • FAIL [expected PASS] subtest: 0041 set in href="" targeting a frame and clicked

      assert_equals: expected "A" but got ""
      

  • 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/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html (#44098)
    • TIMEOUT [expected FAIL] subtest: Replace before load, triggered by formElement.requestSubmit()

      Test timed out
      

  • TIMEOUT /html/browsers/history/the-history-interface/001.html (#12580)
    • PASS [expected FAIL] subtest: traversing history must also traverse hash changes
  • OK /html/browsers/history/the-history-interface/traverse_the_history_4.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • ERROR [expected OK] /html/infrastructure/urls/base-url/base-url-javascript-nav.https.window.html
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html (#29087)
    • TIMEOUT [expected PASS] subtest: &lt;dialog&gt;-contained autofocus element gets focused when the dialog is shown

      Test timed out
      

  • OK /html/semantics/embedded-content/media-elements/media_fragment_seek.html (#24114)
    • FAIL [expected PASS] subtest: Video should seek to time specified in media fragment syntax

      assert_equals: expected 3 but got 0
      

  • OK /html/semantics/embedded-content/media-elements/seeking/seek-to-currentTime.html (#46014)
    • FAIL [expected PASS] subtest: seek to currentTime

      assert_array_equals: fired events lengths differ, expected array ["seeking", "timeupdate", "seeked"] length 3, got ["seeking", "timeupdate", "timeupdate", "seeked"] length 4
      

  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-reload-location-reload.html (#32595)
    • FAIL [expected PASS] subtest: Reloading iframe loading='lazy' before it is loaded: location.reload

      uncaught exception: Error: assert_equals: expected "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?src" but got "about:blank"
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigate_other_frame_popup.sub.html (#39702)
    • TIMEOUT [expected FAIL] subtest: Sandboxed iframe can not navigate other frame's popup

      Test timed out
      

  • CRASH [expected FAIL] /html/semantics/interestfor/interestfor-plain-inline-element.tentative.html
  • TIMEOUT [expected OK] /html/syntax/parsing/html5lib_write_single.html?file=tests23
    • TIMEOUT [expected PASS] subtest: &lt;p&gt;&lt;font size=4&gt;&lt;font color=red&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font color=red&gt;&lt;p&gt;X

      Test timed out
      

    • TIMEOUT [expected PASS] subtest: &lt;p&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;p&gt;X

      Test timed out
      

    • TIMEOUT [expected PASS] subtest: &lt;p&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;font size="5"&gt;&lt;font size=4&gt;&lt;p&gt;X

      Test timed out
      

    • TIMEOUT [expected PASS] subtest: &lt;p&gt;&lt;font size=4 id=a&gt;&lt;font size=4 id=b&gt;&lt;font size=4&gt;&lt;font size=4&gt;&lt;p&gt;X

      Test timed out
      

    • TIMEOUT [expected PASS] subtest: &lt;p&gt;&lt;b id=a&gt;&lt;b id=a&gt;&lt;b id=a&gt;&lt;b&gt;&lt;object&gt;&lt;b id=a&gt;&lt;b id=a&gt;X&lt;/object&gt;&lt;p&gt;Y

      Test timed out
      

  • TIMEOUT [expected OK] /html/user-activation/propagation-sameorigin.html
    • NOTRUN [expected PASS] subtest: Propagation test
  • OK /html/webappapis/dynamic-markup-insertion/document-write/module-dynamic-import.html
    • FAIL [expected PASS] subtest: document.write in an imported module

      assert_true: onload must be called expected true got false
      

  • OK /html/webappapis/dynamic-markup-insertion/document-write/module-tla-import.html (#42419)
    • FAIL [expected PASS] subtest: document.write in an imported module

      assert_true: onload must be called expected true got false
      

  • OK /paint-timing/fcp-only/fcp-out-of-bounds-translate.html
    • PASS [expected FAIL] subtest: First contentful paint fires due to transform-based intersection with document.
  • TIMEOUT [expected OK] /pointerevents/compat/pointerevent_touch-action_two-finger_interaction.html (#44693)
    • NOTRUN [expected PASS] subtest: touch two-finger pan on 'touch-action: pan-x pan-y'
    • NOTRUN [expected FAIL] subtest: touch two-finger pan on 'touch-action: pinch-zoom'
  • OK /pointerevents/pointerevent_pointercapture_in_frame.html?touch (#45445)
    • PASS [expected FAIL] subtest: Test touchpointer capture in same-origin frame: Pointer down at inner frame and set pointer capture.
    • PASS [expected FAIL] subtest: Test touchpointer capture in same-origin frame: Pointer down at inner frame and set pointer capture to outer frame should not capture.
  • CRASH [expected OK] /trusted-types/TrustedTypePolicyFactory-createPolicy-unenforced.html
  • TIMEOUT /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected TIMEOUT] subtest: Navigate a frame via anchor with javascript:-urls w/ default policy in report-only mode.
  • TIMEOUT [expected OK] /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in enforcing mode.

      Test timed out
      

  • TIMEOUT [expected OK] /webstorage/storage_local_setitem_quotaexceedederr.window.html
Stable unexpected results that are known to be intermittent (18)
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • FAIL [expected PASS] subtest: Payload size restriction should be accumulated: type = string

      assert_false: expected false got true
      

  • OK /dom/nodes/moveBefore/iframe-document-preserve.window.html (#43152)
    • FAIL [expected PASS] subtest: moveBefore(): cross-origin iframe is preserved: remove self

      assert_equals: iframe does not fire a second load event expected 1 but got 0
      

    • FAIL [expected PASS] subtest: moveBefore(): cross-origin iframe is preserved: remove self via replaceChildren()

      assert_equals: iframe does not fire a second load event expected 1 but got 0
      

  • TIMEOUT /fetch/metadata/generated/css-images.https.sub.tentative.html (#42229)
    • PASS [expected FAIL] subtest: content sec-fetch-site - Same-Origin -&gt; Cross-Site -&gt; Same-Origin redirect
    • FAIL [expected PASS] subtest: content sec-fetch-site - Cross-Site -&gt; Same-Site

      assert_unreached: Reached unreachable code
      

  • TIMEOUT /fetch/metadata/generated/css-images.sub.tentative.html (#29047)
    • TIMEOUT [expected PASS] subtest: background-image sec-fetch-site - Not sent to non-trustworthy same-origin destination

      Test timed out
      

    • FAIL [expected PASS] subtest: content sec-fetch-site - HTTPS downgrade-upgrade

      assert_unreached: Reached unreachable code
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html (#28691)
    • PASS [expected FAIL] subtest: load event does not fire on window.open('about:blank')
  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • CRASH [expected ERROR] /html/browsers/history/the-location-interface/location_replace_session_history.html (#41896)
  • 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/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/media-elements/src_object_blob.html (#40340)
    • TIMEOUT [expected PASS] subtest: HTMLMediaElement.srcObject blob

      Test timed out
      

  • OK [expected TIMEOUT] /html/syntax/parsing/html5lib_write.html?file=processing-instructions (#46205)
  • TIMEOUT [expected OK] /html/syntax/parsing/html5lib_write_single.html?file=processing-instructions (#46206)
    • TIMEOUT [expected FAIL] subtest: &lt;body&gt;&lt;?view-port data&gt;

      Test timed out
      

    • TIMEOUT [expected FAIL] subtest: &lt;body&gt;&lt;?config-v2&gt;

      Test timed out
      

    • TIMEOUT [expected FAIL] subtest: &lt;body&gt;&lt;?config-v2 data&gt;

      Test timed out
      

    • TIMEOUT [expected FAIL] subtest: &lt;body&gt;&lt;?x&gt;

      Test timed out
      

  • TIMEOUT [expected OK] /infrastructure/testdriver/click_nested.html (#43887)
    • NOTRUN [expected FAIL] subtest: TestDriver click method with multiple windows and nested iframe
  • OK /mixed-content/tentative/autoupgrades/audio-upgrade.https.sub.html (#41697)
    • FAIL [expected PASS] subtest: Audio of other host autoupgraded

      assert_equals: Length of other host audio is correct expected 1 but got Infinity
      

  • OK /paint-timing/with-lcp/fcp-document-opacity-image.html (#43068)
    • PASS [expected FAIL] subtest: Test that FCP after opacity change is not a larger value than LCP
  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (iframe)
    • FAIL [expected PASS] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (xmlhttprequest)

      assert_equals: expected 74.4 but got 74.39
      

  • TIMEOUT [expected OK] /trusted-types/trusted-types-navigation.html?16-20 (#44835)
    • FAIL [expected PASS] subtest: Navigate a frame via area with javascript:-urls in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • TIMEOUT [expected PASS] subtest: Navigate a frame via area with javascript:-urls w/ default policy in report-only mode.

      Test timed out
      

Stable unexpected results (1)
  • FAIL [expected PASS] /html/rendering/replaced-elements/attributes-for-embedded-content-and-images/input-min-bsize-centered.html

@github-actions

Copy link
Copy Markdown

⚠️ Try run (#29577731042) failed!

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks reasonable to me, although I do not have full context around Servo's layout system to fully review.

Wanted to note that it may be worth looking at implementing align-content in block sooner rather than later: the diff to implement that in Taffy was pretty small https://github.com/DioxusLabs/taffy/pull/959/changes

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Jul 17, 2026
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Jul 17, 2026
@Loirooriol

Copy link
Copy Markdown
Contributor Author

it may be worth looking at implementing align-content

Yes, it doesn't seem too complex, but there is the question of whether it should be done during layout (like flex containers or now buttons), or after layout (like table cells). And it should ideally be the same approach everywhere, or at least if table cells are different, not align the contents twice.

@Loirooriol Loirooriol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dcoapp recheck

@servo-wpt-sync

Copy link
Copy Markdown
Collaborator

🤖 Opened new upstream WPT pull request (web-platform-tests/wpt#61372) with upstreamable changes.

@Loirooriol Loirooriol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dcoapp recheck

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dcoapp recheck

Mark `<button>` elements and `<input>` that lay out as buttons with a
new `FragmentFlags::IS_BUTTON` flag. Then, if they contain a block
formatting context, center their contents in the block axis.

This is done similarly as for `vertical-align` on table cells: we wrap
the contents inside a `PositioningFragment`, which is then shifted to
appear at the correct place. But unlike table cells, this is done before
caching the layout results, not afterwards.

In the future, when we implement `align-content` for block containers,
we should probably switch to that.

Co-authored-by: Martin Robinson <mrobinson@igalia.com>
Signed-off-by: Oriol Brufau <obrufau@igalia.com>
@servo-wpt-sync

Copy link
Copy Markdown
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#61372).

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Jul 19, 2026
@mrobinson
mrobinson added this pull request to the merge queue Jul 19, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Jul 19, 2026
Merged via the queue into servo:main with commit 5f837eb Jul 19, 2026
31 checks passed
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Jul 19, 2026
@Loirooriol
Loirooriol deleted the button-layout branch July 22, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-layout/2020 https://github.com/servo/servo/wiki/Layout-2020

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Button content vertical alignment is incorrect

5 participants