From d9c93ba3ae753e5e8ddac485263f47c752595908 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 24 Jul 2025 08:37:22 +0200 Subject: [PATCH 1/5] Init button wrapper shadow node --- .../react-native-gesture-handler/package.json | 3 +- .../ComponentDescriptors.h | 2 + ...eHandlerButtonWrapperComponentDescriptor.h | 32 ++++++++++ ...NGestureHandlerButtonWrapperShadowNode.cpp | 59 +++++++++++++++++++ .../RNGestureHandlerButtonWrapperShadowNode.h | 57 ++++++++++++++++++ .../RNGestureHandlerButtonWrapperState.h | 32 ++++++++++ ...tureHandlerButtonWrapperNativeComponent.ts | 11 ++++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h create mode 100644 packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h create mode 100644 packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts diff --git a/packages/react-native-gesture-handler/package.json b/packages/react-native-gesture-handler/package.json index 3dcc46b08f..8c0e79a783 100644 --- a/packages/react-native-gesture-handler/package.json +++ b/packages/react-native-gesture-handler/package.json @@ -137,7 +137,8 @@ "ios": { "componentProvider": { "RNGestureHandlerButton": "RNGestureHandlerButtonComponentView", - "RNGestureHandlerDetector": "RNGestureHandlerDetector" + "RNGestureHandlerDetector": "RNGestureHandlerDetector", + "RNGestureHandlerButtonWrapper": "RNGestureHandlerButtonWrapper" } } }, diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h index d9f3374c48..5621db20e7 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h @@ -15,12 +15,14 @@ #include #include +#include #include namespace facebook::react { using RNGestureHandlerButtonComponentDescriptor = ConcreteComponentDescriptor; + using RNGestureHandlerRootViewComponentDescriptor = ConcreteComponentDescriptor; diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h new file mode 100644 index 0000000000..c3323ae823 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperComponentDescriptor.h @@ -0,0 +1,32 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateComponentDescriptorH.js + */ + +#pragma once + +#include + +#include "RNGestureHandlerButtonWrapperShadowNode.h" + +namespace facebook::react { + +class RNGestureHandlerButtonWrapperComponentDescriptor final + : public ConcreteComponentDescriptor< + RNGestureHandlerButtonWrapperShadowNode> { + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + void adopt(ShadowNode &shadowNode) const override { + react_native_assert( + dynamic_cast(&shadowNode)); + + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp new file mode 100644 index 0000000000..e8a45f5caf --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp @@ -0,0 +1,59 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateShadowNodeCpp.js + */ + +#include "RNGestureHandlerButtonWrapperShadowNode.h" + +namespace facebook::react { + +extern const char RNGestureHandlerButtonWrapperComponentName[] = + "RNGestureHandlerButtonWrapper"; + +void RNGestureHandlerButtonWrapperShadowNode::initialize() { + // Disable forcing view flattening + ShadowNode::traits_.unset(ShadowNodeTraits::ForceFlattenView); + + // When the button wrapper is cloned and has a child node, the child node + // should be cloned as well to ensure it is mutable. + const auto &children = getChildren(); + if (!children.empty()) { + react_native_assert( + children.size() == 1 && + "RNGestureHandlerButtonWrapper received more than one child"); + + const auto clonedChild = children[0]->clone({}); + replaceChild(*children[0], clonedChild); + } +} + +void RNGestureHandlerButtonWrapperShadowNode::layout( + LayoutContext layoutContext) { + YogaLayoutableShadowNode::layout(layoutContext); + // TODO: consider allowing more than one child and doing bounding box + react_native_assert(getChildren().size() == 1); + + auto child = std::static_pointer_cast( + getChildren()[0]); + + child->ensureUnsealed(); + auto mutableChild = std::const_pointer_cast(child); + + // TODO: figure out the correct way to setup metrics between button wrapper + // and the child + auto metrics = child->getLayoutMetrics(); + metrics.frame = child->getLayoutMetrics().frame; + setLayoutMetrics(metrics); + + auto childmetrics = child->getLayoutMetrics(); + childmetrics.frame.origin = Point{}; + mutableChild->setLayoutMetrics(childmetrics); +} + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h new file mode 100644 index 0000000000..ef0a0ca2ff --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h @@ -0,0 +1,57 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateShadowNodeH.js + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "RNGestureHandlerButtonWrapperState.h" + +namespace facebook::react { + +JSI_EXPORT extern const char RNGestureHandlerButtonWrapperComponentName[]; + +/* + * `ShadowNode` for component. + */ +class RNGestureHandlerButtonWrapperShadowNode final + : public ConcreteViewShadowNode< + RNGestureHandlerButtonWrapperComponentName, + RNGestureHandlerButtonWrapperProps, + RNGestureHandlerButtonWrapperEventEmitter, + RNGestureHandlerButtonWrapperState> { + public: + RNGestureHandlerButtonWrapperShadowNode( + const ShadowNodeFragment &fragment, + const ShadowNodeFamily::Shared &family, + ShadowNodeTraits traits) + : ConcreteViewShadowNode(fragment, family, traits) { + initialize(); + } + + RNGestureHandlerButtonWrapperShadowNode( + const ShadowNode &sourceShadowNode, + const ShadowNodeFragment &fragment) + : ConcreteViewShadowNode(sourceShadowNode, fragment) { + initialize(); + } + + void layout(LayoutContext layoutContext) override; + + private: + void initialize(); +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h new file mode 100644 index 0000000000..7d9fa242e0 --- /dev/null +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperState.h @@ -0,0 +1,32 @@ +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateStateH.js + */ +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +class RNGestureHandlerButtonWrapperState { + public: + RNGestureHandlerButtonWrapperState() = default; + +#ifdef ANDROID + RNGestureHandlerButtonWrapperState( + RNGestureHandlerButtonWrapperState const &previousState, + folly::dynamic data){}; + folly::dynamic getDynamic() const { + return {}; + }; +#endif +}; + +} // namespace facebook::react diff --git a/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts new file mode 100644 index 0000000000..d84e610aaa --- /dev/null +++ b/packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonWrapperNativeComponent.ts @@ -0,0 +1,11 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps } from 'react-native'; + +interface NativeProps extends ViewProps {} + +export default codegenNativeComponent( + 'RNGestureHandlerButtonWrapper', + { + interfaceOnly: true, + } +); From ac6835894d2e9950110b96ef65f66681461e04f7 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 24 Jul 2025 08:37:40 +0200 Subject: [PATCH 2/5] Implement iOS view --- .../apple/RNGestureHandlerButtonWrapper.h | 19 ++++++++ .../apple/RNGestureHandlerButtonWrapper.mm | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h create mode 100644 packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h new file mode 100644 index 0000000000..c94ecf946e --- /dev/null +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.h @@ -0,0 +1,19 @@ +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +#import + +#import + +using namespace facebook::react; + +NS_ASSUME_NONNULL_BEGIN + +@interface RNGestureHandlerButtonWrapper : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm new file mode 100644 index 0000000000..308b69ac76 --- /dev/null +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButtonWrapper.mm @@ -0,0 +1,48 @@ +#import "RNGestureHandlerButtonWrapper.h" +#import "RNGestureHandlerButtonWrapperComponentDescriptor.h" +#import "RNGestureHandlerModule.h" + +#import +#import + +#import +#import +#import + +#include + +@interface RNGestureHandlerButtonWrapper () +@end + +@implementation RNGestureHandlerButtonWrapper + +#if TARGET_OS_OSX ++ (BOOL)shouldBeRecycled +{ + return NO; +} +#endif + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + + return self; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +@end + +Class RNGestureHandlerButtonWrapperCls(void) +{ + return RNGestureHandlerButtonWrapper.class; +} From d9dc96c13f6f0cb1020b6714dfc291967130ed6e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 24 Jul 2025 09:01:13 +0200 Subject: [PATCH 3/5] Implement Android view --- .../gesturehandler/RNGestureHandlerPackage.kt | 5 +++ .../RNGestureHandlerButtonWrapperView.kt | 6 ++++ ...NGestureHandlerButtonWrapperViewManager.kt | 33 +++++++++++++++++++ .../react-native.config.js | 5 ++- 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperView.kt create mode 100644 packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt index 95f071671f..d5c7001e20 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt @@ -11,6 +11,7 @@ import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager +import com.swmansion.gesturehandler.react.RNGestureHandlerButtonWrapperViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerDetectorViewManager import com.swmansion.gesturehandler.react.RNGestureHandlerModule import com.swmansion.gesturehandler.react.RNGestureHandlerRootViewManager @@ -34,6 +35,9 @@ class RNGestureHandlerPackage : RNGestureHandlerDetectorViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { RNGestureHandlerDetectorViewManager() }, + RNGestureHandlerButtonWrapperViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { + RNGestureHandlerButtonWrapperViewManager() + }, ) } @@ -41,6 +45,7 @@ class RNGestureHandlerPackage : RNGestureHandlerRootViewManager(), RNGestureHandlerButtonViewManager(), RNGestureHandlerDetectorViewManager(), + RNGestureHandlerButtonWrapperViewManager(), ) override fun getViewManagerNames(reactContext: ReactApplicationContext) = viewManagers.keys.toList() diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperView.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperView.kt new file mode 100644 index 0000000000..f6b899968d --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperView.kt @@ -0,0 +1,6 @@ +package com.swmansion.gesturehandler.react + +import android.content.Context +import com.facebook.react.views.view.ReactViewGroup + +class RNGestureHandlerButtonWrapperView(context: Context) : ReactViewGroup(context) diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt new file mode 100644 index 0000000000..617d7b0877 --- /dev/null +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonWrapperViewManager.kt @@ -0,0 +1,33 @@ +package com.swmansion.gesturehandler.react + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerDelegate +import com.facebook.react.viewmanagers.RNGestureHandlerButtonWrapperManagerInterface + +@ReactModule(name = RNGestureHandlerButtonWrapperViewManager.REACT_CLASS) +class RNGestureHandlerButtonWrapperViewManager : + ViewGroupManager(), + RNGestureHandlerButtonWrapperManagerInterface { + private val mDelegate: ViewManagerDelegate + + init { + mDelegate = + RNGestureHandlerButtonWrapperManagerDelegate< + RNGestureHandlerButtonWrapperView, + RNGestureHandlerButtonWrapperViewManager, + >(this) + } + + override fun getDelegate(): ViewManagerDelegate = mDelegate + + override fun getName() = REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext) = RNGestureHandlerButtonWrapperView(reactContext) + + companion object { + const val REACT_CLASS = "RNGestureHandlerButtonWrapper" + } +} diff --git a/packages/react-native-gesture-handler/react-native.config.js b/packages/react-native-gesture-handler/react-native.config.js index 4ec41154cd..46e3de4682 100644 --- a/packages/react-native-gesture-handler/react-native.config.js +++ b/packages/react-native-gesture-handler/react-native.config.js @@ -2,7 +2,10 @@ module.exports = { dependency: { platforms: { android: { - componentDescriptors: ['RNGestureHandlerDetectorComponentDescriptor'], + componentDescriptors: [ + 'RNGestureHandlerDetectorComponentDescriptor', + 'RNGestureHandlerButtonWrapperComponentDescriptor', + ], cmakeListsPath: './CMakeLists.txt', }, }, From 0c07a4ed821771717d0c52bcc910c455492656cc Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 24 Jul 2025 12:27:35 +0200 Subject: [PATCH 4/5] What are you? A button sandwitch. --- ...NGestureHandlerButtonWrapperShadowNode.cpp | 60 +++++++++++++++---- .../RNGestureHandlerButtonWrapperShadowNode.h | 2 + .../src/components/GestureHandlerButton.tsx | 29 ++++++++- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp index e8a45f5caf..7382a63237 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.cpp @@ -9,6 +9,8 @@ * @generated by codegen project: GenerateShadowNodeCpp.js */ +#include + #include "RNGestureHandlerButtonWrapperShadowNode.h" namespace facebook::react { @@ -22,38 +24,70 @@ void RNGestureHandlerButtonWrapperShadowNode::initialize() { // When the button wrapper is cloned and has a child node, the child node // should be cloned as well to ensure it is mutable. + if (!getChildren().empty()) { + prepareChildren(); + } +} + +void RNGestureHandlerButtonWrapperShadowNode::prepareChildren() { const auto &children = getChildren(); - if (!children.empty()) { - react_native_assert( - children.size() == 1 && - "RNGestureHandlerButtonWrapper received more than one child"); + react_native_assert( + children.size() == 1 && + "RNGestureHandlerButtonWrapper received more than one child"); - const auto clonedChild = children[0]->clone({}); - replaceChild(*children[0], clonedChild); - } + const auto directChild = children[0]; + react_native_assert( + directChild->getChildren().size() == 1 && + "RNGestureHandlerButtonWrapper received more than one grandchild"); + + const auto clonedChild = directChild->clone({}); + + const auto childWithProtectedAccess = + std::static_pointer_cast( + clonedChild); + childWithProtectedAccess->traits_.unset(ShadowNodeTraits::ForceFlattenView); + + replaceChild(*directChild, clonedChild); + + const auto grandChild = clonedChild->getChildren()[0]; + const auto clonedGrandChild = grandChild->clone({}); + clonedChild->replaceChild(*grandChild, clonedGrandChild); +} + +void RNGestureHandlerButtonWrapperShadowNode::appendChild( + const ShadowNode::Shared &child) { + YogaLayoutableShadowNode::appendChild(child); + prepareChildren(); } void RNGestureHandlerButtonWrapperShadowNode::layout( LayoutContext layoutContext) { YogaLayoutableShadowNode::layout(layoutContext); - // TODO: consider allowing more than one child and doing bounding box react_native_assert(getChildren().size() == 1); + react_native_assert(getChildren()[0]->getChildren().size() == 1); auto child = std::static_pointer_cast( getChildren()[0]); + auto grandChild = std::static_pointer_cast( + child->getChildren()[0]); child->ensureUnsealed(); + grandChild->ensureUnsealed(); + auto mutableChild = std::const_pointer_cast(child); + auto mutableGrandChild = + std::const_pointer_cast(grandChild); // TODO: figure out the correct way to setup metrics between button wrapper // and the child - auto metrics = child->getLayoutMetrics(); - metrics.frame = child->getLayoutMetrics().frame; + auto metrics = grandChild->getLayoutMetrics(); setLayoutMetrics(metrics); - auto childmetrics = child->getLayoutMetrics(); - childmetrics.frame.origin = Point{}; - mutableChild->setLayoutMetrics(childmetrics); + auto metricsNoOrigin = grandChild->getLayoutMetrics(); + metricsNoOrigin.frame.origin = Point{}; + + mutableChild->setLayoutMetrics(metricsNoOrigin); + mutableGrandChild->setLayoutMetrics(metricsNoOrigin); } } // namespace facebook::react diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h index ef0a0ca2ff..bc83e7a2a3 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerButtonWrapperShadowNode.h @@ -49,9 +49,11 @@ class RNGestureHandlerButtonWrapperShadowNode final } void layout(LayoutContext layoutContext) override; + void appendChild(const ShadowNode::Shared &child) override; private: void initialize(); + void prepareChildren(); }; } // namespace facebook::react diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index b6f0c391c0..40b2f7ea58 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -1,5 +1,30 @@ -import { HostComponent } from 'react-native'; +import { HostComponent, StyleSheet, View } from 'react-native'; import type { RawButtonProps } from './GestureButtonsProps'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; +import RNGestureHandlerButtonWrapperNativeComponent from '../specs/RNGestureHandlerButtonWrapperNativeComponent'; -export default RNGestureHandlerButtonNativeComponent as HostComponent; +const ButtonComponent = + RNGestureHandlerButtonNativeComponent as HostComponent; + +export default function GestureHandlerButton({ + style, + ...rest +}: RawButtonProps) { + const flattenedStyle = StyleSheet.flatten(style); + + const { width, height, ...restStyle } = flattenedStyle; + + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + contents: { + display: 'contents', + }, +}); From c5661d13d9b01782756a95c4aac8833133b865ac Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 24 Jul 2025 15:43:04 +0200 Subject: [PATCH 5/5] Style splitting --- apps/basic-example/src/RuntimeDecoration.tsx | 580 +++++++++++++++--- .../src/components/GestureHandlerButton.tsx | 129 +++- 2 files changed, 602 insertions(+), 107 deletions(-) diff --git a/apps/basic-example/src/RuntimeDecoration.tsx b/apps/basic-example/src/RuntimeDecoration.tsx index d3baa9eb3c..540cf1ef1c 100644 --- a/apps/basic-example/src/RuntimeDecoration.tsx +++ b/apps/basic-example/src/RuntimeDecoration.tsx @@ -1,105 +1,190 @@ -import * as React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; - -import { COLORS } from './colors'; -import Animated, { - runOnJS, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; -import { useState } from 'react'; - -export function RuntimeChecker({ - runGestureOnJS, -}: { - runGestureOnJS: boolean; -}) { - const pressed = useSharedValue(false); - const active = useSharedValue(false); - const posX = useSharedValue(0); - const posY = useSharedValue(0); - - const start = useSharedValue({ x: 0, y: 0 }); - const [isUIRuntime, setIsUIRuntime] = useState(null); - - const style = useAnimatedStyle(() => { - return { - transform: [ - { translateX: posX.value }, - { translateY: posY.value }, - { scale: pressed.value ? 1.2 : 1 }, - ], - backgroundColor: active.value ? COLORS.KINDA_GREEN : COLORS.KINDA_BLUE, - }; - }); - - const gesture = Gesture.Manual() - .runOnJS(runGestureOnJS) - .onTouchesDown((e) => { - if (!pressed.value) { - pressed.value = true; - start.value = { - x: e.allTouches[0].absoluteX, - y: e.allTouches[0].absoluteY, - }; - } - runOnJS(setIsUIRuntime)(_WORKLET ?? false); - }) - .onTouchesMove((e, state) => { - const dist = Math.sqrt( - Math.pow(e.allTouches[0].absoluteX - start.value.x, 2) + - Math.pow(e.allTouches[0].absoluteY - start.value.y, 2) - ); - - if (active.value) { - posX.value = e.allTouches[0].absoluteX - start.value.x; - posY.value = e.allTouches[0].absoluteY - start.value.y; - } else { - if (dist > 10) { - state.activate(); - start.value = { - x: e.allTouches[0].absoluteX, - y: e.allTouches[0].absoluteY, - }; - } - } - }) - .onTouchesUp((e, state) => { - if (e.allTouches.length === e.changedTouches.length) { - state.end(); - } - }) - .onStart(() => { - active.value = true; - }) - .onFinalize(() => { - pressed.value = false; - active.value = false; - }); +import React from 'react'; +import { View, StyleSheet, Text, SafeAreaView } from 'react-native'; +import { + GestureHandlerRootView, + ScrollView, + RectButton, +} from 'react-native-gesture-handler'; +const MyButton = RectButton; + +export default function ComplexUI() { + return ( + + + + + + + + + + + + + + + + + ); +} + +const colors = ['#782AEB', '#38ACDD', '#57B495', '#FF6259', '#FFD61E']; + +function Avatars() { + return ( + + {colors.map((color) => ( + + {color.slice(1, 3)} + + ))} + + ); +} + +function Gallery() { + return ( + + Basic Gallery + + + + + + + + + + ); +} + +function SizeConstraints() { return ( - - - {isUIRuntime === null - ? 'Start the gesture to check the runtime' - : `Running on the ${isUIRuntime ? 'UI' : 'JS'} runtime`} - - - - + + Size Constraints + + + Min/Max + + + 1:1 + + + Flex + + ); } -export default function RuntimeDecorationExample() { +function FlexboxTests() { return ( - - - - + + Flexbox Layouts + + + Start + + + Center + + + End + + + + + Wrap 1 + + + Wrap 2 + + + Wrap 3 + + + Wrap 4 + + + + ); +} + +function PositioningTests() { + return ( + + Positioning + + + Z-Index + + + Absolute + + + Relative + + + + ); +} + +function SpacingTests() { + return ( + + Spacing & Overflow + + + Padding + + + Margin + + + Overflow Hidden Test + + + + ); +} + +function VisualEffects() { + return ( + + Visual Effects + + + Shadow + + + Opacity + + + + ); +} + +function ComplexCombinations() { + return ( + + Complex Combinations + + + Complex 1 + + + Complex 2 + + + Complex 3 + + + Complex 4 + + ); } @@ -107,17 +192,308 @@ export default function RuntimeDecorationExample() { const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: '#f5f5f5', + }, + scrollContent: { + paddingBottom: 50, + }, + paddedContainer: { + padding: 16, + }, + section: { + marginBottom: 30, + }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + color: '#333', + }, + gap: { + gap: 10, + }, + row: { + flexDirection: 'row', + }, + + // Avatar styles + avatars: { + width: 90, + height: 90, + borderWidth: 2, + borderColor: '#001A72', + borderTopLeftRadius: 30, + borderTopRightRadius: 5, + borderBottomLeftRadius: 5, + borderBottomRightRadius: 30, + marginHorizontal: 4, alignItems: 'center', justifyContent: 'center', - gap: 8, }, - box: { - width: 50, - height: 50, + avatarLabel: { + color: '#F8F9FF', + fontSize: 24, + fontWeight: 'bold', }, - separator: { + + // Gallery styles + fullWidthButton: { width: '100%', - height: 1, - backgroundColor: 'black', + height: 160, + backgroundColor: '#FF6259', + borderTopRightRadius: 30, + borderTopLeftRadius: 30, + borderWidth: 1, + borderColor: '#000', + }, + leftButton: { + flex: 1, + height: 160, + backgroundColor: '#FFD61E', + borderBottomLeftRadius: 30, + borderWidth: 5, + borderColor: '#000', + }, + rightButton: { + flex: 1, + backgroundColor: '#782AEB', + height: 160, + borderBottomRightRadius: 30, + borderWidth: 8, + borderColor: '#000', + }, + + // Size constraint styles + minMaxButton: { + minWidth: 80, + maxWidth: 120, + minHeight: 40, + maxHeight: 80, + backgroundColor: '#38ACDD', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + aspectRatioButton: { + width: 80, + aspectRatio: 1, + backgroundColor: '#57B495', + borderRadius: 40, + justifyContent: 'center', + alignItems: 'center', + }, + flexGrowButton: { + flexGrow: 1, + height: 60, + backgroundColor: '#FF6259', + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + }, + + // Flexbox styles + flexContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + height: 80, + backgroundColor: '#e0e0e0', + borderRadius: 10, + padding: 10, + marginBottom: 10, + }, + flexStart: { + alignSelf: 'flex-start', + backgroundColor: '#782AEB', + padding: 10, + borderRadius: 5, + }, + flexCenter: { + alignSelf: 'center', + backgroundColor: '#38ACDD', + padding: 10, + borderRadius: 5, + }, + flexEnd: { + alignSelf: 'flex-end', + backgroundColor: '#57B495', + padding: 10, + borderRadius: 5, + }, + flexWrapContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 5, + }, + wrapItem: { + width: '48%', + height: 50, + backgroundColor: '#FFD61E', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, + + // Positioning styles + positionContainer: { + height: 80, + backgroundColor: '#e0e0e0', + borderRadius: 10, + position: 'relative', + }, + absoluteButton: { + position: 'absolute', + top: 10, + right: 10, + backgroundColor: '#FF6259', + padding: 8, + borderRadius: 5, + }, + relativeButton: { + position: 'relative', + top: 20, + left: 20, + backgroundColor: '#782AEB', + padding: 8, + borderRadius: 5, + }, + zIndexButton: { + position: 'absolute', + top: 10, + left: 10, + zIndex: 10, + backgroundColor: '#57B495', + padding: 8, + borderRadius: 5, + }, + + // Spacing styles + paddingButton: { + flex: 1, + backgroundColor: '#38ACDD', + paddingVertical: 20, + paddingHorizontal: 15, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + marginButton: { + flex: 1, + backgroundColor: '#FFD61E', + margin: 10, + padding: 10, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + overflowButton: { + flex: 1, + height: 60, + backgroundColor: '#782AEB', + borderRadius: 10, + overflow: 'hidden', + justifyContent: 'center', + alignItems: 'center', + }, + + // Visual effect styles + shadowButton: { + flex: 1, + height: 60, + backgroundColor: '#FF6259', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + opacityButton: { + flex: 1, + height: 60, + backgroundColor: '#782AEB', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + opacity: 0.7, + }, + + // Complex combination styles + complexGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + complexButton1: { + width: '48%', + height: 100, + backgroundColor: '#FF6259', + borderRadius: 20, + borderWidth: 2, + borderColor: '#782AEB', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 2, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 4, + marginBottom: 5, + }, + complexButton2: { + width: '48%', + minHeight: 80, + maxHeight: 120, + backgroundColor: '#38ACDD', + borderTopLeftRadius: 30, + borderBottomRightRadius: 30, + paddingVertical: 15, + paddingHorizontal: 10, + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + }, + complexButton3: { + width: '48%', + aspectRatio: 1.5, + backgroundColor: '#57B495', + borderRadius: 15, + borderWidth: 4, + borderColor: '#FFD61E', + justifyContent: 'center', + alignItems: 'center', + opacity: 0.9, + marginTop: 10, + }, + complexButton4: { + width: '48%', + height: 80, + backgroundColor: '#FFD61E', + borderRadius: 10, + borderWidth: 1, + borderColor: '#FF6259', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#782AEB', + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.4, + shadowRadius: 10, + elevation: 10, + marginTop: 10, + }, + + // Text styles + buttonText: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + }, + longText: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center', + fontSize: 16, }, }); diff --git a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx index 40b2f7ea58..51fc9bdf4c 100644 --- a/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx +++ b/packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx @@ -2,6 +2,7 @@ import { HostComponent, StyleSheet, View } from 'react-native'; import type { RawButtonProps } from './GestureButtonsProps'; import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent'; import RNGestureHandlerButtonWrapperNativeComponent from '../specs/RNGestureHandlerButtonWrapperNativeComponent'; +import { useMemo } from 'react'; const ButtonComponent = RNGestureHandlerButtonNativeComponent as HostComponent; @@ -10,14 +11,129 @@ export default function GestureHandlerButton({ style, ...rest }: RawButtonProps) { - const flattenedStyle = StyleSheet.flatten(style); + const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]); - const { width, height, ...restStyle } = flattenedStyle; + const { + // Layout properties + display, + width, + height, + minWidth, + maxWidth, + minHeight, + maxHeight, + flex, + flexGrow, + flexShrink, + flexBasis, + flexDirection, + flexWrap, + justifyContent, + alignItems, + alignContent, + alignSelf, + aspectRatio, + gap, + rowGap, + columnGap, + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginVertical, + marginHorizontal, + marginStart, + marginEnd, + padding, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + paddingVertical, + paddingHorizontal, + paddingStart, + paddingEnd, + position, + top, + right, + bottom, + left, + start, + end, + overflow, + zIndex, + + // Visual properties + ...restStyle + } = flattenedStyle; + + // Layout styles for ButtonComponent + const layoutStyle = useMemo( + () => ({ + display, + width, + height, + minWidth, + maxWidth, + minHeight, + maxHeight, + flex, + flexGrow, + flexShrink, + flexBasis, + flexDirection, + flexWrap, + justifyContent, + alignItems, + alignContent, + alignSelf, + aspectRatio, + gap, + rowGap, + columnGap, + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginVertical, + marginHorizontal, + marginStart, + marginEnd, + padding, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + paddingVertical, + paddingHorizontal, + paddingStart, + paddingEnd, + position, + top, + right, + bottom, + left, + start, + end, + overflow, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [flattenedStyle] + ); return ( - - - + + + ); @@ -27,4 +143,7 @@ const styles = StyleSheet.create({ contents: { display: 'contents', }, + overflowHidden: { + overflow: 'hidden', + }, });