Skip to content

Commit 4b0a6eb

Browse files
parthematicsmeta-codesync[bot]
authored andcommitted
Fix zero-width "I" on iOS after an empty Text is measured (#58798)
Summary: On iOS, a `<Text>` whose entire content is `I` is laid out zero wide, and so is never drawn, if an empty `<Text>` with the same text attributes and layout constraints was measured earlier. `TextLayoutManager::measure` replaces an empty attributed string with a placeholder via `ensurePlaceholderIfEmpty_DO_NOT_USE`, and that placeholder string is `"I"`. The measure cache key is built from the placeholder, so an empty string and a real `"I"` share one `textMeasureCache_` entry. The compensation for the empty case (`measurement.size.width = 0`) runs inside the lambda that produces the cached value, so the zero width is what gets stored, and a later real `"I"` reads it back. The compensation was added in #55472 (537d2a6) to fix #55468, where an empty `<Text>` took up the width of the placeholder. Before it existed the shared entry was harmless, because both lookups got the placeholder's real width. 0.83.6 does not have it and is not affected; 0.86.0 through 0.86.3 and `main` are. This PR applies the compensation after the cache lookup instead of inside the cached value, so the cache always holds the true measurement for its key. Behavior for empty strings is unchanged. ## Changelog: [IOS] [FIXED] - Fix `Text` containing only "I" being measured zero wide after an empty `Text` with the same style was measured Pull Request resolved: #58798 Test Plan: ### Repro (run on React Native 0.86.3, New Architecture, iPhone Air simulator on iOS 27.0) 1. Render this component as a screen in a freshly launched app (a fresh launch matters, so the text measure cache has no entry for this style yet): ```tsx import { Text, View } from "react-native"; const style = { fontSize: 41, color: "white" } as const; export default function Repro() { return ( <View style={{ flex: 1, alignItems: "center", paddingTop: 200, backgroundColor: "black" }}> <Text style={style} onLayout={(e) => console.log("empty", e.nativeEvent.layout.width)}></Text> <Text style={style} onLayout={(e) => console.log("I", e.nativeEvent.layout.width)}>I</Text> <Text style={style} onLayout={(e) => console.log("A", e.nativeEvent.layout.width)}>A</Text> </View> ); } ``` 2. Look at the screen and the logged widths. Actual: only `A` is drawn. Logged widths: ``` empty 0 I 0 A 26.666656494140625 ``` Expected: `I` and `A` are both drawn, and `I` has a non-zero width. 3. Remove the empty `<Text>` and relaunch: `I` is drawn. ### How we hit it A letter tile that renders `<Text style={tileText}>{letter}</Text>`, where `letter` starts as `""` and becomes the typed letter. Typing any letter except `I` works. For `I` (AvenirNext-Bold, 33.6pt, inside a 56.7pt box with `alignItems: "center"`) the text reported: ``` onTextLayout: [{ text: "I", width: 10.68, height: 41.16 }] onLayout: { x: 28.33, width: 0, height: 41.33 } ``` The same happened on a physical device on iOS 26.6 with a release build. Not rendering the empty `Text` made `I` appear, which matches the cache explanation above. ### Verification of this change I have not yet run a build of React Native from source with this change (the app above links the prebuilt React Native core). The repro and the root cause are verified as described; the fix itself is verified by reading the code only. I'm happy to run the repro in RNTester and attach the result if that helps. Reviewed By: javache, cipolleschi Differential Revision: D126750629 Pulled By: zeyap fbshipit-source-id: 2133801bb4c6b5a54c826850074c185ef7d26503
1 parent 7b33c40 commit 4b0a6eb

1 file changed

Lines changed: 12 additions & 8 deletions

File tree

  • packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,24 @@
6969
layoutContext:layoutContext
7070
layoutConstraints:layoutConstraints];
7171

72-
// TODO(D63303709): We compensate for the placeholder character
73-
// being used to represent empty string. iOS TextLayoutManager
74-
// should instead measure using `baseTextAttributes` of the
75-
// `AttributedString`.
76-
if (originalAttributedString.isEmpty()) {
77-
measurement.size.width = 0;
78-
}
79-
8072
if (telemetry) {
8173
telemetry->didMeasureText();
8274
}
8375

8476
return measurement;
8577
});
78+
79+
// TODO(D63303709): We compensate for the placeholder character
80+
// being used to represent empty string. iOS TextLayoutManager
81+
// should instead measure using `baseTextAttributes` of the
82+
// `AttributedString`.
83+
//
84+
// This must happen outside of the cached value: the cache key is built
85+
// from the placeholder, so an empty string shares its entry with a real
86+
// string equal to the placeholder.
87+
if (originalAttributedString.isEmpty()) {
88+
measurement.size.width = 0;
89+
}
8690
break;
8791
}
8892

0 commit comments

Comments
 (0)