Skip to content

Compute the paragraph drawing frame at mount time, not in layoutSubviews - #58632

Open
janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:safe-area/1-paragraph-drawing-frame
Open

janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:safe-area/1-paragraph-drawing-frame

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Summary:

RCTParagraphComponentView derives the text view's frame and drawing frame in layoutSubviews, requested with setNeedsLayout from updateState and updateLayoutMetrics. The layout pass itself dates from #46081, but until #57408 drawRect: read the content frame straight from _layoutMetrics, which is always current; #57408 moved drawing onto a drawingFrame computed in layoutSubviews, so drawing now assumes a layout pass will run before the view is displayed. It does not when the mount itself happens inside Core Animation's display phase — which is where AppleEventBeat processes a synchronous event requested during layout (#58530). This transaction's layout pass is already over, so the text view displays the new attributed string in the previous drawing frame and the text is cut off at the old width. The queued layoutSubviews updates the frame in the next transaction, but drawingFrame is a plain property that does not invalidate the display, so the clipped drawing stays until something else redraws the view.

Nothing on main emits a synchronous event from layout yet, so this is latent today; #58109 is the first thing that does (layoutSubviews reporting safe area insets on rotation) and it hits this on every rotation.

The frames are now computed in finalizeUpdates:, which the mounting manager calls once per view after all of a mutation's update* calls. That coalesces state and layout-metrics changes the same way the layout pass did — one computation per mount, as before — without depending on a layout pass that may already have happened, and it removes the extra layout pass altogether. The layoutSubviews override goes away; updateState and updateLayoutMetrics keep their setNeedsDisplay.

Changelog:

[IOS] [FIXED] - Text no longer renders clipped when mounted from a synchronous event during layout

Test Plan:

Reproduced with #58109 on top of this branch, RNTester "Safe area insets" example, iPhone 17 Pro simulator: present the padded modal, apply insets, rotate. The readout text is re-rendered synchronously from the rotation's layout pass.

Before After

Before: top: 0, right: 62, bottom: 19.99996 — the accessibility label carries the full …9482421875, left: 62, and the layout box is the right size; only the drawing is cut at the previous width. The paragraph under it overflows on one line for the same reason. After: both render at their new width. Rotating back to portrait renders correctly too, and ordinary text throughout RNTester (normal asynchronous mounts) is unchanged.


Found while testing #58109, which hits this on every rotation; standalone, that stack does not include this change.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 22, 2026
@facebook-github-tools facebook-github-tools Bot added the Contributor A React Native contributor. label Sep 22, 2026
@janicduplessis
janicduplessis marked this pull request as ready for review September 22, 2026 00:24
`RCTParagraphComponentView` derives the text view's frame and drawing frame
in `layoutSubviews`, requested with `setNeedsLayout` from `updateState` and
`updateLayoutMetrics`. When a mount runs inside Core Animation's display
phase — which is where `AppleEventBeat` processes a synchronous event
requested during layout — this transaction's layout pass has already
happened, so the text view displays with the new attributed string but the
previous drawing frame: the text is cut off at the old width. The queued
`layoutSubviews` then updates the drawing frame in the next transaction,
but that property does not invalidate the display, so the clipped drawing
stays until something else redraws the view.

Compute the frames in `finalizeUpdates:` instead. The mounting manager calls
it once per view after all of a mutation's `update*` calls, so it coalesces
state and layout-metrics changes the same way the layout pass did, without
depending on a layout pass that may already be over. One computation per
mount, as before, minus the layout pass.
@janicduplessis
janicduplessis force-pushed the safe-area/1-paragraph-drawing-frame branch from 04e01ef to bba719d Compare September 22, 2026 00:26
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 22, 2026
@meta-codesync

meta-codesync Bot commented Sep 22, 2026

Copy link
Copy Markdown

@Abbondanzo has imported this pull request. If you are a Meta employee, you can view this in D121209981.

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Contributor A React Native contributor. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant