Repository navigation
[web_paragraph] Map code units without a cluster to a cluster - #195384
auto-submit[bot] merged 4 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the web text layout engine to properly handle clusterless characters, such as soft hyphens, zero-width spaces, and directional marks, by mapping them to adjacent clusters and preventing empty visual runs or disconnected line ranges. Corresponding edge-case tests are also added. The review feedback identifies a potential RangeError in _TextClusterMapping.toClusterRange when _clusters is empty (e.g., for paragraphs containing only clusterless characters) and suggests adding a guard clause along with a test case to cover this scenario.
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. A cluster range that ends at one at the start of the text ends before the first cluster. Bidi regions and span slices that contain only these characters have no clusters, so they add no bidi run and no block to the line. A line's text range starts where the previous line ended, or at 0, so a line that starts with one of these characters still contains it. Co-authored-by: Julia Lavrova <[email protected]>
Add contract tests for paragraphs containing only clusterless characters, soft line breaks on clusterless code units before directional marks, and clusterless characters following trailing spaces. Without the fix, all 6 new tests fail (3 StateError crashes on lines.first, 2 line count mismatches 1 vs 2, and 1 AssertionError at wrapper.dart:101). Assisted-by: Gemini Pro Next
Check allClusters.length == 1 in TextLayout.wrapText and lines.isEmpty in TextLayout._calculatePaintBounds so non-empty paragraphs containing only clusterless characters (such as U+00AD, U+200B, or U+200F) get empty-text metrics and Rect.zero paintBounds instead of crashing on lines.first. Scan codeUnitFlags from the previous cluster's end to the current cluster's start in TextWrapper._isSoftLineBreak so soft line breaks that land on unmapped clusterless code units before directional marks are not missed, and use hardlineTextRange.start in TextLayout.addLine so clusterless characters after trailing spaces are included in allTextRange. Without the fix, the 6 new tests in paragraph_edge_cases_test.dart fail (3 StateError crashes on lines.first, 2 line count mismatches 1 vs 2, and 1 AssertionError at wrapper.dart:101). Assisted-by: Gemini Pro Next
5512fe0 to
f50fa68
Compare
|
Thank you for the review. I rebased onto master after #194980. The only conflict was the comment in |
| // A soft break opportunity after U+200B (LB8) lands on the immediately following | ||
| // directional mark, which has no cluster of its own. Line wrapping must still detect the | ||
| // soft break before the next visible cluster. |
There was a problem hiding this comment.
On Chrome 145 these pass without the fix, so the break doesn't always land on the mark:
| // A soft break opportunity after U+200B (LB8) lands on the immediately following | |
| // directional mark, which has no cluster of its own. Line wrapping must still detect the | |
| // soft break before the next visible cluster. | |
| // Depending on the browser's line breaker, the soft break after U+200B (LB8) can land on | |
| // the following directional mark, which has no cluster of its own. Line wrapping must | |
| // still detect the soft break before the next visible cluster. |
There was a problem hiding this comment.
Thank you, that is clearer. Applied in 4cf9b62.
| // Use hardlineTextRange.start rather than whitespaceTextRange.end so unmapped clusterless | ||
| // characters following trailing spaces are included in this line's text range. |
There was a problem hiding this comment.
These characters are mapped now (to the cluster before them):
| // Use hardlineTextRange.start rather than whitespaceTextRange.end so unmapped clusterless | |
| // characters following trailing spaces are included in this line's text range. | |
| // Use hardlineTextRange.start rather than whitespaceTextRange.end so clusterless characters | |
| // following trailing spaces are included in this line's text range. |
There was a problem hiding this comment.
Thank you, that is clearer. Applied in 4cf9b62.
|
I applied both suggestions from @mdebbar, which only change comments. The push dismissed the approvals and the CICD label, so could one of you re-approve and add CICD and autosubmit when you have a chance? |
|
auto label is removed for flutter/flutter/195384, Failed to enqueue flutter/flutter/195384 with HTTP 400: Something went wrong while executing your query on 2026-11-02T16:09:08Z. Please include |
…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]>
Roll Flutter from fcfb4f203eae to 934720261be0 (38 revisions) flutter/flutter@fcfb4f2...9347202 2026-11-03 [email protected] Roll Skia from 5bceaa033999 to c733ad3e37cb (5 revisions) (flutter/flutter#195872) 2026-11-03 [email protected] Roll Skia from 34d2bf479385 to 5bceaa033999 (9 revisions) (flutter/flutter#195864) 2026-11-03 [email protected] fix: prefer pointer events over semantics tap for mouse clicks on web (flutter/flutter#191529) 2026-11-03 [email protected] [VPAT] Expose a semantics text field value while it is not being edited (flutter/flutter#195445) 2026-11-03 [email protected] [Impeller] Make a GL context current before constructing an AiksContext without a surface (flutter/flutter#195629) 2026-11-03 [email protected] [flutter_tools] Eliminate globals.dart from project.dart (flutter/flutter#194066) 2026-11-03 [email protected] [tool] Complete context runner elimination and migrate AppContext usages to ToolContext (flutter/flutter#190797) 2026-11-03 [email protected] [web] Unify font collections between CanvasKit and Skwasm (flutter/flutter#195539) 2026-11-03 [email protected] [Android] Replay predictive back state for retained engines (flutter/flutter#192286) 2026-11-03 [email protected] [vulkan]: Adds depth/stencil synchronization for subpasses (flutter/flutter#195689) 2026-11-03 [email protected] [web] Honor requested upscaling in instantiateImageCodecWithSize and ImageDescriptor.instantiateCodec (flutter/flutter#195836) 2026-11-03 [email protected] Roll Skia from 866b61ca92c9 to 34d2bf479385 (9 revisions) (flutter/flutter#195848) 2026-11-03 [email protected] Roll Fuchsia Test Scripts from xl1LWBuppzu-BnOzV... to B6zarykZp6PKWRueg... (flutter/flutter#195847) 2026-11-03 [email protected] Remove unused ffi (flutter/flutter#195710) 2026-11-02 [email protected] Revert: [Android] Let the raster thread use every core of a chip without little cores (flutter/flutter#195838) 2026-11-02 [email protected] Run 10 impeller iOS benchmarks as one devicelab task (flutter/flutter#195661) 2026-11-02 [email protected] Use named uniforms instead of setFloat in StretchEffect (flutter/flutter#195372) 2026-11-02 [email protected] [flutter_tools] Report when the browser disconnects during a web test run (flutter/flutter#192198) 2026-11-02 [email protected] fix: Resolve memory leak in image_list example (flutter/flutter#189526) 2026-11-02 [email protected] Only run the common local engine configurations in presubmit (flutter/flutter#195688) 2026-11-02 [email protected] Update gcloud_version to version [email protected] (flutter/flutter#195769) 2026-11-02 [email protected] [Impeller] Order the Vulkan copy-texture-to-texture layout transition after the copy (flutter/flutter#195814) 2026-11-02 [email protected] [Impeller] Order the Vulkan ResizeTexture layout transition after its blit (flutter/flutter#195721) 2026-11-02 [email protected] [web] Fix CanvasKit fragment shader uniforms being freed or mutated after recording (flutter/flutter#195718) 2026-11-02 [email protected] Roll Fuchsia Linux SDK from MqypI6RNyVZ_FLUE-... to T_-6-6X1lDMghhAvX... (flutter/flutter#195809) 2026-11-02 [email protected] [Android] Log why Impeller falls back to OpenGLES (flutter/flutter#194764) 2026-11-02 [email protected] Roll Skia from 3f35fb6e591b to 866b61ca92c9 (11 revisions) (flutter/flutter#195802) 2026-11-02 [email protected] Roll Skia from 3f35fb6e591b to 866b61ca92c9 (11 revisions) (flutter/flutter#195802) 2026-11-02 [email protected] Only run the common local engine configurations in presubmit (flutter/flutter#195688) 2026-11-02 [email protected] Balance tool_integration_tests subshards by expected duration (flutter/flutter#195628) 2026-11-02 [email protected] [web_paragraph] Map code units without a cluster to a cluster (flutter/flutter#195384) 2026-11-02 [email protected] [web] Hide synthetic autofill inputs from assistive technology (flutter/flutter#195618) 2026-11-02 [email protected] Roll Packages from 3742a4e to 3f34a6f (8 revisions) (flutter/flutter#195795) 2026-11-02 [email protected] [web] Unskip web tests that pass on CanvasKit and Skwasm: platform_view_test, route_notification_messages_test (flutter/flutter#195605) 2026-11-02 [email protected] Roll Skia from e5ad3868b8db to 3f35fb6e591b (1 revision) (flutter/flutter#195789) 2026-11-02 [email protected] Roll Skia from f5215e74aa16 to e5ad3868b8db (3 revisions) (flutter/flutter#195779) 2026-11-02 [email protected] Roll Skia from e43478a2f9f8 to f5215e74aa16 (1 revision) (flutter/flutter#195776) 2026-11-01 [email protected] Roll Fuchsia Linux SDK from kW0A0xxNkqf38ljm4... to MqypI6RNyVZ_FLUE-... (flutter/flutter#195768) 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 ...
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:
_TextClusterMapping.toTextRange.StateError.'ab \u200F\u200Fcd', fails an assertion inTextWrapper.breakLines.getLineNumberAtfails an assertion for a line that starts with one of these characters.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 #195366 (soft hyphens in WebParagraph, for #193506). @Rusino found three of these cases while reviewing that PR, wrote the fixes and tests in #195377, and suggested landing this part on its own first. Her two commits from #195386 add the rest: paragraphs with only these characters, and line breaks next to them.
Testing
paragraph_edge_cases_test.darthas 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-uisuite locally on macOS withfelt test --gcs-prod. Everything passes except "Paragraph with different locales for the same language" and "paint overflows" inparagraph_test.dart, which fail the same way on master on my machine.Pre-launch Checklist
///).