Skip to content

[web_paragraph] Honor TextHeightBehavior.applyHeightToFirstAscent and applyHeightToLastDescent - #193773

Merged
Rusino merged 6 commits into
flutter:masterfrom
Rusino:webparagraph_text_height_behavior
Oct 22, 2026
Merged

Rusino merged 6 commits into
flutter:masterfrom
Rusino:webparagraph_text_height_behavior

Conversation

@Rusino

@Rusino Rusino commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Honors TextHeightBehavior.applyHeightToFirstAscent and applyHeightToLastDescent when computing WebParagraph line metrics (for non-empty paragraphs). Part of #193731.

Note

Split out from #193403 to keep each change small and focused.

Problem

Both applyHeightToFirstAscent and applyHeightToLastDescent were ignored: the first line always kept its height-scaled ascent and the last line always kept its height-scaled descent.

Fix

  • TextLine.rawFontBoundingBoxAscent / rawFontBoundingBoxDescent: accumulate unscaled font metrics alongside height-scaled metrics in TextLine.updateBoundingBox (and copy them in TextLine.inheritMetricsFrom).
  • Identifying the first and last lines:
    • isFirstLine is determined in TextLayout._finishLine via lines.isEmpty before the line is appended.
    • isLastLine is passed from _LineBuilder.build (wrapper.dart) and stored on line.lastLine in _finishLine: a content line is the last line when it hits the maxLines limit or reaches the artificial EOF cluster without a trailing newline; when a trailing \n is present (and maxLines is not yet reached), the synthetic empty line added after \n is marked isLastLine: true.
  • Single-pass metric adjustment (TextLayout._finishLine -> TextLine.applyTextHeightBehavior): before computing line.advance, isFirstLine && !applyHeightToFirstAscent sets usesRawAscent = true and reverts fontBoundingBoxAscent to rawFontBoundingBoxAscent, and isLastLine && !applyHeightToLastDescent sets usesRawDescent = true and reverts fontBoundingBoxDescent to rawFontBoundingBoxDescent. Because this happens before line.advance is returned to the wrapper, line tops and paragraph height are accumulated in a single pass.
  • Placeholders & backgrounds: PlaceholderBlock.positionInLine positions placeholders after applyTextHeightBehavior so first-line placeholders align against the reverted ascent, and WebParagraphPainter._paintAllBlocks bounds background rects by raw ascent/descent when usesRawAscent/usesRawDescent are set.
  • Trailing newline: the synthetic empty line created after a trailing \n inherits the preceding line's scaled and raw metrics via inheritMetricsFrom, so the synthetic last line drops its scaled descent while the preceding text line keeps its scaled descent — matching SkParagraph.

Tests

  • paragraph_line_height_test.dart (+6 tests):
    • TextHeightBehavior reverts a single line to the unscaled font metrics
    • TextHeightBehavior affects only the first line ascent and the last line descent
    • TextHeightBehavior treats the empty line after a trailing newline as the last line
    • TextHeightBehavior applies applyHeightToLastDescent to the maxLines limit line
    • TextHeightBehavior positions first-line placeholders against the reverted ascent
    • TextHeightBehavior bounds background rects by reverted first ascent and last descent
  • WebParagraph corpus (preferWebParagraph + forceTestFonts vs origin/master 2e53c3af41a):
    • Part 1 (web_ui test/webparagraph/* + test/ui/*): +6 new tests passing (207 vs 201), 0 regressions.
    • Part 2 (packages/flutter): +2 tests fixed (test/painting/text_painter_test.dart: TextHeightBehavior with strut on empty paragraph and test/painting/text_painter_test.dart: TextPainter widget span), 0 regressions.

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.

mdebbar added a commit to mdebbar/flutter that referenced this pull request Oct 15, 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]>
@Rusino
Rusino force-pushed the webparagraph_text_height_behavior branch 3 times, most recently from 5c16a50 to 47700fc Compare October 15, 2026 20:32
@Rusino
Rusino requested a review from mdebbar October 15, 2026 23:19
@Rusino
Rusino force-pushed the webparagraph_text_height_behavior branch from 47700fc to b75702b Compare October 16, 2026 14:11
@Rusino
Rusino marked this pull request as ready for review October 16, 2026 14:35

@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 implements support for TextHeightBehavior in the web engine's text layout, allowing height multipliers to be stripped from the first line's ascent and the last line's descent. It introduces raw font metrics to TextLine, refactors line finalization, and adds comprehensive tests. The review feedback suggests adhering to the Flutter style guide by using triple-slash documentation comments for private members, and recommends copying the _hasLineMetrics flag when inheriting metrics to avoid potential state inconsistency.

… applyHeightToLastDescent

Track unscaled line ascent and descent on TextLine. The wrapper tells addLine whether the line being added is the last one (end of text, trailing newline, or the maxLines limit), while the first line is identified via lines.isEmpty. TextLayout._finishLine reverts the first line's ascent and the last line's descent to the raw metrics before computing the line advance, so the wrapper accumulates final line heights in a single pass.

Assisted-by: Gemini 4 Argon (Medium)
@Rusino
Rusino force-pushed the webparagraph_text_height_behavior branch from b75702b to 25e5e08 Compare October 16, 2026 14:45
…tHeightBehavior

Position PlaceholderBlocks in _finishLine after applyTextHeightBehavior so first-line placeholders align against the reverted ascent, and bound background rects by raw ascent/descent when TextHeightBehavior disables height multipliers on the first or last line. Also set line.lastLine directly in _finishLine and add unit tests covering placeholder positioning and background rects.

Assisted-by: Gemini 4 Argon (High)
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
mdebbar
mdebbar previously approved these changes Oct 19, 2026
@Rusino
Rusino requested a review from mdebbar October 19, 2026 18:46
@Rusino Rusino self-assigned this Oct 19, 2026
@mdebbar mdebbar added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
@Rusino
Rusino enabled auto-merge October 20, 2026 16:21
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
@auto-submit

auto-submit Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/193773, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@mdebbar mdebbar added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 21, 2026
@mdebbar
mdebbar disabled auto-merge October 21, 2026 15:41
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 21, 2026
@auto-submit

auto-submit Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/193773, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@Rusino
Rusino added this pull request to the merge queue Oct 22, 2026
Merged via the queue into flutter:master with commit f1586fc Oct 22, 2026
18 checks passed
@Rusino
Rusino deleted the webparagraph_text_height_behavior branch October 22, 2026 20:43
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
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
… applyHeightToLastDescent (flutter#193773)

Honors `TextHeightBehavior.applyHeightToFirstAscent` and
`applyHeightToLastDescent` when computing `WebParagraph` line metrics
(for non-empty paragraphs). Part of flutter#193731.

> [!NOTE]
> Split out from flutter#193403 to keep each change small and focused.

### Problem
Both `applyHeightToFirstAscent` and `applyHeightToLastDescent` were
ignored: the first line always kept its height-scaled ascent and the
last line always kept its height-scaled descent.

### Fix
- `TextLine.rawFontBoundingBoxAscent` / `rawFontBoundingBoxDescent`:
accumulate unscaled font metrics alongside height-scaled metrics in
`TextLine.updateBoundingBox` (and copy them in
`TextLine.inheritMetricsFrom`).
- **Identifying the first and last lines**:
- `isFirstLine` is determined in `TextLayout._finishLine` via
`lines.isEmpty` before the line is appended.
- `isLastLine` is passed from `_LineBuilder.build` (`wrapper.dart`) and
stored on `line.lastLine` in `_finishLine`: a content line is the last
line when it hits the `maxLines` limit or reaches the artificial EOF
cluster without a trailing newline; when a trailing `\n` is present (and
`maxLines` is not yet reached), the synthetic empty line added after
`\n` is marked `isLastLine: true`.
- **Single-pass metric adjustment (`TextLayout._finishLine` ->
`TextLine.applyTextHeightBehavior`)**: before computing `line.advance`,
`isFirstLine && !applyHeightToFirstAscent` sets `usesRawAscent = true`
and reverts `fontBoundingBoxAscent` to `rawFontBoundingBoxAscent`, and
`isLastLine && !applyHeightToLastDescent` sets `usesRawDescent = true`
and reverts `fontBoundingBoxDescent` to `rawFontBoundingBoxDescent`.
Because this happens before `line.advance` is returned to the wrapper,
line tops and paragraph height are accumulated in a single pass.
- **Placeholders & backgrounds**: `PlaceholderBlock.positionInLine`
positions placeholders after `applyTextHeightBehavior` so first-line
placeholders align against the reverted ascent, and
`WebParagraphPainter._paintAllBlocks` bounds background rects by raw
ascent/descent when `usesRawAscent`/`usesRawDescent` are set.
- **Trailing newline**: the synthetic empty line created after a
trailing `\n` inherits the preceding line's scaled and raw metrics via
`inheritMetricsFrom`, so the synthetic last line drops its scaled
descent while the preceding text line keeps its scaled descent —
matching `SkParagraph`.

### Tests
- `paragraph_line_height_test.dart` (+6 tests):
- `TextHeightBehavior reverts a single line to the unscaled font
metrics`
- `TextHeightBehavior affects only the first line ascent and the last
line descent`
- `TextHeightBehavior treats the empty line after a trailing newline as
the last line`
- `TextHeightBehavior applies applyHeightToLastDescent to the maxLines
limit line`
- `TextHeightBehavior positions first-line placeholders against the
reverted ascent`
- `TextHeightBehavior bounds background rects by reverted first ascent
and last descent`
- **WebParagraph corpus (`preferWebParagraph` + `forceTestFonts` vs
`origin/master` `2e53c3af41a`)**:
- Part 1 (`web_ui` `test/webparagraph/*` + `test/ui/*`): **+6 new tests
passing (`207` vs `201`), 0 regressions**.
- Part 2 (`packages/flutter`): **+2 tests fixed
(`test/painting/text_painter_test.dart: TextHeightBehavior with strut on
empty paragraph` and `test/painting/text_painter_test.dart: TextPainter
widget span`), 0 regressions**.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] 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]>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants