Skip to content

Commit aaf3c6b

Browse files
NickGerlemanmeta-codesync[bot]
authored andcommitted
Wire native CSS parsing for transform
Summary: 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] Differential Revision: D94052735
1 parent e568b36 commit aaf3c6b

3 files changed

Lines changed: 329 additions & 2 deletions

File tree

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: 217 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,154 @@ 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(
554+
const std::variant<
555+
CSSMatrix,
556+
CSSTranslate,
557+
CSSTranslateX,
558+
CSSTranslateY,
559+
CSSTranslate3D,
560+
CSSScale,
561+
CSSScaleX,
562+
CSSScaleY,
563+
CSSRotate,
564+
CSSRotateX,
565+
CSSRotateY,
566+
CSSRotateZ,
567+
CSSSkewX,
568+
CSSSkewY,
569+
CSSPerspective> &cssTransform)
570+
{
571+
auto Zero = ValueUnit(0, UnitType::Point);
572+
auto One = ValueUnit(1, UnitType::Point);
573+
574+
return std::visit(
575+
[&](auto &&func) -> std::optional<TransformOperation> {
576+
using T = std::decay_t<decltype(func)>;
577+
578+
if constexpr (std::is_same_v<T, CSSRotate>) {
579+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
580+
return TransformOperation{
581+
.type = TransformOperationType::Rotate, .x = Zero, .y = Zero, .z = ValueUnit(radians, UnitType::Point)};
582+
}
583+
584+
if constexpr (std::is_same_v<T, CSSRotateX>) {
585+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
586+
return TransformOperation{
587+
.type = TransformOperationType::Rotate, .x = ValueUnit(radians, UnitType::Point), .y = Zero, .z = Zero};
588+
}
589+
590+
if constexpr (std::is_same_v<T, CSSRotateY>) {
591+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
592+
return TransformOperation{
593+
.type = TransformOperationType::Rotate, .x = Zero, .y = ValueUnit(radians, UnitType::Point), .z = Zero};
594+
}
595+
596+
if constexpr (std::is_same_v<T, CSSRotateZ>) {
597+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
598+
return TransformOperation{
599+
.type = TransformOperationType::Rotate, .x = Zero, .y = Zero, .z = ValueUnit(radians, UnitType::Point)};
600+
}
601+
602+
if constexpr (std::is_same_v<T, CSSTranslate>) {
603+
auto x = cssLengthPercentageToValueUnit(func.x);
604+
auto y = cssLengthPercentageToValueUnit(func.y);
605+
if (!x || !y) {
606+
return std::nullopt;
607+
}
608+
return TransformOperation{.type = TransformOperationType::Translate, .x = x, .y = y, .z = Zero};
609+
}
610+
611+
if constexpr (std::is_same_v<T, CSSTranslateX>) {
612+
auto x = cssLengthPercentageToValueUnit(func.value);
613+
if (!x) {
614+
return std::nullopt;
615+
}
616+
return TransformOperation{.type = TransformOperationType::Translate, .x = x, .y = Zero, .z = Zero};
617+
}
618+
619+
if constexpr (std::is_same_v<T, CSSTranslateY>) {
620+
auto y = cssLengthPercentageToValueUnit(func.value);
621+
if (!y) {
622+
return std::nullopt;
623+
}
624+
return TransformOperation{.type = TransformOperationType::Translate, .x = Zero, .y = y, .z = Zero};
625+
}
626+
627+
if constexpr (std::is_same_v<T, CSSTranslate3D>) {
628+
auto x = cssLengthPercentageToValueUnit(func.x);
629+
auto y = cssLengthPercentageToValueUnit(func.y);
630+
if (!x || !y || func.z.unit != CSSLengthUnit::Px) {
631+
return std::nullopt;
632+
}
633+
return TransformOperation{
634+
.type = TransformOperationType::Translate, .x = x, .y = y, .z = ValueUnit(func.z.value, UnitType::Point)};
635+
}
636+
637+
if constexpr (std::is_same_v<T, CSSScale>) {
638+
return TransformOperation{
639+
.type = TransformOperationType::Scale,
640+
.x = ValueUnit(func.x, UnitType::Point),
641+
.y = ValueUnit(func.y, UnitType::Point),
642+
.z = One};
643+
}
644+
645+
if constexpr (std::is_same_v<T, CSSScaleX>) {
646+
return TransformOperation{
647+
.type = TransformOperationType::Scale, .x = ValueUnit(func.value, UnitType::Point), .y = One, .z = One};
648+
}
649+
650+
if constexpr (std::is_same_v<T, CSSScaleY>) {
651+
return TransformOperation{
652+
.type = TransformOperationType::Scale, .x = One, .y = ValueUnit(func.value, UnitType::Point), .z = One};
653+
}
654+
655+
if constexpr (std::is_same_v<T, CSSSkewX>) {
656+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
657+
return TransformOperation{
658+
.type = TransformOperationType::Skew, .x = ValueUnit(radians, UnitType::Point), .y = Zero, .z = Zero};
659+
}
660+
661+
if constexpr (std::is_same_v<T, CSSSkewY>) {
662+
auto radians = static_cast<float>(func.degrees * M_PI / 180.0f);
663+
return TransformOperation{
664+
.type = TransformOperationType::Skew, .x = Zero, .y = ValueUnit(radians, UnitType::Point), .z = Zero};
665+
}
666+
667+
if constexpr (std::is_same_v<T, CSSPerspective>) {
668+
if (func.length.unit != CSSLengthUnit::Px) {
669+
return std::nullopt;
670+
}
671+
return TransformOperation{
672+
.type = TransformOperationType::Perspective,
673+
.x = ValueUnit(func.length.value, UnitType::Point),
674+
.y = Zero,
675+
.z = Zero};
676+
}
677+
678+
if constexpr (std::is_same_v<T, CSSMatrix>) {
679+
return TransformOperation{.type = TransformOperationType::Arbitrary, .x = Zero, .y = Zero, .z = Zero};
680+
}
681+
682+
return std::nullopt;
683+
},
684+
cssTransform);
685+
}
686+
687+
inline void parseProcessedTransform(const PropsParserContext & /*context*/, const RawValue &value, Transform &result)
537688
{
538689
auto transformMatrix = Transform{};
539690
react_native_expect(value.hasType<std::vector<RawValue>>());
@@ -770,6 +921,71 @@ inline void fromRawValue(const PropsParserContext & /*context*/, const RawValue
770921
result = transformMatrix;
771922
}
772923

924+
inline void parseUnprocessedTransformString(const std::string &value, Transform &result)
925+
{
926+
auto transformList = parseCSSProperty<CSSTransformList>(value);
927+
if (!std::holds_alternative<CSSTransformList>(transformList)) {
928+
result = {};
929+
return;
930+
}
931+
932+
auto transformMatrix = Transform{};
933+
const auto &cssFuncs = std::get<CSSTransformList>(transformList);
934+
transformMatrix.operations.reserve(cssFuncs.size());
935+
for (const auto &cssFunc : cssFuncs) {
936+
auto op = fromCSSTransformFunction(cssFunc);
937+
if (!op.has_value()) {
938+
result = {};
939+
return;
940+
}
941+
942+
if (op->type == TransformOperationType::Arbitrary) {
943+
// CSSMatrix: expand 6-value 2D matrix to 4x4 matrix
944+
if (std::holds_alternative<CSSMatrix>(cssFunc)) {
945+
const auto &m = std::get<CSSMatrix>(cssFunc);
946+
transformMatrix.matrix[0] = m.values[0];
947+
transformMatrix.matrix[1] = m.values[1];
948+
transformMatrix.matrix[2] = 0;
949+
transformMatrix.matrix[3] = 0;
950+
transformMatrix.matrix[4] = m.values[2];
951+
transformMatrix.matrix[5] = m.values[3];
952+
transformMatrix.matrix[6] = 0;
953+
transformMatrix.matrix[7] = 0;
954+
transformMatrix.matrix[8] = 0;
955+
transformMatrix.matrix[9] = 0;
956+
transformMatrix.matrix[10] = 1;
957+
transformMatrix.matrix[11] = 0;
958+
transformMatrix.matrix[12] = m.values[4];
959+
transformMatrix.matrix[13] = m.values[5];
960+
transformMatrix.matrix[14] = 0;
961+
transformMatrix.matrix[15] = 1;
962+
}
963+
}
964+
965+
transformMatrix.operations.push_back(*op);
966+
}
967+
968+
result = transformMatrix;
969+
}
970+
971+
inline void parseUnprocessedTransform(const PropsParserContext &context, const RawValue &value, Transform &result)
972+
{
973+
if (value.hasType<std::string>()) {
974+
parseUnprocessedTransformString((std::string)value, result);
975+
} else {
976+
parseProcessedTransform(context, value, result);
977+
}
978+
}
979+
980+
inline void fromRawValue(const PropsParserContext &context, const RawValue &value, Transform &result)
981+
{
982+
if (ReactNativeFeatureFlags::enableNativeCSSParsing()) {
983+
parseUnprocessedTransform(context, value, result);
984+
} else {
985+
parseProcessedTransform(context, value, result);
986+
}
987+
}
988+
773989
inline void fromRawValue(const PropsParserContext &context, const RawValue &value, TransformOrigin &result)
774990
{
775991
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

0 commit comments

Comments
 (0)