Fix text wrapping in absolutely positioned elements on Android (#56651)#56651
Open
clocksarestupid wants to merge 1 commit intofacebook:mainfrom
Open
Fix text wrapping in absolutely positioned elements on Android (#56651)#56651clocksarestupid wants to merge 1 commit intofacebook:mainfrom
clocksarestupid wants to merge 1 commit intofacebook:mainfrom
Conversation
|
@clocksarestupid has exported this pull request. If you are a Meta employee, you can view the originating Diff in D102920508. |
clocksarestupid
added a commit
to clocksarestupid/react-native
that referenced
this pull request
Apr 29, 2026
…ook#56651) Summary: ## Context On Android TV, text inside absolutely-positioned Views wraps unexpectedly when the computed width from Yoga is fractional. This manifests in Instagram TV as "Keep watching" breaking to two lines in the exit dialog when the button text is focused (rendered via an absolutely-positioned overlay for color animation). The root cause is a rounding mismatch in React Native's Android `TextLayoutManager`: `desiredWidth` (the text's needed width) uses `ceil()` (rounds up), but `layoutWidth` in `EXACTLY` mode uses `floor()` (rounds down). When Yoga passes a fractional width (e.g. 258.5px), the container gets `floor(258.5) = 258px` but the text needs `ceil(258.3) = 259px`, causing a 1px shortfall that triggers wrapping. ## Fix In `TextLayoutManager.createLayout()`, change `floor(width).toInt()` to `ceil(width).toInt()` for `YogaMeasureMode.EXACTLY` in **both** layout paths so the behavior is consistent regardless of which `Layout` class is chosen: - BoringLayout (single-line text that fits) - StaticLayout (multi-line or complex text) `YogaMeasureMode.AT_MOST` is intentionally left as `floor(width).toInt()`. `AT_MOST` is a constraint contract from Yoga ("do not exceed this width"), so flooring remains the correct conservative behavior — ceiling there could violate the constraint by up to 1px. The BoringLayout entry guard (`boring.width <= floor(width)`) is also left unchanged. If a boring text fails the guard, it falls through to the StaticLayout path, which now also ceils for `EXACTLY`, so no truncation results — the only effect is a slightly less optimal layout class choice in a narrow edge case. ## Why ceiling `EXACTLY` is safe `EXACTLY` means Yoga has guaranteed this width — the container has been allocated at least the full fractional width upstream. Ceiling the local layout width by 1px cannot exceed what Yoga has reserved, while flooring it produced a 1px shortfall that mismatched `desiredWidth`'s ceiling and triggered wrapping. The compensating mechanism in `calculateWidth()` — which returns the raw fractional width to Yoga for `EXACTLY` mode rather than the floored `layout.width` — is preserved, so Yoga's upstream allocation reasoning is unchanged. This was the subpixel-safety property introduced in D74685353; only the local pixel rounding inside `createLayout` changes from "round down 1px and risk wrapping" to "round up 1px and match `desiredWidth`". ## Notes This was confirmed as a problem with an absolutely positioned style with `left:0, right:0` applied. Width sizing was confirmed to be the issue when `left:-1, right:-1` resolved the issue. Further investigation found this fix in text sizing. Only `EXACTLY` is needed to fix the observed Instagram TV bug. `AT_MOST` is left untouched because the constraint semantics differ. ## Changelog: [Android] [Fixed] - Fix 1px text wrapping in absolutely positioned elements caused by fractional Yoga widths Differential Revision: D102920508
d7a608e to
674163d
Compare
…ook#56651) Summary: ## Context On Android TV, text inside absolutely-positioned Views wraps unexpectedly when the computed width from Yoga is fractional. This manifests in Instagram TV as "Keep watching" breaking to two lines in the exit dialog when the button text is focused (rendered via an absolutely-positioned overlay for color animation). The root cause is a rounding mismatch in React Native's Android `TextLayoutManager`: `desiredWidth` (the text's needed width) uses `ceil()` (rounds up), but `layoutWidth` in `EXACTLY` mode uses `floor()` (rounds down). When Yoga passes a fractional width (e.g. 258.5px), the container gets `floor(258.5) = 258px` but the text needs `ceil(258.3) = 259px`, causing a 1px shortfall that triggers wrapping. ## Fix In `TextLayoutManager.createLayout()`, change `floor(width).toInt()` to `ceil(width).toInt()` for `YogaMeasureMode.EXACTLY` in **both** layout paths so the behavior is consistent regardless of which `Layout` class is chosen: - BoringLayout (single-line text that fits) - StaticLayout (multi-line or complex text) `YogaMeasureMode.AT_MOST` is intentionally left as `floor(width).toInt()`. `AT_MOST` is a constraint contract from Yoga ("do not exceed this width"), so flooring remains the correct conservative behavior — ceiling there could violate the constraint by up to 1px. The BoringLayout entry guard (`boring.width <= floor(width)`) is also left unchanged. If a boring text fails the guard, it falls through to the StaticLayout path, which now also ceils for `EXACTLY`, so no truncation results — the only effect is a slightly less optimal layout class choice in a narrow edge case. ## Why ceiling `EXACTLY` is safe `EXACTLY` means Yoga has guaranteed this width — the container has been allocated at least the full fractional width upstream. Ceiling the local layout width by 1px cannot exceed what Yoga has reserved, while flooring it produced a 1px shortfall that mismatched `desiredWidth`'s ceiling and triggered wrapping. The compensating mechanism in `calculateWidth()` — which returns the raw fractional width to Yoga for `EXACTLY` mode rather than the floored `layout.width` — is preserved, so Yoga's upstream allocation reasoning is unchanged. This was the subpixel-safety property introduced in D74685353; only the local pixel rounding inside `createLayout` changes from "round down 1px and risk wrapping" to "round up 1px and match `desiredWidth`". ## Testing Added a new test case to cover this behavior. If this needs to be adjusted in the future, a failure will highlight this bug and ensure it's covered or mitigated with additional test cases. ## Notes This was confirmed as a problem with an absolutely positioned style with `left:0, right:0` applied. Width sizing was confirmed to be the issue when `left:-1, right:-1` resolved the issue. Further investigation found this fix in text sizing. Only `EXACTLY` is needed to fix the observed Instagram TV bug. `AT_MOST` is left untouched because the constraint semantics differ. ## Changelog: [Android] [Fixed] - Fix 1px text wrapping in absolutely positioned elements caused by fractional Yoga widths Differential Revision: D102920508
674163d to
84a3701
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
Context
On Android TV, text inside absolutely-positioned Views wraps unexpectedly when the computed width from Yoga is fractional. This manifests in Instagram TV as "Keep watching" breaking to two lines in the exit dialog when the button text is focused (rendered via an absolutely-positioned overlay for color animation).
The root cause is a rounding mismatch in React Native's Android
TextLayoutManager:desiredWidth(the text's needed width) usesceil()(rounds up), butlayoutWidthinEXACTLYmode usesfloor()(rounds down). When Yoga passes a fractional width (e.g. 258.5px), the container getsfloor(258.5) = 258pxbut the text needsceil(258.3) = 259px, causing a 1px shortfall that triggers wrapping.Fix
In
TextLayoutManager.createLayout(), changefloor(width).toInt()toceil(width).toInt()forYogaMeasureMode.EXACTLYin both layout paths so the behavior is consistent regardless of whichLayoutclass is chosen:YogaMeasureMode.AT_MOSTis intentionally left asfloor(width).toInt().AT_MOSTis a constraint contract from Yoga ("do not exceed this width"), so flooring remains the correct conservative behavior — ceiling there could violate the constraint by up to 1px.The BoringLayout entry guard (
boring.width <= floor(width)) is also left unchanged. If a boring text fails the guard, it falls through to the StaticLayout path, which now also ceils forEXACTLY, so no truncation results — the only effect is a slightly less optimal layout class choice in a narrow edge case.Why ceiling
EXACTLYis safeEXACTLYmeans Yoga has guaranteed this width — the container has been allocated at least the full fractional width upstream. Ceiling the local layout width by 1px cannot exceed what Yoga has reserved, while flooring it produced a 1px shortfall that mismatcheddesiredWidth's ceiling and triggered wrapping.The compensating mechanism in
calculateWidth()— which returns the raw fractional width to Yoga forEXACTLYmode rather than the flooredlayout.width— is preserved, so Yoga's upstream allocation reasoning is unchanged. This was the subpixel-safety property introduced in D74685353; only the local pixel rounding insidecreateLayoutchanges from "round down 1px and risk wrapping" to "round up 1px and matchdesiredWidth".Testing
Added a new test case to cover this behavior. If this needs to be adjusted in the future, a failure will highlight this bug and ensure it's covered or mitigated with additional test cases.
Notes
This was confirmed as a problem with an absolutely positioned style with
left:0, right:0applied. Width sizing was confirmed to be the issue whenleft:-1, right:-1resolved the issue. Further investigation found this fix in text sizing.Only
EXACTLYis needed to fix the observed Instagram TV bug.AT_MOSTis left untouched because the constraint semantics differ.Changelog: [Android] [Fixed] - Fix 1px text wrapping in absolutely positioned elements caused by fractional Yoga widths
Differential Revision: D102920508