diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-conicGradient-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-conicGradient-itest.js new file mode 100644 index 000000000000..d9e7e6e8c272 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-conicGradient-itest.js @@ -0,0 +1,327 @@ +/** + * 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. + * + * @flow strict-local + * @fantom_flags enableNativeCSSParsing:* + * @fantom_flags enableCppPropsIteratorSetter:* + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {ColorValue} from '../../../StyleSheet/StyleSheet'; +import type { + BackgroundImageValue, + RadialGradientPosition, +} from '../../../StyleSheet/StyleSheetTypes'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {View} from 'react-native'; + +type BackgroundImage = string | ReadonlyArray; + +function mountedBackgroundImage(backgroundImage: BackgroundImage): ?string { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + return root.getRenderedOutput({props: ['backgroundImage']}).toJSONObject() + .props.backgroundImage; +} + +const colors: ReadonlyArray<{ + color: ColorValue, + positions?: ReadonlyArray, +}> = [{color: 'red'}, {color: 'blue'}]; +const serializedColors = 'rgba(255, 0, 0, 1), rgba(0, 0, 255, 1)'; + +describe('conic gradient native props', () => { + // Assert the complete native value: matching only the right/bottom suffix + // would miss stale top/left defaults, which take precedence on iOS. + const positions: Array<{ + css: string, + object: RadialGradientPosition, + expected: string, + }> = [ + {css: 'center', object: {left: '50%', top: '50%'}, expected: '50% 50%'}, + {css: '25% 75%', object: {left: '25%', top: '75%'}, expected: '25% 75%'}, + { + css: 'left 10px top 20px', + object: {left: 10, top: 20}, + expected: '10px 20px', + }, + { + css: 'right 10px top 20px', + object: {right: 10, top: 20}, + expected: '20px 10px', + }, + { + css: 'left 10px bottom 20px', + object: {left: 10, bottom: 20}, + expected: '10px 20px', + }, + { + css: 'right 10px bottom 20px', + object: {right: 10, bottom: 20}, + expected: '10px 20px', + }, + { + css: 'bottom 20px right 10px', + object: {bottom: 20, right: 10}, + expected: '10px 20px', + }, + { + css: 'right 0px bottom 0px', + object: {right: 0, bottom: 0}, + expected: '0px 0px', + }, + { + css: 'right 10% bottom 20%', + object: {right: '10%', bottom: '20%'}, + expected: '10% 20%', + }, + { + css: 'right -10px bottom -20px', + object: {right: -10, bottom: -20}, + expected: '-10px -20px', + }, + { + css: 'right 110% bottom 120%', + object: {right: '110%', bottom: '120%'}, + expected: '110% 120%', + }, + { + css: 'right 0.5px bottom 1.25px', + object: {right: 0.5, bottom: 1.25}, + expected: '0.5px 1.25px', + }, + ]; + + for (const {css, object, expected} of positions) { + it(`preserves string position ${css}`, () => { + expect( + mountedBackgroundImage( + `conic-gradient(from 0deg at ${css}, red, blue)`, + ), + ).toBe( + `[conic-gradient(from 0deg at ${expected} , ${serializedColors})]`, + ); + }); + it(`preserves object position ${JSON.stringify(object)}`, () => { + expect( + mountedBackgroundImage([ + {type: 'conic-gradient', position: object, colorStops: colors}, + ]), + ).toBe( + `[conic-gradient(from 0deg at ${expected} , ${serializedColors})]`, + ); + }); + } + + for (const {position, expected} of [ + {position: {right: 10}, expected: '50% 10px'}, + {position: {bottom: 20}, expected: '50% 20px'}, + {position: {left: 10}, expected: '10px 50%'}, + {position: {top: 20}, expected: '50% 20px'}, + ]) { + it(`centers only the unspecified axis for ${JSON.stringify(position)}`, () => { + expect( + // $FlowFixMe[incompatible-type] - exercise partial positions from untyped callers. + mountedBackgroundImage([ + {type: 'conic-gradient', position, colorStops: colors}, + ]), + ).toBe( + `[conic-gradient(from 0deg at ${expected} , ${serializedColors})]`, + ); + }); + } + + for (const [angle, expected] of [ + ['0', '0'], + ['90deg', '90'], + ['100grad', '90'], + ['0.25turn', '90'], + ['-90deg', '-90'], + ['720deg', '720'], + ]) { + for (const syntax of ['string', 'object']) { + it(`preserves ${angle} in ${syntax} syntax`, () => { + const input: BackgroundImage = + syntax === 'string' + ? `conic-gradient(from ${angle}, red, blue)` + : [{type: 'conic-gradient', from: angle, colorStops: colors}]; + expect(mountedBackgroundImage(input)).toBe( + `[conic-gradient(from ${expected}deg at 50% 50% , ${serializedColors})]`, + ); + }); + } + } + + it('expands two-position hard stops without losing their order', () => { + expect( + mountedBackgroundImage( + 'conic-gradient(red 0deg 90deg, blue 90deg 1turn)', + ), + ).toBe( + '[conic-gradient(from 0deg at 50% 50% , rgba(255, 0, 0, 1) 0%, rgba(255, 0, 0, 1) 25%, rgba(0, 0, 255, 1) 25%, rgba(0, 0, 255, 1) 100%)]', + ); + }); + + for (const invalid of [ + 'conic-gradient()', + 'conic-gradient(red)', + 'conic-gradient(from 10px, red, blue)', + 'conic-gradient(from 1, red, blue)', + 'conic-gradient(from 50%, red, blue)', + 'conic-gradient(from, red, blue)', + 'conic-gradient(at, red, blue)', + 'conic-gradient(at right left, red, blue)', + 'conic-gradient(at right 10px left 20px, red, blue)', + 'conic-gradient(red 10px, blue)', + 'conic-gradient(red 20, blue)', + 'conic-gradient(20%, red, blue)', + 'conic-gradient(red, blue, 20%)', + 'conic-gradient(red, 20%, 30%, blue)', + 'conic-gradient(red,, blue)', + 'conic-gradient(, red, blue)', + 'conic-gradient(red, blue,)', + 'conic-gradient(red, not-a-color)', + 'conic-gradient(from 90deg red, blue)', + 'conic-gradient(at center red, blue)', + ]) { + it(`rejects ${invalid}`, () => { + expect(mountedBackgroundImage(invalid)).toBeUndefined(); + }); + } + + for (const colorStops of [ + [{color: 'red', positions: ['0deg', '45deg', '90deg']}, {color: 'blue'}], + [{positions: ['20%']}, {color: 'red'}, {color: 'blue'}], + [{color: 'red'}, {color: 'blue'}, {positions: ['20%']}], + [ + {color: 'red'}, + {positions: ['20%']}, + {positions: ['30%']}, + {color: 'blue'}, + ], + [{color: 'red'}, {positions: ['20%', '30%']}, {color: 'blue'}], + [ + {color: 'red'}, + {color: 'not-a-color', positions: ['25%']}, + {color: 'blue'}, + ], + ]) { + it(`rejects malformed object stops through native props: ${JSON.stringify(colorStops)}`, () => { + const input = [{type: 'conic-gradient', colorStops}]; + // $FlowFixMe[incompatible-type] - exercise malformed stops from untyped callers. + expect(mountedBackgroundImage(input)).toBeUndefined(); + }); + } + + for (const hint of [ + {positions: ['90deg']}, + {color: null, positions: ['90deg']}, + ]) { + it(`preserves a valid object hint ${JSON.stringify(hint)}`, () => { + const input = [ + { + type: 'conic-gradient', + colorStops: [{color: 'red'}, hint, {color: 'blue'}], + }, + ]; + // $FlowFixMe[incompatible-type] - gradient hint types currently require a color. + expect(mountedBackgroundImage(input)).toBe( + '[conic-gradient(from 0deg at 50% 50% , rgba(255, 0, 0, 1), rgba(0, 0, 0, 0) 25%, rgba(0, 0, 255, 1))]', + ); + }); + } + + it('preserves layer order when conic, linear, and radial gradients are mixed', () => { + expect( + mountedBackgroundImage( + 'conic-gradient(red, blue), linear-gradient(red, blue), radial-gradient(red, blue)', + ), + ).toBe( + `[conic-gradient(from 0deg at 50% 50% , ${serializedColors}), linear-gradient(180deg, ${serializedColors}), radial-gradient(ellipse farthest-corner at 50% 50% , ${serializedColors})]`, + ); + }); + + const updates: Array<{ + name: string, + before: BackgroundImage, + after: BackgroundImage | void, + expected: ?string, + }> = [ + { + name: 'replaces left/top with right/bottom', + before: 'conic-gradient(at left 30px top 40px, red, blue)', + after: [ + { + type: 'conic-gradient', + position: {right: 10, bottom: 20}, + colorStops: colors, + }, + ], + expected: `[conic-gradient(from 0deg at 10px 20px , ${serializedColors})]`, + }, + { + name: 'resets edge offsets to the default center', + before: 'conic-gradient(at right 10px bottom 20px, red, blue)', + after: 'conic-gradient(red, blue)', + expected: `[conic-gradient(from 0deg at 50% 50% , ${serializedColors})]`, + }, + { + name: 'removes a gradient with an empty list', + before: 'conic-gradient(red, blue)', + after: [], + expected: undefined, + }, + { + name: 'removes a gradient with undefined', + before: 'conic-gradient(red, blue)', + after: undefined, + expected: undefined, + }, + { + name: 'clears a gradient after an invalid replacement', + before: 'conic-gradient(red, blue)', + after: 'conic-gradient(red 10px, blue)', + expected: undefined, + }, + { + name: 'replaces a conic gradient with a linear gradient', + before: 'conic-gradient(red, blue)', + after: 'linear-gradient(red, blue)', + expected: `[linear-gradient(180deg, ${serializedColors})]`, + }, + { + name: 'recovers after invalid input', + before: 'conic-gradient(red 10px, blue)', + after: 'conic-gradient(red, blue)', + expected: `[conic-gradient(from 0deg at 50% 50% , ${serializedColors})]`, + }, + ]; + for (const {name, before, after, expected} of updates) { + it(name, () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + , + ); + }); + Fantom.runTask(() => { + root.render( + , + ); + }); + expect( + root.getRenderedOutput({props: ['backgroundImage']}).toJSONObject() + .props.backgroundImage, + ).toBe(expected); + }); + } +}); diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index 4e3ebfbf8425..98529da1f0b7 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -352,24 +352,55 @@ describe('', () => { ], }} /> + + , ); }); - const expectedProps = { + const expectedRadialProps = { backgroundImage: '[radial-gradient(ellipse farthest-corner at 50% 50% , rgba(230, 100, 101, 1), rgba(145, 152, 229, 1))]', }; + const expectedConicProps = { + backgroundImage: + '[conic-gradient(from 45deg at 50% 50% , rgba(230, 100, 101, 1), rgba(145, 152, 229, 1))]', + }; expect(root.getRenderedOutput().toJSON()).toEqual([ { children: [], - props: expectedProps, + props: expectedRadialProps, + type: 'View', + }, + { + children: [], + props: expectedRadialProps, + type: 'View', + }, + { + children: [], + props: expectedConicProps, type: 'View', }, { children: [], - props: expectedProps, + props: expectedConicProps, type: 'View', }, ]); diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-nativeCSSParsing-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-nativeCSSParsing-itest.js index d82df171b772..eba8b1b3d7ce 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-nativeCSSParsing-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-nativeCSSParsing-itest.js @@ -71,5 +71,28 @@ describe(' native CSS parsing', () => { expect(backgroundImage).toContain('linear-gradient'); expect(backgroundImage).toContain('rgba(230, 100, 101, 1)'); }); + + it('parses a conic-gradient()', () => { + const backgroundImage = mountedProp( + { + backgroundImage: 'conic-gradient(from 45deg, #e66465, #9198e5)', + }, + 'backgroundImage', + ); + expect(backgroundImage).toBe( + '[conic-gradient(from 45deg at 50% 50% , rgba(230, 100, 101, 1), rgba(145, 152, 229, 1))]', + ); + }); + + it('parses a four-value conic-gradient position', () => { + expect( + mountedProp( + { + backgroundImage: 'conic-gradient(at top 20px left 10px, red, blue)', + }, + 'backgroundImage', + ), + ).toContain('at 10px 20px'); + }); }); }); diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 0e926394122a..05f07b64221a 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -769,7 +769,18 @@ type RadialGradientValue = { }>, }; -export type BackgroundImageValue = LinearGradientValue | RadialGradientValue; +type ConicGradientValue = { + type: 'conic-gradient', + from?: string, + position?: RadialGradientPosition, + colorStops: ReadonlyArray<{ + color: ____ColorValue_Internal, + positions?: ReadonlyArray, + }>, +}; + +export type BackgroundImageValue = + LinearGradientValue | RadialGradientValue | ConicGradientValue; export type BackgroundSizeValue = { x: string | number, diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 5dc05646b27e..df98bfcb9b36 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -42,6 +42,97 @@ describe('processBackgroundImage', () => { ]); }); + it('should process a conic gradient string', () => { + const result = processBackgroundImage( + 'conic-gradient(from 45deg at 25% 75%, red 0deg, blue .5turn, red 100%)', + ); + + expect(result).toEqual([ + { + type: 'conic-gradient', + from: 45, + position: {left: '25%', top: '75%'}, + colorStops: [ + {color: processColor('red'), position: '0%'}, + {color: processColor('blue'), position: '50%'}, + {color: processColor('red'), position: '100%'}, + ], + }, + ]); + }); + + it('should process a conic gradient with four-value position', () => { + expect( + processBackgroundImage( + 'conic-gradient(at top 20px left 10px, red, blue)', + ), + ).toEqual([ + { + type: 'conic-gradient', + from: 0, + position: {top: 20, left: 10}, + colorStops: [ + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, + ], + }, + ]); + }); + + it('should process conic gradient object syntax', () => { + const input: ReadonlyArray = [ + { + type: 'conic-gradient', + from: '0.25turn', + position: {top: '50%', left: '50%'}, + colorStops: [ + {color: 'red', positions: ['0deg', '90deg']}, + {color: 'blue', positions: ['90deg', '360deg']}, + ], + }, + ]; + + expect(processBackgroundImage(input)).toEqual([ + { + type: 'conic-gradient', + from: 90, + position: {top: '50%', left: '50%'}, + colorStops: [ + {color: processColor('red'), position: '0%'}, + {color: processColor('red'), position: '25%'}, + {color: processColor('blue'), position: '25%'}, + {color: processColor('blue'), position: '100%'}, + ], + }, + ]); + }); + + it('should reject invalid conic gradient object stops', () => { + expect( + processBackgroundImage([ + { + type: 'conic-gradient', + colorStops: [{color: 'red'}], + }, + ]), + ).toEqual([]); + + const numericStop = [ + { + type: 'conic-gradient', + colorStops: [{color: 'red', positions: [50]}, {color: 'blue'}], + }, + ]; + // $FlowFixMe[incompatible-type] - verifies runtime validation. + expect(processBackgroundImage(numericStop)).toEqual([]); + }); + + it('should reject conic gradient length color stops', () => { + expect(processBackgroundImage('conic-gradient(red 10px, blue)')).toEqual( + [], + ); + }); + it('should process a diagonal linear gradient', () => { const input = 'linear-gradient(to bottom right, red, blue)'; const result = processBackgroundImage(input); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js index c8d7a6049b33..376808b71131 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js @@ -287,6 +287,12 @@ describe('processBackgroundPosition', () => { ]); }); + it('should parse vertical position before horizontal position', () => { + expect(processBackgroundPosition('top 75% left 25%')).toEqual([ + {top: '75%', left: '25%'}, + ]); + }); + // Test multiple background positions (comma-separated) it('should parse multiple background positions', () => { expect(processBackgroundPosition('left top, right bottom')).toEqual([ diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processConicGradient-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processConicGradient-itest.js new file mode 100644 index 000000000000..7e2cc5d0ab19 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processConicGradient-itest.js @@ -0,0 +1,285 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {BackgroundImageValue} from '../StyleSheetTypes'; + +import processBackgroundImage from '../processBackgroundImage'; +import processColor from '../processColor'; + +const red = processColor('red'); +const blue = processColor('blue'); + +describe('conic gradient processing', () => { + for (const [angle, degrees] of [ + ['0', 0], + ['-0deg', 0], + ['90deg', 90], + ['100grad', 90], + ['0.25turn', 90], + ['1.5707963267948966rad', 90], + ['-0.25turn', -90], + ['2turn', 720], + ['.5deg', 0.5], + ]) { + it(`converts ${angle} in string and object syntax`, () => { + const inputs: Array> = [ + `conic-gradient(from ${angle}, red, blue)`, + [ + { + type: 'conic-gradient', + from: angle, + colorStops: [{color: 'red'}, {color: 'blue'}], + }, + ], + ]; + for (const input of inputs) { + const result = processBackgroundImage(input); + expect(result).toHaveLength(1); + expect(result[0].from).toBeCloseTo(degrees); + expect(result[0].position).toEqual({left: '50%', top: '50%'}); + expect(result[0].colorStops).toEqual([ + {color: red, position: null}, + {color: blue, position: null}, + ]); + } + }); + } + + for (const [css, expected] of [ + [ + 'red 0, blue 1turn', + [ + {color: red, position: '0%'}, + {color: blue, position: '100%'}, + ], + ], + [ + 'red -90deg, blue 450deg', + [ + {color: red, position: '-25%'}, + {color: blue, position: '125%'}, + ], + ], + [ + 'red 75%, blue 25%', + [ + {color: red, position: '75%'}, + {color: blue, position: '25%'}, + ], + ], + [ + 'red 100grad, blue .5turn', + [ + {color: red, position: '25%'}, + {color: blue, position: '50%'}, + ], + ], + [ + 'red 0deg 90deg, blue 90deg 360deg', + [ + {color: red, position: '0%'}, + {color: red, position: '25%'}, + {color: blue, position: '25%'}, + {color: blue, position: '100%'}, + ], + ], + [ + 'red, 90deg, blue', + [ + {color: red, position: null}, + {color: null, position: '25%'}, + {color: blue, position: null}, + ], + ], + [ + 'red 0%, 25%, blue 100%', + [ + {color: red, position: '0%'}, + {color: null, position: '25%'}, + {color: blue, position: '100%'}, + ], + ], + ]) { + it(`preserves color stops for ${css}`, () => { + expect(processBackgroundImage(`conic-gradient(${css})`)).toEqual([ + { + type: 'conic-gradient', + from: 0, + position: {left: '50%', top: '50%'}, + colorStops: expected, + }, + ]); + }); + } + + it('handles commas within color functions and transparent stops', () => { + expect( + processBackgroundImage( + 'conic-gradient(rgba(255, 0, 0, 0.5) 0%, transparent 50%, rgb(0, 0, 255) 100%)', + ), + ).toEqual([ + { + type: 'conic-gradient', + from: 0, + position: {left: '50%', top: '50%'}, + colorStops: [ + {color: processColor('rgba(255, 0, 0, 0.5)'), position: '0%'}, + {color: processColor('transparent'), position: '50%'}, + {color: blue, position: '100%'}, + ], + }, + ]); + }); + + it('preserves an object transition hint between two colors', () => { + const input = [ + { + type: 'conic-gradient', + colorStops: [{color: 'red'}, {positions: ['90deg']}, {color: 'blue'}], + }, + ]; + // $FlowFixMe[incompatible-type] - gradient hint types currently require a color. + const result = processBackgroundImage(input); + expect(result).toEqual([ + { + type: 'conic-gradient', + from: 0, + position: {left: '50%', top: '50%'}, + colorStops: [ + {color: red, position: null}, + {color: null, position: '25%'}, + {color: blue, position: null}, + ], + }, + ]); + }); + + for (const from of [ + '1', + '10px', + '50%', + 'NaNdeg', + 'Infinitydeg', + '90deg junk', + '', + ]) { + it(`rejects invalid object angle ${JSON.stringify(from)}`, () => { + expect( + processBackgroundImage([ + { + type: 'conic-gradient', + from, + colorStops: [{color: 'red'}, {color: 'blue'}], + }, + ]), + ).toEqual([]); + }); + } + + const invalidStops = [ + [], + [{color: 'red'}], + [{color: 'red', positions: ['10px']}, {color: 'blue'}], + [{color: 'red', positions: ['0deg', '45deg', '90deg']}, {color: 'blue'}], + [{positions: ['20%']}, {color: 'red'}, {color: 'blue'}], + [{color: 'red'}, {color: 'blue'}, {positions: ['20%']}], + [ + {color: 'red'}, + {positions: ['20%']}, + {positions: ['30%']}, + {color: 'blue'}, + ], + [{color: 'red'}, {color: 'not-a-color'}], + ]; + for (const colorStops of invalidStops) { + it(`rejects invalid object stops ${JSON.stringify(colorStops)}`, () => { + expect( + // $FlowFixMe[incompatible-type] - exercise malformed stops from untyped callers. + processBackgroundImage([{type: 'conic-gradient', colorStops}]), + ).toEqual([]); + }); + } + + for (const input of [ + 'conic-gradient(red, blue), conic-gradient(red 10px, blue)', + 'conic-gradient(red 10px, blue), conic-gradient(red, blue)', + 'conic-gradient(red 10px, blue), conic-gradient(red, blue), conic-gradient(red 10px, blue)', + ]) { + it(`preserves existing per-layer filtering for ${input}`, () => { + expect(processBackgroundImage(input)).toEqual([ + { + type: 'conic-gradient', + from: 0, + position: {left: '50%', top: '50%'}, + colorStops: [ + {color: red, position: null}, + {color: blue, position: null}, + ], + }, + ]); + }); + } + + it('does not mutate reusable style objects', () => { + const input: ReadonlyArray = [ + { + type: 'conic-gradient', + from: '.25turn', + position: {right: 10, bottom: 20}, + colorStops: [ + {color: 'red', positions: ['0deg', '90deg']}, + {color: 'blue'}, + ], + }, + ]; + for (const gradient of input) { + for (const stop of gradient.colorStops) { + if (stop.positions != null) { + Object.freeze(stop.positions); + } + Object.freeze(stop); + } + Object.freeze(gradient.colorStops); + if (gradient.type === 'conic-gradient') { + Object.freeze(gradient.position); + } + Object.freeze(gradient); + } + Object.freeze(input); + const expected = [ + { + type: 'conic-gradient', + from: 90, + position: {right: 10, bottom: 20}, + colorStops: [ + {color: red, position: '0%'}, + {color: red, position: '25%'}, + {color: blue, position: null}, + ], + }, + ]; + expect(processBackgroundImage(input)).toEqual(expected); + expect(processBackgroundImage(input)).toEqual(expected); + expect(input).toEqual([ + { + type: 'conic-gradient', + from: '.25turn', + position: {right: 10, bottom: 20}, + colorStops: [ + {color: 'red', positions: ['0deg', '90deg']}, + {color: 'blue'}, + ], + }, + ]); + expect(input[0].colorStops[0].positions).toEqual(['0deg', '90deg']); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index 23c0b20cc525..17c878c5076c 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -18,11 +18,14 @@ import type { RadialGradientSize, } from './StyleSheetTypes'; +import processBackgroundPosition from './processBackgroundPosition'; + const processColor = require('./processColor').default; // Pre-compiled regex patterns for performance - avoids regex compilation on each call const NEWLINE_REGEX = /\n/g; -const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/; +const GRADIENT_REGEX = + /^(linear|radial|conic)-gradient\(((?:\([^)]*\)|[^()])*)\)/; const COMMA_SPLIT_REGEX = /,(?![^(]*\))/; const WHITESPACE_SPLIT_REGEX = /\s+/; const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g; @@ -70,13 +73,32 @@ type RadialGradientBackgroundImage = { }>, }; +// Conic Gradient +const DEFAULT_CONIC_FROM = 0; +const DEFAULT_CONIC_POSITION: RadialGradientPosition = { + top: '50%', + left: '50%', +}; + +type ConicGradientBackgroundImage = { + type: 'conic-gradient', + from: number, + position: RadialGradientPosition, + colorStops: ReadonlyArray<{ + color: ColorStopColor, + position: ColorStopPosition, + }>, +}; + // null color indicate that the transition hint syntax is used. e.g. red, 20%, blue type ColorStopColor = ProcessedColorValue | null; // percentage or pixel value type ColorStopPosition = number | string | null; type ParsedBackgroundImageValue = - LinearGradientBackgroundImage | RadialGradientBackgroundImage; + | LinearGradientBackgroundImage + | RadialGradientBackgroundImage + | ConicGradientBackgroundImage; export default function processBackgroundImage( backgroundImage: ?(ReadonlyArray | string), @@ -92,7 +114,10 @@ export default function processBackgroundImage( ); } else if (Array.isArray(backgroundImage)) { for (const bgImage of backgroundImage) { - const processedColorStops = processColorStops(bgImage); + const processedColorStops = processColorStops( + bgImage, + bgImage.type === 'conic-gradient', + ); if (processedColorStops == null) { // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. return []; @@ -184,6 +209,29 @@ export default function processBackgroundImage( position, colorStops: processedColorStops, }); + } else if (bgImage.type === 'conic-gradient') { + let from = DEFAULT_CONIC_FROM; + if (bgImage.from != null) { + const parsedFrom = getAngleInDegrees(bgImage.from, true); + if (parsedFrom == null) { + return []; + } + from = parsedFrom; + } + + if ( + processedColorStops.filter(colorStop => colorStop.color != null) + .length < 2 + ) { + return []; + } + + result = result.concat({ + type: 'conic-gradient', + from, + position: bgImage.position ?? {...DEFAULT_CONIC_POSITION}, + colorStops: processedColorStops, + }); } } } @@ -191,7 +239,10 @@ export default function processBackgroundImage( return result; } -function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{ +function processColorStops( + bgImage: BackgroundImageValue, + allowAngularPositions: boolean = false, +): ReadonlyArray<{ color: ColorStopColor, position: ColorStopPosition, }> | null { @@ -203,25 +254,33 @@ function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{ for (let index = 0; index < bgImage.colorStops.length; index++) { const colorStop = bgImage.colorStops[index]; const positions = colorStop.positions; + if ( + bgImage.type === 'conic-gradient' && + ((positions != null && positions.length > 2) || + (colorStop.color == null && + (index === 0 || + index === bgImage.colorStops.length - 1 || + bgImage.colorStops[index - 1].color == null))) + ) { + return null; + } // Color transition hint syntax (red, 20%, blue) if ( colorStop.color == null && Array.isArray(positions) && positions.length === 1 ) { - const position = positions[0]; - if ( - typeof position === 'number' || - (typeof position === 'string' && position.endsWith('%')) - ) { - processedColorStops.push({ - color: null, - position, - }); - } else { - // If a position is invalid, return null and do not apply gradient. Same as web. + const position = parseColorStopPosition( + positions[0], + allowAngularPositions, + ); + if (position == null) { return null; } + processedColorStops.push({ + color: null, + position, + }); } else { const processedColor = processColor(colorStop.color); if (processedColor == null) { @@ -229,19 +288,18 @@ function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{ return null; } if (positions != null && positions.length > 0) { - for (const position of positions) { - if ( - typeof position === 'number' || - (typeof position === 'string' && position.endsWith('%')) - ) { - processedColorStops.push({ - color: processedColor, - position, - }); - } else { - // If a position is invalid, return null and do not apply gradient. Same as web. + for (const rawPosition of positions) { + const position = parseColorStopPosition( + rawPosition, + allowAngularPositions, + ); + if (position == null) { return null; } + processedColorStops.push({ + color: processedColor, + position, + }); } } else { processedColorStops.push({ @@ -266,10 +324,12 @@ function parseBackgroundImageCSSString( const match = GRADIENT_REGEX.exec(bgImage); if (match) { const [, type, gradientContent] = match; - const isRadial = type.toLowerCase() === 'radial'; - const gradient = isRadial - ? parseRadialGradientCSSString(gradientContent) - : parseLinearGradientCSSString(gradientContent); + const gradient = + type === 'radial' + ? parseRadialGradientCSSString(gradientContent) + : type === 'conic' + ? parseConicGradientCSSString(gradientContent) + : parseLinearGradientCSSString(gradientContent); if (gradient != null) { gradients.push(gradient); @@ -593,6 +653,64 @@ function parseRadialGradientCSSString( }; } +function parseConicGradientCSSString( + gradientContent: string, +): ConicGradientBackgroundImage | null { + const parts = gradientContent.split(COMMA_SPLIT_REGEX); + const firstPart = parts[0]?.trim() ?? ''; + let from = DEFAULT_CONIC_FROM; + let position: RadialGradientPosition = {...DEFAULT_CONIC_POSITION}; + let hasPrelude = false; + + if (firstPart.startsWith('from ')) { + const match = /^from\s+(\S+)(?:\s+at\s+(.+))?$/.exec(firstPart); + if (match == null) { + return null; + } + + const parsedFrom = getAngleInDegrees(match[1], true); + if (parsedFrom == null) { + return null; + } + from = parsedFrom; + hasPrelude = true; + + if (match[2] != null) { + const parsedPositions = processBackgroundPosition(match[2]); + if (parsedPositions.length !== 1) { + return null; + } + position = parsedPositions[0]; + } + } else if (firstPart.startsWith('at ')) { + const parsedPositions = processBackgroundPosition(firstPart.slice(3)); + if (parsedPositions.length !== 1) { + return null; + } + position = parsedPositions[0]; + hasPrelude = true; + } + + if (hasPrelude) { + parts.shift(); + } + + const colorStops = parseColorStopsCSSString(parts, true); + if ( + colorStops == null || + colorStops.filter(colorStop => colorStop.color != null).length < 2 + ) { + return null; + } + + return { + type: 'conic-gradient', + from, + position, + colorStops, + }; +} + function parseLinearGradientCSSString( gradientContent: string, ): LinearGradientBackgroundImage | null { @@ -636,7 +754,10 @@ function parseLinearGradientCSSString( }; } -function parseColorStopsCSSString(parts: Array): Array<{ +function parseColorStopsCSSString( + parts: Array, + allowAngularPositions: boolean = false, +): Array<{ color: ColorStopColor, position: ColorStopPosition, }> | null { @@ -660,8 +781,14 @@ function parseColorStopsCSSString(parts: Array): Array<{ // Case 1: [color, position, position] if (colorStopParts.length === 3) { const color = colorStopParts[0]; - const position1 = getPositionFromCSSValue(colorStopParts[1]); - const position2 = getPositionFromCSSValue(colorStopParts[2]); + const position1 = parseColorStopPosition( + colorStopParts[1], + allowAngularPositions, + ); + const position2 = parseColorStopPosition( + colorStopParts[2], + allowAngularPositions, + ); const processedColor = processColor(color); if (processedColor == null) { // If a color is invalid, return null and do not apply any gradient. Same as web. @@ -685,7 +812,10 @@ function parseColorStopsCSSString(parts: Array): Array<{ // Case 2: [color, position] else if (colorStopParts.length === 2) { const color = colorStopParts[0]; - const position = getPositionFromCSSValue(colorStopParts[1]); + const position = parseColorStopPosition( + colorStopParts[1], + allowAngularPositions, + ); const processedColor = processColor(color); if (processedColor == null) { // If a color is invalid, return null and do not apply any gradient. Same as web. @@ -703,13 +833,17 @@ function parseColorStopsCSSString(parts: Array): Array<{ // Case 3: [color] // Case 4: [position] => transition hint syntax else if (colorStopParts.length === 1) { - const position = getPositionFromCSSValue(colorStopParts[0]); + const position = parseColorStopPosition( + colorStopParts[0], + allowAngularPositions, + ); if (position != null) { // handle invalid transition hint syntax. transition hint syntax must have color before and after the position. e.g. red, 20%, blue if ( (prevStop != null && prevStop.length === 1 && - getPositionFromCSSValue(prevStop[0]) != null) || + parseColorStopPosition(prevStop[0], allowAngularPositions) != + null) || i === stops.length - 1 || i === 0 ) { @@ -776,32 +910,54 @@ function getDirectionForKeyword(direction?: string): ?LinearGradientDirection { } } -function getAngleInDegrees(angle?: string): ?number { +function getAngleInDegrees( + angle?: string, + allowUnitlessZero: boolean = false, +): ?number { if (angle == null) { return null; } + if (allowUnitlessZero && angle.trim() === '0') { + return 0; + } const match = angle.match(LINEAR_GRADIENT_ANGLE_UNIT_REGEX); if (!match) { return null; } const [, value, unit] = match; - const numericValue = parseFloat(value); - switch (unit) { + switch (unit.toLowerCase()) { case 'deg': return numericValue; case 'grad': - return numericValue * 0.9; // 1 grad = 0.9 degrees + return numericValue * 0.9; case 'rad': return (numericValue * 180) / Math.PI; case 'turn': - return numericValue * 360; // 1 turn = 360 degrees + return numericValue * 360; default: return null; } } +function parseColorStopPosition( + position: string | number, + allowAngularPositions: boolean, +): ColorStopPosition { + if (typeof position === 'number') { + return allowAngularPositions ? null : position; + } + if (position.endsWith('%')) { + return position; + } + if (allowAngularPositions) { + const angle = getAngleInDegrees(position, true); + return angle == null ? null : `${angle / 3.6}%`; + } + return getPositionFromCSSValue(position) ?? null; +} + function getPositionFromCSSValue(position: string) { if (position.endsWith('px')) { return parseFloat(position); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js b/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js index 0b3800c4966e..2aa514a62438 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundPosition.js @@ -219,16 +219,28 @@ const parseBackgroundPositionCSSString = ( if (value1 == null || value2 == null) { return []; } - if (keyword1 === 'left') { - left = value1; - } else if (keyword1 === 'right') { - right = value1; - } - - if (keyword2 === 'top') { - top = value2; - } else if (keyword2 === 'bottom') { - bottom = value2; + const assignPosition = ( + keyword: string, + value: string | number, + ): boolean => { + if (keyword === 'left') { + left = value; + } else if (keyword === 'right') { + right = value; + } else if (keyword === 'top') { + top = value; + } else if (keyword === 'bottom') { + bottom = value; + } else { + return false; + } + return true; + }; + if ( + !assignPosition(keyword1, value1) || + !assignPosition(keyword2, value2) + ) { + return []; } } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 61ef98f921cd..1e8b808097a0 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -20,6 +20,7 @@ #import #import #import +#import #import #import #import @@ -1315,9 +1316,12 @@ - (void)invalidateLayer backgroundSize:backgroundSize backgroundRepeat:backgroundRepeat]; - CALayer *gradientLayer; + CALayer *gradientLayer = nil; - if (std::holds_alternative(backgroundImage)) { + if (std::holds_alternative(backgroundImage)) { + const auto &conicGradient = std::get(backgroundImage); + gradientLayer = [RCTConicGradient gradientLayerWithSize:backgroundImageSize gradient:conicGradient]; + } else if (std::holds_alternative(backgroundImage)) { const auto &linearGradient = std::get(backgroundImage); gradientLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; } else if (std::holds_alternative(backgroundImage)) { diff --git a/packages/react-native/React/Fabric/Utils/RCTConicGradient.h b/packages/react-native/React/Fabric/Utils/RCTConicGradient.h new file mode 100644 index 000000000000..05a570776dda --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTConicGradient.h @@ -0,0 +1,19 @@ +/* + * 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTConicGradient : NSObject + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const facebook::react::ConicGradient &)gradient; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTConicGradient.mm b/packages/react-native/React/Fabric/Utils/RCTConicGradient.mm new file mode 100644 index 000000000000..4e9ea6b13bdf --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTConicGradient.mm @@ -0,0 +1,63 @@ +/* + * 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 "RCTConicGradient.h" + +#import "RCTGradientUtils.h" + +#import +#import + +using namespace facebook::react; + +@implementation RCTConicGradient + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const ConicGradient &)gradient +{ + CALayer *container = [CALayer layer]; + container.frame = CGRectMake(0.0f, 0.0f, size.width, size.height); + container.masksToBounds = YES; + if (size.width <= 0 || size.height <= 0) { + return container; + } + + CAGradientLayer *gradientLayer = [CAGradientLayer layer]; + gradientLayer.type = kCAGradientLayerConic; + + CGPoint centerPoint = CGPointMake(size.width / 2.0, size.height / 2.0); + if (gradient.position.top.has_value()) { + centerPoint.y = gradient.position.top->resolve(static_cast(size.height)); + } else if (gradient.position.bottom.has_value()) { + centerPoint.y = size.height - gradient.position.bottom->resolve(static_cast(size.height)); + } + if (gradient.position.left.has_value()) { + centerPoint.x = gradient.position.left->resolve(static_cast(size.width)); + } else if (gradient.position.right.has_value()) { + centerPoint.x = size.width - gradient.position.right->resolve(static_cast(size.width)); + } + + // A square layer keeps the conic sweep's angles independent of the view's aspect ratio. + CGFloat side = std::max(size.width, size.height); + CGPoint normalizedCenter = CGPointMake(centerPoint.x / side, centerPoint.y / side); + CGFloat radians = gradient.from * M_PI / 180.0; + gradientLayer.startPoint = normalizedCenter; + gradientLayer.endPoint = CGPointMake(normalizedCenter.x + std::sin(radians), normalizedCenter.y - std::cos(radians)); + + const auto colorStops = [RCTGradientUtils getFixedColorStops:gradient.colorStops gradientLineLength:1.0]; + NSMutableArray *colors = [NSMutableArray array]; + NSMutableArray *locations = [NSMutableArray array]; + [RCTGradientUtils getColors:colors andLocations:locations fromColorStops:colorStops]; + + gradientLayer.frame = CGRectMake(0.0f, 0.0f, side, side); + gradientLayer.colors = colors; + gradientLayer.locations = locations; + // Preserve the requested tile size for background repetition and clip the oversized square. + [container addSublayer:gradientLayer]; + return container; +} + +@end diff --git a/packages/react-native/React/Tests/Utils/RCTConicGradientTests.mm b/packages/react-native/React/Tests/Utils/RCTConicGradientTests.mm new file mode 100644 index 000000000000..c01aad9ac9ab --- /dev/null +++ b/packages/react-native/React/Tests/Utils/RCTConicGradientTests.mm @@ -0,0 +1,211 @@ +/* + * 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 +#import +#import +#import + +using namespace facebook::react; + +namespace { +ConicGradient hardStopGradient() +{ + const auto red = colorFromRGBA(255, 0, 0, 255); + const auto blue = colorFromRGBA(0, 0, 255, 255); + const auto green = colorFromRGBA(0, 255, 0, 255); + return { + .colorStops = { + {red, {0, UnitType::Percent}}, + {red, {12.5, UnitType::Percent}}, + {blue, {12.5, UnitType::Percent}}, + {blue, {50, UnitType::Percent}}, + {green, {50, UnitType::Percent}}, + {green, {100, UnitType::Percent}}}}; +} + +UIImage *renderLayer(CALayer *layer, CGSize size) +{ + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1; + format.opaque = NO; + format.preferredRange = UIGraphicsImageRendererFormatRangeStandard; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:format]; + return [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [layer renderInContext:context.CGContext]; + }]; +} +} // namespace + +@interface RCTConicGradientTests : XCTestCase +@end + +@implementation RCTConicGradientTests + +- (void)assertImage:(UIImage *)image atPoint:(CGPoint)point hasColor:(UIColor *)expected +{ + CGImageRef pixel = CGImageCreateWithImageInRect(image.CGImage, CGRectMake(point.x, point.y, 1, 1)); + XCTAssertNotEqual(pixel, nullptr); + if (pixel == nullptr) { + return; + } + uint8_t rgba[4] = {}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = CGBitmapContextCreate( + rgba, + 1, + 1, + 8, + 4, + colorSpace, + static_cast(kCGImageAlphaPremultipliedLast) | kCGBitmapByteOrder32Big); + XCTAssertNotEqual(context, nullptr); + if (context != nullptr) { + CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), pixel); + CGFloat red, green, blue, alpha; + [expected getRed:&red green:&green blue:&blue alpha:&alpha]; + // Compare sectors, allowing Core Animation color-space conversion. + XCTAssertEqualWithAccuracy(rgba[0] / 255.0, red * alpha, 0.25, @"red at %@", NSStringFromCGPoint(point)); + XCTAssertEqualWithAccuracy(rgba[1] / 255.0, green * alpha, 0.25, @"green at %@", NSStringFromCGPoint(point)); + XCTAssertEqualWithAccuracy(rgba[2] / 255.0, blue * alpha, 0.25, @"blue at %@", NSStringFromCGPoint(point)); + XCTAssertEqualWithAccuracy(rgba[3] / 255.0, alpha, 0.04, @"alpha at %@", NSStringFromCGPoint(point)); + CGContextRelease(context); + } + CGColorSpaceRelease(colorSpace); + CGImageRelease(pixel); +} + +- (void)assertUnrotatedSectors:(UIImage *)image center:(CGPoint)center +{ + // Samples straddle the 45-degree hard stop, away from antialiased edges. + // Cardinal-only samples would not detect the rectangular-layer distortion. + [self assertImage:image atPoint:CGPointMake(center.x + 4, center.y - 7) hasColor:UIColor.redColor]; + [self assertImage:image atPoint:CGPointMake(center.x + 7, center.y - 4) hasColor:UIColor.blueColor]; + [self assertImage:image atPoint:CGPointMake(center.x + 6, center.y + 6) hasColor:UIColor.blueColor]; + [self assertImage:image atPoint:CGPointMake(center.x - 6, center.y + 6) hasColor:UIColor.greenColor]; + [self assertImage:image atPoint:CGPointMake(center.x - 6, center.y - 6) hasColor:UIColor.greenColor]; +} + +- (void)testSquareGradientProvidesRenderingControl +{ + const CGSize size = CGSizeMake(120, 120); + CALayer *layer = [RCTConicGradient gradientLayerWithSize:size gradient:hardStopGradient()]; + [self assertUnrotatedSectors:renderLayer(layer, size) center:CGPointMake(60, 60)]; +} + +- (void)testWideGradientPreservesAnglesAndTileSize +{ + const CGSize size = CGSizeMake(240, 120); + CALayer *layer = [RCTConicGradient gradientLayerWithSize:size gradient:hardStopGradient()]; + XCTAssertTrue(CGSizeEqualToSize(layer.bounds.size, size)); + [self assertUnrotatedSectors:renderLayer(layer, size) center:CGPointMake(120, 60)]; +} + +- (void)testTallGradientPreservesAnglesAndTileSize +{ + const CGSize size = CGSizeMake(120, 240); + CALayer *layer = [RCTConicGradient gradientLayerWithSize:size gradient:hardStopGradient()]; + XCTAssertTrue(CGSizeEqualToSize(layer.bounds.size, size)); + [self assertUnrotatedSectors:renderLayer(layer, size) center:CGPointMake(60, 120)]; +} + +- (void)testExtremeAspectRatios +{ + const CGSize wide = CGSizeMake(600, 30); + const CGSize tall = CGSizeMake(30, 600); + [self assertUnrotatedSectors:renderLayer( + [RCTConicGradient gradientLayerWithSize:wide gradient:hardStopGradient()], wide) + center:CGPointMake(300, 15)]; + [self assertUnrotatedSectors:renderLayer( + [RCTConicGradient gradientLayerWithSize:tall gradient:hardStopGradient()], tall) + center:CGPointMake(15, 300)]; +} + +- (void)testEmptyTilesDoNotPaint +{ + for (const auto size : {CGSizeZero, CGSizeMake(0, 100), CGSizeMake(100, 0)}) { + CALayer *layer = [RCTConicGradient gradientLayerWithSize:size gradient:hardStopGradient()]; + XCTAssertTrue(CGSizeEqualToSize(layer.bounds.size, size)); + [self assertImage:renderLayer(layer, CGSizeMake(20, 20)) atPoint:CGPointMake(10, 10) hasColor:UIColor.clearColor]; + } +} + +- (void)testRotationOnRectangularGradient +{ + auto gradient = hardStopGradient(); + gradient.from = 90; + const CGSize size = CGSizeMake(240, 120); + UIImage *image = renderLayer([RCTConicGradient gradientLayerWithSize:size gradient:gradient], size); + [self assertImage:image atPoint:CGPointMake(127, 64) hasColor:UIColor.redColor]; + [self assertImage:image atPoint:CGPointMake(124, 67) hasColor:UIColor.blueColor]; + [self assertImage:image atPoint:CGPointMake(114, 54) hasColor:UIColor.greenColor]; +} + +- (void)testRightAndBottomPixelOffsets +{ + auto gradient = hardStopGradient(); + gradient.position.right = {10, UnitType::Point}; + gradient.position.bottom = {20, UnitType::Point}; + const CGSize size = CGSizeMake(240, 120); + [self assertUnrotatedSectors:renderLayer([RCTConicGradient gradientLayerWithSize:size gradient:gradient], size) + center:CGPointMake(230, 100)]; +} + +- (void)testPercentageOffsetsResolveAgainstOriginalRectangle +{ + auto gradient = hardStopGradient(); + gradient.position.right = {25, UnitType::Percent}; + gradient.position.bottom = {25, UnitType::Percent}; + const CGSize size = CGSizeMake(240, 120); + [self assertUnrotatedSectors:renderLayer([RCTConicGradient gradientLayerWithSize:size gradient:gradient], size) + center:CGPointMake(180, 90)]; +} + +- (void)testResizingRecalculatesCenter +{ + auto gradient = hardStopGradient(); + gradient.position.left = {25, UnitType::Percent}; + gradient.position.top = {75, UnitType::Percent}; + const CGSize wide = CGSizeMake(240, 120); + const CGSize tall = CGSizeMake(120, 240); + [self assertUnrotatedSectors:renderLayer([RCTConicGradient gradientLayerWithSize:wide gradient:gradient], wide) + center:CGPointMake(60, 90)]; + [self assertUnrotatedSectors:renderLayer([RCTConicGradient gradientLayerWithSize:tall gradient:gradient], tall) + center:CGPointMake(30, 180)]; +} + +- (void)testOversizedGradientDoesNotPaintOutsideItsTile +{ + const CGSize size = CGSizeMake(240, 120); + CALayer *parent = [CALayer layer]; + parent.frame = CGRectMake(0, 0, 260, 260); + [parent addSublayer:[RCTConicGradient gradientLayerWithSize:size gradient:hardStopGradient()]]; + UIImage *image = renderLayer(parent, parent.bounds.size); + [self assertImage:image atPoint:CGPointMake(100, 140) hasColor:UIColor.clearColor]; + [self assertImage:image atPoint:CGPointMake(250, 60) hasColor:UIColor.clearColor]; + [self assertUnrotatedSectors:image center:CGPointMake(120, 60)]; +} + +- (void)testRepeatedRectangularTilesKeepIndependentCenters +{ + const CGSize tileSize = CGSizeMake(120, 60); + const CGRect area = CGRectMake(0, 0, 240, 120); + CALayer *tile = [RCTConicGradient gradientLayerWithSize:tileSize gradient:hardStopGradient()]; + CALayer *layer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:area + paintingArea:area + itemSize:tileSize + backgroundPosition:BackgroundPosition {} + backgroundRepeat:BackgroundRepeat {} + itemLayer:tile]; + UIImage *image = renderLayer(layer, area.size); + [self assertUnrotatedSectors:image center:CGPointMake(60, 30)]; + [self assertUnrotatedSectors:image center:CGPointMake(180, 30)]; + [self assertUnrotatedSectors:image center:CGPointMake(60, 90)]; + [self assertUnrotatedSectors:image center:CGPointMake(180, 90)]; +} + +@end diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt index 8e5cc7d8c2a4..bbe43c54c493 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt @@ -15,10 +15,11 @@ import com.facebook.react.bridge.ReadableType /** * Represents a single layer of a background image, typically containing a gradient. * - * This class encapsulates gradient definitions (linear or radial) that can be applied as background - * layers to React Native views. It provides parsing from React Native bridge data and shader - * generation for rendering. + * This class encapsulates gradient definitions that can be applied as background layers to React + * Native views. It provides parsing from React Native bridge data and shader generation for + * rendering. * + * @see ConicGradient * @see LinearGradient * @see RadialGradient */ @@ -33,8 +34,8 @@ public class BackgroundImageLayer() { /** * Parses a ReadableMap into a BackgroundImageLayer. * - * The map should contain gradient configuration including a "type" key specifying either - * "linear-gradient" or "radial-gradient". + * The map should contain gradient configuration including a "type" key specifying a supported + * gradient type. * * @param gradientMap The map containing gradient configuration * @param context Android context for resource resolution @@ -54,6 +55,7 @@ public class BackgroundImageLayer() { } return when (gradientMap.getString("type")) { + "conic-gradient" -> ConicGradient.parse(gradientMap, context) "linear-gradient" -> LinearGradient.parse(gradientMap, context) "radial-gradient" -> RadialGradient.parse(gradientMap, context) else -> null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ConicGradient.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ConicGradient.kt new file mode 100644 index 000000000000..4ba99f6de865 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ConicGradient.kt @@ -0,0 +1,99 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager.style + +import android.content.Context +import android.graphics.Matrix +import android.graphics.Shader +import android.graphics.SweepGradient +import com.facebook.react.bridge.ColorPropConverter +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.PixelUtil.dpToPx + +internal class ConicGradient( + val from: Float, + val position: RadialGradient.Position, + val colorStops: List, +) : Gradient { + companion object { + fun parse(gradientMap: ReadableMap, context: Context): Gradient? { + if (!gradientMap.hasKey("from") || !gradientMap.hasKey("position")) { + return null + } + val from = gradientMap.getDouble("from").toFloat() + val positionMap = gradientMap.getMap("position") ?: return null + var top: LengthPercentage? = null + var left: LengthPercentage? = null + var right: LengthPercentage? = null + var bottom: LengthPercentage? = null + + if (positionMap.hasKey("top")) { + top = LengthPercentage.setFromDynamic(positionMap.getDynamic("top"), allowNegative = true) + } else if (positionMap.hasKey("bottom")) { + bottom = LengthPercentage.setFromDynamic(positionMap.getDynamic("bottom"), allowNegative = true) + } + if (positionMap.hasKey("left")) { + left = LengthPercentage.setFromDynamic(positionMap.getDynamic("left"), allowNegative = true) + } else if (positionMap.hasKey("right")) { + right = LengthPercentage.setFromDynamic(positionMap.getDynamic("right"), allowNegative = true) + } + + val colorStopsArray = gradientMap.getArray("colorStops") ?: return null + val colorStops = ArrayList(colorStopsArray.size()) + for (i in 0 until colorStopsArray.size()) { + val colorStop = colorStopsArray.getMap(i) ?: continue + val color: Int? = + when { + !colorStop.hasKey("color") || colorStop.isNull("color") -> null + colorStop.getType("color") == ReadableType.Map -> + ColorPropConverter.getColor(colorStop.getMap("color"), context) + else -> colorStop.getInt("color") + } + val stopPosition = LengthPercentage.setFromDynamic(colorStop.getDynamic("position")) + colorStops.add(ColorStop(color, stopPosition)) + } + + if (colorStops.size < 2) { + return null + } + return ConicGradient(from, RadialGradient.Position(top, left, right, bottom), colorStops) + } + } + + override fun getShader(width: Float, height: Float): Shader { + var centerX = width / 2f + var centerY = height / 2f + position.top?.let { + centerY = if (it.type == LengthPercentageType.PERCENT) it.resolve(height) else it.resolve(height).dpToPx() + } + position.bottom?.let { + centerY = if (it.type == LengthPercentageType.PERCENT) height - it.resolve(height) else height - it.resolve(height).dpToPx() + } + position.left?.let { + centerX = if (it.type == LengthPercentageType.PERCENT) it.resolve(width) else it.resolve(width).dpToPx() + } + position.right?.let { + centerX = if (it.type == LengthPercentageType.PERCENT) width - it.resolve(width) else width - it.resolve(width).dpToPx() + } + + val finalStops = ColorStopUtils.getFixedColorStops(colorStops, 1f) + val colors = IntArray(finalStops.size) + val positions = FloatArray(finalStops.size) + finalStops.forEachIndexed { index, colorStop -> + colors[index] = colorStop.color ?: 0 + positions[index] = colorStop.position ?: 0f + } + + val shader = SweepGradient(centerX, centerY, colors, positions) + shader.setLocalMatrix(Matrix().apply { setRotate(from - 90f, centerX, centerY) }) + return shader + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/style/ConicGradientTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/style/ConicGradientTest.kt new file mode 100644 index 000000000000..61957ca2c10c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/style/ConicGradientTest.kt @@ -0,0 +1,212 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager.style + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.DisplayMetrics +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset.offset +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.GraphicsMode + +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +class ConicGradientTest { + @Before + fun setUp() { + setDensity(1f) + } + + private fun setDensity(density: Float) { + val metrics = DisplayMetrics().apply { this.density = density } + DisplayMetricsHolder.setScreenDisplayMetrics(metrics) + } + + private fun percent(value: Float) = LengthPercentage(value, LengthPercentageType.PERCENT) + + private fun point(value: Float) = LengthPercentage(value, LengthPercentageType.POINT) + + private fun gradient( + from: Float = 0f, + position: RadialGradient.Position = RadialGradient.Position(null, null, null, null), + ) = + ConicGradient( + from, + position, + listOf( + ColorStop(Color.RED, percent(0f)), + ColorStop(Color.RED, percent(12.5f)), + ColorStop(Color.BLUE, percent(12.5f)), + ColorStop(Color.BLUE, percent(50f)), + ColorStop(Color.GREEN, percent(50f)), + ColorStop(Color.GREEN, percent(100f)), + ), + ) + + private fun render(gradient: Gradient, width: Int, height: Int): Bitmap { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val paint = Paint().apply { shader = gradient.getShader(width.toFloat(), height.toFloat()) } + Canvas(bitmap).drawPaint(paint) + return bitmap + } + + private fun assertColor(bitmap: Bitmap, x: Int, y: Int, expected: Int) { + val actual = bitmap.getPixel(x, y) + assertThat(Color.red(actual)) + .describedAs("red at (%s, %s)", x, y) + .isCloseTo(Color.red(expected), offset(10)) + assertThat(Color.green(actual)) + .describedAs("green at (%s, %s)", x, y) + .isCloseTo(Color.green(expected), offset(10)) + assertThat(Color.blue(actual)) + .describedAs("blue at (%s, %s)", x, y) + .isCloseTo(Color.blue(expected), offset(10)) + assertThat(Color.alpha(actual)) + .describedAs("alpha at (%s, %s)", x, y) + .isEqualTo(Color.alpha(expected)) + } + + private fun assertSectors(bitmap: Bitmap, x: Int, y: Int) { + assertColor(bitmap, x + 4, y - 7, Color.RED) + assertColor(bitmap, x + 7, y - 4, Color.BLUE) + assertColor(bitmap, x + 6, y + 6, Color.BLUE) + assertColor(bitmap, x - 6, y + 6, Color.GREEN) + assertColor(bitmap, x - 6, y - 6, Color.GREEN) + } + + @Test + fun preservesAnglesOnSquareWideAndTallViews() { + assertSectors(render(gradient(), 120, 120), 60, 60) + assertSectors(render(gradient(), 240, 120), 120, 60) + assertSectors(render(gradient(), 120, 240), 60, 120) + assertSectors(render(gradient(), 600, 30), 300, 15) + assertSectors(render(gradient(), 30, 600), 15, 300) + } + + @Test + fun rotatesClockwiseAndAcceptsNegativeAndMultiTurnAngles() { + for (angle in listOf(90f, -270f, 450f)) { + val bitmap = render(gradient(from = angle), 240, 120) + assertColor(bitmap, 127, 64, Color.RED) + assertColor(bitmap, 124, 67, Color.BLUE) + assertColor(bitmap, 114, 54, Color.GREEN) + } + } + + @Test + fun resolvesRightAndBottomOffsetsInDensityIndependentPixels() { + setDensity(2f) + val position = RadialGradient.Position(null, null, point(10f), point(20f)) + assertSectors(render(gradient(position = position), 240, 120), 220, 80) + } + + @Test + fun doesNotApplyDensityToPercentageOffsets() { + setDensity(3f) + val position = RadialGradient.Position(null, null, percent(25f), percent(25f)) + assertSectors(render(gradient(position = position), 240, 120), 180, 90) + } + + @Test + fun centersTheUnspecifiedAxis() { + val rightOnly = RadialGradient.Position(null, null, point(10f), null) + val bottomOnly = RadialGradient.Position(null, null, null, point(20f)) + assertSectors(render(gradient(position = rightOnly), 240, 120), 230, 60) + assertSectors(render(gradient(position = bottomOnly), 240, 120), 120, 100) + } + + @Test + fun recalculatesPercentagePositionsWhenResized() { + val value = gradient(position = RadialGradient.Position(percent(75f), percent(25f), null, null)) + assertSectors(render(value, 240, 120), 60, 90) + assertSectors(render(value, 120, 240), 30, 180) + } + + private fun parsePosition(position: JavaOnlyMap): Gradient { + val map = + JavaOnlyMap.of( + "from", + 0.0, + "position", + position, + "colorStops", + JavaOnlyArray.of( + JavaOnlyMap.of("color", Color.RED, "position", "0%"), + JavaOnlyMap.of("color", Color.RED, "position", "12.5%"), + JavaOnlyMap.of("color", Color.BLUE, "position", "12.5%"), + JavaOnlyMap.of("color", Color.BLUE, "position", "50%"), + JavaOnlyMap.of("color", Color.GREEN, "position", "50%"), + JavaOnlyMap.of("color", Color.GREEN, "position", "100%"), + ), + ) + val parsed = ConicGradient.parse(map, RuntimeEnvironment.getApplication()) + assertThat(parsed).isInstanceOf(ConicGradient::class.java) + return requireNotNull(parsed) + } + + @Test + fun parsesEdgeOffsetsFromNativeProps() { + val parsed = parsePosition(JavaOnlyMap.of("right", 10.0, "bottom", 20.0)) + assertSectors(render(parsed, 240, 120), 230, 100) + } + + @Test + fun parsesNegativePointOffsetsOnEveryEdge() { + setDensity(2f) + val left = render(parsePosition(JavaOnlyMap.of("left", -20.0)), 240, 120) + val top = render(parsePosition(JavaOnlyMap.of("top", -20.0)), 240, 120) + val right = render(parsePosition(JavaOnlyMap.of("right", -20.0)), 240, 120) + val bottom = render(parsePosition(JavaOnlyMap.of("bottom", -20.0)), 240, 120) + assertColor(left, 10, 20, Color.BLUE) + assertColor(top, 150, 10, Color.BLUE) + assertColor(right, 230, 20, Color.GREEN) + assertColor(bottom, 160, 115, Color.RED) + } + + @Test + fun parsesNegativePercentagesWithoutApplyingDensity() { + setDensity(3f) + val left = render(parsePosition(JavaOnlyMap.of("left", "-10%")), 240, 120) + val top = render(parsePosition(JavaOnlyMap.of("top", "-10%")), 240, 120) + val right = render(parsePosition(JavaOnlyMap.of("right", "-10%")), 240, 120) + val bottom = render(parsePosition(JavaOnlyMap.of("bottom", "-20%")), 240, 120) + assertColor(left, 10, 20, Color.RED) + assertColor(top, 150, 10, Color.BLUE) + assertColor(right, 230, 20, Color.GREEN) + assertColor(bottom, 140, 110, Color.RED) + assertColor(bottom, 160, 110, Color.BLUE) + } + + @Test + fun rendersCenterAboveAndLeftOfTheViewFromNativeProps() { + val parsed = parsePosition(JavaOnlyMap.of("left", -20.0, "top", -20.0)) + val bitmap = render(parsed, 240, 120) + for ((x, y) in listOf(0 to 0, 239 to 0, 0 to 119, 239 to 119, 120 to 60)) { + assertColor(bitmap, x, y, Color.BLUE) + } + } + + @Test + fun supportsZeroSizedShaderRequests() { + for ((width, height) in listOf(0f to 0f, 0f to 100f, 100f to 0f)) { + assertThat(gradient().getShader(width, height)).isNotNull() + } + } +} diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp index 7759acebe2a6..8f0a06ef7a02 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,48 @@ inline GradientKeyword parseGradientKeyword(const std::string& keyword) { } } +inline ValueUnit toConicColorStopValueUnit(const RawValue& value) { + if (!value.hasType()) { + return {}; + } + + auto position = (std::string)value; + if (position == "0") { + return {0.0f, UnitType::Percent}; + } + + auto angle = parseCSSProperty(position); + if (std::holds_alternative(angle)) { + return {std::get(angle).degrees / 3.6f, UnitType::Percent}; + } + + auto percentage = toValueUnit(value); + return percentage.unit == UnitType::Percent ? percentage : ValueUnit{}; +} + +inline void parseGradientPosition( + const RawValueMap& positionMap, + RadialGradientPosition& position) { + auto topIt = positionMap.find("top"); + auto bottomIt = positionMap.find("bottom"); + auto leftIt = positionMap.find("left"); + auto rightIt = positionMap.find("right"); + + if (topIt != positionMap.end()) { + position.top = toValueUnit(topIt->second); + } else if (bottomIt != positionMap.end()) { + position.top.reset(); + position.bottom = toValueUnit(bottomIt->second); + } + + if (leftIt != positionMap.end()) { + position.left = toValueUnit(leftIt->second); + } else if (rightIt != positionMap.end()) { + position.left.reset(); + position.right = toValueUnit(rightIt->second); + } +} + void parseProcessedBackgroundImage( const PropsParserContext& context, const RawValue& value, @@ -208,6 +251,25 @@ void parseProcessedBackgroundImage( } backgroundImage.emplace_back(std::move(radialGradient)); + } else if (type == "conic-gradient") { + ConicGradient conicGradient; + conicGradient.position.top = {50.0f, UnitType::Percent}; + conicGradient.position.left = {50.0f, UnitType::Percent}; + auto fromIt = rawBackgroundImageMap.find("from"); + if (fromIt != rawBackgroundImageMap.end() && + fromIt->second.hasType()) { + conicGradient.from = (Float)fromIt->second; + } + + auto positionIt = rawBackgroundImageMap.find("position"); + if (positionIt != rawBackgroundImageMap.end() && + positionIt->second.hasType()) { + parseGradientPosition( + static_cast(positionIt->second), + conicGradient.position); + } + conicGradient.colorStops = colorStops; + backgroundImage.emplace_back(std::move(conicGradient)); } } @@ -248,6 +310,22 @@ void parseUnprocessedBackgroundImageList( auto stopMap = static_cast(stop); auto positionsIt = stopMap.find("positions"); auto colorIt = stopMap.find("color"); + if (type == "conic-gradient") { + const bool isHint = + colorIt == stopMap.end() || !colorIt->second.hasValue(); + const auto positionCount = + positionsIt != stopMap.end() && + positionsIt->second.hasType() + ? static_cast(positionsIt->second).size() + : 0; + if ((isHint && + (positionCount != 1 || colorStops.empty() || + !colorStops.back().color)) || + (!isHint && positionCount > 2)) { + result = {}; + return; + } + } // has only color. e.g. (red, green) if (positionsIt == stopMap.end() || (positionsIt->second.hasType() && @@ -269,7 +347,9 @@ void parseUnprocessedBackgroundImageList( positionsIt->second.hasType()) { auto positions = static_cast(positionsIt->second); for (const auto& position : positions) { - auto positionValue = toValueUnit(position); + auto positionValue = type == "conic-gradient" + ? toConicColorStopValueUnit(position) + : toValueUnit(position); if (!positionValue) { // invalid position result = {}; @@ -282,6 +362,10 @@ void parseUnprocessedBackgroundImageList( auto color = coerceColor(colorIt->second, context); if (color) { colorStop.color = color; + } else if (type == "conic-gradient" && + colorIt->second.hasValue()) { + result = {}; + return; } } colorStops.emplace_back(colorStop); @@ -291,6 +375,12 @@ void parseUnprocessedBackgroundImageList( } } + if (type == "conic-gradient" && + (colorStops.size() < 2 || !colorStops.back().color)) { + result = {}; + return; + } + if (type == "linear-gradient") { LinearGradient linearGradient; @@ -409,6 +499,35 @@ void parseUnprocessedBackgroundImageList( } backgroundImage.emplace_back(std::move(radialGradient)); + } else if (type == "conic-gradient") { + ConicGradient conicGradient; + conicGradient.position.top = {50.0f, UnitType::Percent}; + conicGradient.position.left = {50.0f, UnitType::Percent}; + auto fromIt = rawBackgroundImageMap.find("from"); + if (fromIt != rawBackgroundImageMap.end() && + fromIt->second.hasType()) { + auto from = (std::string)fromIt->second; + if (from == "0") { + conicGradient.from = 0.0f; + } else { + auto angle = parseCSSProperty(from); + if (!std::holds_alternative(angle)) { + result = {}; + return; + } + conicGradient.from = std::get(angle).degrees; + } + } + + auto positionIt = rawBackgroundImageMap.find("position"); + if (positionIt != rawBackgroundImageMap.end() && + positionIt->second.hasType()) { + parseGradientPosition( + static_cast(positionIt->second), + conicGradient.position); + } + conicGradient.colorStops = colorStops; + backgroundImage.emplace_back(std::move(conicGradient)); } } @@ -470,9 +589,72 @@ void fromCSSColorStop( } } +ValueUnit convertAngularPositionToValueUnit(const CSSAngularPosition& value) { + if (std::holds_alternative(value)) { + return {std::get(value).degrees / 3.6f, UnitType::Percent}; + } + return {std::get(value).value, UnitType::Percent}; +} + +void fromCSSConicColorStop( + const std::variant& item, + std::vector& colorStops) { + if (std::holds_alternative(item)) { + ColorStop hint; + hint.position = convertAngularPositionToValueUnit( + std::get(item).position); + colorStops.push_back(hint); + return; + } + + const auto& colorStop = std::get(item); + ColorStop start; + start.color = fromCSSColor(colorStop.color); + if (colorStop.startPosition.has_value()) { + start.position = + convertAngularPositionToValueUnit(*colorStop.startPosition); + } + colorStops.push_back(start); + + if (colorStop.endPosition.has_value()) { + ColorStop end; + end.color = fromCSSColor(colorStop.color); + end.position = convertAngularPositionToValueUnit(*colorStop.endPosition); + colorStops.push_back(end); + } +} + std::optional fromCSSBackgroundImage( const CSSBackgroundImage& cssBackgroundImage) { - if (std::holds_alternative(cssBackgroundImage)) { + if (std::holds_alternative(cssBackgroundImage)) { + const auto& gradient = + std::get(cssBackgroundImage); + ConicGradient conicGradient; + conicGradient.from = gradient.from.degrees; + + const auto& position = gradient.position; + if (position.top.has_value()) { + conicGradient.position.top = + convertLengthPercentageToValueUnit(*position.top); + } + if (position.bottom.has_value()) { + conicGradient.position.bottom = + convertLengthPercentageToValueUnit(*position.bottom); + } + if (position.left.has_value()) { + conicGradient.position.left = + convertLengthPercentageToValueUnit(*position.left); + } + if (position.right.has_value()) { + conicGradient.position.right = + convertLengthPercentageToValueUnit(*position.right); + } + for (const auto& item : gradient.items) { + fromCSSConicColorStop(item, conicGradient.colorStops); + } + return BackgroundImage{conicGradient}; + } else if (std::holds_alternative( + cssBackgroundImage)) { const auto& gradient = std::get(cssBackgroundImage); LinearGradient linearGradient; diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h index 1af10833c169..ad74b86ab751 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,12 @@ enum class CSSGradientDirectionKeyword : std::underlying_type_t { static_assert(CSSDataType); +enum class CSSGradientFromKeyword : std::underlying_type_t { + From = to_underlying(CSSKeyword::From), +}; + +static_assert(CSSDataType); + enum class CSSGradientAtKeyword : std::underlying_type_t { At = to_underlying(CSSKeyword::At), }; @@ -273,6 +280,82 @@ struct CSSDataTypeParser { static_assert(CSSDataType); +using CSSAngularPosition = std::variant; + +struct CSSConicColorHint { + CSSAngularPosition position{}; + + bool operator==(const CSSConicColorHint &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consume(CSSValueParser &parser) -> std::optional + { + auto angle = parser.parseNextValue(); + if (std::holds_alternative(angle)) { + return CSSConicColorHint{std::get(angle)}; + } + auto zero = parser.parseNextValue(); + if (std::holds_alternative(zero)) { + return CSSConicColorHint{CSSAngle{0.0f}}; + } + auto percentage = parser.parseNextValue(); + if (std::holds_alternative(percentage)) { + return CSSConicColorHint{std::get(percentage)}; + } + return {}; + } +}; + +static_assert(CSSDataType); + +struct CSSConicColorStop { + CSSColor color{}; + std::optional startPosition{}; + std::optional endPosition{}; + + bool operator==(const CSSConicColorStop &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consume(CSSValueParser &parser) -> std::optional + { + auto color = parser.parseNextValue(); + if (!std::holds_alternative(color)) { + return {}; + } + + CSSConicColorStop colorStop{.color = std::get(color)}; + colorStop.startPosition = parsePosition(parser, CSSDelimiter::Whitespace); + if (colorStop.startPosition.has_value()) { + colorStop.endPosition = parsePosition(parser, CSSDelimiter::Whitespace); + } + return colorStop; + } + + private: + static constexpr std::optional parsePosition(CSSValueParser &parser, CSSDelimiter delimiter) + { + auto angle = parser.parseNextValue(delimiter); + if (std::holds_alternative(angle)) { + return std::get(angle); + } + auto zero = parser.parseNextValue(delimiter); + if (std::holds_alternative(zero)) { + return CSSAngle{0.0f}; + } + auto percentage = parser.parseNextValue(delimiter); + if (std::holds_alternative(percentage)) { + return std::get(percentage); + } + return {}; + } +}; + +static_assert(CSSDataType); + struct CSSLinearGradientFunction { std::optional direction{}; std::vector> items{}; // Color stops and color hints @@ -790,6 +873,256 @@ struct CSSDataTypeParser { static_assert(CSSDataType); +struct CSSConicGradientFunction { + CSSAngle from{}; + CSSRadialGradientPosition position{}; + std::vector> items{}; + + bool operator==(const CSSConicGradientFunction &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "conic-gradient")) { + return {}; + } + + CSSConicGradientFunction gradient; + gradient.position.top = CSSPercentage{50.0f}; + gradient.position.left = CSSPercentage{50.0f}; + + auto fromKeyword = parser.parseNextValue(); + if (std::holds_alternative(fromKeyword)) { + parser.syntaxParser().consumeWhitespace(); + auto angle = parser.parseNextValue(); + if (std::holds_alternative(angle)) { + gradient.from = std::get(angle); + } else { + auto zero = parser.parseNextValue(); + if (!std::holds_alternative(zero)) { + return {}; + } + gradient.from = CSSAngle{0.0f}; + } + parser.syntaxParser().consumeWhitespace(); + } + + auto atKeyword = parser.parseNextValue(); + if (std::holds_alternative(atKeyword)) { + parser.syntaxParser().consumeWhitespace(); + auto first = parsePositionComponent(parser); + if (!first.has_value()) { + return {}; + } + parser.syntaxParser().consumeWhitespace(); + auto second = parsePositionComponent(parser); + std::optional third; + std::optional fourth; + if (second.has_value()) { + parser.syntaxParser().consumeWhitespace(); + third = parsePositionComponent(parser); + } + if (third.has_value()) { + parser.syntaxParser().consumeWhitespace(); + fourth = parsePositionComponent(parser); + if (!fourth.has_value()) { + return {}; + } + } + auto position = resolvePosition(*first, second, third, fourth); + if (!position.has_value()) { + return {}; + } + gradient.position = *position; + } + + const bool hasPrelude = std::holds_alternative(fromKeyword) || + std::holds_alternative(atKeyword); + if (hasPrelude && !parser.syntaxParser().consumeDelimiter(CSSDelimiter::Comma)) { + return {}; + } + int colorStopCount = 0; + std::optional previousColorStop; + do { + auto colorStop = parser.parseNextValue(); + if (std::holds_alternative(colorStop)) { + auto parsedColorStop = std::get(colorStop); + gradient.items.emplace_back(parsedColorStop); + previousColorStop = parsedColorStop; + colorStopCount++; + } else { + auto colorHint = parser.parseNextValue(); + if (!std::holds_alternative(colorHint) || !previousColorStop.has_value()) { + return {}; + } + auto nextColorStop = parser.peekNextValue(CSSDelimiter::Comma); + if (!std::holds_alternative(nextColorStop)) { + return {}; + } + gradient.items.emplace_back(std::get(colorHint)); + } + } while (parser.syntaxParser().consumeDelimiter(CSSDelimiter::Comma)); + + if (colorStopCount < 2) { + return {}; + } + return gradient; + } + + private: + using PositionComponent = std::variant; + + static std::optional parsePositionComponent(CSSValueParser &parser) + { + auto keyword = parser.parseNextValue(); + if (std::holds_alternative(keyword)) { + return std::get(keyword); + } + auto value = parser.parseNextValue(); + if (std::holds_alternative(value)) { + return std::get(value); + } + if (std::holds_alternative(value)) { + return std::get(value); + } + return {}; + } + + static std::variant positionValue(const PositionComponent &component) + { + if (std::holds_alternative(component)) { + return std::get(component); + } + return std::get(component); + } + + static std::optional resolvePosition( + const PositionComponent &first, + const std::optional &second, + const std::optional &third, + const std::optional &fourth) + { + CSSRadialGradientPosition position; + if (third.has_value()) { + if (!second.has_value() || !fourth.has_value() || !std::holds_alternative(first) || + !std::holds_alternative(*third) || + std::holds_alternative(*second) || + std::holds_alternative(*fourth)) { + return {}; + } + + bool hasHorizontal = false; + bool hasVertical = false; + auto setOffset = [&](CSSGradientPositionKeyword keyword, const PositionComponent &value) { + if (keyword == CSSGradientPositionKeyword::Left && !hasHorizontal) { + position.left = positionValue(value); + hasHorizontal = true; + } else if (keyword == CSSGradientPositionKeyword::Right && !hasHorizontal) { + position.right = positionValue(value); + hasHorizontal = true; + } else if (keyword == CSSGradientPositionKeyword::Top && !hasVertical) { + position.top = positionValue(value); + hasVertical = true; + } else if (keyword == CSSGradientPositionKeyword::Bottom && !hasVertical) { + position.bottom = positionValue(value); + hasVertical = true; + } else { + return false; + } + return true; + }; + + if (!setOffset(std::get(first), *second) || + !setOffset(std::get(*third), *fourth)) { + return {}; + } + return position; + } + if (!second.has_value()) { + if (std::holds_alternative(first)) { + switch (std::get(first)) { + case CSSGradientPositionKeyword::Left: + position.left = CSSPercentage{0.0f}; + position.top = CSSPercentage{50.0f}; + break; + case CSSGradientPositionKeyword::Right: + position.right = CSSPercentage{0.0f}; + position.top = CSSPercentage{50.0f}; + break; + case CSSGradientPositionKeyword::Top: + position.left = CSSPercentage{50.0f}; + position.top = CSSPercentage{0.0f}; + break; + case CSSGradientPositionKeyword::Bottom: + position.left = CSSPercentage{50.0f}; + position.bottom = CSSPercentage{0.0f}; + break; + case CSSGradientPositionKeyword::Center: + position.left = CSSPercentage{50.0f}; + position.top = CSSPercentage{50.0f}; + break; + } + } else { + position.left = positionValue(first); + position.top = CSSPercentage{50.0f}; + } + return position; + } + + auto setHorizontal = [&position](const PositionComponent &component) { + if (std::holds_alternative(component)) { + auto keyword = std::get(component); + if (keyword == CSSGradientPositionKeyword::Left) { + position.left = CSSPercentage{0.0f}; + } else if (keyword == CSSGradientPositionKeyword::Right) { + position.right = CSSPercentage{0.0f}; + } else if (keyword == CSSGradientPositionKeyword::Center) { + position.left = CSSPercentage{50.0f}; + } else { + return false; + } + } else { + position.left = positionValue(component); + } + return true; + }; + auto setVertical = [&position](const PositionComponent &component) { + if (std::holds_alternative(component)) { + auto keyword = std::get(component); + if (keyword == CSSGradientPositionKeyword::Top) { + position.top = CSSPercentage{0.0f}; + } else if (keyword == CSSGradientPositionKeyword::Bottom) { + position.bottom = CSSPercentage{0.0f}; + } else if (keyword == CSSGradientPositionKeyword::Center) { + position.top = CSSPercentage{50.0f}; + } else { + return false; + } + } else { + position.top = positionValue(component); + } + return true; + }; + + bool firstIsVertical = std::holds_alternative(first) && + (std::get(first) == CSSGradientPositionKeyword::Top || + std::get(first) == CSSGradientPositionKeyword::Bottom); + if (firstIsVertical) { + if (!setVertical(first) || !setHorizontal(*second)) { + return {}; + } + } else if (!setHorizontal(first) || !setVertical(*second)) { + return {}; + } + return position; + } +}; + +static_assert(CSSDataType); + template <> struct CSSDataTypeParser { static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) @@ -828,7 +1161,8 @@ static_assert(CSSDataType); * Representation of * https://www.w3.org/TR/css-backgrounds-3/#background-image */ -using CSSBackgroundImage = CSSCompoundDataType; +using CSSBackgroundImage = + CSSCompoundDataType; /** * Representation of diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSKeyword.h b/packages/react-native/ReactCommon/react/renderer/css/CSSKeyword.h index eba64a40ef7c..07067fa6e588 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSKeyword.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSKeyword.h @@ -47,6 +47,7 @@ enum class CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -169,6 +170,7 @@ CSS_DEFINE_KEYWORD(Fixed, "fixed") CSS_DEFINE_KEYWORD(Flex, "flex") CSS_DEFINE_KEYWORD(FlexEnd, "flex-end") CSS_DEFINE_KEYWORD(FlexStart, "flex-start") +CSS_DEFINE_KEYWORD(From, "from") CSS_DEFINE_KEYWORD(Grid, "grid") CSS_DEFINE_KEYWORD(Groove, "groove") CSS_DEFINE_KEYWORD(Hidden, "hidden") @@ -282,6 +284,7 @@ constexpr std::optional parseCSSKeyword(std::string_view ident) CSS_HANDLE_KEYWORD(Flex) CSS_HANDLE_KEYWORD(FlexEnd) CSS_HANDLE_KEYWORD(FlexStart) + CSS_HANDLE_KEYWORD(From) CSS_HANDLE_KEYWORD(Grid) CSS_HANDLE_KEYWORD(Groove) CSS_HANDLE_KEYWORD(Hidden) diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp index 9851cc87277f..1472d4065992 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSBackgroundImageTest.cpp @@ -539,6 +539,45 @@ TEST_F(CSSBackgroundImageTest, RadialGradientMultipleColorStops) { ASSERT_EQ(result, expected); } +TEST_F(CSSBackgroundImageTest, ConicGradientWithAngleAndPosition) { + auto result = parseCSSProperty( + "conic-gradient(from 45deg at 25% 75%, red 0deg, blue 180deg, red 100%)"); + decltype(result) expected = CSSConicGradientFunction{ + .from = CSSAngle{.degrees = 45.0f}, + .position = + CSSRadialGradientPosition{ + .top = CSSPercentage{.value = 75.0f}, + .left = CSSPercentage{.value = 25.0f}}, + .items = { + CSSConicColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSAngle{.degrees = 0.0f}}, + CSSConicColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}, + .startPosition = CSSAngle{.degrees = 180.0f}}, + CSSConicColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}, + .startPosition = CSSPercentage{.value = 100.0f}}}}; + ASSERT_EQ(result, expected); +} + +TEST_F(CSSBackgroundImageTest, ConicGradientWithFourValuePosition) { + auto result = parseCSSProperty( + "conic-gradient(at top 20px left 10px, red, blue)"); + decltype(result) expected = CSSConicGradientFunction{ + .from = CSSAngle{.degrees = 0.0f}, + .position = + CSSRadialGradientPosition{ + .top = CSSLength{.value = 20.0f, .unit = CSSLengthUnit::Px}, + .left = CSSLength{.value = 10.0f, .unit = CSSLengthUnit::Px}}, + .items = { + CSSConicColorStop{ + .color = CSSColor{.r = 255, .g = 0, .b = 0, .a = 255}}, + CSSConicColorStop{ + .color = CSSColor{.r = 0, .g = 0, .b = 255, .a = 255}}}}; + ASSERT_EQ(result, expected); +} + TEST_F(CSSBackgroundImageTest, InvalidGradientFunctionName) { const std::string input = "aoeusntial-gradient(red 0%, yellow 30%, green 60%, blue 100%)"; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.cpp index 9ee16ac87392..2d9f171e2a7f 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.cpp @@ -11,7 +11,9 @@ namespace facebook::react { #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic(const BackgroundImage& backgroundImage) { - if (std::holds_alternative(backgroundImage)) { + if (std::holds_alternative(backgroundImage)) { + return std::get(backgroundImage).toDynamic(); + } else if (std::holds_alternative(backgroundImage)) { return std::get(backgroundImage).toDynamic(); } else if (std::holds_alternative(backgroundImage)) { return std::get(backgroundImage).toDynamic(); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h index ab0c62e506ca..34b5174cdc49 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BackgroundImage.h @@ -9,12 +9,13 @@ #include +#include #include #include namespace facebook::react { -using BackgroundImage = std::variant; +using BackgroundImage = std::variant; #ifdef RN_SERIALIZABLE_STATE folly::dynamic toDynamic(const BackgroundImage &backgroundImage); @@ -32,7 +33,9 @@ inline std::string toString(std::vector &value) } const auto &backgroundImage = value[i]; - if (std::holds_alternative(backgroundImage)) { + if (std::holds_alternative(backgroundImage)) { + std::get(backgroundImage).toString(ss); + } else if (std::holds_alternative(backgroundImage)) { std::get(backgroundImage).toString(ss); } else if (std::holds_alternative(backgroundImage)) { std::get(backgroundImage).toString(ss); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.cpp new file mode 100644 index 000000000000..d5212aaf176b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.cpp @@ -0,0 +1,55 @@ +/* + * 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 "ConicGradient.h" + +namespace facebook::react { + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic ConicGradient::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["type"] = "conic-gradient"; + result["from"] = from; + result["position"] = position.toDynamic(); + + folly::dynamic colorStopsArray = folly::dynamic::array(); + for (const auto& colorStop : colorStops) { + colorStopsArray.push_back(colorStop.toDynamic()); + } + result["colorStops"] = colorStopsArray; + + return result; +} +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE +void ConicGradient::toString(std::stringstream& ss) const { + ss << "conic-gradient(from " << from << "deg at "; + + if (position.left.has_value()) { + ss << position.left->toString() << " "; + } + if (position.top.has_value()) { + ss << position.top->toString() << " "; + } + if (position.right.has_value()) { + ss << position.right->toString() << " "; + } + if (position.bottom.has_value()) { + ss << position.bottom->toString() << " "; + } + + for (const auto& colorStop : colorStops) { + ss << ", "; + colorStop.toString(ss); + } + + ss << ")"; +} +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.h new file mode 100644 index 000000000000..a2143726bce2 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ConicGradient.h @@ -0,0 +1,39 @@ +/* + * 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 + +#include +#include +#include +#include + +#if RN_DEBUG_STRING_CONVERTIBLE +#include +#endif + +#include + +namespace facebook::react { + +struct ConicGradient { + Float from{}; + RadialGradientPosition position; + std::vector colorStops; + + bool operator==(const ConicGradient &other) const = default; + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 2891dd53661e..103f2837cc36 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3ce9599aceb36dd1f09ded3db3332daa>> + * @generated SignedSource<<39c5cbcfe8610724d7a8df66ca6bb392>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1579,7 +1579,8 @@ declare type AttributeType = readonly process?: (arg1: V) => T } declare type AutoCapitalize = "characters" | "none" | "sentences" | "words" -declare type BackgroundImageValue = LinearGradientValue | RadialGradientValue +declare type BackgroundImageValue = + ConicGradientValue | LinearGradientValue | RadialGradientValue declare type BackgroundPositionValue = | { bottom: number | string @@ -1768,6 +1769,15 @@ declare function configureNext( onAnimationDidEnd?: OnAnimationDidEndCallback, onAnimationDidFail?: OnAnimationDidFailCallback, ): void +declare type ConicGradientValue = { + colorStops: ReadonlyArray<{ + color: ____ColorValue_Internal + positions?: ReadonlyArray + }> + from?: string + position?: RadialGradientPosition + type: "conic-gradient" +} declare type ContentAvailable = 1 | null | void declare type Context = { readonly cellKey: string | undefined @@ -5762,18 +5772,18 @@ export { AccessibilityValue, // cf8bcb74 ActionSheetIOS, // b558559e ActionSheetIOSOptions, // 1756eb5a - ActivityIndicator, // b8c4f05a + ActivityIndicator, // 2a77d6b9 ActivityIndicatorInstance, // 1b5e0dd5 - ActivityIndicatorProps, // ab46ec7e + ActivityIndicatorProps, // 6527bf4e Alert, // a398a509 AlertButton, // bf1a3b60 AlertButtonStyle, // ec9fb242 AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 990adcb6 + Animated, // bde3a579 AppConfig, // 35c0ca70 - AppRegistry, // edec2a30 + AppRegistry, // 64c1b19e AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 @@ -5785,8 +5795,8 @@ export { BackPressEventName, // 4620fb76 BlurEvent, // d5e68e96 BoxShadowValue, // b679703f - Button, // 35d2ff2d - ButtonInstance, // 8d4861a8 + Button, // 7672d022 + ButtonInstance, // 91aff59d ButtonProps, // 5f462ce0 Clipboard, // 41addb89 CodegenTypes, // 9067fd06 @@ -5805,9 +5815,9 @@ export { DimensionsPayload, // 653bc26c DisplayMetrics, // 1dc35cef DisplayMetricsAndroid, // 872e62eb - DrawerLayoutAndroid, // 9385b054 + DrawerLayoutAndroid, // 99450940 DrawerLayoutAndroidInstance, // aa243cbf - DrawerLayoutAndroidProps, // a4ddbda9 + DrawerLayoutAndroidProps, // 3ea2ed54 DrawerSlideEvent, // 38213685 DropShadowValue, // e9df2606 DynamicColorIOS, // d96c228c @@ -5823,9 +5833,9 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // 76dfb3dc - FlatListInstance, // 1d6b2ce4 - FlatListProps, // 14bbe483 + FlatList, // 0701da0e + FlatListInstance, // 7959d6fc + FlatListProps, // b02e81f8 FocusEvent, // f2e4363d FontVariant, // 7c7558bb GestureResponderEvent, // 5cd2a663 @@ -5836,17 +5846,17 @@ export { IEventEmitter, // fbef6131 IOSKeyboardEvent, // e67bfe3a IgnorePattern, // ec6f6ece - Image, // 048b0a2a - ImageBackground, // 3d3dc917 - ImageBackgroundInstance, // 400a5719 - ImageBackgroundProps, // 43ae7a8a + Image, // 37612471 + ImageBackground, // 994249b3 + ImageBackgroundInstance, // 04e72daa + ImageBackgroundProps, // 3ea19f79 ImageErrorEvent, // b6227714 ImageInstance, // df709524 ImageLoadEvent, // 31709c38 ImageProgressEventIOS, // 9243d460 - ImageProps, // 1fcb686b + ImageProps, // 2ac90fbe ImagePropsAndroid, // ee00e1d5 - ImagePropsBase, // c744a0da + ImagePropsBase, // 44d85ef6 ImagePropsIOS, // 1998f91a ImageRequireSource, // 681d683b ImageResizeMode, // d51106e2 @@ -5854,19 +5864,19 @@ export { ImageSize, // 1c47cf88 ImageSource, // ea31cf4a ImageSourcePropType, // f522e093 - ImageStyle, // 85dc694a + ImageStyle, // 5b107e73 ImageURISource, // 443d047c - InputAccessoryView, // ab6e2a66 - InputAccessoryViewProps, // 73685b83 + InputAccessoryView, // 812a1e43 + InputAccessoryViewProps, // 61c51e3b InputModeOptions, // 4e8581b9 Insets, // e7fe432a KeyDownEvent, // a12d46ea KeyEvent, // 20fa4267 KeyUpEvent, // 0b5d0144 Keyboard, // 49414c97 - KeyboardAvoidingView, // 9dab9980 - KeyboardAvoidingViewInstance, // 2772afb3 - KeyboardAvoidingViewProps, // 84e4ee76 + KeyboardAvoidingView, // 5c66ea15 + KeyboardAvoidingViewInstance, // 28f9b88c + KeyboardAvoidingViewProps, // fed9ba6c KeyboardEvent, // c3f895d4 KeyboardEventEasing, // af4091c8 KeyboardEventName, // 59299ad6 @@ -5891,10 +5901,10 @@ export { MeasureInWindowOnSuccessCallback, // a285f598 MeasureLayoutOnSuccessCallback, // 3592502a MeasureOnSuccessCallback, // 82824e59 - Modal, // 62f03222 + Modal, // f7a05aeb ModalBaseProps, // a6e4334f ModalInstance, // 0f04c1bf - ModalProps, // b9129837 + ModalProps, // d40c4384 ModalPropsAndroid, // 515fb173 ModalPropsIOS, // a65a8f5c ModeChangeEvent, // ccf30dc4 @@ -5930,15 +5940,15 @@ export { PointerEvent, // a506d9d7 PressabilityConfig, // 0fd655fe PressabilityEventHandlers, // be359ba3 - Pressable, // c52c0096 + Pressable, // 27fb5558 PressableAndroidRippleConfig, // ee32eaca PressableInstance, // ed26f040 - PressableProps, // 97cdaaa1 + PressableProps, // 89cbcfd8 PressableStateCallbackType, // f83728cd ProcessedColorValue, // 33f74304 - ProgressBarAndroid, // a2626c30 + ProgressBarAndroid, // 5d675ae5 ProgressBarAndroidInstance, // f17242be - ProgressBarAndroidProps, // 7205b2a5 + ProgressBarAndroidProps, // 9ed201cc PublicRootInstance, // 8040afd7 PublicTextInstance, // f9adf69a PushNotificationEventName, // 84e7e150 @@ -5946,9 +5956,9 @@ export { PushNotificationPermissions, // c2e7ae4f Rationale, // 5df1b1c1 ReactNativeVersion, // abd76827 - RefreshControl, // db24bb7e - RefreshControlInstance, // d076b876 - RefreshControlProps, // f95b4cd0 + RefreshControl, // a28bb205 + RefreshControlInstance, // 4107d49a + RefreshControlProps, // e4bad0f5 RefreshControlPropsAndroid, // 8ac931ca RefreshControlPropsIOS, // 72a36381 Registry, // 6c39216d @@ -5957,27 +5967,27 @@ export { Role, // af7b889d RootTag, // 3cd10504 RootTagContext, // 38bfc8f6 - RootViewStyleProvider, // d5909148 + RootViewStyleProvider, // 69ce4edc Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaView, // d917b6ec + SafeAreaView, // 0121b68c SafeAreaViewInstance, // 2b5d4391 ScaledSize, // 07e417c7 ScrollEvent, // 956fdb27 - ScrollResponderType, // e41ccb2a + ScrollResponderType, // d3cde407 ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // 651915c2 - ScrollViewImperativeMethods, // cb73da62 - ScrollViewInstance, // 099b48e2 - ScrollViewProps, // 7e38d97b + ScrollView, // 2e5fe920 + ScrollViewImperativeMethods, // 501bd894 + ScrollViewInstance, // 55f51160 + ScrollViewProps, // ea31f904 ScrollViewPropsAndroid, // 02f3df2e ScrollViewPropsIOS, // c378379b ScrollViewScrollToOptions, // 3313411e SectionBase, // 9f13db00 - SectionList, // 542a3017 + SectionList, // 51d22bef SectionListData, // 1a4de01a - SectionListInstance, // ea2f03e4 - SectionListProps, // d6282d2b + SectionListInstance, // e072ef81 + SectionListProps, // 42ec37da SectionListRenderItem, // 715b2086 SectionListRenderItemInfo, // 4a48a922 Separators, // 6a45f7e3 @@ -5994,19 +6004,19 @@ export { StatusBarProps, // c2a44d88 StatusBarStyle, // 78f53eea StyleProp, // fa0e9b4a - StyleSheet, // 8bb13f55 + StyleSheet, // 5707f87f SubmitBehavior, // c4ddf490 - Switch, // 408983ac + Switch, // 0a7053aa SwitchChangeEvent, // 3b472666 SwitchInstance, // 73dce99f - SwitchProps, // fc2b9dfe + SwitchProps, // bbb2dad6 Systrace, // 626d178c TVViewPropsIOS, // 1c612e4e TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 39a4d091 + Text, // 21a12bda TextContentType, // 239b3ecc - TextInput, // 9981d7ab + TextInput, // 3ecbb07f TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b4c7a58c TextInputChangeEvent, // 92363e10 @@ -6016,46 +6026,46 @@ export { TextInputIOSProps, // fb3c9327 TextInputInstance, // 53fe7b73 TextInputKeyPressEvent, // cbbc322a - TextInputProps, // 253dd17d + TextInputProps, // 1a69976d TextInputSelectionChangeEvent, // 4a8f10c2 TextInputSubmitEditingEvent, // e811fdbd TextInstance, // 7c7e2069 TextLayoutEvent, // 1a21e24a - TextProps, // e4fdaaad - TextStyle, // 898adfd1 + TextProps, // 57710952 + TextStyle, // 30961df1 ToastAndroid, // 88a8969a - TouchableHighlight, // c5f5b1c2 + TouchableHighlight, // 1c0670bd TouchableHighlightInstance, // 811af7ca - TouchableHighlightProps, // c35edf1d - TouchableNativeFeedback, // bd8a9aa5 - TouchableNativeFeedbackInstance, // 7010d8d4 - TouchableNativeFeedbackProps, // 827f5e03 - TouchableOpacity, // db1102e5 + TouchableHighlightProps, // 1e14a6a1 + TouchableNativeFeedback, // 561338d9 + TouchableNativeFeedbackInstance, // 3e4b4bc6 + TouchableNativeFeedbackProps, // 30561ca4 + TouchableOpacity, // 4e63eeb2 TouchableOpacityInstance, // 0cd8a347 - TouchableOpacityProps, // 1bc8fbaa - TouchableWithoutFeedback, // a397bd6c - TouchableWithoutFeedbackProps, // 9dfdc3eb + TouchableOpacityProps, // a21bb478 + TouchableWithoutFeedback, // 0bb43ec0 + TouchableWithoutFeedbackProps, // ea461b24 TransformsStyle, // 65e70f18 TurboModule, // dfe29706 TurboModuleRegistry, // 4ace6db2 UIManager, // 6a18fc05 UTFSequence, // ad625158 Vibration, // 31e4bbf8 - View, // fe623c3d + View, // 63db59b0 ViewInstance, // 0283d2fc - ViewProps, // 7fb992e4 + ViewProps, // b3275e90 ViewPropsAndroid, // 66d760c6 ViewPropsIOS, // 58ee19bf - ViewStyle, // 4e2746d6 + ViewStyle, // 9581393f ViewabilityConfig, // eb3f7ed8 ViewabilityConfigCallbackPair, // bdd0788c VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e VirtualizedListInstance, // 423ee7c0 - VirtualizedListProps, // 7a7c618a + VirtualizedListProps, // 81486260 VirtualizedSectionList, // 9fd9cd61 VirtualizedSectionListInstance, // 12b706d5 - VirtualizedSectionListProps, // 8373f27c + VirtualizedSectionListProps, // 95466fda WrapperComponentProvider, // 9b4247f6 codegenNativeCommands, // 322f3f4e codegenNativeComponent, // e8334853 diff --git a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js index 949369ed4137..d414300322b7 100644 --- a/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js +++ b/packages/rn-tester/js/examples/BackgroundImage/BackgroundImageExample.js @@ -12,6 +12,7 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import ConicGradientCornerCases from './ConicGradientCornerCases'; import {StyleSheet, Text, View} from 'react-native'; type ViewStyleProp = NonNullable>; @@ -60,6 +61,13 @@ exports.title = 'BackgroundImage'; exports.category = 'UI'; exports.description = 'Examples of background gradients applied to views.'; exports.examples = [ + { + title: 'Conic Gradient Corner Cases', + name: 'conic-corner-cases', + description: + 'Compare geometry, positions, stops, tiling, and updates in string and object syntax.', + render: () => , + }, { title: 'Basic Linear Gradient', description: 'A simple linear gradient from top to bottom.', @@ -181,6 +189,40 @@ exports.examples = [ ); }, }, + { + title: 'Conic Gradient', + description: + 'Conic gradients rotate color stops clockwise around a configurable center.', + name: 'conic', + render(): React.Node { + return ( + + + + Quadrants + + + + Rotated and off-center + + + + + ); + }, + }, { title: 'Gradient with Background Repeat', name: 'repeat', diff --git a/packages/rn-tester/js/examples/BackgroundImage/ConicGradientCornerCases.js b/packages/rn-tester/js/examples/BackgroundImage/ConicGradientCornerCases.js new file mode 100644 index 000000000000..1c4f725a5059 --- /dev/null +++ b/packages/rn-tester/js/examples/BackgroundImage/ConicGradientCornerCases.js @@ -0,0 +1,338 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import type { + BackgroundImageValue, + RadialGradientPosition, +} from 'react-native/Libraries/StyleSheet/StyleSheetTypes'; + +import RNTesterButton from '../../components/RNTesterButton'; +import * as React from 'react'; +import {StyleSheet, Text, View} from 'react-native'; + +type ViewStyleProp = NonNullable>; +type Example = { + title: string, + expected: string, + width: number, + height: number, + css: string, + object: ReadonlyArray, + tile?: boolean, + center?: (width: number, height: number) => {left: number, top: number}, +}; + +const stopCSS = + '#ef4444 0deg 45deg, #2563eb 45deg 180deg, #22c55e 180deg 360deg'; +function conic( + from: string = '0deg', + position?: RadialGradientPosition, +): BackgroundImageValue { + return { + type: 'conic-gradient', + from, + position, + colorStops: [ + {color: '#ef4444', positions: ['0deg', '45deg']}, + {color: '#2563eb', positions: ['45deg', '180deg']}, + {color: '#22c55e', positions: ['180deg', '360deg']}, + ], + }; +} +const centered = (width: number, height: number) => ({ + left: width / 2, + top: height / 2, +}); +const examples: ReadonlyArray = [ + { + title: 'Wide rectangle', + expected: 'The red wedge spans 45 degrees, regardless of aspect ratio.', + width: 240, + height: 120, + css: `conic-gradient(${stopCSS})`, + object: [conic()], + center: centered, + }, + { + title: 'Tall rectangle', + expected: 'The red wedge has the same angle as the wide rectangle.', + width: 120, + height: 240, + css: `conic-gradient(${stopCSS})`, + object: [conic()], + center: centered, + }, + { + title: 'Extreme aspect ratio', + expected: 'A thin strip must not stretch the red wedge.', + width: 280, + height: 28, + css: `conic-gradient(${stopCSS})`, + object: [conic()], + center: centered, + }, + { + title: '90-degree rotation', + expected: 'The red wedge begins at the right and sweeps clockwise.', + width: 240, + height: 120, + css: `conic-gradient(from 90deg, ${stopCSS})`, + object: [conic('90deg')], + center: centered, + }, + { + title: 'Right and bottom offsets', + expected: 'The center is 10 points from the right and 20 from the bottom.', + width: 240, + height: 120, + css: `conic-gradient(at right 10px bottom 20px, ${stopCSS})`, + object: [conic('0deg', {right: 10, bottom: 20})], + center: (w, h) => ({left: w - 10, top: h - 20}), + }, + { + title: 'Percentage offsets', + expected: + 'The center stays at 75% of the original width and height after resizing.', + width: 240, + height: 120, + css: `conic-gradient(at right 25% bottom 25%, ${stopCSS})`, + object: [conic('0deg', {right: '25%', bottom: '25%'})], + center: (w, h) => ({left: w * 0.75, top: h * 0.75}), + }, + { + title: 'Zero edge offsets', + expected: 'The center is exactly at the bottom-right corner.', + width: 240, + height: 120, + css: `conic-gradient(at right 0px bottom 0px, ${stopCSS})`, + object: [conic('0deg', {right: 0, bottom: 0})], + center: (w, h) => ({left: w, top: h}), + }, + { + title: 'Fractional offsets', + expected: + 'The center is 10.5 points from the right and 20.25 from the bottom.', + width: 240, + height: 120, + css: `conic-gradient(at right 10.5px bottom 20.25px, ${stopCSS})`, + object: [conic('0deg', {right: 10.5, bottom: 20.25})], + center: (w, h) => ({left: w - 10.5, top: h - 20.25}), + }, + { + title: 'Center outside the view', + expected: + 'With the center above and left of the view, the visible area should be blue.', + width: 240, + height: 120, + css: `conic-gradient(at left -20px top -20px, ${stopCSS})`, + object: [conic('0deg', {left: -20, top: -20})], + }, + { + title: 'Equivalent negative rotation', + expected: 'Minus 270 degrees should look exactly like the 90-degree case.', + width: 240, + height: 120, + css: `conic-gradient(from -270deg, ${stopCSS})`, + object: [conic('-270deg')], + center: centered, + }, + { + title: 'Repeated rectangular tiles', + expected: + 'Three columns and three rows of 80 by 40 tiles. Each has its own undistorted center.', + width: 240, + height: 120, + css: `conic-gradient(${stopCSS})`, + object: [conic()], + tile: true, + }, + { + title: 'Transition hint', + expected: + 'Red and blue blend, with the midpoint at 90 degrees. Both syntaxes should match.', + width: 240, + height: 120, + css: 'conic-gradient(#ef4444, 90deg, #2563eb)', + object: [ + { + type: 'conic-gradient', + colorStops: [ + {color: '#ef4444'}, + {color: null, positions: ['90deg']}, + {color: '#2563eb'}, + ], + }, + ], + center: centered, + }, + { + title: 'Mixed background layers', + expected: + 'The transparent half of the conic layer reveals the black-to-white linear gradient.', + width: 240, + height: 120, + css: 'conic-gradient(#ef4444 0deg 180deg, transparent 180deg 360deg), linear-gradient(90deg, black, white)', + object: [ + { + type: 'conic-gradient', + colorStops: [ + {color: '#ef4444', positions: ['0deg', '180deg']}, + {color: 'transparent', positions: ['180deg', '360deg']}, + ], + }, + { + type: 'linear-gradient', + direction: '90deg', + colorStops: [{color: 'black'}, {color: 'white'}], + }, + ], + }, + { + title: 'Invalid trailing hint', + expected: 'No gradient: only the gray fallback background should remain.', + width: 240, + height: 120, + css: 'conic-gradient(red, blue, 20%)', + object: [ + { + type: 'conic-gradient', + colorStops: [ + {color: 'red'}, + {color: 'blue'}, + {color: null, positions: ['20%']}, + ], + }, + ], + }, + { + title: 'Too many stop positions', + expected: 'No gradient: a color stop cannot have three positions.', + width: 240, + height: 120, + css: 'conic-gradient(red 0deg 45deg 90deg, blue)', + object: [ + { + type: 'conic-gradient', + colorStops: [ + {color: 'red', positions: ['0deg', '45deg', '90deg']}, + {color: 'blue'}, + ], + }, + ], + }, + { + title: 'Zero-size view', + expected: + 'Nothing should paint or crash. Next restores a visible gradient.', + width: 0, + height: 0, + css: `conic-gradient(${stopCSS})`, + object: [conic()], + }, +]; + +export default function ConicGradientCornerCases(): React.Node { + const [index, setIndex] = React.useState(0); + const [objectSyntax, setObjectSyntax] = React.useState(false); + const [swapped, setSwapped] = React.useState(false); + const [hidden, setHidden] = React.useState(false); + const example = examples[index]; + const width = swapped ? example.height : example.width; + const height = swapped ? example.width : example.height; + const center = example.center?.(width, height); + const previewStyle: ViewStyleProp = { + width, + height, + backgroundColor: '#d1d5db', + backgroundImage: hidden ? [] : objectSyntax ? example.object : example.css, + backgroundSize: example.tile === true ? '80px 40px' : 'auto', + backgroundRepeat: example.tile === true ? 'repeat' : 'no-repeat', + }; + function select(offset: number) { + setIndex((index + offset + examples.length) % examples.length); + setSwapped(false); + setHidden(false); + } + return ( + + + {index + 1}/{examples.length}: {example.title} + + {example.expected} + + select(-1)}> + Previous + + select(1)}> + Next + + setObjectSyntax(!objectSyntax)}> + {objectSyntax ? 'Object syntax' : 'String syntax'} + + + + {width} × {height} · {hidden ? 'Gradient hidden' : 'Gradient visible'} + + + + {center != null && !hidden ? ( + + ) : null} + + + + setSwapped(!swapped)}> + Swap dimensions + + setHidden(!hidden)}> + {hidden ? 'Show gradient' : 'Hide gradient'} + + + + The white ring marks the expected center. Compare both syntaxes, swap + dimensions, and hide/show without remounting the view. + + + {example.css} + + + ); +} + +const styles = StyleSheet.create({ + title: {fontSize: 17, fontWeight: '600'}, + description: {marginVertical: 8}, + buttons: {flexDirection: 'row', flexWrap: 'wrap'}, + stage: { + height: 290, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f3f4f6', + marginVertical: 8, + overflow: 'hidden', + }, + center: { + position: 'absolute', + width: 8, + height: 8, + borderWidth: 2, + borderColor: 'white', + borderRadius: 4, + transform: [{translateX: -4}, {translateY: -4}], + }, + syntax: {fontSize: 12, marginVertical: 8}, +}); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index c61ae05c5935..cd60ff113aa4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -583,14 +583,15 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -5922,6 +5923,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -5962,6 +5967,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -7019,6 +7025,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -7289,6 +7314,14 @@ struct facebook::react::ColorStop { public folly::dynamic toDynamic() const; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public folly::dynamic toDynamic() const; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -9874,6 +9907,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -10227,6 +10272,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -10346,6 +10392,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 9d5008343459..52806c181883 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -582,14 +582,15 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -5732,6 +5733,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -5772,6 +5777,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -6829,6 +6835,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -7099,6 +7124,14 @@ struct facebook::react::ColorStop { public folly::dynamic toDynamic() const; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public folly::dynamic toDynamic() const; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -9490,6 +9523,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -9843,6 +9888,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -9962,6 +10008,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 2470a7083fb9..eca42981a4cd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -583,14 +583,15 @@ using facebook::react::AndroidSwipeRefreshLayoutState = facebook::react::StateDa using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -5913,6 +5914,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -5953,6 +5958,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -7010,6 +7016,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -7280,6 +7305,14 @@ struct facebook::react::ColorStop { public folly::dynamic toDynamic() const; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public folly::dynamic toDynamic() const; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -9721,6 +9754,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -10074,6 +10119,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -10193,6 +10239,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index a8f040724fa6..5037e6cffff5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -846,6 +846,10 @@ interface RCTComposedViewRegistry : public NSMutableDictionary { public virtual instancetype initWithUIManager:andRegistry:(RCTUIManager* uiManager, NSDictionary* registry); } +interface RCTConicGradient : public NSObject { + public virtual static CALayer* gradientLayerWithSize:gradient:(CGSize size, const facebook::react::ConicGradient& gradient); +} + interface RCTConvert : public NSObject { public typedef BOOL css_backface_visibility_t; public typedef NSArray CGColorArray; @@ -3410,14 +3414,15 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -8033,6 +8038,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -8073,6 +8082,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -9158,6 +9168,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -9432,6 +9461,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -11716,6 +11752,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -12064,6 +12112,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -12183,6 +12232,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 3c0a535fb10e..2044f2e77b9d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -846,6 +846,10 @@ interface RCTComposedViewRegistry : public NSMutableDictionary { public virtual instancetype initWithUIManager:andRegistry:(RCTUIManager* uiManager, NSDictionary* registry); } +interface RCTConicGradient : public NSObject { + public virtual static CALayer* gradientLayerWithSize:gradient:(CGSize size, const facebook::react::ConicGradient& gradient); +} + interface RCTConvert : public NSObject { public typedef BOOL css_backface_visibility_t; public typedef NSArray CGColorArray; @@ -3401,14 +3405,15 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -7875,6 +7880,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -7915,6 +7924,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -9000,6 +9010,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -9274,6 +9303,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -11398,6 +11434,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -11746,6 +11794,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -11865,6 +11914,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 958586fc805e..145b161faf37 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -846,6 +846,10 @@ interface RCTComposedViewRegistry : public NSMutableDictionary { public virtual instancetype initWithUIManager:andRegistry:(RCTUIManager* uiManager, NSDictionary* registry); } +interface RCTConicGradient : public NSObject { + public virtual static CALayer* gradientLayerWithSize:gradient:(CGSize size, const facebook::react::ConicGradient& gradient); +} + interface RCTConvert : public NSObject { public typedef BOOL css_backface_visibility_t; public typedef NSArray CGColorArray; @@ -3410,14 +3414,15 @@ using facebook::react::ActivityIndicatorViewState = facebook::react::StateData; using facebook::react::AnimationEndCallback = facebook::react::AsyncCallback; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -8024,6 +8029,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -8064,6 +8073,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -9149,6 +9159,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -9423,6 +9452,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::Float from; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -11573,6 +11609,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -11921,6 +11969,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -12040,6 +12089,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 8b46097a89f9..a20bc7ee1d78 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -212,14 +212,15 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4291,6 +4292,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -4331,6 +4336,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -5344,6 +5350,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -5602,6 +5627,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public Float from; + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -6915,6 +6947,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -7263,6 +7307,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -7382,6 +7427,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 692b5064e583..acf0e74cfcfe 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -211,14 +211,15 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4141,6 +4142,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -4181,6 +4186,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -5194,6 +5200,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -5452,6 +5477,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public Float from; + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -6739,6 +6771,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -7087,6 +7131,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -7206,6 +7251,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; }); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index ff0ef95bbfdb..e0f12f4b7b03 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -212,14 +212,15 @@ using facebook::react::ActivePointerRegistry = std::unordered_map; using facebook::react::AnimationTimestamp = std::chrono::duration; using facebook::react::BackgroundExecutor = std::function&& callback)>; -using facebook::react::BackgroundImage = std::variant; +using facebook::react::BackgroundImage = std::variant; using facebook::react::BackgroundSize = std::variant; using facebook::react::BorderColors = facebook::react::RectangleEdges; using facebook::react::BorderCurves = facebook::react::RectangleCorners; using facebook::react::BorderRadii = facebook::react::RectangleCorners; using facebook::react::BorderStyles = facebook::react::RectangleEdges; using facebook::react::BorderWidths = facebook::react::RectangleEdges; -using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; +using facebook::react::CSSAngularPosition = std::variant; +using facebook::react::CSSBackgroundImage = facebook::react::CSSCompoundDataType; using facebook::react::CSSBackgroundImageList = facebook::react::CSSCommaSeparatedList; using facebook::react::CSSFilterFunction = facebook::react::CSSCompoundDataType; using facebook::react::CSSFilterList = facebook::react::CSSWhitespaceSeparatedList; @@ -4282,6 +4283,10 @@ enum facebook::react::CSSGradientDirectionKeyword : std::underlying_type_t { + From, +} + enum facebook::react::CSSGradientPositionKeyword : std::underlying_type_t { Bottom, Center, @@ -4322,6 +4327,7 @@ enum facebook::react::CSSKeyword : uint8_t { Flex, FlexEnd, FlexStart, + From, Grid, Groove, Hidden, @@ -5335,6 +5341,25 @@ struct facebook::react::CSSColorStop { public std::optional> startPosition; } +struct facebook::react::CSSConicColorHint { + public bool operator==(const facebook::react::CSSConicColorHint& rhs) const = default; + public facebook::react::CSSAngularPosition position; +} + +struct facebook::react::CSSConicColorStop { + public bool operator==(const facebook::react::CSSConicColorStop& rhs) const = default; + public facebook::react::CSSColor color; + public std::optional endPosition; + public std::optional startPosition; +} + +struct facebook::react::CSSConicGradientFunction { + public bool operator==(const facebook::react::CSSConicGradientFunction& rhs) const = default; + public facebook::react::CSSAngle from; + public facebook::react::CSSRadialGradientPosition position; + public std::vector> items; +} + struct facebook::react::CSSContrastFilter { public constexpr bool operator==(const facebook::react::CSSContrastFilter& rhs) const = default; public float amount; @@ -5593,6 +5618,13 @@ struct facebook::react::ColorStop { public facebook::react::ValueUnit position; } +struct facebook::react::ConicGradient { + public Float from; + public bool operator==(const facebook::react::ConicGradient& other) const = default; + public facebook::react::RadialGradientPosition position; + public std::vector colorStops; +} + struct facebook::react::CornerRadii { public bool operator==(const facebook::react::CornerRadii& other) const = default; public float horizontal; @@ -6906,6 +6938,18 @@ struct facebook::react::CSSDataTypeParser { public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); } +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static constexpr std::optional consume(facebook::react::CSSValueParser& parser); +} + +struct facebook::react::CSSDataTypeParser { + public static std::optional consumeFunctionBlock(const facebook::react::CSSFunctionBlock& func, facebook::react::CSSValueParser& parser); +} + struct facebook::react::CSSDataTypeParser : public facebook::react::detail::CSSFilterSimpleAmountParser { } @@ -7254,6 +7298,7 @@ constexpr std::string_view facebook::react::detail::css::keywords::Fixed; constexpr std::string_view facebook::react::detail::css::keywords::Flex; constexpr std::string_view facebook::react::detail::css::keywords::FlexEnd; constexpr std::string_view facebook::react::detail::css::keywords::FlexStart; +constexpr std::string_view facebook::react::detail::css::keywords::From; constexpr std::string_view facebook::react::detail::css::keywords::Grid; constexpr std::string_view facebook::react::detail::css::keywords::Groove; constexpr std::string_view facebook::react::detail::css::keywords::Hidden; @@ -7373,6 +7418,8 @@ concept facebook::react::detail::css::keywords::hasFlexEnd = (CSSKeywordSet & template concept facebook::react::detail::css::keywords::hasFlexStart = (CSSKeywordSet && requires() { T::FlexStart; }); template +concept facebook::react::detail::css::keywords::hasFrom = (CSSKeywordSet && requires() { T::From; }); +template concept facebook::react::detail::css::keywords::hasGrid = (CSSKeywordSet && requires() { T::Grid; }); template concept facebook::react::detail::css::keywords::hasGroove = (CSSKeywordSet && requires() { T::Groove; });