From da71845180830e48f86430da5e78672370d17668 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 24 Jul 2026 09:33:44 -0700 Subject: [PATCH] PlatformColor lazy fallback: honor a raw-color fallback on iOS and macOS (native) Summary: This is the first diff in a stack that adds an optional trailing `{fallback: ''}` argument to `PlatformColor(...)`. The fallback is carried lazily to native and parsed only when every supplied color token fails to resolve. This diff wires up the iOS and macOS native color resolvers to honor that fallback; a later diff adds the JS argument that emits it, so on its own this change is a no-op for existing call sites. The fallback is supported on the new architecture (Fabric) only. To distinguish a genuine miss (no token resolves) from a token that legitimately resolves to a transparent color, the semantic-color resolver gains an `OrNil` variant: - `RCTPlatformColorFromSemanticItemsOrNil` returns `nil` on a miss instead of collapsing to `clearColor`. `RCTPlatformColorFromSemanticItems` keeps its existing non-null contract by wrapping the `OrNil` variant. - `Color::createSemanticColor` returns the undefined-color sentinel (a null underlying `UIColor`) on a miss, so `PlatformColorParser.mm` applies the fallback only on a true miss and otherwise renders transparent exactly as before. - The fallback string is parsed with the shared CSS color parser (`parseCSSProperty`), so `transparent`/`rgba(0,0,0,0)` are honored rather than mistaken for a parse failure. This spans the `xplat` iOS and macOS graphics resolvers. Changelog: [Internal] - PlatformColor: iOS/macOS native support for a lazy raw-color fallback Differential Revision: D111837102 --- .../renderer/graphics/HostPlatformColor.h | 3 ++ .../renderer/graphics/HostPlatformColor.mm | 7 +++- .../renderer/graphics/PlatformColorParser.mm | 32 ++++++++++++++++++- .../renderer/graphics/RCTPlatformColorUtils.h | 9 ++++++ .../graphics/RCTPlatformColorUtils.mm | 12 +++++-- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h index b9535f14e363..a7dd98139991 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h @@ -28,6 +28,9 @@ struct Color { int32_t getColor() const; std::size_t getUIColorHash() const; + // Returns the UndefinedColor sentinel (null underlying UIColor) on a miss, so + // callers can tell a miss from a name that resolves to transparent. Callers + // reaching into getUIColor() must null-check it. static Color createSemanticColor(std::vector &semanticItems); std::shared_ptr getUIColor() const diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 93ecb7e33190..e47deaa95cf6 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -237,7 +237,12 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) Color Color::createSemanticColor(std::vector &semanticItems) { - auto semanticColor = RCTPlatformColorFromSemanticItems(semanticItems); + UIColor *semanticColor = RCTPlatformColorFromSemanticItemsOrNil(semanticItems); + if (semanticColor == nil) { + // Undefined-color sentinel on a miss, distinct from a name that resolves to + // transparent (getColor() is still 0, preserving the old render). + return HostPlatformColor::UndefinedColor; + } return Color(wrapManagedObject(semanticColor)); } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm index 377783f0f195..890db576eba9 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm @@ -8,9 +8,13 @@ #import "PlatformColorParser.h" #import +#import +#import +#import #import #import #import +#import #import #import @@ -51,13 +55,39 @@ return SharedColor(color); } +// nullopt only on a parse failure, so a fallback that resolves to transparent is +// still honored. +static std::optional fallbackColorFromString(const std::string &fallbackString) +{ + auto cssColor = parseCSSProperty(fallbackString); + if (std::holds_alternative(cssColor)) { + const auto &c = std::get(cssColor); + return colorFromRGBA(c.r, c.g, c.b, c.a); + } + return std::nullopt; +} + SharedColor parsePlatformColor(const ContextContainer &contextContainer, int32_t surfaceId, const RawValue &value) { if (value.hasType>()) { auto items = (std::unordered_map)value; if (items.find("semantic") != items.end() && items.at("semantic").hasType>()) { auto semanticItems = (std::vector)items.at("semantic"); - return SharedColor(Color::createSemanticColor(semanticItems)); + auto semanticColor = SharedColor(Color::createSemanticColor(semanticItems)); + // The sentinel (null UIColor) means a true miss; apply the fallback only + // then, not when a name resolves to transparent. + if (!semanticColor) { + if (items.find("fallback") != items.end() && items.at("fallback").hasType()) { + auto fallbackColor = fallbackColorFromString((std::string)items.at("fallback")); + // has_value(), not != 0, so a transparent fallback is kept. + if (fallbackColor.has_value()) { + return *fallbackColor; + } + } + // Miss with no usable fallback: clearColor, never leaking the sentinel. + return clearColor(); + } + return semanticColor; } else if ( items.find("dynamic") != items.end() && items.at("dynamic").hasType>()) { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h index f487564c043d..ad021c1e413f 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h @@ -15,6 +15,15 @@ struct ColorComponents; struct Color; } // namespace facebook::react +NS_ASSUME_NONNULL_BEGIN + facebook::react::ColorComponents RCTPlatformColorComponentsFromSemanticItems(std::vector &semanticItems); UIColor *RCTPlatformColorFromSemanticItems(std::vector &semanticItems); +// Like RCTPlatformColorFromSemanticItems but returns nil on a miss, so callers +// can tell a miss from a name that resolves to transparent. +UIColor *_Nullable RCTPlatformColorFromSemanticItemsOrNil(std::vector &semanticItems); +// Precondition: `color` is a resolved color, never the miss sentinel, so the +// result stays _Nonnull. UIColor *RCTPlatformColorFromColor(const facebook::react::Color &color); + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 4a0a53e2b504..882f38805672 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -192,7 +192,7 @@ return _ColorComponentsFromUIColor(RCTPlatformColorFromSemanticItems(semanticItems)); } -UIColor *RCTPlatformColorFromSemanticItems(std::vector &semanticItems) +UIColor *_Nullable RCTPlatformColorFromSemanticItemsOrNil(std::vector &semanticItems) { for (const auto &semanticCString : semanticItems) { NSString *semanticNSString = _NSStringFromCString(semanticCString); @@ -206,7 +206,15 @@ } } - return UIColor.clearColor; + return nil; +} + +UIColor *RCTPlatformColorFromSemanticItems(std::vector &semanticItems) +{ + // Non-null contract (e.g. for RCTPlatformColorComponentsFromSemanticItems); + // callers needing to detect a miss use the OrNil variant. + UIColor *uiColor = RCTPlatformColorFromSemanticItemsOrNil(semanticItems); + return uiColor != nil ? uiColor : UIColor.clearColor; } UIColor *RCTPlatformColorFromColor(const facebook::react::Color &color)