Skip to content

Support soft hyphen (U+00AD) rendering with a Hyphens API - #185152

Merged
auto-submit[bot] merged 12 commits into
flutter:masterfrom
dbebawy:soft-hyphen-enable-18443
Oct 3, 2026
Merged

auto-submit[bot] merged 12 commits into
flutter:masterfrom
dbebawy:soft-hyphen-enable-18443

Conversation

@dbebawy

@dbebawy dbebawy commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds visible soft hyphen (U+00AD) rendering at line breaks, with a new Hyphens API to opt out. Fixes #18443 on native platforms; web rendering is tracked in #193506.

A line break that falls on a soft hyphen now renders a visible - by default. A new Hyphens { manual, hidden } enum controls this:

  • Hyphens.manual (default): render a hyphen at the break.
  • Hyphens.hidden: don't render the hyphen. The soft hyphen is still a break opportunity.

hyphens is a parameter on Text / RichText, TextPainter, RenderParagraph, TextStyle.getParagraphStyle and dart:ui's ParagraphStyle, following textAlign / maxLines. It is not a TextStyle field. Customizing the hyphen string is deferred to #189617.

Design doc: flutter.dev/go/soft-hyphens (tracking issue #185154).

Background

Flutter already breaks lines at U+00AD but never draws the hyphen, so it behaves like a zero-width space. This change doesn't add any new break opportunities or change where lines break, but the rendered hyphen makes its line wider, so it can change Paragraph.longestLine and the size of text laid out with TextWidthBasis.longestLine.

The Skia side landed in 98daf58d (Skia CL 1208837), behind ParagraphStyle::setRenderSoftHyphens(bool), which defaults to false for other Skia clients.

Changes

Engine

  • txt::ParagraphStyle gains a render_soft_hyphens flag (default true), which ParagraphBuilderSkia::TxtToSkia forwards to setRenderSoftHyphens. The Impeller interop toolkit builds txt::ParagraphStyle too, so it also gets the new default.
  • paragraph_builder.cc decodes ParagraphStyle.hyphens from the FFI buffer (mask bit 13, slot 7).
  • dart:ui adds the public Hyphens enum and ParagraphStyle.hyphens. The web dart:ui accepts and stores the value; web rendering is [web] Render soft hyphens (Hyphens API) on WebParagraph #193506.

Framework

  • Hyphens is re-exported from dart:ui via painting/basic_types.dart.
  • hyphens is threaded through TextStyle.getParagraphStyle → TextPainter → RenderParagraph → RichText → Text / Text.rich, including the selectable-text path.

Breaking change

Known limitations

  • The hyphen is appended after line breaking, so a line can exceed maxWidth by the hyphen's advance.
  • The rendered hyphen isn't included in selection highlight rects (Rendered soft-hyphen glyph is excluded from selection highlight (skparagraph getRectsForRange skips hyphens) #189234).
  • EditableText (and so TextField) and SelectableText don't take a hyphens parameter yet, so they always render the hyphen.
  • Skia also draws the hyphen when a line ends in U+00AD without breaking there (text ending in U+00AD, or U+00AD right before a space or newline). The Skia fix is in review (CL 1378356) and will come in with a Skia roll once it lands.

Test plan

  • paragraph_builder_skia_tests.cc::RenderSoftHyphensEnabled: render_soft_hyphens maps onto Skia in both states
  • testing/dart/text_test.dart: ParagraphStyle.hyphens encodes and round-trips in toString
  • testing/dart/paragraph_test.dart: ParagraphStyle.hyphens reaches the text layout (a line that breaks at a soft hyphen is wider with manual than with hidden, and unset matches manual), which covers decoding the setting in the engine
  • test/widgets/rich_text_test.dart: RichText exposes hyphens via debugFillProperties
  • test/widgets/text_test.dart: Text forwards hyphens to RenderParagraph (incl. selectable path); default resolves to Hyphens.manual
  • test/rendering/paragraph_test.dart: RenderParagraph.hyphens setter triggers relayout
  • test/painting/text_painter_test.dart: TextPainter.hyphens setter; the same width check through TextPainter, and changing hyphens after layout changes the width
  • Google internal testing
  • Manual smoke: Text('inter\u00ADnational') in a narrow container shows inter- / national

@github-actions github-actions Bot added the engine flutter/engine related. See also e: labels. label Apr 16, 2026
@dbebawy

dbebawy commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

@LongCatIsLooong — this is the draft PR per your suggestion in #18443. Would appreciate it if you could kick off Google testing to surface any regressions in internal apps.

The change is intentionally minimal (one-line engine opt-in + gtest). A design doc covering this approach vs. exposing a Dart-level hyphens API (per @chunhtai's suggestion) is in progress and will be linked here once drafted.

@google-cla

This comment was marked as resolved.

@dbebawy

dbebawy commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Design doc is now posted: https://flutter.dev/go/soft-hyphens (redirect PR: flutter/website#13282; direct: https://docs.google.com/document/d/1w_d53wb-RMuwXmXIPq67JrZYG97XF1mDaKEoKBDt8-E)

Tracking issue: #185154

This PR is Stage 1 of the two-stage rollout described in the doc. Stage 2 (Dart Hyphens API) will be a separate PR once the design doc settles.

@LongCatIsLooong

Copy link
Copy Markdown
Contributor

I put on the CICD label. The tests should finish in an hour or two, and then I'll start google testing.

@LongCatIsLooong

Copy link
Copy Markdown
Contributor

../../../flutter/txt/src/skia/paragraph_builder_skia.cc:142:8: error: no member named 'setRenderSoftHyphens' in 'skia::textlayout::ParagraphStyle'
142 | skia.setRenderSoftHyphens(true);
| ~~~~ ^

The skia version with your reland commit should have already been rolled into flutter: #185131

@dbebawy
dbebawy force-pushed the soft-hyphen-enable-18443 branch from ae3aa44 to fa25665 Compare April 16, 2026 17:27
@dbebawy

dbebawy commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

@LongCatIsLooong good catch — thanks. My branch base (commit 6154f75) didn't include the Skia roll in #185131, so DEPS was still pinning the pre-fix Skia SHA. I've rebased onto current upstream/master (now pinning 391cdbe3ffe9, which is ahead of the reland commit 98daf58d4c), and force-pushed. The build should go through on the next CI run.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 16, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 16, 2026
@justinmc

Copy link
Copy Markdown
Contributor

I'm really curious if this will pass Google and customer tests or not.

If so and we consider merging this, we should be certain that the phase 2 Dart solution does not need to touch Material/Cupertino, so it's not blocked by the decoupling code freeze, just in case someone shows up desperately needing to opt out.

@LongCatIsLooong

Copy link
Copy Markdown
Contributor

it looks like the test is failing to compile:

../../../flutter/txt/tests/paragraph_builder_skia_tests.cc:39:23: error: 'TxtToSkia' is a private member of 'txt::ParagraphBuilderSkia'
39 | ASSERT_TRUE(builder.TxtToSkia(style).getRenderSoftHyphens());
| ~~~~~~~~^~~~~~~~~
../../../flutter/third_party/googletest/googletest/include/gtest/gtest.h:1838:50: note: expanded from macro 'ASSERT_TRUE'
1838 | #define ASSERT_TRUE(condition) GTEST_ASSERT_TRUE(condition)
| ^~~~~~~~~
../../../flutter/third_party/googletest/googletest/include/gtest/gtest.h:1821:23: note: expanded from macro 'GTEST_ASSERT_TRUE'
1821 | GTEST_TEST_BOOLEAN_(condition, #condition, false, true, GTEST_FATAL_FAILURE_)
| ^~~~~~~~~
../../../flutter/third_party/googletest/googletest/include/gtest/internal/gtest-internal.h:1451:38: note: expanded from macro 'GTEST_TEST_BOOLEAN_'
1451 | ::testing::AssertionResult(expression))
| ^~~~~~~~~~
../../../flutter/txt/src/skia/paragraph_builder_skia.h:41:36: note: declared private here
41 | skia::textlayout::ParagraphStyle TxtToSkia(const ParagraphStyle& txt);

@dbebawy
dbebawy force-pushed the soft-hyphen-enable-18443 branch from fa25665 to 2571b41 Compare April 16, 2026 20:08
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 16, 2026
@dbebawy

dbebawy commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a fix (2571b41e646). TxtToSkia is private, so the existing ParagraphStrutStyle test friends the generated gtest class in the header; I added the matching friend declaration for the new RenderSoftHyphensEnabled test:

friend class SkiaParagraphBuilderTests_ParagraphStrutStyle_Test;
friend class SkiaParagraphBuilderTests_RenderSoftHyphensEnabled_Test;

@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 16, 2026
@Renzo-Olivares
Renzo-Olivares self-requested a review April 16, 2026 20:15
@LongCatIsLooong

Copy link
Copy Markdown
Contributor

It looks like everything (include Google testing) passed. 🎉

sfshaza2 pushed a commit to flutter/website that referenced this pull request Apr 17, 2026
Adds a redirect for flutter.dev/go/soft-hyphens pointing to the design
doc for soft hyphen (U+00AD) rendering support in Flutter.

Design doc:
https://docs.google.com/document/d/1w_d53wb-RMuwXmXIPq67JrZYG97XF1mDaKEoKBDt8-E
Tracking issue: flutter/flutter#18443
Implementation PR: flutter/flutter#185152
@LongCatIsLooong

Copy link
Copy Markdown
Contributor

Hi @dbebawy are you still working on this? In case you want to include this change in the next stable release, 3.47 will be cut soon (in about 2 weeks).

@dbebawy

dbebawy commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Hi @LongCatIsLooong — yes, still on it, and I'd like to land this in 3.47 (Q3 cutoff is July 7, ~2 weeks out).

Quick status so we're aligned. This PR currently only carries the engine-side opt-in (setRenderSoftHyphens(true) in ParagraphBuilderSkia). Per the 4/23 design update, the plan is to expand it to the combined scope so soft hyphens ship with an opt-out from day one:

  • Hyphens { manual, none } enum (no auto)
  • exposed as a direct widget parameter on Text/RichText plus ui.ParagraphStyle (not TextStyle)
  • hyphenCharacter deferred (Skia shapeEllipsis hardcodes LTR bidi)

The only thing blocking me from writing that code is design sign-off — the doc has had no reviewer feedback since that 4/23 reconciliation, and I'd rather not implement the full API and then find the shape needs to change in review on a tight clock.

Could I get a gut-check from you (and/or @chunhtai @justinmc) on the reconciled design this week? If the enum + widget-param shape looks right, I can have the combined PR up within a few days, leaving runway for review before the cutoff. Happy to hop on a short sync if that's faster — or to just proceed against the doc as written and iterate in code review if you'd prefer that.

Thanks!

@dbebawy
dbebawy force-pushed the soft-hyphen-enable-18443 branch from 2571b41 to 126e8d9 Compare June 26, 2026 13:09
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Jun 26, 2026
@github-actions github-actions Bot added the a: text input Entering text in a text field or keyboard related problems label Jun 26, 2026
LongCatIsLooong
LongCatIsLooong previously approved these changes Oct 1, 2026
String? ellipsis,
int? maxLines,
TextHeightBehavior? textHeightBehavior,
Hyphens? hyphens,

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.

I think that if we land this PR with this change, then the flutter/packages CI will fail and it will have to be rolled back. The CI in flutter/packages runs against both master and stable branches of flutter/flutter, so any breaking change between the two will cause it to fail.

The reason why flutter/packages will fail is because material_ui has the corresponding test where this change was added (see our previous discussion earlier in this PR in https://github.com/flutter/flutter/pull/185152/changes#r3509107642).

The only way to land changes like this now is to make the change in a two-step way that is not breaking. For example, we could introduce a parallel getParagraphStyleWithHyphens and deprecate getParagraphStyle. We should probably think hard about what we want this API to look like given these constraints, though.

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.

I was second guessing myself and thinking maybe this isn't a breaking change because it's an optional parameter. But no, the problem is that it is implemented, not that it is called, in the Material tests.

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.

Ok I'm changing my mind. I now see that @LongCatIsLooong has landed flutter/packages#12728, which should be compatible with flutter/flutter both before and after this PR. The test below can also stay as it is, since it only runs off of the TextStyle that's in its version of flutter/flutter.

That said, we should cross our fingers and be ready to revert if it fails on the autoroller.

justinmc
justinmc previously approved these changes Oct 2, 2026
String? ellipsis,
int? maxLines,
TextHeightBehavior? textHeightBehavior,
Hyphens? hyphens,

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.

Ok I'm changing my mind. I now see that @LongCatIsLooong has landed flutter/packages#12728, which should be compatible with flutter/flutter both before and after this PR. The test below can also stay as it is, since it only runs off of the TextStyle that's in its version of flutter/flutter.

That said, we should cross our fingers and be ready to revert if it fails on the autoroller.

…ng order

Applies mdebbar's review suggestions: document that the hyphen glyph
isn't rendered on the web yet, and print hyphens after locale in the
CanvasKit and Skwasm ParagraphStyle toString, as native does.
@dbebawy

dbebawy commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Applied @mdebbar's suggestions. The push reset the approvals and the CICD label, so could you all take another look, and could someone re-add CICD and add autosubmit once it's green?

@justinmc

justinmc commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@dbebawy Congrats on finishing this and thank your for your perseverance! I think this feature is really worth it.

@dbebawy dbebawy mentioned this pull request Oct 5, 2026
11 tasks
@dbebawy

dbebawy commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@jtmcdole heads-up on a merge queue problem with Tree_analyze. When a PR adds dart:ui API and uses it in the framework, Tree_analyze passes on the PR but fails in the merge queue, because the merge queue run analyzes against the parent commit's engine. It isn't a required check, so nothing was blocked, but it marks the merge group red, along with the groups queued behind it since they include the same change.

Examples

What happens in the merge queue run (from 38334834175)

  1. has-engine-changes reports a change, and wait_for_tests waits for Linux linux_host_engine and Linux linux_android_debug_engine on the merge group commit b535b37. Both succeed after about 10 minutes.
  2. Both Redirect engine.version steps are skipped, because they only run when github.event_name == 'pull_request' (action.yml#L40-L50).
  3. So the engine version comes from content_aware_hash.sh. The job checks out the merge group commit as a detached HEAD with full history (tree-analyze.yml#L18-L24), and there's no LUCI_CONTEXT on GitHub Actions. That means the gh-readonly-queue/master/pr-* exception doesn't match, and the script hashes the merge base with master instead (content_aware_hash.sh#L41-L66).
  4. It downloads engine 2230fe9b7d48…, which is the content hash of the parent commit 2b089ff, not of the merge group commit:
$ git ls-tree 2b089ff2b42 -- DEPS engine bin/internal/release-candidate-branch.version | git hash-object --stdin
2230fe9b7d48a27900b2916a56f26d5d4209de64
$ git ls-tree b535b37972a -- DEPS engine bin/internal/release-candidate-branch.version | git hash-object --stdin
6c5179ef87f50bf0c9cdbaabc3ac4d663a8a4a75

For comparison, the PR run sets FLUTTER_PREBUILT_ENGINE_VERSION to the PR head (7cfe883) and finds no issues.

Two possible fixes: also redirect for merge_group (the wait step already waits on github.sha at L36), if merge queue engine builds publish to the same place as presubmit ones, or make the content hash use HEAD in this job so the merge queue exception applies. Artifacts for 6c5179ef87f5… are in flutter_infra_release now, though I can't tell whether they were there yet when that queue ran.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems CICD Run CI/CD engine flutter/engine related. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. override code freeze Override an active code freeze. p: material_ui material_ui package in flutter/packages 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.

Support soft hyphenation (line breaks at U+00AD plus rendering a hyphen at the end of the line)

10 participants