-
Notifications
You must be signed in to change notification settings - Fork 24k
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
feat(iOS/fabric): percentage support in translate #43192
Conversation
There was a problem hiding this 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.
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), | ||
); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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(); | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
packages/react-native/ReactCommon/react/renderer/graphics/Transform.h
Outdated
Show resolved
Hide resolved
@@ -262,6 +279,14 @@ bool Transform::operator==(const Transform& rhs) const { | |||
return false; | |||
} | |||
} | |||
if (this->operations.size() != rhs.operations.size()) { |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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
finalView.layoutMetrics.frame.size.width, | ||
finalView.layoutMetrics.frame.size.height |
There was a problem hiding this comment.
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>
There was a problem hiding this 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
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), | ||
); |
There was a problem hiding this comment.
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.
Float viewWidth, | ||
Float viewHeight) const { |
There was a problem hiding this comment.
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
Float viewWidth, | |
Float viewHeight) const { | |
const Size& size) const { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid the multiplication
transformMatrix = transformMatrix * transform; | |
transformMatrix = transform; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
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]); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
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(); | ||
}; |
There was a problem hiding this comment.
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?
@javache @intergalacticspacehighway anything pending here? |
@NickGerleman i got occupied with stuff at work 😅. @javache has commented in other PRs too. I am gonna wrap it up this week. |
No worries! I will take a look again after next revision. |
Base commit: 03a51da |
Co-authored-by: Pieter De Baets <pieter.debaets@gmail.com>
@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.
|
@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 🙏 |
@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 👍 |
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: |
I'll import this and see how it looks |
@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
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 |
Tests pass! Small changes I needed to make:
|
There was a problem hiding this 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") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@NickGerleman merged this pull request in f997b81. |
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? |
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. |
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
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
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
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:
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