Repository navigation
[web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional - #193736
Conversation
…ult to proportional
cc98644 to
786ba7d
Compare
|
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. |
There was a problem hiding this comment.
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.
…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
|
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 Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
Test that a child TextStyle's leadingDistribution takes precedence over the paragraph's TextHeightBehavior.leadingDistribution. Assisted-by: Gemini 3.8 Flash (High)
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
…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
…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
…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]>
…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
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.leadingDistributionwas ignored.WebParagraphStylenever passedtextHeightBehavior.leadingDistributionto the rootWebTextStyle, andRootStyleNode._leadingDistributionalways returnednull, so the value could not reachLineBlock. Only an explicitTextStyle(leadingDistribution: …)had any effect.even.LineBlocktreatednullthe same asTextLeadingDistribution.even(half-leading). SkParagraph (and therefore the native engine and CanvasKit) default toproportional:TextStyle::fHalfLeadingisfalseunlessleadingDistribution == evenis set, anddart:ui'sTextHeightBehavior.leadingDistributiondefaults toTextLeadingDistribution.proportional.fontBoundingBoxDescentto0.0.TextLineinitializedfontBoundingBoxAscentandfontBoundingBoxDescentto0.0and accumulated every block withmath.max(0.0, ...). WithTextLeadingDistribution.evenand a smallheightmultiplier (whereheight * fontSize < rawAscent + rawDescent),extraLeading / 2is negative andmultipliedFontBoundingBoxDescentcan become negative;math.max(0.0, ...)clamped it to0.0, inflating the line height aboveheight * fontSize.Fix
WebParagraphStylepropagatestextHeightBehavior?.leadingDistributioninto the rootWebTextStyle;RootStyleNode._leadingDistributionreturnsstyle.leadingDistribution. ChildTextStyles still override it throughChildStyleNodeas before.LineBlock:nullnow falls through to theproportionalbranch (ascent and descent are scaled byheight * fontSize / (ascent + descent)),evenkeeps the half-leading behavior.TextLine.updateBoundingBox: track_hasLineMetricsso the first block on the line initializesfontBoundingBoxAscentandfontBoundingBoxDescentdirectly (preserving a negativedescent), while subsequent blocks takemath.max.Behavior change
For any paragraph with a
heightmultiplier and no explicitleadingDistribution, 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 sameParagraphStyle; WebParagraph is not the default renderer yet, so no app-facing change on stable.Tests
paragraph_line_height_test.dart:Height multiplier scales ascent and descent proportionally by default— checks the scaled line keepsheight * fontSize, that ascent/descent scale by the same factor by default, thatTextHeightBehavior(leadingDistribution: even)adds equal leading on both sides, and that the two differ. Fails onmaster(default wasevenand the THB value was ignored).Half-leading with a small height multiplier preserves negative descent— checks thatheight: 0.1withTextLeadingDistribution.evenkeeps a negativedescentand total height0.1 * fontSize.webparagraph/*andui/paragraph_builder_test.dartsuites pass.preferWebParagraph+forceTestFontsvsorigin/masterb7315d76579e): 0 fixed standalone, +2 new tests passing, 0 regressions (base of the [web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional #193736 -> [web_paragraph] Honor TextHeightBehavior.applyHeightToFirstAscent and applyHeightToLastDescent #193773 -> [web_paragraph] Apply StrutStyle to line metrics and placeholders #193774 -> [web_paragraph] Compute empty paragraph line metrics with height, TextHeightBehavior, and StrutStyle #193403 stack that fixes 34 corpus tests; frameworkTextPainter line-heighttests intest/painting/text_painter_test.dartalso callgetBoxesForSelectionwithBoxHeightStyle.tightand are unlocked when combined with [web_paragraph] Fix placeholder positioning and BoxHeightStyle.tight boxes #193734).Pre-launch Checklist
///).