Skip to content

[web_paragraph] Companion fixes and tests for soft hyphens (#195366) - #195377

Closed
Rusino wants to merge 4 commits into
flutter:masterfrom
Rusino:companion-pr-195366
Closed

Rusino wants to merge 4 commits into
flutter:masterfrom
Rusino:companion-pr-195366

Conversation

@Rusino

@Rusino Rusino commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Companion draft PR stacked on top of #195366 (8f0ff938c0b8337ccb5580e584526aa01c3773ab) by @dbebawy.

Clean diff of just the 2 companion commits on top of 8f0ff938c0b (without #195366's own commits mixed into Files changed):
8f0ff93...269f171

Commit Structure (stacked on 8f0ff938c0b)

  1. Commit 1 (e50f01a9c85) — [web_paragraph] Add failing soft hyphen tests:
    Adds 9 contract tests in paragraph_edge_cases_test.dart and paragraph_soft_hyphen_test.dart that fail on [web_paragraph] Render soft hyphens #195366 (+73 -7 in felt test).
  2. Commit 2 (269f171b2c4) — [web_paragraph] Fix soft hyphen edge cases:
    Fixes the 5 underlying edge cases in layout.dart and wrapper.dart (all 80 tests in both files pass: +80 -0).

Feel free to cherry-pick both commits, fold the changes into #195366, or take only the parts you want — I will close this draft PR once #195366 is updated.

dbebawy and others added 4 commits October 26, 2026 17:06
Chrome gives some invisible characters, such as a soft hyphen (U+00AD)
or a zero width space (U+200B), no text cluster. Their code units were
left mapped to cluster 0, so a text range that included one could map
to a cluster range that went backwards. Laying out a paragraph that
ends with one of these characters failed the assertion in
_TextClusterMapping.toTextRange.

Map each of those code units to the cluster before it, or to the first
cluster at the start of the text. A cluster range that starts at such a
code unit then starts at the next cluster, so a span that starts with
one covers only its own clusters.
Draws a hyphen at the end of a line that breaks at a soft hyphen
(U+00AD), unless ParagraphStyle.hyphens is Hyphens.hidden. This matches
SkParagraph, which CanvasKit, Skwasm and the native engine use:

* The hyphen is drawn only where the line breaks at the soft hyphen:
  not at the end of the text, before a space or newline, or on a line
  with an ellipsis.
* A soft hyphen break only fits if the hyphen fits too. Otherwise the
  line breaks at the last earlier break that fits, or the hyphen
  overflows if there is none.
* minIntrinsicWidth and longestLine include the hyphen;
  maxIntrinsicWidth and LineMetrics.width do not.
* The hyphen takes the soft hyphen's style, is shaped in the direction
  of the text it ends, and goes at the end of the line in the
  paragraph's direction.
* Selection boxes, glyph info and hit testing for the soft hyphen use
  the drawn hyphen.

Fixes flutter#193506.
Add contract tests for clusterless characters (U+00AD, U+200B, U+200F)
at paragraph and line starts, in standalone styled spans, and before
newlines, as well as soft hyphens immediately after whitespace and
upstream TextAffinity when hit-testing a drawn soft hyphen.

Assisted-by: Gemini 4 Argon (Medium)
Fix five edge cases in cluster mapping, line layout, and soft-hyphen
wrapping:

1. In _TextClusterMapping.toClusterRange, decrement endClusterIndex when
   _clusters[endClusterIndex - 1].start >= end so leading clusterless
   code units at offset 0 map to an empty ClusterRange(0, 0) instead of
   ClusterRange(0, 1).
2. In TextLayout.extractBidiRuns and TextLayout.addLine, skip empty
   clusterRange and bidiLineSpanRange slices produced by standalone
   clusterless characters (such as U+200F, U+00AD, or U+200B).
3. In TextLayout.addLine, start contentTextRange and allTextRange at 0
   for the first line and at lines.last.hardLineBreakRange.end for
   subsequent lines so leading clusterless characters do not leave gaps
   in WebParagraph.getLineNumberAt.
4. In _LineBuilder.markSoftLineBreak and consumePendingText, ignore
   softHyphen when !hasPendingText so a soft hyphen immediately after
   whitespace breaks at the space without reserving hyphen width or
   inflating minIntrinsicWidth.
5. In TextLayout.getPositionForOffset, return TextAffinity.upstream when
   hit-testing a SoftHyphenBlock so line-boundary and closest-glyph
   queries stay on the hyphenated line.

Without this fix, 9 tests in paragraph_edge_cases_test.dart and
paragraph_soft_hyphen_test.dart fail (+73 -7 in felt test).

Assisted-by: Gemini 3.8 Flash (router)
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team labels Oct 26, 2026
@Rusino Rusino mentioned this pull request Oct 26, 2026
10 of 11 tasks
@Rusino Rusino closed this Oct 27, 2026
dbebawy added a commit to dbebawy/flutter that referenced this pull request Nov 2, 2026
…r#195384)

Chrome gives some invisible characters, such as a soft hyphen (U+00AD),
a zero width space (U+200B) or a right-to-left mark (U+200F), no text
cluster. WebParagraph left their code units mapped to cluster 0, so a
text range that includes one can map to the wrong clusters. On master:

* Laying out a paragraph that ends with a soft hyphen or zero width
space fails the assertion in `_TextClusterMapping.toTextRange`.
* Laying out a paragraph whose only text is one of these characters
throws a `StateError`.
* Wrapping text where a space is followed by one of these characters,
such as `'ab \u200F\u200Fcd'`, fails an assertion in
`TextWrapper.breakLines`.
* `getLineNumberAt` fails an assertion for a line that starts with one
of these characters.
* A right-to-left mark at the start of an LTR paragraph makes the first
letter count twice in the line width.
* A styled span that contains only one of these characters adds an empty
box to `getBoxesForRange`.

This maps each of those code units to the cluster before it, or to the
first cluster at the start of the text, so a range that starts or ends
at one of them does not take a cluster next to it. Bidi regions and span
slices with no clusters add no bidi run and no block to the line, and a
line's text range starts where the previous line ended and ends where
the next line starts. A paragraph with no clusters is laid out like
empty text, and the line breaker also finds a soft line break that lands
on one of these characters.

This was the first commit of flutter#195366 (soft hyphens in WebParagraph, for
flutter#193506). @Rusino found three of these cases while reviewing that PR,
wrote the fixes and tests in flutter#195377, and suggested landing this part on
its own first. Her two commits from flutter#195386 add the rest: paragraphs
with only these characters, and line breaks next to them.

## Testing

`paragraph_edge_cases_test.dart` has a new group for these characters.
Eleven of its tests fail on master. The two tests for a soft line break
before a direction mark after a zero width space pass on master with
Chrome 145 on my machine, so they may depend on the Chrome version.

I ran the `chrome-dart2js-webparagraph-ui` suite locally on macOS with
`felt test --gcs-prod`. Everything passes except "Paragraph with
different locales for the same language" and "paint overflows" in
`paragraph_test.dart`, which fail the same way on master on my machine.

## 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].
- [ ] 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.
- [ ] 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/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Julia Lavrova <[email protected]>
gmackall pushed a commit to gmackall/flutter that referenced this pull request Nov 3, 2026
…r#195384)

Chrome gives some invisible characters, such as a soft hyphen (U+00AD),
a zero width space (U+200B) or a right-to-left mark (U+200F), no text
cluster. WebParagraph left their code units mapped to cluster 0, so a
text range that includes one can map to the wrong clusters. On master:

* Laying out a paragraph that ends with a soft hyphen or zero width
space fails the assertion in `_TextClusterMapping.toTextRange`.
* Laying out a paragraph whose only text is one of these characters
throws a `StateError`.
* Wrapping text where a space is followed by one of these characters,
such as `'ab \u200F\u200Fcd'`, fails an assertion in
`TextWrapper.breakLines`.
* `getLineNumberAt` fails an assertion for a line that starts with one
of these characters.
* A right-to-left mark at the start of an LTR paragraph makes the first
letter count twice in the line width.
* A styled span that contains only one of these characters adds an empty
box to `getBoxesForRange`.

This maps each of those code units to the cluster before it, or to the
first cluster at the start of the text, so a range that starts or ends
at one of them does not take a cluster next to it. Bidi regions and span
slices with no clusters add no bidi run and no block to the line, and a
line's text range starts where the previous line ended and ends where
the next line starts. A paragraph with no clusters is laid out like
empty text, and the line breaker also finds a soft line break that lands
on one of these characters.

This was the first commit of flutter#195366 (soft hyphens in WebParagraph, for
flutter#193506). @Rusino found three of these cases while reviewing that PR,
wrote the fixes and tests in flutter#195377, and suggested landing this part on
its own first. Her two commits from flutter#195386 add the rest: paragraphs
with only these characters, and line breaks next to them.

## Testing

`paragraph_edge_cases_test.dart` has a new group for these characters.
Eleven of its tests fail on master. The two tests for a soft line break
before a direction mark after a zero width space pass on master with
Chrome 145 on my machine, so they may depend on the Chrome version.

I ran the `chrome-dart2js-webparagraph-ui` suite locally on macOS with
`felt test --gcs-prod`. Everything passes except "Paragraph with
different locales for the same language" and "paint overflows" in
`paragraph_test.dart`, which fail the same way on master on my machine.

## 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].
- [ ] 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.
- [ ] 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/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Julia Lavrova <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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