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 CanvasKit, Skwasm, and 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
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 1, 2026
@dbebawy

dbebawy commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @LongCatIsLooong! Synced to master, and CI and Google testing passed, including analyze_dart_ui. I've since pushed the smaller dart:ui test from your nit, which dropped the CICD label again. Could you re-add it, and also add override code freeze? The PR still touches test/material/theme_test.dart, but only to fix the in-repo _TextStyleProxy's getParagraphStyle override so it compiles. #12728 already removed material_ui's copy, so there's nothing to port.

@Renzo-Olivares I filed #193506 for the web side and pointed the web TODOs at it, so this PR can close #18443. I have a Skwasm follow-up ready for after this lands. For CanvasKit, the Skia change that exposes the setting is in review: https://skia-review.googlesource.com/c/skia/+/1378836.

While testing I also found that Skia draws the hyphen in a few spots where the line doesn't actually break at the soft hyphen, for example a soft hyphen right before a space. The fix is in review too, at https://skia-review.googlesource.com/c/skia/+/1378356, and will come in with a Skia roll once it lands.

@justinmc with the material_ui proxy gone this no longer breaks flutter/packages. Could you take another look when you get a chance?

mdebbar
mdebbar previously approved these changes Oct 1, 2026

@mdebbar mdebbar 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.

Web changes look good to me with some nits.

Comment on lines +1066 to +1068
/// Defaults to [Hyphens.manual], which renders a hyphen glyph at a line break
/// that falls on a soft hyphen. [Hyphens.hidden] suppresses the glyph; the line
/// still breaks at U+00AD regardless.

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.

Let's mention that this isn't supported on the web yet, so web developers aren't surprised:

Suggested change
/// Defaults to [Hyphens.manual], which renders a hyphen glyph at a line break
/// that falls on a soft hyphen. [Hyphens.hidden] suppresses the glyph; the line
/// still breaks at U+00AD regardless.
/// Defaults to [Hyphens.manual], which renders a hyphen glyph at a line break
/// that falls on a soft hyphen. [Hyphens.hidden] suppresses the glyph; the line
/// still breaks at U+00AD regardless.
///
/// Not yet supported on the web, where the hyphen glyph is never rendered.

Comment on lines 319 to 320
'hyphens: ${_hyphens ?? "unspecified"}, '
'locale: ${_locale ?? "unspecified"}'

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.

nit: native prints hyphens after locale. Let's keep the same order:

Suggested change
'hyphens: ${_hyphens ?? "unspecified"}, '
'locale: ${_locale ?? "unspecified"}'
'locale: ${_locale ?? "unspecified"}, '
'hyphens: ${_hyphens ?? "unspecified"}'

Comment on lines 911 to 912
'hyphens: ${_hyphens ?? "unspecified"}, '
'locale: ${_locale ?? "unspecified"}'

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.

Same here:

Suggested change
'hyphens: ${_hyphens ?? "unspecified"}, '
'locale: ${_locale ?? "unspecified"}'
'locale: ${_locale ?? "unspecified"}, '
'hyphens: ${_hyphens ?? "unspecified"}'

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.

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