|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +#include <react/renderer/graphics/PlatformColorParser.h> |
| 9 | + |
| 10 | +#include "configurePlatformColorCacheInvalidationHook.h" |
| 11 | + |
| 12 | +#include <fbjni/fbjni.h> |
| 13 | +#include <folly/container/EvictingCacheMap.h> |
| 14 | +#include <react/renderer/css/CSSColor.h> |
| 15 | +#include <react/renderer/css/CSSValueParser.h> |
| 16 | +#include <functional> |
| 17 | +#include <mutex> |
| 18 | +#include <optional> |
| 19 | +#include <unordered_map> |
| 20 | + |
| 21 | +namespace facebook::react { |
| 22 | + |
| 23 | +size_t hashGetColourArguments( |
| 24 | + int32_t surfaceId, |
| 25 | + const std::vector<std::string>& resourcePaths) { |
| 26 | + size_t seed = std::hash<int32_t>{}(surfaceId); |
| 27 | + for (const auto& path : resourcePaths) { |
| 28 | + seed ^= |
| 29 | + std::hash<std::string>{}(path) + 0x9e3779b9 + (seed << 6) + (seed >> 2); |
| 30 | + } |
| 31 | + return seed; |
| 32 | +} |
| 33 | + |
| 34 | +SharedColor parsePlatformColor( |
| 35 | + const ContextContainer& contextContainer, |
| 36 | + int32_t surfaceId, |
| 37 | + const RawValue& value) { |
| 38 | + Color color{}; |
| 39 | + if (value.hasType<std::unordered_map<std::string, RawValue>>()) { |
| 40 | + // Mixed array + string values, so read as a map of RawValue (a map of |
| 41 | + // vector<string> would assert on the fallback string). |
| 42 | + auto map = (std::unordered_map<std::string, RawValue>)value; |
| 43 | + |
| 44 | + std::vector<std::string> resourcePaths; |
| 45 | + auto resourcePathsIt = map.find("resource_paths"); |
| 46 | + if (resourcePathsIt != map.end() && |
| 47 | + resourcePathsIt->second.hasType<std::vector<std::string>>()) { |
| 48 | + resourcePaths = (std::vector<std::string>)resourcePathsIt->second; |
| 49 | + } |
| 50 | + |
| 51 | + bool resolved = false; |
| 52 | + if (!resourcePaths.empty()) { |
| 53 | + // Cache the (costly) JNI results. A cached nullopt is an explicit miss, |
| 54 | + // distinct from a path that resolves to transparent (ARGB 0). |
| 55 | + static std::mutex getColorCacheMutex; |
| 56 | + static folly::EvictingCacheMap<size_t, std::optional<Color>> |
| 57 | + getColorCache(64); |
| 58 | + |
| 59 | + // Listen for appearance changes, which should invalidate the cache |
| 60 | + static std::once_flag setupCacheInvalidation; |
| 61 | + std::call_once( |
| 62 | + setupCacheInvalidation, |
| 63 | + configurePlatformColorCacheInvalidationHook, |
| 64 | + [&] { |
| 65 | + std::scoped_lock lock(getColorCacheMutex); |
| 66 | + getColorCache.clear(); |
| 67 | + }); |
| 68 | + |
| 69 | + auto hash = hashGetColourArguments(surfaceId, resourcePaths); |
| 70 | + std::optional<Color> resolvedColor; |
| 71 | + { |
| 72 | + std::scoped_lock lock(getColorCacheMutex); |
| 73 | + auto iterator = getColorCache.find(hash); |
| 74 | + if (iterator != getColorCache.end()) { |
| 75 | + resolvedColor = iterator->second; |
| 76 | + } else { |
| 77 | + const auto& fabricUIManager = |
| 78 | + contextContainer.at<jni::global_ref<jobject>>("FabricUIManager"); |
| 79 | + // Boxed Integer: null is an explicit miss; a non-null value may be 0 |
| 80 | + // (transparent black). |
| 81 | + static auto getColorFromJava = |
| 82 | + fabricUIManager->getClass() |
| 83 | + ->getMethod<jni::JInteger::javaobject( |
| 84 | + jint, jni::JArrayClass<jni::JString>)>("getColor"); |
| 85 | + auto javaResourcePaths = |
| 86 | + jni::JArrayClass<jni::JString>::newArray(resourcePaths.size()); |
| 87 | + |
| 88 | + for (int i = 0; i < resourcePaths.size(); i++) { |
| 89 | + javaResourcePaths->setElement( |
| 90 | + i, *jni::make_jstring(resourcePaths[i])); |
| 91 | + } |
| 92 | + auto boxedColor = |
| 93 | + getColorFromJava(fabricUIManager, surfaceId, *javaResourcePaths); |
| 94 | + if (boxedColor) { |
| 95 | + resolvedColor = static_cast<Color>(boxedColor->value()); |
| 96 | + } |
| 97 | + getColorCache.set(hash, resolvedColor); |
| 98 | + } |
| 99 | + } |
| 100 | + if (resolvedColor.has_value()) { |
| 101 | + color = *resolvedColor; |
| 102 | + resolved = true; |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + // No path resolved: parse the raw fallback with the shared CSS parser (the |
| 107 | + // same parser iOS Fabric uses). |
| 108 | + if (!resolved) { |
| 109 | + auto fallbackIt = map.find("fallback"); |
| 110 | + if (fallbackIt != map.end() && |
| 111 | + fallbackIt->second.hasType<std::string>()) { |
| 112 | + auto cssColor = |
| 113 | + parseCSSProperty<CSSColor>((std::string)fallbackIt->second); |
| 114 | + if (std::holds_alternative<CSSColor>(cssColor)) { |
| 115 | + const auto& c = std::get<CSSColor>(cssColor); |
| 116 | + color = hostPlatformColorFromRGBA(c.r, c.g, c.b, c.a); |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + return color; |
| 123 | +} |
| 124 | + |
| 125 | +} // namespace facebook::react |
0 commit comments