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

feat(iOS/fabric): percentage support in translate #43192

Conversation

intergalacticspacehighway
Copy link
Contributor

Summary:

This PR adds percentage support in translate properties for new arch iOS. Isolating this PR for easier reviews.

The approach taken here introduces usage of ValueUnit struct for transform operations so it can support % in translates and delay the generation of actual transform matrix until view dimensions are known. I have tried to keep the changes minimal and reuse existing APIs, open to changes if there's an alternative approach.

Changelog:

[IOS] [ADDED] - Percentage support in translate in new arch.

Test Plan:

  • Checkout TransformExample.js -> Translate percentage example.
  • Added a simple test in processTransform-test.js. The regex is not perfect (values like 20px%, 20%px will pass, can be improved, let me know!)

Related PRs - #43193, #43191

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Feb 25, 2024
Copy link
Contributor

@NickGerleman NickGerleman left a comment

Choose a reason for hiding this comment

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

Overall LGTM (left a couple of nits). I think @javache was wanting to take a closer look at this, who is more familiar with some of this code.

When we import this, we should add a quick screenshot test to the RNTester example added.

Comment on lines +263 to +269
invariant(
typeof value === 'number' ||
(typeof value === 'string' && value.endsWith('%')),
'Transform with key of "%s" must be number or a percentage. Passed value: %s.',
key,
stringifySafe(transformation),
);
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 this is an existing pattern, but for the future, we've been trying to move towards no-oping on invalid values, instead of creating a redbox, or something more disruptive.

Even longer into the future (not worth thinking about yet), on Fabric side, we want to just do all this parsing in native.

Copy link
Member

Choose a reason for hiding this comment

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

All of this parsing does happen in native. validateTransform is just an additional DEV-time only validation of the input to get better signal to the developer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, but we have though redbox during development on invalid style is too aggressive/disruptive.

Comment on lines +14 to +29
ValueUnit ValueUnit::getValueUnitFromRawValue(const RawValue& value) {
if (value.hasType<Float>()) {
auto number = (float)value;
return ValueUnit(number, UnitType::Point);
} else if (value.hasType<std::string>()) {
const auto stringValue = (std::string)value;
if (stringValue.back() == '%') {
auto tryValue = folly::tryTo<float>(
std::string_view(stringValue).substr(0, stringValue.length() - 1));
if (tryValue.hasValue()) {
return ValueUnit(tryValue.value(), UnitType::Percent);
}
}
}
return ValueUnit();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I still need to hook this up to OSS build more generally, but this code will eventually be replaced with a couple bits already landed.

CSSValueVariant<CSSLength, CSSPercentage>

Along with the equivalent parseCSSProp or parseCSSComponentValue.

Feel free not to use those yet though, since it's not yet hooked up more broadly.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we'd want to use these here though @NickGerleman, since the code is fully layout independent? Or are you happy for these CSS utils to be exposed for non-layout purposes?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am planning to move the CSS values to be used in View/Image code as well, but am wanting to start with layout (mostly because it can happen before a chunk of work to change how styles in view managers work).

@@ -262,6 +279,14 @@ bool Transform::operator==(const Transform& rhs) const {
return false;
}
}
if (this->operations.size() != rhs.operations.size()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Since the operations and matrix are the only members, and we aren't doing any sort of clever comparison (e.g. fp error or NaN insensitive), I think we could just default this operator. vector and array already generate equality operators, so I'm not sure why the existing code was looping to do this.

lhsOp.x + (rhsOp.x - lhsOp.x) * animationProgress,
lhsOp.y + (rhsOp.y - lhsOp.y) * animationProgress,
lhsOp.z + (rhsOp.z - lhsOp.z) * animationProgress});
ValueUnit(lhsOp.x.value + (rhsOp.x.value - lhsOp.x.value) * animationProgress, UnitType::Point),
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain why we use UnitType::Point unconditionally 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.

My bad, I assumed it'll be point always. I should be checking whether lhs and rhs have same unit and use that. wdyt of below checks above the result and use one of the op's unit?

react_native_assert(lhsOp.x.unit == rhsOp.x.unit);
react_native_assert(lhsOp.y.unit == rhsOp.y.unit);
react_native_assert(lhsOp.z.unit == rhsOp.z.unit);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the above checks. I think lhs and rhs would have same units during interpolate

Comment on lines 1122 to 1123
finalView.layoutMetrics.frame.size.width,
finalView.layoutMetrics.frame.size.height
Copy link
Contributor

Choose a reason for hiding this comment

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

I double check that the spec defaults to border-box for reference box by default, so this should be good for now.

C++ 20 lets you default this operator (does memberwise-comparison)
C++ 20 will automatically define != if you define ==

Co-authored-by: Nick Gerleman <nick@nickgerleman.com>
Copy link
Member

@javache javache left a comment

Choose a reason for hiding this comment

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

Can we have some C++ unit tests for the behaviours added here?

I have some concerns with the design of ValueUnit, as it makes it a bit too easy to have gaps in the implementation

Comment on lines +263 to +269
invariant(
typeof value === 'number' ||
(typeof value === 'string' && value.endsWith('%')),
'Transform with key of "%s" must be number or a percentage. Passed value: %s.',
key,
stringifySafe(transformation),
);
Copy link
Member

Choose a reason for hiding this comment

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

All of this parsing does happen in native. validateTransform is just an additional DEV-time only validation of the input to get better signal to the developer.

Comment on lines 1634 to 1635
Float viewWidth,
Float viewHeight) const {
Copy link
Member

Choose a reason for hiding this comment

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

Can you use Size? https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/renderer/graphics/Size.h

Suggested change
Float viewWidth,
Float viewHeight) const {
const Size& size) const {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

auto transformMatrix = Transform{};

if (viewWidth == 0 && viewHeight == 0){
return transformMatrix;
Copy link
Member

Choose a reason for hiding this comment

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

Different from previous behaviour which returned the plain transform. Not sure it matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True. Added this as an optimisation to avoid the operations loop as FromTransformOperation has become size dependent (we can also prevent calling this method when size is 0). I am also not sure transformation with zero dimensions matter (with border-box). It's an assumption though.

At some point I would like to explore moving transform matrix generation to shadow view and native view can implement a method, similar to how updateLayoutMetrics works.


// transform is matrix
if (transform.operations.size() == 1 && transform.operations[0].type == TransformOperationType::Arbitrary) {
transformMatrix = transformMatrix * transform;
Copy link
Member

Choose a reason for hiding this comment

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

Avoid the multiplication

Suggested change
transformMatrix = transformMatrix * transform;
transformMatrix = transform;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +411 to +419
if (transformOrigin.isSet()) {
std::array<float, 3> translateOffsets =
getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin);
transformMatrix =
Transform::Translate(
-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]);
return newTransform;
translateOffsets[0], translateOffsets[1], translateOffsets[2])
* transformMatrix
* Transform::Translate(-translateOffsets[0], -translateOffsets[1], -translateOffsets[2]);
}
Copy link
Member

Choose a reason for hiding this comment

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

Why not do this before the transform processing loop? That way we can avoid less intermediate objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can do translateOffsets[0], translateOffsets[1], translateOffsets[2]) before the loop but -translateOffsets[0], -translateOffsets[1], -translateOffsets[2] needs to be done post processing for transform origin to work correctly. I am missing how it can lead to less intermediate objects. Let me know, happy to fix it! Also, we want to prevent additional transformOrigin.isSet() checks.

}
if (transformOperation.type == TransformOperationType::Scale) {
return Transform::Scale(
transformOperation.x, transformOperation.y, transformOperation.z);
transformOperation.x.value, transformOperation.y.value, transformOperation.z.value);
Copy link
Member

Choose a reason for hiding this comment

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

This is quite a type-unsafe API now, as it's very easy to "forget" to check whether it's a percentage of a point value. Yoga used to require you to call something like resolveValue, can we make something explicit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

Comment on lines +14 to +29
ValueUnit ValueUnit::getValueUnitFromRawValue(const RawValue& value) {
if (value.hasType<Float>()) {
auto number = (float)value;
return ValueUnit(number, UnitType::Point);
} else if (value.hasType<std::string>()) {
const auto stringValue = (std::string)value;
if (stringValue.back() == '%') {
auto tryValue = folly::tryTo<float>(
std::string_view(stringValue).substr(0, stringValue.length() - 1));
if (tryValue.hasValue()) {
return ValueUnit(tryValue.value(), UnitType::Percent);
}
}
}
return ValueUnit();
};
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we'd want to use these here though @NickGerleman, since the code is fully layout independent? Or are you happy for these CSS utils to be exposed for non-layout purposes?

@NickGerleman
Copy link
Contributor

@javache @intergalacticspacehighway anything pending here?

@intergalacticspacehighway
Copy link
Contributor Author

@NickGerleman i got occupied with stuff at work 😅. @javache has commented in other PRs too. I am gonna wrap it up this week.

@NickGerleman
Copy link
Contributor

No worries! I will take a look again after next revision.

@analysis-bot
Copy link

analysis-bot commented Mar 30, 2024

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 19,427,379 +16,635
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 22,800,763 +16,493
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: 03a51da
Branch: main

@intergalacticspacehighway
Copy link
Contributor Author

@NickGerleman @javache Any pointers or command how can i run these transform tests?

I'll add some tests here for translate percentage.

@NickGerleman
Copy link
Contributor

@NickGerleman @javache Any pointers or command how can i run these transform tests?

I'll add some tests here for translate percentage.

I think @cortinico wired this up to Android CMake build. I think this is called by Gradle inc in OSS CI, but I haven't run myself.

@intergalacticspacehighway
Copy link
Contributor Author

@NickGerleman @javache added a testcase. CI is fine, so hopefully the test is running? Seems to be ready to take another look at this and the other PRs 🙏

@cortinico
Copy link
Contributor

I think @cortinico wired this up to Android CMake build. I think this is called by Gradle inc in OSS CI, but I haven't run myself.

@intergalacticspacehighway Sadly those tests don't run automatically in OSS as we don't have a running device on CI. They will execute when we import the PR internally though so it's good that you added one 👍

@intergalacticspacehighway
Copy link
Contributor Author

Thanks @cortinico Any way i can run these tests locally, just to confirm? i can see it bundles the cpp test files in android local build. But i couldn't find a way to run them. Is it like a separate project/gradle-task?

@cortinico
Copy link
Contributor

Thanks @cortinico Any way i can run these tests locally, just to confirm? i can see it bundles the cpp test files in android local build. But i couldn't find a way to run them. Is it like a separate project/gradle-task?

Instructions on how to run them are here:
https://github.com/ShiraOzeri/Android-NDK-with-Google-Test#android-ndk-with-google-test
Just note that we don't run those in OSS so they might just be broken, but once we import it internally we can tell if they're working or not

@NickGerleman
Copy link
Contributor

I'll import this and see how it looks

@facebook-github-bot
Copy link
Contributor

@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@intergalacticspacehighway
Copy link
Contributor Author

Something seems off. FB internal tests are running indefinitely 😅

@NickGerleman
Copy link
Contributor

Something seems off. FB internal tests are running indefinitely 😅

Needs some build fixes for Buck. Going to see if I can take a look at this today

@NickGerleman
Copy link
Contributor

Tests pass!

Small changes I needed to make:

  1. Formatting
  2. A shadowing warning the OSS build didn't complain as loudly about
  3. Remove getValueUnitFromRawValue() in favor of fromRawValue overload we added recently. For dependency organization purposes, ValueUnit should not depend on RawValue

Copy link
Contributor

@NickGerleman NickGerleman left a comment

Choose a reason for hiding this comment

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

This was causing a test outside RN to fail, but I think I narrowed it down to an unrelated bug #44429

@@ -513,48 +515,52 @@ inline void fromRawValue(
transformMatrix.matrix[i++] = number;
}
transformMatrix.operations.push_back(
TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0});
TransformOperation{TransformOperationType::Arbitrary, Zero, Zero, Zero});
} else if (operation == "perspective") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I missed on my initial look-through that we are using ValueUnit (Point, or Percentage) for operations which take radians, which is a little confusing.

Don't want to make this PR too big, but we would replace ValueUnit will CSSValueVariant after the dust on that settles.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

radians

You mean operations like this right? transformMatrix.operations.push_back( TransformOperation{TransformOperationType::Rotate, ValueUnit(toRadians(parameters, 0.0f), UnitType::Point), Zero, Zero})

Yeah, I considered UnitType::Point like a regular Float as ValueUnit eventually will get replaced and to keep the PR small but we can add Angle units in CSSValueVariant.

Replacing ValueUnit with CSSValueVariant

For sure, i can see it is in renderer/css, can we import and start using it? We can also replace ValueUnit in transformOrigin, i guess a separate PR to do that would be better.

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label May 17, 2024
@facebook-github-bot
Copy link
Contributor

@NickGerleman merged this pull request in f997b81.

Copy link

This pull request was successfully merged by @intergalacticspacehighway in f997b81.

When will my fix make it into a release? | How to file a pick request?

@intergalacticspacehighway
Copy link
Contributor Author

cc - @NickGerleman Thanks for reviewing. fyi two more PRs are there - #43193 #43191 🙏

@NickGerleman
Copy link
Contributor

cc - @NickGerleman Thanks for reviewing. fyi two more PRs are there - #43193 #43191 🙏

I will try to take a closer look at Android one soon. In general we have been recently wanting to avoid non-trivial Paper-specific changes (though for Android it is kinda shared), so the iOS one might make less sense.

facebook-github-bot pushed a commit that referenced this pull request May 24, 2024
Summary:
This PR adds percentage support in translate properties for android. Isolating this PR for easier reviews.

## Changelog:
[Android] [ADDED] - Percentage support in translate
<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: #43193

Test Plan:
- Checkout TransformExample.js -> Translate percentage example.
- Added a simple test in `processTransform-test.js`. The regex is not perfect (values like 20px%, 20%px will pass, can be improved, let me know!)

Related PRs - #43191, #43192

Reviewed By: joevilches

Differential Revision: D57723216

Pulled By: NickGerleman

fbshipit-source-id: c9da007678341b62745df858f043821bcc662a98
kosmydel pushed a commit to kosmydel/react-native that referenced this pull request Jun 11, 2024
Summary:
This PR adds percentage support in translate properties for new arch iOS. Isolating this PR for easier reviews.

The approach taken here introduces usage of `ValueUnit` struct for transform operations so it can support `%` in translates and delay the generation of actual transform matrix until view dimensions are known. I have tried to keep the changes minimal and reuse existing APIs, open to changes if there's an alternative approach.

## Changelog:
[IOS] [ADDED] - Percentage support in translate in new arch.
<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: facebook#43192

Test Plan:
- Checkout TransformExample.js -> Translate percentage example.
- Added a simple test in `processTransform-test.js`. The regex is not perfect (values like 20px%, 20%px will pass, can be improved, let me know!)

Related PRs - facebook#43193, facebook#43191

Reviewed By: javache

Differential Revision: D56802425

Pulled By: NickGerleman

fbshipit-source-id: 978cbbdde004afe1e68ffee9a3c7eb7d16336b46
kosmydel pushed a commit to kosmydel/react-native that referenced this pull request Jun 11, 2024
Summary:
This PR adds percentage support in translate properties for android. Isolating this PR for easier reviews.

## Changelog:
[Android] [ADDED] - Percentage support in translate
<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: facebook#43193

Test Plan:
- Checkout TransformExample.js -> Translate percentage example.
- Added a simple test in `processTransform-test.js`. The regex is not perfect (values like 20px%, 20%px will pass, can be improved, let me know!)

Related PRs - facebook#43191, facebook#43192

Reviewed By: joevilches

Differential Revision: D57723216

Pulled By: NickGerleman

fbshipit-source-id: c9da007678341b62745df858f043821bcc662a98
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants