diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index cb3a6000c30d..f062132f2741 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -66,6 +66,10 @@ class TextMeasureCacheKey final { AttributedString attributedString{}; ParagraphAttributes paragraphAttributes{}; LayoutConstraints layoutConstraints{}; + // The measured size depends on the pixel scale factor because layout metrics + // are rounded to the pixel grid. Two otherwise-identical measures at different + // densities are not interchangeable, so the scale factor is part of the key. + Float pointScaleFactor{}; }; // The Key type that is used for Line Measure Cache. @@ -87,6 +91,9 @@ class PreparedTextCacheKey final { AttributedString attributedString{}; ParagraphAttributes paragraphAttributes{}; LayoutConstraints layoutConstraints{}; + // A prepared layout is rounded to the pixel grid, so it is only reusable at + // the pixel scale factor it was laid out at. + Float pointScaleFactor{}; }; /* @@ -252,7 +259,8 @@ inline size_t attributedStringHashDisplayWise(const AttributedString &attributed inline bool operator==(const TextMeasureCacheKey &lhs, const TextMeasureCacheKey &rhs) { return areAttributedStringsEquivalentLayoutWise(lhs.attributedString, rhs.attributedString) && - lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints; + lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints && + floatEquality(lhs.pointScaleFactor, rhs.pointScaleFactor); } inline bool operator==(const LineMeasureCacheKey &lhs, const LineMeasureCacheKey &rhs) @@ -264,7 +272,8 @@ inline bool operator==(const LineMeasureCacheKey &lhs, const LineMeasureCacheKey inline bool operator==(const PreparedTextCacheKey &lhs, const PreparedTextCacheKey &rhs) { return areAttributedStringsEquivalentDisplayWise(lhs.attributedString, rhs.attributedString) && - lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints; + lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints && + floatEquality(lhs.pointScaleFactor, rhs.pointScaleFactor); } } // namespace facebook::react @@ -276,7 +285,10 @@ struct hash { size_t operator()(const facebook::react::TextMeasureCacheKey &key) const { return facebook::react::hash_combine( - attributedStringHashLayoutWise(key.attributedString), key.paragraphAttributes, key.layoutConstraints); + attributedStringHashLayoutWise(key.attributedString), + key.paragraphAttributes, + key.layoutConstraints, + key.pointScaleFactor); } }; @@ -294,7 +306,10 @@ struct hash { size_t operator()(const facebook::react::PreparedTextCacheKey &key) const { return facebook::react::hash_combine( - attributedStringHashDisplayWise(key.attributedString), key.paragraphAttributes, key.layoutConstraints); + attributedStringHashDisplayWise(key.attributedString), + key.paragraphAttributes, + key.layoutConstraints, + key.pointScaleFactor); } }; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index ce6ecb15373a..99886999293d 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -193,7 +193,8 @@ TextMeasurement TextLayoutManager::measure( : textMeasureCache_.get( {.attributedString = attributedString, .paragraphAttributes = paragraphAttributes, - .layoutConstraints = layoutConstraints}, + .layoutConstraints = layoutConstraints, + .pointScaleFactor = layoutContext.pointScaleFactor}, std::move(measureText)); measurement.size = layoutConstraints.clamp(measurement.size); @@ -315,7 +316,8 @@ TextLayoutManager::PreparedTextLayout TextLayoutManager::prepareLayout( const auto [key, preparedText] = preparedTextCache_.getWithKey( {.attributedString = attributedString, .paragraphAttributes = paragraphAttributes, - .layoutConstraints = layoutConstraints}, + .layoutConstraints = layoutConstraints, + .pointScaleFactor = layoutContext.pointScaleFactor}, [&]() { const auto& fabricUIManager = contextContainer_->at>("FabricUIManager"); diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm index cddafb5a6ef2..82cad8fd2eed 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm @@ -43,7 +43,8 @@ measurement = textMeasureCache_.get( {.attributedString = attributedString, .paragraphAttributes = paragraphAttributes, - .layoutConstraints = layoutConstraints}, + .layoutConstraints = layoutConstraints, + .pointScaleFactor = layoutContext.pointScaleFactor}, [&]() { auto telemetry = TransactionTelemetry::threadLocalTelemetry(); if (telemetry) { diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp index 8d1152bcc9aa..bd0a053f64bc 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp @@ -44,3 +44,56 @@ TEST(TextLayoutManagerTest, maxFontSizeMultiplierAffectsLayoutCacheHash) { EXPECT_NE( textAttributesHashLayoutWise(lhs), textAttributesHashLayoutWise(rhs)); } + +// Measurements are rounded to the pixel grid, so a measurement cached at one +// pixel scale factor must not satisfy a lookup at another. Keys that differ +// only by pointScaleFactor must compare unequal. +TEST(TextLayoutManagerTest, pointScaleFactorAffectsTextMeasureCacheEquality) { + TextMeasureCacheKey lhs; + TextMeasureCacheKey rhs; + + lhs.pointScaleFactor = 2.0; + rhs.pointScaleFactor = 1.6; + EXPECT_FALSE(lhs == rhs); + + rhs.pointScaleFactor = 2.0; + EXPECT_TRUE(lhs == rhs); +} + +TEST(TextLayoutManagerTest, pointScaleFactorAffectsTextMeasureCacheHash) { + TextMeasureCacheKey lhs; + TextMeasureCacheKey rhs; + + lhs.pointScaleFactor = 2.0; + rhs.pointScaleFactor = 1.6; + + EXPECT_NE( + std::hash{}(lhs), + std::hash{}(rhs)); +} + +// Same invariant for the prepared-text cache: a prepared layout is pixel-grid +// rounded and is only reusable at the pixel scale factor it was prepared at. +TEST(TextLayoutManagerTest, pointScaleFactorAffectsPreparedTextCacheEquality) { + PreparedTextCacheKey lhs; + PreparedTextCacheKey rhs; + + lhs.pointScaleFactor = 2.0; + rhs.pointScaleFactor = 1.6; + EXPECT_FALSE(lhs == rhs); + + rhs.pointScaleFactor = 2.0; + EXPECT_TRUE(lhs == rhs); +} + +TEST(TextLayoutManagerTest, pointScaleFactorAffectsPreparedTextCacheHash) { + PreparedTextCacheKey lhs; + PreparedTextCacheKey rhs; + + lhs.pointScaleFactor = 2.0; + rhs.pointScaleFactor = 1.6; + + EXPECT_NE( + std::hash{}(lhs), + std::hash{}(rhs)); +} diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 23da100989c0..e54e4bb6acbb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4129,6 +4129,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -5064,6 +5065,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 12885e3d63cd..fbaf6d503d9d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3973,6 +3973,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -4890,6 +4891,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index c5c78b10c55d..cce1cbacb787 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4126,6 +4126,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -5055,6 +5056,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 3dfdc05855de..2a078f3c4408 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -6316,6 +6316,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -7283,6 +7284,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index e49d4cdde961..c2aa6cdfc4ed 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -6188,6 +6188,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -7137,6 +7138,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index b89188d3b837..040d1db716a8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -6313,6 +6313,7 @@ class facebook::react::PointerHoverTracker { class facebook::react::PreparedTextCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } @@ -7274,6 +7275,7 @@ class facebook::react::TextLayoutManager { class facebook::react::TextMeasureCacheKey { public facebook::react::AttributedString attributedString; + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 6d18214f9b43..d4113b38f16e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2732,6 +2732,7 @@ class facebook::react::PointerHoverTracker { } class facebook::react::PreparedTextCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; @@ -3554,6 +3555,7 @@ class facebook::react::TextInputState { } class facebook::react::TextMeasureCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 2d462c2c4aeb..6908e99301b7 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2616,6 +2616,7 @@ class facebook::react::PointerHoverTracker { } class facebook::react::PreparedTextCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; @@ -3420,6 +3421,7 @@ class facebook::react::TextInputState { } class facebook::react::TextMeasureCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 5d4384fa8356..d64d649e6a7e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2729,6 +2729,7 @@ class facebook::react::PointerHoverTracker { } class facebook::react::PreparedTextCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes; @@ -3545,6 +3546,7 @@ class facebook::react::TextInputState { } class facebook::react::TextMeasureCacheKey { + public Float pointScaleFactor; public facebook::react::AttributedString attributedString; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::ParagraphAttributes paragraphAttributes;