Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Wide Gamut (DisplayP3) Colors to React Native #1

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
134b675
update normalizeColor and processColor to handle color()
ryanlntn Dec 19, 2023
396d05e
initial DisplayP3 support for iOS
ryanlntn Dec 21, 2023
f4539c6
fix testIDs
ryanlntn Dec 21, 2023
ee6d760
update js to use enum for color space
ryanlntn Jan 2, 2024
12e3c59
add global flag and reuse createColorFrom
ryanlntn Jan 2, 2024
88b16da
add static defaultColorSpace to ColorComponents
ryanlntn Jan 3, 2024
284653c
add colorSpaceFromString remove nested ternary
ryanlntn Jan 3, 2024
51414ea
fix RCTConversions and graphicsConversions
ryanlntn Jan 3, 2024
4c81a65
missed a cast
ryanlntn Jan 3, 2024
c5eab07
fix ColorComponents defaultColorSpace by providing getter
ryanlntn Jan 3, 2024
c9bddc8
update graphicsConversions to use getDefaultColorSpace
ryanlntn Jan 3, 2024
53090da
fix animated interpolation without native driver
ryanlntn Jan 4, 2024
a36c9fc
handle color space in AnimatedColor
ryanlntn Jan 4, 2024
7c5eeb9
mostly fix animation with native driver
ryanlntn Jan 5, 2024
4b6a0e8
temp include example animation
ryanlntn Jan 5, 2024
53aba9b
split out ColorComponents implementation to fix Android build
ryanlntn Jan 8, 2024
9f30b94
Revert "split out ColorComponents implementation to fix Android build"
ryanlntn Jan 8, 2024
f303df1
fix RCTGetDefaultColorSpace and RCTSetDefaultColorSpace
ryanlntn Jan 9, 2024
f975b6b
refactor away from RCTColorFromComponents in RCTColorAnimatedNode
ryanlntn Jan 9, 2024
1b41b02
refactor RCTInterpolateColorInRange
ryanlntn Jan 9, 2024
6a99355
Reapply "split out ColorComponents implementation to fix Android build"
ryanlntn Jan 9, 2024
2083fa0
initial Android handling of color function values (always sRGB for now)
ryanlntn Jan 9, 2024
f396fc3
fix crash in ColorAnimatedNode
ryanlntn Jan 10, 2024
e4b0949
fix color interpolation with native driver by reprocessing as srgb
ryanlntn Jan 11, 2024
506d1ab
set ReactActivity window color mode to COLOR_MODE_WIDE_COLOR_GAMUT
ryanlntn Jan 18, 2024
96ade88
get p3 backgrounds working
ryanlntn Jan 19, 2024
d3895f9
get display-p3 borders working in Android
ryanlntn Jan 20, 2024
a8d00a4
fix issues with bad defaults
ryanlntn Jan 20, 2024
e2884ad
update remaining color annotated props to long
ryanlntn Jan 23, 2024
f21a2a5
update MapBuffer to support long values
ryanlntn Jan 26, 2024
f0e1f69
change toAndroidRepr return type to int64_t
ryanlntn Jan 27, 2024
d65366e
update toAndroidRepr to support DisplayP3
ryanlntn Jan 30, 2024
5352b6d
update TextAttributesProps to support long colors
ryanlntn Jan 30, 2024
b5431ee
update ReactForegroundColorSpan so P3 text works
ryanlntn Jan 31, 2024
366001c
fix issues with basic FlatList example
ryanlntn Jan 31, 2024
6344460
update foreground and background color spans
ryanlntn Feb 1, 2024
e8cee4d
pass longs through scroll view managers to scroll views
ryanlntn Feb 1, 2024
d07c577
update endFillColor to support long colors
ryanlntn Feb 2, 2024
c6f8d99
update switch
ryanlntn Feb 2, 2024
b072f06
clean up ReactViewBackgroundDrawable
ryanlntn Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/normalize-color/__tests__/normalizeColor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,37 @@ it('handles number colors properly', () => {
expect(normalizeColor(0x01234567)).toBe(0x01234567);
});

it('handles color function properly', () => {
expect(normalizeColor('color(display-p3 1 0 0)')).toEqual({
space: 'display-p3',
r: 1,
g: 0,
b: 0,
a: 1,
});
expect(normalizeColor('color(display-p3 1 0 0 / 0.5)')).toEqual({
space: 'display-p3',
r: 1,
g: 0,
b: 0,
a: 0.5,
});
expect(normalizeColor('color(srgb 1 0 0)')).toEqual({
space: 'srgb',
r: 1,
g: 0,
b: 0,
a: 1,
});
expect(normalizeColor('color(srgb 1 0 0 / 0.5)')).toEqual({
space: 'srgb',
r: 1,
g: 0,
b: 0,
a: 0.5,
});
});

it('returns the same color when it is already normalized', () => {
const normalizedColor = normalizeColor('red') || 0;
expect(normalizeColor(normalizedColor)).toBe(normalizedColor);
Expand Down
26 changes: 26 additions & 0 deletions packages/normalize-color/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ function normalizeColor(color) {
);
}

if ((match = matchers.color.exec(color))) {
return match[2]
? {
space: match[2],
r: parseFloat(match[3]),
g: parseFloat(match[4]),
b: parseFloat(match[5]),
a: 1,
}
: {
space: match[6],
r: parseFloat(match[7]),
g: parseFloat(match[8]),
b: parseFloat(match[9]),
a: parseFloat(match[10]),
};
}

return null;
}

Expand Down Expand Up @@ -209,6 +227,7 @@ function hwbToRgb(h, w, b) {
);
}

const COLOR_SPACE = 'display-p3|srgb';
const NUMBER = '[-+]?\\d*\\.?\\d+';
const PERCENTAGE = NUMBER + '%';

Expand All @@ -235,6 +254,13 @@ let cachedMatchers;
function getMatchers() {
if (cachedMatchers === undefined) {
cachedMatchers = {
color: new RegExp(
'color(' +
call(COLOR_SPACE, NUMBER, NUMBER, NUMBER) +
'|' +
callWithSlashSeparator(COLOR_SPACE, NUMBER, NUMBER, NUMBER, NUMBER) +
')',
),
rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)),
rgba: new RegExp(
'rgba(' +
Expand Down
16 changes: 15 additions & 1 deletion packages/normalize-color/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,18 @@
* @flow strict
*/

declare module.exports: (color: ?(string | number)) => null | number;
enum ColorSpace {
SRGB = 'srgb',
DisplayP3 = 'display-p3',
}

export type RgbaValue = {
+space?: ColorSpace,
+r: number,
+g: number,
+b: number,
+a: number,
...
};

declare module.exports: (color: ?(string | number)) => null | number | RgbaValue;
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ function isRgbaAnimatedValue(value: any): boolean {
}

export default class AnimatedColor extends AnimatedWithChildren {
space: ?string;
r: AnimatedValue;
g: AnimatedValue;
b: AnimatedValue;
Expand Down Expand Up @@ -142,6 +143,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
this.nativeColor = (processedColor: NativeColorValue);
}

this.space = initColor.space;
this.r = new AnimatedValue(initColor.r);
this.g = new AnimatedValue(initColor.g);
this.b = new AnimatedValue(initColor.b);
Expand Down Expand Up @@ -266,6 +268,10 @@ export default class AnimatedColor extends AnimatedWithChildren {
__getValue(): ColorValue {
if (this.nativeColor != null) {
return this.nativeColor;
} else if (this.space) {
return `color(${
this.space
} ${this.r.__getValue()} ${this.g.__getValue()} ${this.b.__getValue()} / ${this.a.__getValue()})`;
} else {
return `rgba(${this.r.__getValue()}, ${this.g.__getValue()}, ${this.b.__getValue()}, ${this.a.__getValue()})`;
}
Expand Down Expand Up @@ -310,6 +316,7 @@ export default class AnimatedColor extends AnimatedWithChildren {
__getNativeConfig(): {...} {
return {
type: 'color',
space: this.space,
r: this.r.__getNativeTag(),
g: this.g.__getNativeTag(),
b: this.b.__getNativeTag(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ function mapStringToNumericComponents(
| {isColor: false, components: $ReadOnlyArray<number | string>} {
let normalizedColor = normalizeColor(input);
invariant(
normalizedColor == null || typeof normalizedColor !== 'object',
normalizedColor == null ||
typeof normalizedColor !== 'object' ||
normalizedColor.hasOwnProperty('space'),
'PlatformColors are not supported',
);

Expand Down Expand Up @@ -392,7 +394,10 @@ export default class AnimatedInterpolation<
// $FlowIgnoreMe[incompatible-cast]
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
const processedColor = processColor(value);
if (typeof processedColor === 'number') {
if (
typeof processedColor === 'number' ||
processedColor.hasOwnProperty('space')
) {
outputType = 'color';
return processedColor;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ - (void)performUpdate
RCTValueAnimatedNode *bNode = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:self.config[@"b"]];
RCTValueAnimatedNode *aNode = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:self.config[@"a"]];

_color = RCTColorFromComponents(rNode.value, gNode.value, bNode.value, aNode.value);
_color = self.config[@"space"] != nil
? RCTColorFromComponents(rNode.value * 255, gNode.value * 255, bNode.value * 255, aNode.value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should pass the space, no?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cipolleschi Yeah I believe that's the better solution. Is there any value in converting back and forth from components to integer at all anymore? Or should we just create the UIColor from the color components directly?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think that at this point we can keep the UIColors. I have no idea why we used to use ints for colors, but it looks a very bad API to me.

: RCTColorFromComponents(rNode.value, gNode.value, bNode.value, aNode.value);

// TODO (T111179606): Support platform colors for color animations
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ describe('processColor', () => {
});
});

describe('color() strings', () => {
it('should convert color(s r g b)', () => {
const colorFromString = processColor('color(srgb 1 0 0)');
expect(colorFromString).toEqual({space: 'srgb', r: 1, g: 0, b: 0, a: 1});
});

it('should convert color(s r g b / a)', () => {
const colorFromString = processColor('color(display-p3 1 0 0 / 0.5)');
expect(colorFromString).toEqual({
space: 'display-p3',
r: 1,
g: 0,
b: 0,
a: 0.5,
});
});
});

describe('RGB strings', () => {
it('should convert rgb(x, y, z)', () => {
const colorFromString = processColor('rgb(10, 20, 30)');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ describe('processColorArray', () => {
expect(colorFromStringArray).toEqual(expectedIntArray);
});

it('should convert array of color type color(display-p3 x y z)', () => {
const colorFromDisplayP3Array = processColorArray([
'color(display-p3 0.1 0.2 0.3)',
'color(display-p3 0.2 0.3 0.4)',
'color(display-p3 0.3 0.4 0.5)',
]);
expect(colorFromDisplayP3Array).toEqual([
{space: 'display-p3', r: 0.1, g: 0.2, b: 0.3, a: 1},
{space: 'display-p3', r: 0.2, g: 0.3, b: 0.4, a: 1},
{space: 'display-p3', r: 0.3, g: 0.4, b: 0.5, a: 1},
]);
});

it('should convert array of color type rgb(x, y, z)', () => {
const colorFromRGBArray = processColorArray([
'rgb(10, 20, 30)',
Expand Down
17 changes: 17 additions & 0 deletions packages/react-native/React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
#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 +101,13 @@ typedef NSURL RCTFileURL;

+ (CGAffineTransform)CGAffineTransform:(id)json;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#import "RCTParserUtils.h"
#import "RCTUtils.h"

#import <react/renderer/graphics/ColorComponents.h>
Copy link
Collaborator

@cipolleschi cipolleschi Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for us: this might require us to update the podspec to include react-graphics, if that's not like this already. Not a problem, but just mentioning not to forget about it if we see failures in CI due to Dynamic Frameworks.


@implementation RCTConvert

RCT_CONVERTER(id, id, self)
Expand Down Expand Up @@ -439,7 +441,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC
mapping = temporaryMapping;
});

UIKeyboardType type = RCTConvertEnumValue("UIKeyboardType", mapping, @(UIKeyboardTypeDefault), json).integerValue;
UIKeyboardType type = (UIKeyboardType)RCTConvertEnumValue("UIKeyboardType", mapping, @(UIKeyboardTypeDefault), json).integerValue;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unrelated. I appreaciate the effort of improving the current codebase, but I'd rather keep the work here focused on wide gamut.
Could you put this line update in a different PR? 🙏

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was trying to avoid this as well but after converting this file to ObjC++ these were required to get this compiling again. They aren't required with just ObjC. Should I include converting this to ObjC++ as well in that separate PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, let's keep it here in this case.

return type;
}

Expand Down Expand Up @@ -844,7 +846,7 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json
RCTAssert([UIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector.");
Class klass = [UIColor class];
IMP imp = [klass methodForSelector:selector];
id (*getSemanticColorObject)(id, SEL) = (void *)imp;
id (*getSemanticColorObject)(id, SEL) = (id (*)(id, SEL))imp;
id colorObject = getSemanticColorObject(klass, selector);
if ([colorObject isKindOfClass:[UIColor class]]) {
color = colorObject;
Expand Down Expand Up @@ -878,6 +880,38 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json
return names;
}

static RCTColorSpace defaultColorSpace = (RCTColorSpace)facebook::react::defaultColorSpace;
RCTColorSpace RCTGetDefaultColorSpace(void)
{
return (RCTColorSpace)facebook::react::defaultColorSpace;
}
void RCTSetDefaultColorSpace(RCTColorSpace colorSpace)
{
facebook::react::setDefaultColorSpace((facebook::react::ColorSpace)colorSpace);
}

+ (UIColor *)createColorFrom:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a
{
RCTColorSpace space = RCTGetDefaultColorSpace();
return [self createColorFrom:r green:g blue:b alpha:a andColorSpace:space];
}
+ (UIColor *)createColorFrom:(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)colorSpaceFromString:(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 @@ -886,7 +920,7 @@ + (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]]
return [self createColorFrom:[self CGFloat:components[0]]
green:[self CGFloat:components[1]]
blue:[self CGFloat:components[2]]
alpha:alpha];
Expand All @@ -896,11 +930,19 @@ + (UIColor *)UIColor:(id)json
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 createColorFrom: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 ([@[@"display-p3", @"srgb"] containsObject:rawColorSpace]) {
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 colorSpaceFromString: rawColorSpace];
return [self createColorFrom: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
7 changes: 6 additions & 1 deletion packages/react-native/React/Fabric/RCTConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

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

#import <react/renderer/components/view/AccessibilityPrimitives.h>
Expand Down Expand Up @@ -53,7 +54,11 @@ inline UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::Share
}

auto components = facebook::react::colorComponentsFromColor(sharedColor);
return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha];
return [RCTConvert createColorFrom:components.red
green:components.green
blue:components.blue
alpha:components.alpha
andColorSpace:(RCTColorSpace)components.colorSpace];
}

inline CF_RETURNS_RETAINED CGColorRef _Nullable RCTCreateCGColorRefFromSharedColor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ inline void fromRawValue(
colorComponents.blue = items.at(2);
colorComponents.alpha = length == 4 ? items.at(3) : 1.0f;
} else {
if (value.hasType<std::unordered_map<std::string, RawValue>>()) {
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(context, value);
return;
}
Expand Down
Loading
Loading