Skip to content

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

Closed
Yash-Singh1 wants to merge 1 commit into
software-mansion:mainfrom
pingdotgg:fix/ios-hard-break-intrinsic-width
Closed

Yash-Singh1 wants to merge 1 commit into
software-mansion:mainfrom
pingdotgg:fix/ios-hard-break-intrinsic-width

Conversation

@Yash-Singh1

@Yash-Singh1 Yash-Singh1 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What/Why?

Short text such as one\ntwo expands to maxWidth on iOS because measurement treats every second line as a soft wrap. Detect explicit separators between TextKit line fragments so those lines retain their intrinsic width. Actual soft wrapping still reports the available width, including when it occurs after an explicit break.

Closes #990. This draft provides the proposed implementation for maintainer discussion requested by the contribution guide.

Testing

  • Six XCTest regression methods cover single-line text, LF/CR/CRLF/Unicode separators, trailing newlines, soft wrapping, mixed explicit/soft breaks, and emoji before a break.
  • Ran those methods against the actual before/after measurement implementation through macOS native TextKit. Before failed six explicit-break cases; after passed all cases. The harness uses AppKit instead of UIKit and is additional verification, not a claim that the iOS XCTest lane has run locally.
  • Built and ran the upstream React Native example on iOS 26.5 and Android 15. The explicit-break view measured 296.0 pt before and 31.7 pt after on iOS; Android remained 30.9 pt. Before/after iOS screenshots.
  • Targeted clang-format and whitespace checks passed.
  • The Maestro suite has not been run locally. The new XCTest file is discovered by the existing iOS unit-test target.

PR Checklist

  • Code compiles and runs on iOS through the consuming app
  • 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, focused XCTest regression coverage added instead

Generated by GPT 6.1 Sol using the Codex harness.

@Yash-Singh1

Yash-Singh1 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Note

Generated by GPT 6.1 Sol

Verified in the upstream React Native example on iOS 26.5. The explicit-break input one \ntwo measures 296.0 pt before and 31.7 pt after. The blue background shows the measured native view. The naturally wrapped paragraph below retains the available width.

Before uses upstream base 766fc0e; after uses this PR at bd6f647. Both captures use the same fixture and layout.

Before: 296.0 pt After: 31.7 pt
Before: an explicit line break incorrectly expands the native view to 296.0 pt After: the same text retains its intrinsic width of 31.7 pt

@Yash-Singh1
Yash-Singh1 marked this pull request as ready for review October 15, 2026 00:05
@hryhoriiK97

Copy link
Copy Markdown
Collaborator

Thanks, the detection is right and this is the correct place for it. I reviewed it against the measurement paths and the renderer and found one problem the previous pin was masking, so I put a follow-up commit on top of yours. GitHub refuses maintainer pushes to organization-owned forks, so the branch lives upstream as fix/ios-hard-break-intrinsic-width, your commit first, then 7f1dc89.

What I found

usedRect.size.width is not the width the content needs. TextKit's used rect starts at the head indent (so the indent only moves its origin) and never includes a negative tailIndent. Lists, code blocks and blockquotes all rely on those, so once the pin no longer applied to them they measured too narrow and re-wrapped when Yoga handed the width back as the frame. Rendered through the real parser and renderer at 300pt, then relaid at the measured width:

Markdown Measured Lines at 300 Lines at measured
- alpha beta\n- gamma 67.7 2 3
1. alpha beta\n2. gamma 67.7 2 4
fenced code, 2 lines 115.7 5 6
> quoted words here 121.3 4 5

Single-line lists already had this on main; multi-line lists and every code block and blockquote were protected by the pin. The view re-measures after layout and requests a height update, so it heals into a bubble that wraps a list item or code line that had room, rather than clipping.

What the follow-up changes

  • ENRMScanLineFragments walks the fragments once. It stops at the first soft wrap and reports maxWidth as before. Otherwise it returns a tight width that adds each line's head indent (first-line variant on paragraph starts) and negative tail indent to its used rect. That is direction and alignment independent, so RTL needs no branch.
  • Hard breaks are any newlineCharacterSet member at a fragment's end, read via characterRangeForGlyphRange:, which also covers NEL, VT and FF.
  • The scan exits early once the tight width reaches maxWidth, since the result is maxWidth either way, and bails out on fragments past a numberOfLines clamp.
  • ENRMFinalizeMeasuredTextSize reads the layout manager's own text storage and drops the text parameter, so the glyph indices and the string can't drift.
  • Tests moved to 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; that second assertion fails on the previous implementation for the rows above. Pinned cases cover soft wrap alone, after a paragraph, in a list and in a code block. The \r and \r\n cases are gone because md4c normalizes them to soft breaks before rendering; U+2028 and U+2029 survive and are still covered.

Verified

  • The example app's iOS unit-test target on iPhone 17 / iOS 26.5, 6 tests green, clang-format clean.
  • In the example chat screen a bullet-list bubble now hugs its widest item with the marker indent included and no re-wrap, and a long line still fills the bubble's maxWidth.

@hryhoriiK97

Copy link
Copy Markdown
Collaborator

Carried as #1017, which has your commit bd6f647 first and the follow-up above on top. GitHub does not allow maintainer pushes to organization-owned forks, so the branch lives upstream. We'll close this one when #1017 merges; both commits keep their authorship in the history. Thanks again for the fix and the write-up.

@Yash-Singh1

Copy link
Copy Markdown
Contributor Author

GitHub does not allow maintainer pushes to organization-owned forks, so the branch lives upstream.

Oops, sorry about that. I'll create PRs from my personal account next time.

@hryhoriiK97

Copy link
Copy Markdown
Collaborator

GitHub does not allow maintainer pushes to organization-owned forks, so the branch lives upstream.

Oops, sorry about that. I'll create PRs from my personal account next time.

@Yash-Singh1, no, this is okay now! I've already got access to the pingdotgg fork 🙂

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

2 participants