Skip to content

Commit

Permalink
Add basic DisplayP3 color support (#42830)
Browse files Browse the repository at this point in the history
Summary:
This adds initial support for wide gamut (DisplayP3) colors to React Native iOS per the [RFC](react-native-community/discussions-and-proposals#738). It provides the ability to set the default color space to sRGB or DisplayP3 and provides the native code necessary to support `color()` function syntax per the [W3C CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#color-function) spec. It does _not_ yet support animations and requires additional JS code before fully supporting the `color()` function syntax.

bypass-github-export-checks

## Changelog:

[IOS] [ADDED] - Add basic DisplayP3 color support

Pull Request resolved: #42830

Test Plan:
![Screenshot_20240131-100112](https://github.com/facebook/react-native/assets/1944151/bbd011b1-dab0-47d6-b341-74fa8fac6757)

Follow test steps from #42831 to test support for `color()` function syntax.

To globally change the default color space to DisplayP3 make the following changes to RNTester AppDelegate.mm:
```diff
+ #import <React/RCTConvert.h>

   - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
   {
     // ...
+    RCTSetDefaultColorSpace(RCTColorSpaceDisplayP3);
     return [super application:application didFinishLaunchingWithOptions:launchOptions];
   }
```

Reviewed By: javache

Differential Revision: D53380407

Pulled By: cipolleschi

fbshipit-source-id: 938523958f9021e8d98bdb1d4e254047e3ecdad7
  • Loading branch information
ryanlntn authored and facebook-github-bot committed Feb 27, 2024
1 parent 3b93f0e commit a40bd8e
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 7 deletions.
6 changes: 6 additions & 0 deletions packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h
Expand Up @@ -6,6 +6,7 @@
*/

#import <React/RCTBridgeDelegate.h>
#import <React/RCTConvert.h>
#import <UIKit/UIKit.h>

@class RCTBridge;
Expand Down Expand Up @@ -131,6 +132,11 @@ NS_ASSUME_NONNULL_BEGIN
/// @return: `YES` to use RuntimeScheduler, `NO` to use JavaScript scheduler. The default value is `YES`.
- (BOOL)runtimeSchedulerEnabled;

/**
* The default `RCTColorSpace` for the app. It defaults to `RCTColorSpaceSRGB`.
*/
@property (nonatomic, readonly) RCTColorSpace defaultColorSpace;

@property (nonatomic, strong) RCTSurfacePresenterBridgeAdapter *bridgeAdapter;

/// This method returns a map of Component Descriptors and Components classes that needs to be registered in the
Expand Down
8 changes: 8 additions & 0 deletions packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm
Expand Up @@ -6,11 +6,13 @@
*/

#import "RCTAppDelegate.h"
#import <React/RCTColorSpaceUtils.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTLog.h>
#import <React/RCTRootView.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <React/RCTUtils.h>
#import <react/renderer/graphics/ColorComponents.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import "RCTAppDelegate+Protected.h"
#import "RCTAppSetupUtils.h"
Expand Down Expand Up @@ -75,6 +77,7 @@ - (instancetype)init
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTSetNewArchEnabled([self newArchEnabled]);
[RCTColorSpaceUtils applyDefaultColorSpace:self.defaultColorSpace];
BOOL enableTM = self.turboModuleEnabled;
BOOL fabricEnabled = self.fabricEnabled;
BOOL enableBridgeless = self.bridgelessEnabled;
Expand Down Expand Up @@ -225,6 +228,11 @@ - (void)windowScene:(UIWindowScene *)windowScene
}
}

- (RCTColorSpace)defaultColorSpace
{
return RCTColorSpaceSRGB;
}

#pragma mark - New Arch Enabled settings

- (BOOL)newArchEnabled
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native/React/Base/RCTConvert.h
Expand Up @@ -17,6 +17,15 @@
#import <React/RCTTextDecorationLineType.h>
#import <yoga/Yoga.h>

typedef NS_ENUM(NSInteger, RCTColorSpace) {
RCTColorSpaceSRGB,
RCTColorSpaceDisplayP3,
};

// Change the default color space
RCTColorSpace RCTGetDefaultColorSpace(void);
RCT_EXTERN void RCTSetDefaultColorSpace(RCTColorSpace colorSpace);

/**
* This class provides a collection of conversion functions for mapping
* JSON objects to native types and classes. These are useful when writing
Expand Down Expand Up @@ -91,6 +100,13 @@ typedef NSURL RCTFileURL;

+ (CGAffineTransform)CGAffineTransform:(id)json;

+ (UIColor *)UIColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
+ (UIColor *)UIColorWithRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha
andColorSpace:(RCTColorSpace)colorSpace;
+ (RCTColorSpace)RCTColorSpaceFromString:(NSString *)colorSpace;
+ (UIColor *)UIColor:(id)json;
+ (CGColorRef)CGColor:(id)json CF_RETURNS_NOT_RETAINED;

Expand Down
62 changes: 56 additions & 6 deletions packages/react-native/React/Base/RCTConvert.mm
Expand Up @@ -879,6 +879,48 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json
return names;
}

// The iOS side is kept in synch with the C++ side by using the
// RCTAppDelegate which, at startup, sets the default color space.
// The usage of dispatch_once and of once_flag ensoure that those are
// set only once when the app starts and that they can't change while
// the app is running.
static RCTColorSpace _defaultColorSpace = RCTColorSpaceSRGB;
RCTColorSpace RCTGetDefaultColorSpace(void)
{
return _defaultColorSpace;
}
void RCTSetDefaultColorSpace(RCTColorSpace colorSpace)
{
_defaultColorSpace = colorSpace;
}

+ (UIColor *)UIColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha
{
RCTColorSpace space = RCTGetDefaultColorSpace();
return [self UIColorWithRed:red green:green blue:blue alpha:alpha andColorSpace:space];
}
+ (UIColor *)UIColorWithRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha
andColorSpace:(RCTColorSpace)colorSpace
{
if (colorSpace == RCTColorSpaceDisplayP3) {
return [UIColor colorWithDisplayP3Red:red green:green blue:blue alpha:alpha];
}
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}

+ (RCTColorSpace)RCTColorSpaceFromString:(NSString *)colorSpace
{
if ([colorSpace isEqualToString:@"display-p3"]) {
return RCTColorSpaceDisplayP3;
} else if ([colorSpace isEqualToString:@"srgb"]) {
return RCTColorSpaceSRGB;
}
return RCTGetDefaultColorSpace();
}

+ (UIColor *)UIColor:(id)json
{
if (!json) {
Expand All @@ -887,21 +929,29 @@ + (UIColor *)UIColor:(id)json
if ([json isKindOfClass:[NSArray class]]) {
NSArray *components = [self NSNumberArray:json];
CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0;
return [UIColor colorWithRed:[self CGFloat:components[0]]
green:[self CGFloat:components[1]]
blue:[self CGFloat:components[2]]
alpha:alpha];
return [self UIColorWithRed:[self CGFloat:components[0]]
green:[self CGFloat:components[1]]
blue:[self CGFloat:components[2]]
alpha:alpha];
} else if ([json isKindOfClass:[NSNumber class]]) {
NSUInteger argb = [self NSUInteger:json];
CGFloat a = ((argb >> 24) & 0xFF) / 255.0;
CGFloat r = ((argb >> 16) & 0xFF) / 255.0;
CGFloat g = ((argb >> 8) & 0xFF) / 255.0;
CGFloat b = (argb & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:a];
return [self UIColorWithRed:r green:g blue:b alpha:a];
} else if ([json isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = json;
id value = nil;
if ((value = [dictionary objectForKey:@"semantic"])) {
NSString *rawColorSpace = [dictionary objectForKey:@"space"];
if ([rawColorSpace isEqualToString:@"display-p3"] || [rawColorSpace isEqualToString:@"srgb"]) {
CGFloat r = [[dictionary objectForKey:@"r"] floatValue];
CGFloat g = [[dictionary objectForKey:@"g"] floatValue];
CGFloat b = [[dictionary objectForKey:@"b"] floatValue];
CGFloat a = [[dictionary objectForKey:@"a"] floatValue];
RCTColorSpace colorSpace = [self RCTColorSpaceFromString:rawColorSpace];
return [self UIColorWithRed:r green:g blue:b alpha:a andColorSpace:colorSpace];
} else if ((value = [dictionary objectForKey:@"semantic"])) {
if ([value isKindOfClass:[NSString class]]) {
NSString *semanticName = value;
UIColor *color = [UIColor colorNamed:semanticName];
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTColorSpaceUtils.h
@@ -0,0 +1,16 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#import <React/RCTConvert.h>

@interface RCTColorSpaceUtils : NSObject

+ (void)applyDefaultColorSpace:(RCTColorSpace)colorSpace;

@end
23 changes: 23 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTColorSpaceUtils.mm
@@ -0,0 +1,23 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RCTColorSpaceUtils.h"

#import <react/renderer/graphics/ColorComponents.h>

@implementation RCTColorSpaceUtils

+ (void)applyDefaultColorSpace:(RCTColorSpace)colorSpace
{
facebook::react::ColorSpace cxxColorSpace =
colorSpace == RCTColorSpaceSRGB ? facebook::react::ColorSpace::sRGB : facebook::react::ColorSpace::DisplayP3;

RCTSetDefaultColorSpace(colorSpace);
facebook::react::setDefaultColorSpace(cxxColorSpace);
}

@end
@@ -0,0 +1,23 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include "ColorComponents.h"
#include <mutex>

namespace facebook::react {

static ColorSpace defaultColorSpace = ColorSpace::sRGB;

ColorSpace getDefaultColorSpace() {
return defaultColorSpace;
}

void setDefaultColorSpace(ColorSpace newColorSpace) {
defaultColorSpace = newColorSpace;
}

} // namespace facebook::react
Expand Up @@ -9,11 +9,17 @@

namespace facebook::react {

enum class ColorSpace { sRGB, DisplayP3 };

ColorSpace getDefaultColorSpace();
void setDefaultColorSpace(ColorSpace newColorSpace);

struct ColorComponents {
float red{0};
float green{0};
float blue{0};
float alpha{0};
ColorSpace colorSpace{getDefaultColorSpace()};
};

} // namespace facebook::react
Expand Up @@ -61,7 +61,8 @@ Pod::Spec.new do |s|

s.dependency "glog"
s.dependency "RCT-Folly/Fabric", folly_version
s.dependency "React-Core/Default", version
s.dependency "React-jsi"
s.dependency "React-jsiexecutor"
s.dependency "React-utils"
s.dependency "DoubleConversion"
s.dependency "fmt", "9.1.0"
Expand Down
Expand Up @@ -44,6 +44,24 @@ inline void fromRawValueShared(

result = colorFromComponents(colorComponents);
} else {
if (value.hasType<std::unordered_map<std::string, RawValue>>()) {
const auto& items = (std::unordered_map<std::string, RawValue>)value;
if (items.find("space") != items.end()) {
colorComponents.red = (float)items.at("r");
colorComponents.green = (float)items.at("g");
colorComponents.blue = (float)items.at("b");
colorComponents.alpha = (float)items.at("a");
colorComponents.colorSpace = getDefaultColorSpace();
std::string space = (std::string)items.at("space");
if (space == "display-p3") {
colorComponents.colorSpace = ColorSpace::DisplayP3;
} else if (space == "srgb") {
colorComponents.colorSpace = ColorSpace::sRGB;
}
result = colorFromComponents(colorComponents);
return;
}
}
result = parsePlatformColor(contextContainer, surfaceId, value);
}
}
Expand Down
Expand Up @@ -87,6 +87,12 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)

UIColor *_Nullable UIColorFromComponentsColor(const facebook::react::ColorComponents &components)
{
if (components.colorSpace == ColorSpace::DisplayP3) {
return [UIColor colorWithDisplayP3Red:components.red
green:components.green
blue:components.blue
alpha:components.alpha];
}
return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha];
}
} // anonymous namespace
Expand Down

0 comments on commit a40bd8e

Please sign in to comment.