Skip to content

Implement vertical-align via Parley - #832

Merged
nicoburns merged 8 commits into
mainfrom
devin/1788386630-vertical-align
Sep 28, 2026
Merged

nicoburns merged 8 commits into
mainfrom
devin/1788386630-vertical-align

Conversation

@nicoburns

@nicoburns nicoburns commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Companion to linebender/parley#766 (now merged: Parley builds the CSS inline alignment tree itself: root strut, ancestor inline boxes, parent-relative baseline shifts, top/bottom aligned subtrees). Cargo.toml pins Parley to linebender main 63927df.

  • stylo_to_parley::vertical_align: Stylo stores vertical-align as the css-inline-3 longhands, and Parley's VerticalAlign { alignment, shift } is the same compound, so both are mapped losslessly: alignment-baseline (baseline|text-top|text-bottom|middle) → parley::AlignmentBaseline, baseline-shift (sub|super|top|bottom|<length-percentage>) → parley::BaselineShift. Percentages resolve against the element's own line-height. baseline-shift: center has no Parley equivalent yet and is approximated as middle; baseline-source is ignored.
  • Set TextStyle::vertical_align on every span and InlineBox::vertical_align on atomic inlines.
  • InlineBox::baseline is now populated for inline-blocks from taffy's baselines.last.or(first) (+ top margin), or None (bottom margin edge) when overflow isn't visible / there are no line boxes (CSS 2 §10.8.1). Inline layout now also reports baselines.last upward.
  • Removed the root_line_height floor on each span's line-height — Parley adds the strut and every ancestor span's own box to the line, so the floor was double counting and broke shifted spans.
  • Floats/abspos inline boxes call append_inline_box_to_line(.., NEG_INFINITY, NEG_INFINITY) so they contribute no height.
  • API churn: Cluster::style_index() replaces Glyph::style_index(); Run::font_metrics(), run.font().font, NormalizedCoord::to_bits().

Not included (follow-up once DioxusLabs/parley#19 / #23 land upstream): painting text decorations at the decorating box's baseline (needs Style::parent / Line::style_baseline, fixes text-decoration-va-length-001) and the FontContext::from_parts adaptation.

WPT (css/CSS2/linebox css/css-inline css/CSS2/text)

454 → 490 tests passing, 1198 → 1299 subtests, 0 crashes. Previously-passing tests that now fail are pre-existing gaps that the old always-tall strut happened to hide, e.g.:

  • linebox/baseline-block-with-overflow-001.html — taffy block layout propagates a child's baseline through an overflow: hidden block; should synthesize from the bottom margin edge (taffy issue).
  • css-inline/empty-span-size-002.html — Blitz doesn't model inline border/padding boxes, so an empty bordered span doesn't make the line visible.
  • dominant-baseline-mixed-writing-modes-002.html — vertical writing modes / dominant-baseline unsupported.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/da7669e341814cc7976e4ebf32b7eacb
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/da7669e341814cc7976e4ebf32b7eacb?variant=devin-insiders
Requested by: @nicoburns

WPT results

Subtests: 204 newly passing, 19 newly failing (net +185).

Full diff (179 changed tests)
+ FAIL => PASS    [1/1]   +1  css/CSS2/backgrounds/background-position-applies-to-007.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/backgrounds/background-position-applies-to-009.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/backgrounds/background-position-applies-to-012.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/backgrounds/background-position-applies-to-013.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/backgrounds/background-position-applies-to-014.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/box-display/containing-block-030.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c43-rpl-ibx-000.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c44-ln-box-000.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c44-ln-box-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c544-valgn-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c5502-mrgn-r-000.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c5504-mrgn-l-000.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/css1/c5505-mrgn-000.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/clear-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/clear-inline-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/float-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/float-replaced-width-006.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/float-replaced-width-008.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/float-replaced-width-009.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/float-replaced-width-011.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/floats-007.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/floats-038.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/floats-040.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/floats-124.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/floats-141.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats-clear/margin-collapse-clear-014.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats/floats-wrap-bfc-002-left-overflow.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats/floats-wrap-bfc-002-left-table.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats/floats-wrap-bfc-002-right-overflow.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/floats/floats-wrap-bfc-002-right-table.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/fonts/font-family-applies-to-001.xht
- PASS => FAIL    [0/1]   -1  css/CSS2/fonts/font-family-applies-to-005.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/fonts/font-family-applies-to-017.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/generated-content/content-174.xht
- PASS => FAIL    [0/1]   -1  css/CSS2/linebox/baseline-block-with-overflow-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/inline-formatting-context-013.xht
+ FAIL => FAIL   [8/13]   +6  css/CSS2/linebox/inline-negative-margin-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/line-breaking-font-size-zero-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/line-height-127.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/line-height-oof-descendants-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-007.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-008.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-019.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-020.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-031.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-032.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-043.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-044.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-055.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-056.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-067.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-068.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-079.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-080.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-091.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-092.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-103.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-104.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-applies-to-008.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-baseline-004a.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/linebox/vertical-align-baseline-005a.xht
+ FAIL => PASS  [20/20]  +16  css/CSS2/linebox/vertical-align-top-bottom-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/margin-padding-clear/margin-percentage-inherit-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/margin-padding-clear/padding-applies-to-017.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-block-replaced-width-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-block-replaced-width-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-block-replaced-width-006.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-block-width-001a.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-block-width-001b.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-replaced-width-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-replaced-width-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inline-replaced-width-006.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/inlines-013.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/max-width-percentage-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/min-width-percentage-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/width-percentage-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/normal-flow/width-percentage-002.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/positioning/absolute-non-replaced-height-008.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/positioning/absolute-non-replaced-max-height-008.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/positioning/abspos-negative-margin-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/selectors/first-line-pseudo-014.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/selectors/first-line-pseudo-015.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/tables/fixed-table-layout-021.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/tables/fixed-table-layout-022.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/tables/fixed-table-layout-023.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/tables/table-anonymous-objects-171.xht
- PASS => FAIL    [0/1]   -1  css/CSS2/text/white-space-applies-to-007.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/text/white-space-normal-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/text/white-space-normal-004.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/text/white-space-pre-001.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/text/white-space-pre-002.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/values/units-003.xht
+ FAIL => PASS    [1/1]   +1  css/CSS2/visudet/content-height-001.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/visudet/content-height-002.html
+ FAIL => PASS    [1/1]   +1  css/CSS2/visudet/content-height-003.html
- PASS => FAIL    [0/1]   -1  css/CSS2/visufx/overflow-applies-to-001.xht
- PASS => FAIL    [0/1]   -1  css/css-align/baseline-of-scrollable-1b.html
- FAIL => FAIL    [0/2]   -1  css/css-align/baseline-of-single-axis-scroll-container.html
+ FAIL => PASS    [7/7]   +3  css/css-align/baseline-rules/synthesized-baseline-flexbox-001.html
+ FAIL => PASS    [7/7]   +3  css/css-align/baseline-rules/synthesized-baseline-grid-001.html
+ FAIL => PASS    [3/3]   +1  css/css-align/baseline-rules/synthesized-baseline-inline-block-001.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-origin/origin-padding-box.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-origin/origin-padding-box_with_radius.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-size-025.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-size-027.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-size-028.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-size-030.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/background-size-031.html
+ FAIL => PASS    [1/1]   +1  css/css-backgrounds/box-shadow-outset-without-border-radius-001.html
+ FAIL => PASS    [1/1]   +1  css/css-break/flexbox/multi-line-row-flex-fragmentation-082a-print.html
+ FAIL => PASS    [1/1]   +1  css/css-break/flexbox/multi-line-row-flex-fragmentation-082b-print.html
+ FAIL => PASS    [1/1]   +1  css/css-break/flexbox/multi-line-row-flex-fragmentation-082c-print.html
+ FAIL => PASS    [1/1]   +1  css/css-break/flexbox/multi-line-row-flex-fragmentation-082d-print.html
+ FAIL => PASS    [1/1]   +1  css/css-contain/contain-content-004.html
+ FAIL => PASS    [1/1]   +1  css/css-contain/contain-layout-cell-002.html
+ FAIL => PASS    [1/1]   +1  css/css-contain/contain-paint-023.html
- PASS => FAIL    [0/1]   -1  css/css-contain/contain-size-flexbox-002.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-align-self-horiz-001-block.xhtml
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-align-self-baseline-vert-001.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-multi-item-vert-001a.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-multi-item-vert-001b.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-multi-line-horiz-001.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-multi-line-horiz-002.html
- PASS => FAIL    [0/1]   -1  css/css-flexbox/flexbox-baseline-multi-line-vert-002.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-baseline-wrap-reverse-unequal-lines.html
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-justify-content-horiz-002.xhtml
+ FAIL => PASS    [1/1]   +1  css/css-flexbox/flexbox-justify-content-horiz-004.xhtml
+ FAIL => FAIL    [1/2]   +1  css/css-flexbox/percentage-size-quirks-002.html
+ FAIL => PASS    [1/1]   +1  css/css-fonts/font-variant-emoji-005.html
+ FAIL => FAIL    [7/9]   +1  css/css-grid/alignment/grid-align-baseline.html
- PASS => FAIL    [0/1]   -1  css/css-grid/alignment/grid-baseline-001.html
- PASS => FAIL    [0/1]   -1  css/css-grid/alignment/grid-baseline-002.html
+ FAIL => FAIL  [16/18]   +9  css/css-grid/alignment/grid-baseline-004.html
+ FAIL => FAIL   [3/11]   +3  css/css-grid/alignment/grid-container-baseline-001.html
- PASS => FAIL    [0/1]   -1  css/css-grid/alignment/grid-inline-baseline.html
+ FAIL => PASS    [1/1]   +1  css/css-grid/alignment/self-baseline/grid-self-baseline-horiz-001.html
+ FAIL => PASS    [1/1]   +1  css/css-grid/alignment/self-baseline/grid-self-baseline-horiz-003.html
+ FAIL => PASS    [1/1]   +1  css/css-grid/alignment/self-baseline/grid-self-baseline-vertical-lr-001.html
+ FAIL => PASS    [1/1]   +1  css/css-grid/alignment/self-baseline/grid-self-baseline-vertical-rl-001.html
- PASS => FAIL    [0/1]   -1  css/css-grid/grid-items/grid-inline-items-002.html
+ FAIL => PASS    [1/1]   +1  css/css-images/image-orientation/image-orientation-none-cross-origin.html
+ FAIL => PASS    [1/1]   +1  css/css-inline/baseline-shift/baseline-shift-bottom.html
+ FAIL => PASS    [1/1]   +1  css/css-inline/baseline-shift/baseline-shift-center.html
+ FAIL => PASS    [1/1]   +1  css/css-inline/baseline-shift/baseline-shift-length-percentage.html
+ FAIL => PASS    [1/1]   +1  css/css-inline/baseline-shift/baseline-shift-top.html
+ FAIL => FAIL  [15/22]   +5  css/css-inline/baseline-source/baseline-source-first-001.html
- PASS => FAIL    [0/1]   -1  css/css-inline/dominant-baseline/dominant-baseline-mixed-writing-modes-002.html
+ FAIL => PASS    [1/1]   +1  css/css-inline/empty-span-size-002.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-list-item-counter-002.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-list-item-counter-004.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-list-item-counter-005.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-value-reversed-016.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-value-reversed-018.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/li-value-reversed-019.html
- PASS => FAIL    [0/1]   -1  css/css-lists/list-and-block-in-inline.html
+ FAIL => PASS    [1/1]   +1  css/css-lists/list-style-type-string-002.html
+ FAIL => PASS    [1/1]   +1  css/css-multicol/multicol-width-negative-001.xht
+ FAIL => PASS    [1/1]   +1  css/css-page/monolithic-overflow-014-print.html
- PASS => FAIL    [0/1]   -1  css/css-sizing/intrinsic-percent-replaced-008.html
- PASS => FAIL    [0/1]   -1  css/css-sizing/intrinsic-percent-replaced-dynamic-008.html
+ FAIL => PASS    [1/1]   +1  css/css-sizing/whitespace-and-break.html
+ FAIL => FAIL   [7/15]   +4  css/css-tables/tentative/baseline-table.html
- PASS => FAIL    [0/1]   -1  css/css-text/letter-spacing/letter-spacing-end-of-line-002.html
+ FAIL => PASS    [1/1]   +1  css/css-text/line-breaking/line-breaking-030.html
+ FAIL => PASS    [1/1]   +1  css/css-text/line-breaking/line-breaking-032.html
+ FAIL => PASS    [4/4]   +4  css/css-text/line-breaking/line-breaking-atomic-nowrap-001.html
+ FAIL => PASS    [1/1]   +1  css/css-text/line-breaking/line-breaking-replaced-001.html
+ FAIL => PASS    [1/1]   +1  css/css-text/line-breaking/line-breaking-replaced-004.html
+ FAIL => PASS    [1/1]   +1  css/css-text/line-breaking/line-breaking-replaced-006.html
+ FAIL => PASS    [1/1]   +1  css/css-text/text-encoding/shaping-join-003.html
+ FAIL => PASS    [1/1]   +1  css/css-text/white-space/white-space-pre-wrap-trailing-spaces-002.html
- PASS => FAIL    [0/1]   -1  css/css-viewport/zoom/vertical-align.html
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/abs-pos-non-replaced-icb-vlr-005.xht
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/abs-pos-non-replaced-icb-vlr-013.xht
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/abs-pos-non-replaced-icb-vrl-004.xht
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/abs-pos-non-replaced-icb-vrl-012.xht
- PASS => FAIL    [0/1]   -1  css/css-writing-modes/abs-pos-with-replaced-child.html
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/baseline-with-orthogonal-flow-001.html
+ FAIL => PASS    [1/1]   +1  css/css-writing-modes/text-combine-upright-shadow.html

Generated by the WPT workflow.

staging-devin-ai-integration Bot pushed a commit that referenced this pull request Sep 3, 2026
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788386630-vertical-align branch 3 times, most recently from 41b9df8 to 2b316d5 Compare September 14, 2026 17:25
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788386630-vertical-align branch 2 times, most recently from 75ab54e to f113887 Compare September 21, 2026 16:47
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788386630-vertical-align branch 2 times, most recently from 6b7e7b5 to 5ee11fe Compare September 24, 2026 19:54
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788386630-vertical-align branch from 5ee11fe to 9168ba6 Compare September 26, 2026 02:48
Rebased onto main as a single commit: main now includes #914 (Parley
upgrade to linebender main), which superseded the Parley repin commits
that made up most of this branch's history. The previous history is
preserved at backup/832-pre-rebase.

- Map stylo vertical-align / alignment-baseline / baseline-shift to
  Parley's VerticalAlign on text styles and inline boxes
- Drop Blitz's root line-height floor and per-span line-height
  pre-computation; Parley now owns line box sizing
- Export inline-block / inline flex / grid baselines to Parley
- Scale ch/ic linearly from font units to match shaped advances
- Position glyph runs, inline boxes and decorations by baseline offset
Underlines/overlines/line-throughs of a decorating element are drawn at that
element's inline-box baseline on each line (via Line::style_baseline through
the parley style parent chain) instead of at each glyph run's shifted
baseline, so vertically aligned descendants no longer carry the decoration
with them. Pin parley to 1fd63fba72b38df9ec14892bd6f20e96fe2c1176, which adds
Style::parent / Line::style_baseline and stops a trailing space from starting
an extra empty line.
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1788386630-vertical-align branch from 8c94c04 to 33b10ca Compare September 28, 2026 13:12
tomcur pushed a commit to tomcur/parley that referenced this pull request Sep 28, 2026
**Implements vertical alignment.**

LLM Contributions: Generated with Fable 5.1 Low. Reviewed by GPT 5.6 Sol
XHigh. This has also been manually reviewed, and gone through several
rounds of iteration. I probably still need to do another round on the
details of the the actual alignment, but I think this is architecturally
in a good place.

- Builds on top of linebender#762
- Blitz integration PR (runs WPT tests)
DioxusLabs/blitz#832

This PR is designed to be reviewed commit-by-commit (4 commits):

1. **Introduce a minimal tree structure**:
Adds `parent: u16` to every style, which allows for upwards
tree-traversal. The `TreeBuilder` encodes it's tree with this new field.
The `RangedBuilder` builds a trivial tree with one root, and where all
style spans are direct children of that root. This builds on the
existing convention that the 0th style is the root/paragraph level
style.

2. **Adds `VerticalAlign` style**. `VerticalAlign` is struct which
consists of `BaselineShift`, and `AlignmentBaseline` fields, and can be
set both as a span style and on inline boxes.

3. **Introduces the concept of first available font and resolves a font
+ metrics for every style**. We thus end up with a tree of spans each
with associated line metrics before we even begin layout. We do not use
the actual resolved fonts for any run/atom/cluster for vertical
alignment at all (this matches browsers, but there are other reasonable
choices here, and we could bring back using run/cluster/atom metrics for
this as an option if desired). Some level of alignment relative to the
parent is already pre-computed at this stage.

(Note: these style lookups are kinda expensive - I have a follow-up PR
that eliminates that overhead with a global cache on the `FontContext` -
DioxusLabs#19 for those who want a
preview)

4. **Implements the actual vertical alignment logic** by building a list
of "aligned subtrees" for each line, and computing the offsets of each
span in each aligned subtree relative to each other. Most lines actually
only have one "aligned subtree", so it is alignment within a tree doing
most of the work. `vertical-align: top` and `bottom` introduce new
"aligned subtrees" which are initially aligned independently of the
other "aligned subtrees" on the line. There is then a final alignment
step when completing each line that aligns the "aligned subtrees" to
each other.

Some notes:

- The mental model for how spans interact with lines is that each span
creates a box fragment in **every** line it intersects, and each a
span's box fragment on a given line is always contained within (and
aligned with) a box fragment for **every** ancestor span up to the root
level, as well as with other spans (and their ancestors) that exist on
the same line.
- This is a generalisation of the "strut metric" concept: spans align
with not just the root, but every span in between. `RangedBuilder`
layouts fallback gracefully to "strut metric"-style layout (spans in
ranged layouts only have one ancestor: the root).


**Changelog**

> ### Added
>
> - `vertical-align` style.
> - TODO

---------

Signed-off-by: Nico Burns <nico@nicoburns.com>
@nicoburns
nicoburns marked this pull request as ready for review September 28, 2026 15:08
Reverts the FontContext::from_parts adaptation and the decorating-box
baseline decoration painting, which depend on the not-yet-upstreamed
DioxusLabs/parley #19 / #23 (Style::parent, Line::style_baseline).
@nicoburns
nicoburns merged commit e445c29 into main Sep 28, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant