Skip to content

fix(ios): retain intrinsic width for explicit line breaks - #1017

Merged
eszlamczyk merged 3 commits into
mainfrom
fix/ios-hard-break-intrinsic-width
Oct 16, 2026
Merged

eszlamczyk merged 3 commits into
mainfrom
fix/ios-hard-break-intrinsic-width

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Collaborator

What/Why?

Short text such as one\n\ntwo in a content-sized view expands to maxWidth on iOS because measurement treats any second line fragment as a soft wrap. Content broken only by explicit newlines now keeps its tight width; soft-wrapped content still reports maxWidth so flexShrink cannot re-wrap it.

This carries @Yash-Singh1's #991 (bd6f647) unchanged as the first commit plus one follow-up. #991 comes from an organization-owned fork, which GitHub does not let maintainers push to, so the follow-up lands here. #991 stays open for the discussion there and closes when this merges; both commits keep their authorship.

Closes #990.

Follow-up commit on top of #991

usedRect.size.width is not the width the content needs: TextKit's used rect starts at the head indent and never includes a negative tailIndent. Lists, code blocks and blockquotes rely on both, so once the pin stopped applying to them they measured too narrow and re-wrapped when Yoga handed the width back as the frame. Single-line lists already had this on main; multi-line lists and every code block and blockquote were protected by the pin.

  • ENRMScanLineFragments walks the fragments once. It stops at the first soft wrap and reports maxWidth. Otherwise it returns a tight width that adds each line's head indent and negative tail indent to its used rect, which is direction and alignment independent.
  • Hard breaks are any newlineCharacterSet member at a fragment's end.
  • The scan exits once the width reaches maxWidth and bails out on fragments past a numberOfLines clamp.
  • ENRMFinalizeMeasuredTextSize reads the layout manager's own text storage and drops its text parameter.
  • Tests live in TextMeasurementTests.mm, named by feature like fix: size ordered list markers to their widest number #1015. Every case renders real markdown with non-zero code block and blockquote padding. Tight cases assert the width is below maxWidth and that relaying at it keeps the line count and height, which fails on the previous implementation for lists, code blocks and blockquotes.

Testing

  • Example app's iOS unit-test target on iPhone 17 / iOS 26.5: 6 tests green. clang-format clean.
  • Example chat screen: a bullet-list bubble hugs its widest item with the marker indent included and no re-wrap; a long line still fills the bubble's maxWidth.
  • Rendered through the parser and renderer at 300pt and relaid at the measured width, before the follow-up: - alpha beta\n- gamma went from 2 to 3 lines, 1. alpha beta\n2. gamma from 2 to 4, a two-line fenced block from 5 to 6, a blockquote from 4 to 5. After: unchanged.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android, unchanged by this iOS-only fix
  • Updated documentation/README if applicable, no public API change
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added, unit tests cover the measurement invariant instead

🤖 Generated with Claude Code

Yash-Singh1 and others added 2 commits October 14, 2026 15:14
…not re-wrap

Walk the line fragments once: stop at the first soft wrap (report maxWidth, as
before) and otherwise return a tight width that adds each line's head indent
and negative tail indent to its used rect. TextKit's usedRect leaves both out,
so lists, code blocks and blockquotes measured narrower than they need and
re-wrapped once Yoga handed the width back as the frame. Hard breaks are any
newlineCharacterSet member at a fragment's end, and the scan exits early once
the width reaches maxWidth. ENRMFinalizeMeasuredTextSize reads the layout
manager's own text storage and drops its text parameter.

Tests render real markdown through the parser and renderer and assert that
relaying at the measured width keeps the line count and height, which fails
on the previous implementation for lists, code blocks and blockquotes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment on lines +33 to +36
typedef struct {
BOOL hasSoftWrap;
CGFloat tightWidth;
} ENRMLineFragmentScan;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this struct is unnecessary. The logic assumed here is something like:

If there is a soft wrap we use maxWidth else we use MIN(maxWidth, calculatedLineWidth) I feel like it would be easy to remove this flag (and this struct at all and just return maxWidth in the early return paths.

The caller then becomes

CGFloat contentWidth = ENRMContentWidth(layoutManager, maxWidth);
if (contentWidth > 0) measuredWidth = contentWidth;

Soft wrap and reaching maxWidth both resolve to maxWidth, and tightWidth
was incomplete whenever hasSoftWrap was set, so the struct carried no
information the caller needed. Pass usedRect width as the fallback
instead of a 0 sentinel so a zero maxWidth still clamps to 0.
@eszlamczyk
eszlamczyk merged commit 01d6139 into main Oct 16, 2026
18 checks passed
@eszlamczyk
eszlamczyk deleted the fix/ios-hard-break-intrinsic-width branch October 16, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: explicit line breaks expand short text to maxWidth

3 participants