Skip to content

[web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional - #193736

Merged
auto-submit[bot] merged 9 commits into
flutter:masterfrom
Rusino:webparagraph_leading_distribution
Oct 15, 2026
Merged

auto-submit[bot] merged 9 commits into
flutter:masterfrom
Rusino:webparagraph_leading_distribution

Conversation

@Rusino

@Rusino Rusino commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Part of #193731. Split out of #193403 (third of four independent PRs; the others are #193733 and #193734, and #193403 is stacked on top of this PR).

Problem

  • TextHeightBehavior.leadingDistribution was ignored. WebParagraphStyle never passed textHeightBehavior.leadingDistribution to the root WebTextStyle, and RootStyleNode._leadingDistribution always returned null, so the value could not reach LineBlock. Only an explicit TextStyle(leadingDistribution: …) had any effect.
  • The default leading distribution was even. LineBlock treated null the same as TextLeadingDistribution.even (half-leading). SkParagraph (and therefore the native engine and CanvasKit) default to proportional: TextStyle::fHalfLeading is false unless leadingDistribution == even is set, and dart:ui's TextHeightBehavior.leadingDistribution defaults to TextLeadingDistribution.proportional.
  • Negative half-leading clamped fontBoundingBoxDescent to 0.0. TextLine initialized fontBoundingBoxAscent and fontBoundingBoxDescent to 0.0 and accumulated every block with math.max(0.0, ...). With TextLeadingDistribution.even and a small height multiplier (where height * fontSize < rawAscent + rawDescent), extraLeading / 2 is negative and multipliedFontBoundingBoxDescent can become negative; math.max(0.0, ...) clamped it to 0.0, inflating the line height above height * fontSize.

Fix

  • WebParagraphStyle propagates textHeightBehavior?.leadingDistribution into the root WebTextStyle; RootStyleNode._leadingDistribution returns style.leadingDistribution. Child TextStyles still override it through ChildStyleNode as before.
  • LineBlock: null now falls through to the proportional branch (ascent and descent are scaled by height * fontSize / (ascent + descent)), even keeps the half-leading behavior.
  • TextLine.updateBoundingBox: track _hasLineMetrics so the first block on the line initializes fontBoundingBoxAscent and fontBoundingBoxDescent directly (preserving a negative descent), while subsequent blocks take math.max.

Behavior change

For any paragraph with a height multiplier and no explicit leadingDistribution, the line height is unchanged but the baseline moves: the extra leading is now split in proportion to the font's ascent/descent instead of 50/50. This brings WebParagraph in line with CanvasKit/Skwasm output for the same ParagraphStyle; WebParagraph is not the default renderer yet, so no app-facing change on stable.

Tests

Pre-launch Checklist

@flutter-dashboard

Copy link
Copy Markdown

This pull request is not mergeable in its current state, likely because of a merge conflict. Pre-submit CI jobs were not triggered. Pushing a new commit to this branch that resolves the issue will result in pre-submit jobs being scheduled.

@Rusino Rusino self-assigned this Oct 9, 2026
@Rusino
Rusino marked this pull request as ready for review October 9, 2026 20:29

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the web paragraph layout engine to propagate the default leadingDistribution from ParagraphStyle to the root text style, and correctly handles proportional and even leading distributions during line layout. It also ensures line metrics are initialized properly using a new _hasLineMetrics flag, and adds comprehensive unit tests for these behaviors. The review feedback suggests formatting two inline comments in layout.dart as complete sentences ending with a period to comply with the style guide.

Hzj-jie pushed a commit to zijiehe-google-com/flutter that referenced this pull request Oct 12, 2026
…flutter#193733)

Part of flutter#193731. Split out of flutter#193403 (first of four independent PRs;
the others are flutter#193734 and flutter#193736, and flutter#193403 itself will be rebased
on top once these land).

### Problem
- `ChildStyleNode._height` converted `style.height ==
ui.kTextHeightNone` to `null` *before* falling back to `parent._height`,
so a child `TextStyle(height: ui.kTextHeightNone)` could never override
a non-null parent height (`ParagraphStyle(height: 3.0)` +
`pushStyle(TextStyle(height: kTextHeightNone))` still laid out the line
at 3x).
- `LineBlock` only checked `span.style.height == null`, so
`ParagraphStyle(height: ui.kTextHeightNone)` (`0.0`) multiplied the line
ascent/descent by `0.0` instead of disabling the height multiplier.

### Fix
- `ChildStyleNode._height` keeps `style.height` as-is: `style.height ??
parent._height`. `kTextHeightNone` now reaches `LineBlock`, which is the
one place that interprets it.
- `LineBlock`: `height == null || height == ui.kTextHeightNone` → raw
font ascent/descent, no scaling (same as the native path, where
`paragraph_builder.cc` sets `has_height_override = style.height !=
kTextHeightNone`).
- `LineBlock` uses `StyleManager.defaultFontSize` instead of a hardcoded
`14.0`.
- `WebStrutStyle` already normalizes `kTextHeightNone` to `null` in its
constructor, so struts need no change.

### Tests
- `paragraph_get_boxes_test.dart`: `layout with kTextHeightNone disables
parent height multiplier`, `layout with
ParagraphStyle(kTextHeightNone)`.
- **WebParagraph corpus (`preferWebParagraph` + `forceTestFonts` vs
`origin/master` `b7315d76579e`)**: 20,012 tests (18,765 executed, 1,247
skipped) — **503 -> 503 failures (0 fixed standalone, +2 new tests
passing, 0 regressions)**.
- *Note:* Existing corpus tests for `kTextHeightNone` also depend on
other issues (`test/ui/paragraph_builder_test.dart: kTextHeightNone
StrutStyle` is unlocked in flutter#193774 once `StrutStyle` is applied;
`test/painting/text_painter_test.dart: kTextHeightNone unsets the text
height multiplier` still fails due to Chromium's integer-rounded
`FlutterTest` font metrics at `10px` where `ascent + descent = 11 !=
10`).

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #193736 at sha 04ad498

@flutter-dashboard flutter-dashboard Bot added the will affect goldens Changes to golden files label Oct 12, 2026
@Rusino
Rusino requested a review from mdebbar October 13, 2026 14:44
mdebbar
mdebbar previously approved these changes Oct 14, 2026
Test that a child TextStyle's leadingDistribution takes precedence over
the paragraph's TextHeightBehavior.leadingDistribution.

Assisted-by: Gemini 3.8 Flash (High)
@mdebbar mdebbar added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 15, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into flutter:master with commit 5c7e45a Oct 15, 2026
23 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 15, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Oct 15, 2026
flutter/flutter@426be96...5c7e45a

2026-10-15 [email protected] [web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional (flutter/flutter#193736)
2026-10-15 [email protected] Roll Skia from 79a00d7c4898 to d131b7789482 (2 revisions) (flutter/flutter#194446)
2026-10-15 [email protected] Roll Dart SDK from b8ffa9233096 to 84686ea04815 (1 revision) (flutter/flutter#194442)
2026-10-15 [email protected] Roll Dart SDK from e81af2dac9d1 to b8ffa9233096 (1 revision) (flutter/flutter#194432)
2026-10-15 [email protected] Roll Skia from ef24b8f87a2a to 79a00d7c4898 (4 revisions) (flutter/flutter#194431)
2026-10-15 [email protected] Roll Skia from 48c51004ee4e to ef24b8f87a2a (1 revision) (flutter/flutter#194427)
2026-10-15 [email protected] Roll Dart SDK from 6b50116630d0 to e81af2dac9d1 (5 revisions) (flutter/flutter#194422)
2026-10-15 [email protected] Roll Skia from b1be97266d3a to 48c51004ee4e (10 revisions) (flutter/flutter#194421)
2026-10-15 [email protected] RawAutocomplete: Fix focusNode listener mismatch in didUpdateWidget/dispose (flutter/flutter#190686)
2026-10-14 [email protected] [flutter_tools] Wait for iOS simulator log stream before launch (flutter/flutter#193142)
2026-10-14 [email protected] Move Material/Cupertino PRs from design triage to framework triage (flutter/flutter#194244)
2026-10-14 [email protected] refactor(flutter_tools): share FlutterCommand options and OptionBundles (flutter/flutter#194356)
2026-10-14 [email protected] Roll pub packages (flutter/flutter#194413)
2026-10-14 [email protected] Add ContextMenuButtonKind (flutter/flutter#193528)
2026-10-14 [email protected] Split SwiftPM plugin into multiple products (flutter/flutter#193521)
2026-10-14 [email protected] Roll Skia from 43d707a3be0e to b1be97266d3a (8 revisions) (flutter/flutter#194392)
2026-10-14 [email protected] Roll pub packages (flutter/flutter#194398)
2026-10-14 [email protected] [flutter_tools] Migrate AndroidSdk and AndroidStudio away from globals (flutter/flutter#193650)
2026-10-14 [email protected] [flutter_tools] Reload widget-preview when data assets change (flutter/flutter#193437)
2026-10-14 [email protected] [flutter tool] suppress sub-tool analytics during first run (flutter/flutter#194320)
2026-10-14 [email protected] Roll Skia from acde3755d2e2 to 43d707a3be0e (1 revision) (flutter/flutter#194385)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
harryterkelsen pushed a commit to harryterkelsen/flutter that referenced this pull request Oct 16, 2026
…boxes (flutter#193734)

Part of flutter#193731. Split out of flutter#193403 (second of four independent PRs;
the others are flutter#193733 and flutter#193736, and flutter#193403 itself will be rebased
on top once these land).

### Problem
- **Placeholder vertical position was computed too early.**
`PlaceholderBlock.calculatePlaceholderTop` set `block.advance` from the
line ascent *as it was at that moment* before folding `block.ascent`
into the line. A placeholder taller than the preceding text (or a later,
taller placeholder on the same line) grew `line.fontBoundingBoxAscent`
afterwards in `line.updateBoundingBox(block)`, leaving placeholder
`advance.top` positioned against a stale ascent.
`getBoxesForPlaceholders` returned those stale rects, so a placeholder
box could sit above the line's top (negative `top`) and disagree with
`getBoxesForRange` for the same range.
- **`BoxHeightStyle.tight` returned the height-scaled box for text.**
`getBoxesForRange` used `multipliedFontBoundingBoxAscent` /
`multipliedHeight`, i.e. the box included the extra leading added by
`TextStyle.height`. SkParagraph's `TextLine::getRectsForRange` returns
the unscaled glyph bounds for `tight` (`Rect::MakeXYWH(...,
-run.ascent(), ..., run.descent())` relative to the baseline). The same
code path was also applied to `PlaceholderBlock`, whose `advance` is
already line-relative, producing a double-shifted box.

### Fix
- `PlaceholderBlock` & `addLine`: split placeholder layout into two
passes:
- Pass 1 calls
`block.calculatePlaceholderMetrics(line.fontBoundingBoxAscent,
line.fontBoundingBoxDescent)` to compute each placeholder's
`ascent`/`descent` and folds them into the line metrics via
`line.updateBoundingBox(block)`.
- Pass 2, after `line.fontBoundingBoxAscent` is final, calls
`block.calculatePlaceholderTop(line.fontBoundingBoxAscent)` to compute
`top = lineAscent - ascent` and set `block.advance =
Rect.fromLTWH(spanShiftFromLineStart, top, span.width, span.height)`.
- `getBoxesForRange`, `BoxHeightStyle.tight`:
- `TextBlock`: `top = line.advance.top + line.fontBoundingBoxAscent -
block.rawFontBoundingBoxAscent`, `height = rawFontBoundingBoxAscent +
rawFontBoundingBoxDescent` (unscaled glyph bounds positioned at the
baseline, matching SkParagraph).
- `PlaceholderBlock`: `top = firstRect.top + line.advance.top`, `height
= span.height` — `block.advance` is already relative to the line, so no
extra ascent shift.
- Removed the unused vertical `.translate(...)` on `firstRect` for
`TextBlock`, since `firstRect` only provides `left` and `width` while
`top` and `bottom` are computed per `BoxHeightStyle`.

### Tests
- `paragraph_get_boxes_test.dart`:
- `getBoxesForRange and getBoxesForPlaceholders return matching boxes
for placeholders` — two placeholders on one line where the second is
taller; checks both APIs agree, `top >= 0`, and the box sizes equal the
placeholder sizes. On `master`: `getBoxesForPlaceholders` returns `(0,
-12, 50, 18)` for the first placeholder while `getBoxesForRange(0, 1)`
returns `(0, 14, 50, 44)`.
- `BoxHeightStyle.tight uses unscaled glyph height and respects
TextHeightBehavior` — `height: 3.0` with `even` leading; `tight` box
height equals the unscaled line height and is centered in the 60px line.
On `master` the `tight` box is 60px tall instead of 22px.
- Existing `paragraph_get_boxes_test.dart` and
`paragraph_placeholders_test.dart` pass.
- **Full WebParagraph corpus (`--full`, `preferWebParagraph` +
`forceTestFonts` vs `origin/master` `b7315d76579e`)**:
- Part 1 (`web_ui`): `16/16` `webparagraph/*` (`+198 ~5 -1`, **+2** new
tests passing) + `60/60` `test/ui/*` (`+1354 ~5 -41`).
- Part 2 (`packages/flutter`, `894/894` suites across `8/8` shards):
`+16712 ~1237 -459` vs baseline `+16710 ~1237 -461` (**+3** existing
tests fixed; **1** apparent golden diff in
`test/cupertino/text_field_test.dart: Overflow clipBehavior none golden`
where on `origin/master` two bugs cancelled out — missing
`StrutStyle(forceStrutHeight: true)` from flutter#193774 inflated the line
ascent by 83px while the old single-pass `calculatePlaceholderTop`
shifted the placeholder by `-83px` detached from its `'Hi'` text;
resolved once flutter#193774 lands).
- Jointly with flutter#193736 + flutter#193773 also unlocks
`test/painting/text_painter_test.dart: TextPainter line-height
half-leading with leading trim`.

<details>
<summary>Fixed corpus tests vs <code>origin/master</code> (3)</summary>

- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter line-height TextLeadingDistribution falls back to paragraph
style`
- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter line-height half-leading`
- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter widget span`
</details>

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…flutter#193733)

Part of flutter#193731. Split out of flutter#193403 (first of four independent PRs;
the others are flutter#193734 and flutter#193736, and flutter#193403 itself will be rebased
on top once these land).

### Problem
- `ChildStyleNode._height` converted `style.height ==
ui.kTextHeightNone` to `null` *before* falling back to `parent._height`,
so a child `TextStyle(height: ui.kTextHeightNone)` could never override
a non-null parent height (`ParagraphStyle(height: 3.0)` +
`pushStyle(TextStyle(height: kTextHeightNone))` still laid out the line
at 3x).
- `LineBlock` only checked `span.style.height == null`, so
`ParagraphStyle(height: ui.kTextHeightNone)` (`0.0`) multiplied the line
ascent/descent by `0.0` instead of disabling the height multiplier.

### Fix
- `ChildStyleNode._height` keeps `style.height` as-is: `style.height ??
parent._height`. `kTextHeightNone` now reaches `LineBlock`, which is the
one place that interprets it.
- `LineBlock`: `height == null || height == ui.kTextHeightNone` → raw
font ascent/descent, no scaling (same as the native path, where
`paragraph_builder.cc` sets `has_height_override = style.height !=
kTextHeightNone`).
- `LineBlock` uses `StyleManager.defaultFontSize` instead of a hardcoded
`14.0`.
- `WebStrutStyle` already normalizes `kTextHeightNone` to `null` in its
constructor, so struts need no change.

### Tests
- `paragraph_get_boxes_test.dart`: `layout with kTextHeightNone disables
parent height multiplier`, `layout with
ParagraphStyle(kTextHeightNone)`.
- **WebParagraph corpus (`preferWebParagraph` + `forceTestFonts` vs
`origin/master` `b7315d76579e`)**: 20,012 tests (18,765 executed, 1,247
skipped) — **503 -> 503 failures (0 fixed standalone, +2 new tests
passing, 0 regressions)**.
- *Note:* Existing corpus tests for `kTextHeightNone` also depend on
other issues (`test/ui/paragraph_builder_test.dart: kTextHeightNone
StrutStyle` is unlocked in flutter#193774 once `StrutStyle` is applied;
`test/painting/text_painter_test.dart: kTextHeightNone unsets the text
height multiplier` still fails due to Chromium's integer-rounded
`FlutterTest` font metrics at `10px` where `ascent + descent = 11 !=
10`).

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…ult to proportional (flutter#193736)

Part of flutter#193731. Split out of flutter#193403 (third of four independent PRs;
the others are flutter#193733 and flutter#193734, and flutter#193403 is stacked on top of
this PR).

### Problem
- **`TextHeightBehavior.leadingDistribution` was ignored.**
`WebParagraphStyle` never passed
`textHeightBehavior.leadingDistribution` to the root `WebTextStyle`, and
`RootStyleNode._leadingDistribution` always returned `null`, so the
value could not reach `LineBlock`. Only an explicit
`TextStyle(leadingDistribution: …)` had any effect.
- **The default leading distribution was `even`.** `LineBlock` treated
`null` the same as `TextLeadingDistribution.even` (half-leading).
SkParagraph (and therefore the native engine and CanvasKit) default to
`proportional`: `TextStyle::fHalfLeading` is `false` unless
`leadingDistribution == even` is set, and `dart:ui`'s
`TextHeightBehavior.leadingDistribution` defaults to
`TextLeadingDistribution.proportional`.
- **Negative half-leading clamped `fontBoundingBoxDescent` to `0.0`.**
`TextLine` initialized `fontBoundingBoxAscent` and
`fontBoundingBoxDescent` to `0.0` and accumulated every block with
`math.max(0.0, ...)`. With `TextLeadingDistribution.even` and a small
`height` multiplier (where `height * fontSize < rawAscent +
rawDescent`), `extraLeading / 2` is negative and
`multipliedFontBoundingBoxDescent` can become negative; `math.max(0.0,
...)` clamped it to `0.0`, inflating the line height above `height *
fontSize`.

### Fix
- `WebParagraphStyle` propagates
`textHeightBehavior?.leadingDistribution` into the root `WebTextStyle`;
`RootStyleNode._leadingDistribution` returns
`style.leadingDistribution`. Child `TextStyle`s still override it
through `ChildStyleNode` as before.
- `LineBlock`: `null` now falls through to the `proportional` branch
(ascent and descent are scaled by `height * fontSize / (ascent +
descent)`), `even` keeps the half-leading behavior.
- `TextLine.updateBoundingBox`: track `_hasLineMetrics` so the first
block on the line initializes `fontBoundingBoxAscent` and
`fontBoundingBoxDescent` directly (preserving a negative `descent`),
while subsequent blocks take `math.max`.

### Behavior change
For any paragraph with a `height` multiplier and no explicit
`leadingDistribution`, the **line height is unchanged** but the
**baseline moves**: the extra leading is now split in proportion to the
font's ascent/descent instead of 50/50. This brings WebParagraph in line
with CanvasKit/Skwasm output for the same `ParagraphStyle`; WebParagraph
is not the default renderer yet, so no app-facing change on stable.

### Tests
- New `paragraph_line_height_test.dart`:
- `Height multiplier scales ascent and descent proportionally by
default` — checks the scaled line keeps `height * fontSize`, that
ascent/descent scale by the same factor by default, that
`TextHeightBehavior(leadingDistribution: even)` adds equal leading on
both sides, and that the two differ. Fails on `master` (default was
`even` and the THB value was ignored).
- `Half-leading with a small height multiplier preserves negative
descent` — checks that `height: 0.1` with `TextLeadingDistribution.even`
keeps a negative `descent` and total height `0.1 * fontSize`.
- Existing `webparagraph/*` and `ui/paragraph_builder_test.dart` suites
pass.
- **WebParagraph corpus (`preferWebParagraph` + `forceTestFonts` vs
`origin/master` `b7315d76579e`)**: **0 fixed standalone, +2 new tests
passing, 0 regressions** (base of the flutter#193736 -> flutter#193773 -> flutter#193774 ->
flutter#193403 stack that fixes **34** corpus tests; framework `TextPainter
line-height` tests in `test/painting/text_painter_test.dart` also call
`getBoxesForSelection` with `BoxHeightStyle.tight` and are unlocked when
combined with flutter#193734).

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Mouad Debbar <[email protected]>
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…boxes (flutter#193734)

Part of flutter#193731. Split out of flutter#193403 (second of four independent PRs;
the others are flutter#193733 and flutter#193736, and flutter#193403 itself will be rebased
on top once these land).

### Problem
- **Placeholder vertical position was computed too early.**
`PlaceholderBlock.calculatePlaceholderTop` set `block.advance` from the
line ascent *as it was at that moment* before folding `block.ascent`
into the line. A placeholder taller than the preceding text (or a later,
taller placeholder on the same line) grew `line.fontBoundingBoxAscent`
afterwards in `line.updateBoundingBox(block)`, leaving placeholder
`advance.top` positioned against a stale ascent.
`getBoxesForPlaceholders` returned those stale rects, so a placeholder
box could sit above the line's top (negative `top`) and disagree with
`getBoxesForRange` for the same range.
- **`BoxHeightStyle.tight` returned the height-scaled box for text.**
`getBoxesForRange` used `multipliedFontBoundingBoxAscent` /
`multipliedHeight`, i.e. the box included the extra leading added by
`TextStyle.height`. SkParagraph's `TextLine::getRectsForRange` returns
the unscaled glyph bounds for `tight` (`Rect::MakeXYWH(...,
-run.ascent(), ..., run.descent())` relative to the baseline). The same
code path was also applied to `PlaceholderBlock`, whose `advance` is
already line-relative, producing a double-shifted box.

### Fix
- `PlaceholderBlock` & `addLine`: split placeholder layout into two
passes:
- Pass 1 calls
`block.calculatePlaceholderMetrics(line.fontBoundingBoxAscent,
line.fontBoundingBoxDescent)` to compute each placeholder's
`ascent`/`descent` and folds them into the line metrics via
`line.updateBoundingBox(block)`.
- Pass 2, after `line.fontBoundingBoxAscent` is final, calls
`block.calculatePlaceholderTop(line.fontBoundingBoxAscent)` to compute
`top = lineAscent - ascent` and set `block.advance =
Rect.fromLTWH(spanShiftFromLineStart, top, span.width, span.height)`.
- `getBoxesForRange`, `BoxHeightStyle.tight`:
- `TextBlock`: `top = line.advance.top + line.fontBoundingBoxAscent -
block.rawFontBoundingBoxAscent`, `height = rawFontBoundingBoxAscent +
rawFontBoundingBoxDescent` (unscaled glyph bounds positioned at the
baseline, matching SkParagraph).
- `PlaceholderBlock`: `top = firstRect.top + line.advance.top`, `height
= span.height` — `block.advance` is already relative to the line, so no
extra ascent shift.
- Removed the unused vertical `.translate(...)` on `firstRect` for
`TextBlock`, since `firstRect` only provides `left` and `width` while
`top` and `bottom` are computed per `BoxHeightStyle`.

### Tests
- `paragraph_get_boxes_test.dart`:
- `getBoxesForRange and getBoxesForPlaceholders return matching boxes
for placeholders` — two placeholders on one line where the second is
taller; checks both APIs agree, `top >= 0`, and the box sizes equal the
placeholder sizes. On `master`: `getBoxesForPlaceholders` returns `(0,
-12, 50, 18)` for the first placeholder while `getBoxesForRange(0, 1)`
returns `(0, 14, 50, 44)`.
- `BoxHeightStyle.tight uses unscaled glyph height and respects
TextHeightBehavior` — `height: 3.0` with `even` leading; `tight` box
height equals the unscaled line height and is centered in the 60px line.
On `master` the `tight` box is 60px tall instead of 22px.
- Existing `paragraph_get_boxes_test.dart` and
`paragraph_placeholders_test.dart` pass.
- **Full WebParagraph corpus (`--full`, `preferWebParagraph` +
`forceTestFonts` vs `origin/master` `b7315d76579e`)**:
- Part 1 (`web_ui`): `16/16` `webparagraph/*` (`+198 ~5 -1`, **+2** new
tests passing) + `60/60` `test/ui/*` (`+1354 ~5 -41`).
- Part 2 (`packages/flutter`, `894/894` suites across `8/8` shards):
`+16712 ~1237 -459` vs baseline `+16710 ~1237 -461` (**+3** existing
tests fixed; **1** apparent golden diff in
`test/cupertino/text_field_test.dart: Overflow clipBehavior none golden`
where on `origin/master` two bugs cancelled out — missing
`StrutStyle(forceStrutHeight: true)` from flutter#193774 inflated the line
ascent by 83px while the old single-pass `calculatePlaceholderTop`
shifted the placeholder by `-83px` detached from its `'Hi'` text;
resolved once flutter#193774 lands).
- Jointly with flutter#193736 + flutter#193773 also unlocks
`test/painting/text_painter_test.dart: TextPainter line-height
half-leading with leading trim`.

<details>
<summary>Fixed corpus tests vs <code>origin/master</code> (3)</summary>

- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter line-height TextLeadingDistribution falls back to paragraph
style`
- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter line-height half-leading`
- `packages/flutter` :: `test/painting/text_painter_test.dart:
TextPainter widget span`
</details>

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants