Skip to content

Commit d0acce9

Browse files
NickGerlemanmeta-codesync[bot]
authored andcommitted
Wire native CSS parsing for transform (#55677)
Summary: Pull Request resolved: #55677 Gate `processTransform` behind `enableNativeCSSParsing()`. When the flag is on, CSS transform strings like `"rotate(45deg) scale(2)"` are parsed natively using the existing CSS transform parser instead of being preprocessed in JS. Changelog: [Internal] Reviewed By: jorge-cab Differential Revision: D94052735 fbshipit-source-id: 4d0b97b3587012853b10ca02270e3d631839ec80
1 parent 6c83352 commit d0acce9

File tree

5 files changed

+320
-6
lines changed

5 files changed

+320
-6
lines changed

packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export const backgroundRepeatAttribute: AnyAttributeType = nativeCSSParsing
5757
? true
5858
: {process: processBackgroundRepeat};
5959

60+
export const transformAttribute: AnyAttributeType = nativeCSSParsing
61+
? true
62+
: {process: processTransform};
63+
6064
const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
6165
/**
6266
* Layout
@@ -150,7 +154,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
150154
/**
151155
* Transform
152156
*/
153-
transform: {process: processTransform},
157+
transform: transformAttribute,
154158
transformOrigin: {process: processTransformOrigin},
155159

156160
/**

packages/react-native/ReactCommon/react/renderer/components/view/conversions.h

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <glog/logging.h>
1111
#include <react/debug/react_native_expect.h>
12+
#include <react/featureflags/ReactNativeFeatureFlags.h>
1213
#include <react/renderer/components/view/primitives.h>
1314
#include <react/renderer/core/LayoutMetrics.h>
1415
#include <react/renderer/core/PropsParserContext.h>
@@ -17,6 +18,9 @@
1718
#include <react/renderer/css/CSSAngle.h>
1819
#include <react/renderer/css/CSSNumber.h>
1920
#include <react/renderer/css/CSSPercentage.h>
21+
#include <react/renderer/css/CSSRatio.h>
22+
#include <react/renderer/css/CSSTransform.h>
23+
#include <react/renderer/css/CSSTransformOrigin.h>
2024
#include <react/renderer/css/CSSValueParser.h>
2125
#include <react/renderer/debug/flags.h>
2226
#include <react/renderer/graphics/BackgroundPosition.h>
@@ -533,7 +537,136 @@ inline void fromRawValue(const PropsParserContext & /*context*/, const RawValue
533537
result = toValueUnit(value);
534538
}
535539

536-
inline void fromRawValue(const PropsParserContext & /*context*/, const RawValue &value, Transform &result)
540+
inline ValueUnit cssLengthPercentageToValueUnit(const std::variant<CSSLength, CSSPercentage> &value)
541+
{
542+
if (std::holds_alternative<CSSLength>(value)) {
543+
auto len = std::get<CSSLength>(value);
544+
if (len.unit != CSSLengthUnit::Px) {
545+
return {};
546+
}
547+
return {len.value, UnitType::Point};
548+
} else {
549+
return {std::get<CSSPercentage>(value).value, UnitType::Percent};
550+
}
551+
}
552+
553+
inline std::optional<TransformOperation> fromCSSTransformFunction(const CSSTransformFunctionVariant &cssTransform)
554+
{
555+
constexpr auto Zero = ValueUnit(0, UnitType::Point);
556+
constexpr auto One = ValueUnit(1, UnitType::Point);
557+
558+
return std::visit(
559+
[&](auto &&func) -> std::optional<TransformOperation> {
560+
using T = std::decay_t<decltype(func)>;
561+
562+
if constexpr (std::is_same_v<T, CSSRotate>) {
563+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
564+
return TransformOperation{
565+
.type = TransformOperationType::Rotate, .x = Zero, .y = Zero, .z = ValueUnit(radians, UnitType::Point)};
566+
}
567+
568+
if constexpr (std::is_same_v<T, CSSRotateX>) {
569+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
570+
return TransformOperation{
571+
.type = TransformOperationType::Rotate, .x = ValueUnit(radians, UnitType::Point), .y = Zero, .z = Zero};
572+
}
573+
574+
if constexpr (std::is_same_v<T, CSSRotateY>) {
575+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
576+
return TransformOperation{
577+
.type = TransformOperationType::Rotate, .x = Zero, .y = ValueUnit(radians, UnitType::Point), .z = Zero};
578+
}
579+
580+
if constexpr (std::is_same_v<T, CSSRotateZ>) {
581+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
582+
return TransformOperation{
583+
.type = TransformOperationType::Rotate, .x = Zero, .y = Zero, .z = ValueUnit(radians, UnitType::Point)};
584+
}
585+
586+
if constexpr (std::is_same_v<T, CSSTranslate>) {
587+
auto x = cssLengthPercentageToValueUnit(func.x);
588+
auto y = cssLengthPercentageToValueUnit(func.y);
589+
if (!x || !y) {
590+
return std::nullopt;
591+
}
592+
return TransformOperation{.type = TransformOperationType::Translate, .x = x, .y = y, .z = Zero};
593+
}
594+
595+
if constexpr (std::is_same_v<T, CSSTranslateX>) {
596+
auto x = cssLengthPercentageToValueUnit(func.value);
597+
if (!x) {
598+
return std::nullopt;
599+
}
600+
return TransformOperation{.type = TransformOperationType::Translate, .x = x, .y = Zero, .z = Zero};
601+
}
602+
603+
if constexpr (std::is_same_v<T, CSSTranslateY>) {
604+
auto y = cssLengthPercentageToValueUnit(func.value);
605+
if (!y) {
606+
return std::nullopt;
607+
}
608+
return TransformOperation{.type = TransformOperationType::Translate, .x = Zero, .y = y, .z = Zero};
609+
}
610+
611+
if constexpr (std::is_same_v<T, CSSTranslate3D>) {
612+
auto x = cssLengthPercentageToValueUnit(func.x);
613+
auto y = cssLengthPercentageToValueUnit(func.y);
614+
if (!x || !y || func.z.unit != CSSLengthUnit::Px) {
615+
return std::nullopt;
616+
}
617+
return TransformOperation{
618+
.type = TransformOperationType::Translate, .x = x, .y = y, .z = ValueUnit(func.z.value, UnitType::Point)};
619+
}
620+
621+
if constexpr (std::is_same_v<T, CSSScale>) {
622+
return TransformOperation{
623+
.type = TransformOperationType::Scale,
624+
.x = ValueUnit(func.x, UnitType::Point),
625+
.y = ValueUnit(func.y, UnitType::Point),
626+
.z = One};
627+
}
628+
629+
if constexpr (std::is_same_v<T, CSSScaleX>) {
630+
return TransformOperation{
631+
.type = TransformOperationType::Scale, .x = ValueUnit(func.value, UnitType::Point), .y = One, .z = One};
632+
}
633+
634+
if constexpr (std::is_same_v<T, CSSScaleY>) {
635+
return TransformOperation{
636+
.type = TransformOperationType::Scale, .x = One, .y = ValueUnit(func.value, UnitType::Point), .z = One};
637+
}
638+
639+
if constexpr (std::is_same_v<T, CSSSkewX>) {
640+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
641+
return TransformOperation{
642+
.type = TransformOperationType::Skew, .x = ValueUnit(radians, UnitType::Point), .y = Zero, .z = Zero};
643+
}
644+
645+
if constexpr (std::is_same_v<T, CSSSkewY>) {
646+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
647+
return TransformOperation{
648+
.type = TransformOperationType::Skew, .x = Zero, .y = ValueUnit(radians, UnitType::Point), .z = Zero};
649+
}
650+
651+
if constexpr (std::is_same_v<T, CSSPerspective>) {
652+
if (func.length.unit != CSSLengthUnit::Px) {
653+
return std::nullopt;
654+
}
655+
return TransformOperation{
656+
.type = TransformOperationType::Perspective,
657+
.x = ValueUnit(func.length.value, UnitType::Point),
658+
.y = Zero,
659+
.z = Zero};
660+
}
661+
662+
if constexpr (std::is_same_v<T, CSSMatrix>) {
663+
return TransformOperation{.type = TransformOperationType::Arbitrary, .x = Zero, .y = Zero, .z = Zero};
664+
}
665+
},
666+
cssTransform);
667+
}
668+
669+
inline void parseProcessedTransform(const PropsParserContext & /*context*/, const RawValue &value, Transform &result)
537670
{
538671
auto transformMatrix = Transform{};
539672
react_native_expect(value.hasType<std::vector<RawValue>>());
@@ -770,6 +903,71 @@ inline void fromRawValue(const PropsParserContext & /*context*/, const RawValue
770903
result = transformMatrix;
771904
}
772905

906+
inline void parseUnprocessedTransformString(const std::string &value, Transform &result)
907+
{
908+
auto transformList = parseCSSProperty<CSSTransformList>(value);
909+
if (!std::holds_alternative<CSSTransformList>(transformList)) {
910+
result = {};
911+
return;
912+
}
913+
914+
auto transformMatrix = Transform{};
915+
const auto &cssFuncs = std::get<CSSTransformList>(transformList);
916+
transformMatrix.operations.reserve(cssFuncs.size());
917+
for (const auto &cssFunc : cssFuncs) {
918+
auto op = fromCSSTransformFunction(cssFunc);
919+
if (!op.has_value()) {
920+
result = {};
921+
return;
922+
}
923+
924+
if (op->type == TransformOperationType::Arbitrary) {
925+
// CSSMatrix: expand 6-value 2D matrix to 4x4 matrix
926+
if (std::holds_alternative<CSSMatrix>(cssFunc)) {
927+
const auto &m = std::get<CSSMatrix>(cssFunc);
928+
transformMatrix.matrix[0] = m.values[0];
929+
transformMatrix.matrix[1] = m.values[1];
930+
transformMatrix.matrix[2] = 0;
931+
transformMatrix.matrix[3] = 0;
932+
transformMatrix.matrix[4] = m.values[2];
933+
transformMatrix.matrix[5] = m.values[3];
934+
transformMatrix.matrix[6] = 0;
935+
transformMatrix.matrix[7] = 0;
936+
transformMatrix.matrix[8] = 0;
937+
transformMatrix.matrix[9] = 0;
938+
transformMatrix.matrix[10] = 1;
939+
transformMatrix.matrix[11] = 0;
940+
transformMatrix.matrix[12] = m.values[4];
941+
transformMatrix.matrix[13] = m.values[5];
942+
transformMatrix.matrix[14] = 0;
943+
transformMatrix.matrix[15] = 1;
944+
}
945+
}
946+
947+
transformMatrix.operations.push_back(*op);
948+
}
949+
950+
result = transformMatrix;
951+
}
952+
953+
inline void parseUnprocessedTransform(const PropsParserContext &context, const RawValue &value, Transform &result)
954+
{
955+
if (value.hasType<std::string>()) {
956+
parseUnprocessedTransformString((std::string)value, result);
957+
} else {
958+
parseProcessedTransform(context, value, result);
959+
}
960+
}
961+
962+
inline void fromRawValue(const PropsParserContext &context, const RawValue &value, Transform &result)
963+
{
964+
if (ReactNativeFeatureFlags::enableNativeCSSParsing()) {
965+
parseUnprocessedTransform(context, value, result);
966+
} else {
967+
parseProcessedTransform(context, value, result);
968+
}
969+
}
970+
773971
inline void fromRawValue(const PropsParserContext &context, const RawValue &value, TransformOrigin &result)
774972
{
775973
if (!value.hasType<std::vector<RawValue>>()) {

packages/react-native/ReactCommon/react/renderer/components/view/tests/ConversionsTest.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <react/renderer/components/view/BoxShadowPropsConversions.h>
1111
#include <react/renderer/components/view/FilterPropsConversions.h>
12+
#include <react/renderer/components/view/conversions.h>
1213

1314
namespace facebook::react {
1415

@@ -250,4 +251,110 @@ TEST(ConversionsTest, unprocessed_filter_objects_unknown_type) {
250251
EXPECT_TRUE(filters.empty());
251252
}
252253

254+
TEST(ConversionsTest, unprocessed_transform_css_string) {
255+
Transform result;
256+
parseUnprocessedTransformString(
257+
"rotate(45deg) scale(2) translateX(10px)", result);
258+
259+
EXPECT_EQ(result.operations.size(), 3);
260+
261+
// rotate(45deg) -> Rotate, z = 45 * PI / 180
262+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Rotate);
263+
EXPECT_NEAR(
264+
result.operations[0].z.value,
265+
static_cast<float>(45.0 * M_PI / 180.0),
266+
0.001f);
267+
268+
// scale(2) -> Scale, x=2, y=2
269+
EXPECT_EQ(result.operations[1].type, TransformOperationType::Scale);
270+
EXPECT_EQ(result.operations[1].x.value, 2.0f);
271+
EXPECT_EQ(result.operations[1].y.value, 2.0f);
272+
273+
// translateX(10px) -> Translate, x=10
274+
EXPECT_EQ(result.operations[2].type, TransformOperationType::Translate);
275+
EXPECT_EQ(result.operations[2].x.value, 10.0f);
276+
EXPECT_EQ(result.operations[2].x.unit, UnitType::Point);
277+
EXPECT_EQ(result.operations[2].y.value, 0.0f);
278+
}
279+
280+
TEST(ConversionsTest, unprocessed_transform_css_translate_percent) {
281+
Transform result;
282+
parseUnprocessedTransformString("translate(10px, 50%)", result);
283+
284+
EXPECT_EQ(result.operations.size(), 1);
285+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Translate);
286+
EXPECT_EQ(result.operations[0].x.value, 10.0f);
287+
EXPECT_EQ(result.operations[0].x.unit, UnitType::Point);
288+
EXPECT_EQ(result.operations[0].y.value, 50.0f);
289+
EXPECT_EQ(result.operations[0].y.unit, UnitType::Percent);
290+
}
291+
292+
TEST(ConversionsTest, unprocessed_transform_css_perspective) {
293+
Transform result;
294+
parseUnprocessedTransformString("perspective(500px)", result);
295+
296+
EXPECT_EQ(result.operations.size(), 1);
297+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Perspective);
298+
EXPECT_EQ(result.operations[0].x.value, 500.0f);
299+
}
300+
301+
TEST(ConversionsTest, unprocessed_transform_css_invalid_string) {
302+
Transform result;
303+
parseUnprocessedTransformString("not-a-transform", result);
304+
305+
EXPECT_TRUE(result.operations.empty());
306+
}
307+
308+
TEST(ConversionsTest, unprocessed_transform_rawvalue_string) {
309+
RawValue value{folly::dynamic("rotate(45deg) scale(2)")};
310+
Transform result;
311+
parseUnprocessedTransform(
312+
PropsParserContext{-1, ContextContainer{}}, value, result);
313+
314+
EXPECT_EQ(result.operations.size(), 2);
315+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Rotate);
316+
EXPECT_EQ(result.operations[1].type, TransformOperationType::Scale);
317+
}
318+
319+
TEST(ConversionsTest, unprocessed_transform_rawvalue_array) {
320+
RawValue value{folly::dynamic::array(
321+
folly::dynamic::object("rotate", "45deg"),
322+
folly::dynamic::object("scale", 2))};
323+
Transform result;
324+
parseUnprocessedTransform(
325+
PropsParserContext{-1, ContextContainer{}}, value, result);
326+
327+
EXPECT_EQ(result.operations.size(), 2);
328+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Rotate);
329+
EXPECT_EQ(result.operations[1].type, TransformOperationType::Scale);
330+
EXPECT_EQ(result.operations[1].x.value, 2.0f);
331+
}
332+
333+
TEST(ConversionsTest, unprocessed_transform_rawvalue_matrix) {
334+
RawValue value{folly::dynamic::array(
335+
folly::dynamic::object(
336+
"matrix",
337+
folly::dynamic::array(
338+
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)))};
339+
Transform result;
340+
parseUnprocessedTransform(
341+
PropsParserContext{-1, ContextContainer{}}, value, result);
342+
343+
EXPECT_EQ(result.operations.size(), 1);
344+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Arbitrary);
345+
}
346+
347+
TEST(ConversionsTest, unprocessed_transform_rawvalue_translate_percent) {
348+
RawValue value{
349+
folly::dynamic::array(folly::dynamic::object("translateX", "50%"))};
350+
Transform result;
351+
parseUnprocessedTransform(
352+
PropsParserContext{-1, ContextContainer{}}, value, result);
353+
354+
EXPECT_EQ(result.operations.size(), 1);
355+
EXPECT_EQ(result.operations[0].type, TransformOperationType::Translate);
356+
EXPECT_EQ(result.operations[0].x.value, 50.0f);
357+
EXPECT_EQ(result.operations[0].x.unit, UnitType::Percent);
358+
}
359+
253360
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/css/CSSTransform.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,11 @@ using CSSTransformFunction = CSSCompoundDataType<
452452
CSSSkewY,
453453
CSSPerspective>;
454454

455+
/**
456+
* Variant of possible CSS transform function types
457+
*/
458+
using CSSTransformFunctionVariant = CSSVariantWithTypes<CSSTransformFunction>;
459+
455460
/**
456461
* Represents the <transform-list> type.
457462
* https://drafts.csswg.org/css-transforms-1/#typedef-transform-list

packages/react-native/ReactCommon/react/renderer/graphics/ValueUnit.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ struct ValueUnit {
2626
float value{0.0f};
2727
UnitType unit{UnitType::Undefined};
2828

29-
ValueUnit() = default;
30-
ValueUnit(float v, UnitType u) : value(v), unit(u) {}
29+
constexpr ValueUnit() = default;
30+
constexpr ValueUnit(float v, UnitType u) : value(v), unit(u) {}
3131

32-
bool operator==(const ValueUnit &other) const
32+
constexpr bool operator==(const ValueUnit &other) const
3333
{
3434
return value == other.value && unit == other.unit;
3535
}
3636

37-
bool operator!=(const ValueUnit &other) const
37+
constexpr bool operator!=(const ValueUnit &other) const
3838
{
3939
return !(*this == other);
4040
}

0 commit comments

Comments
 (0)