Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement transform-origin for old arch iOS #38626

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import processAspectRatio from '../../StyleSheet/processAspectRatio';
import processColor from '../../StyleSheet/processColor';
import processFontVariant from '../../StyleSheet/processFontVariant';
import processTransform from '../../StyleSheet/processTransform';
import processTransformOrigin from '../../StyleSheet/processTransformOrigin';
import sizesDiffer from '../../Utilities/differ/sizesDiffer';

const colorAttributes = {process: processColor};
Expand Down Expand Up @@ -111,6 +112,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
* Transform
*/
transform: {process: processTransform},
transformOrigin: {process: processTransformOrigin},

/**
* View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const validAttributesForNonEventProps = {
overflow: true,
shouldRasterizeIOS: true,
transform: {diff: require('../Utilities/differ/matricesDiffer')},
transformOrigin: true,
accessibilityRole: true,
accessibilityState: true,
nativeID: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export interface TransformsStyle {
>[]
| string
| undefined;
transformOrigin?: Array<string | number> | string | undefined;
/**
* @deprecated Use matrix in transform prop instead.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`processTransformOrigin validation only accepts three values 1`] = `"Transform origin must have exactly 3 values."`;

exports[`processTransformOrigin validation only accepts three values 2`] = `"Transform origin must have exactly 3 values."`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* 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.
*
* @format
* @oncall react_native
*/

import processTransformOrigin from '../processTransformOrigin';

describe('processTransformOrigin', () => {
describe('validation', () => {
it('only accepts three values', () => {
expect(() => {
processTransformOrigin([]);
}).toThrowErrorMatchingSnapshot();
expect(() => {
processTransformOrigin(['50%', '50%']);
}).toThrowErrorMatchingSnapshot();
});

it('should transform a string', () => {
expect(processTransformOrigin('50% 50% 5px')).toEqual(['50%', '50%', 5]);
});

it('should handle one value', () => {
expect(processTransformOrigin('top')).toEqual(['50%', 0, 0]);
expect(processTransformOrigin('right')).toEqual(['100%', '50%', 0]);
expect(processTransformOrigin('bottom')).toEqual(['50%', '100%', 0]);
expect(processTransformOrigin('left')).toEqual([0, '50%', 0]);
});

it('should handle two values', () => {
expect(processTransformOrigin('30% top')).toEqual(['30%', 0, 0]);
expect(processTransformOrigin('right 30%')).toEqual(['100%', '30%', 0]);
expect(processTransformOrigin('30% bottom')).toEqual(['30%', '100%', 0]);
expect(processTransformOrigin('left 30%')).toEqual([0, '30%', 0]);
});

it('should handle two keywords in either order', () => {
expect(processTransformOrigin('right bottom')).toEqual([
'100%',
'100%',
0,
]);
expect(processTransformOrigin('bottom right')).toEqual([
'100%',
'100%',
0,
]);
expect(processTransformOrigin('right bottom 5px')).toEqual([
'100%',
'100%',
5,
]);
expect(processTransformOrigin('bottom right 5px')).toEqual([
'100%',
'100%',
5,
]);
});

it('should not allow specifying same position twice', () => {
expect(() => {
processTransformOrigin('top top');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top top"`,
);
expect(() => {
processTransformOrigin('right right');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin right can only be used for x-position"`,
);
expect(() => {
processTransformOrigin('bottom bottom');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: bottom bottom"`,
);
expect(() => {
processTransformOrigin('left left');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin left can only be used for x-position"`,
);
expect(() => {
processTransformOrigin('top bottom');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top bottom"`,
);
expect(() => {
processTransformOrigin('left right');
}).toThrowErrorMatchingInlineSnapshot(
`"Transform-origin right can only be used for x-position"`,
);
});

it('should handle three values', () => {
expect(processTransformOrigin('30% top 10px')).toEqual(['30%', 0, 10]);
expect(processTransformOrigin('right 30% 10px')).toEqual([
'100%',
'30%',
10,
]);
expect(processTransformOrigin('30% bottom 10px')).toEqual([
'30%',
'100%',
10,
]);
expect(processTransformOrigin('left 30% 10px')).toEqual([0, '30%', 10]);
});

it('should enforce two value ordering', () => {
expect(() => {
processTransformOrigin('top 30%');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30%"`,
);
});

it('should not allow percents for z-position', () => {
expect(() => {
processTransformOrigin('top 30% 30%');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30% 30%"`,
);
expect(() => {
processTransformOrigin('top 30% center');
}).toThrowErrorMatchingInlineSnapshot(
`"Could not parse transform-origin: top 30% center"`,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,16 @@ export type ____TransformStyle_Internal = $ReadOnly<{|
|},
>
| string,
/**
* `transformOrigin` accepts an array with 3 elements - each element either being
* a number, or a string of a number ending with `%`. The last element cannot be
* a percentage, so must be a number.
*
* E.g. transformOrigin: ['30%', '80%', 15]
*
* Alternatively accepts a string of the CSS syntax. You must use `%` or `px`.
*
* E.g. transformOrigin: '30% 80% 15px'
*/
transformOrigin?: Array<string | number> | string,
|}>;
136 changes: 136 additions & 0 deletions packages/react-native/Libraries/StyleSheet/processTransformOrigin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* 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.
*
* @format
* @flow
*/

import invariant from 'invariant';

const INDEX_X = 0;
const INDEX_Y = 1;
const INDEX_Z = 2;

/* eslint-disable no-labels */
export default function processTransformOrigin(
transformOrigin: Array<string | number> | string,
): Array<string | number> {
if (typeof transformOrigin === 'string') {
const transformOriginString = transformOrigin;
const regex = /(top|bottom|left|right|center|\d+(?:%|px)|0)/gi;
const transformOriginArray: Array<string | number> = ['50%', '50%', 0];

let index = INDEX_X;
let matches;
outer: while ((matches = regex.exec(transformOriginString))) {
let nextIndex = index + 1;

const value = matches[0];
const valueLower = value.toLowerCase();

switch (valueLower) {
case 'left':
case 'right': {
invariant(
index === INDEX_X,
'Transform-origin %s can only be used for x-position',
value,
);
transformOriginArray[INDEX_X] = valueLower === 'left' ? 0 : '100%';
break;
}
case 'top':
case 'bottom': {
invariant(
index !== INDEX_Z,
'Transform-origin %s can only be used for y-position',
value,
);
transformOriginArray[INDEX_Y] = valueLower === 'top' ? 0 : '100%';

// Handle [[ center | left | right ] && [ center | top | bottom ]] <length>?
if (index === INDEX_X) {
const horizontal = regex.exec(transformOriginString);
if (horizontal == null) {
break outer;
}

switch (horizontal[0].toLowerCase()) {
case 'left':
transformOriginArray[INDEX_X] = 0;
break;
case 'right':
transformOriginArray[INDEX_X] = '100%';
break;
case 'center':
transformOriginArray[INDEX_X] = '50%';
break;
default:
invariant(
false,
'Could not parse transform-origin: %s',
transformOriginString,
);
}
nextIndex = INDEX_Z;
}

break;
}
case 'center': {
invariant(
index !== INDEX_Z,
'Transform-origin value %s cannot be used for z-position',
value,
);
transformOriginArray[index] = '50%';
break;
}
default: {
if (value.endsWith('%')) {
transformOriginArray[index] = value;
} else {
transformOriginArray[index] = parseFloat(value); // Remove `px`
}
break;
}
}

index = nextIndex;
}

transformOrigin = transformOriginArray;
}

if (__DEV__) {
_validateTransformOrigin(transformOrigin);
}

return transformOrigin;
}

function _validateTransformOrigin(transformOrigin: Array<string | number>) {
invariant(
transformOrigin.length === 3,
'Transform origin must have exactly 3 values.',
);
const [x, y, z] = transformOrigin;
invariant(
typeof x === 'number' || (typeof x === 'string' && x.endsWith('%')),
'Transform origin x-position must be a number. Passed value: %s.',
x,
);
invariant(
typeof y === 'number' || (typeof y === 'string' && y.endsWith('%')),
'Transform origin y-position must be a number. Passed value: %s.',
y,
);
invariant(
typeof z === 'number',
'Transform origin z-position must be a number. Passed value: %s.',
z,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function splitLayoutProps(props: ?____ViewStyle_Internal): {
case 'bottom':
case 'top':
case 'transform':
case 'transformOrigin':
case 'rowGap':
case 'columnGap':
case 'gap':
Expand Down
15 changes: 15 additions & 0 deletions packages/react-native/React/Views/RCTTransformOrigin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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 <Foundation/Foundation.h>
#import <yoga/Yoga.h>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this pulling in Yoga? Does this just want a struct that can be a percentage or integral number? What would YGValueAuto or YGValueUndefined mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Would probably be better to avoid the dependency, since this system does not interact with Yoga.

Apart from general goodness of a clean dependency graph, this currently breaks the internal Buck build because this target didn't need to preciously depend on Yoga.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I can do. I'd need to duplicate the stuff in RCTConvert to do that. Sound reasonable?

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like we missed tackling this before merging this code. Would be great to clean up.


typedef struct {
YGValue x;
YGValue y;
CGFloat z;
} RCTTransformOrigin;
20 changes: 13 additions & 7 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTShadowView.h"
#import "RCTTransformOrigin.h"
#import "RCTUIManager.h"
#import "RCTUIManagerUtils.h"
#import "RCTUtils.h"
Expand Down Expand Up @@ -121,6 +122,16 @@ @implementation RCTConvert (UIAccessibilityTraits)
UIAccessibilityTraitNone,
unsignedLongLongValue)

+ (RCTTransformOrigin)RCTTransformOrigin:(id)json
{
RCTTransformOrigin transformOrigin = {
[RCTConvert YGValue:json[0]],
[RCTConvert YGValue:json[1]],
[RCTConvert CGFloat:json[2]]
};
return transformOrigin;
}

@end

@implementation RCTViewManager
Expand Down Expand Up @@ -216,13 +227,8 @@ - (RCTShadowView *)shadowView
view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale;
}

RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
{
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
// Enable edge antialiasing in rotation, skew, or perspective transforms
view.layer.allowsEdgeAntialiasing =
view.layer.transform.m12 != 0.0f || view.layer.transform.m21 != 0.0f || view.layer.transform.m34 != 0.0f;
}
RCT_REMAP_VIEW_PROPERTY(transform, reactTransform, CATransform3D)
RCT_REMAP_VIEW_PROPERTY(transformOrigin, reactTransformOrigin, RCTTransformOrigin)

RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView)
{
Expand Down
Loading