From 769cd09dcacb3c30a86db300be29ba0979495dc8 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 22 May 2024 14:58:16 -0400 Subject: [PATCH] macOS Delta --- .../AddressSanitizerCrash.js | 13 + .../NativeAddressSanitizerCrash.js | 19 + .../react-native/Libraries/Alert/Alert.js | 118 +++ .../Libraries/Alert/NativeAlertManager.js | 5 + .../Libraries/Alert/RCTAlertManager.macos.js | 16 + .../Libraries/Animated/AnimatedMock.js | 8 +- .../Animated/NativeAnimatedHelper.js | 4 +- .../Libraries/AppDelegate/RCTAppDelegate.h | 17 +- .../Libraries/AppDelegate/RCTAppDelegate.mm | 44 +- .../Libraries/AppDelegate/RCTAppSetupUtils.h | 2 +- .../Libraries/AppDelegate/RCTAppSetupUtils.mm | 4 +- .../AppDelegate/RCTLegacyInteropComponents.mm | 6 +- .../Libraries/AppState/AppState.js | 4 +- .../AccessibilityInfo/AccessibilityInfo.js | 106 ++- .../NativeAccessibilityManager.js | 6 + .../legacySendAccessibilityEvent.macos.js | 16 + .../Libraries/Components/Button.flow.js | 53 ++ .../Libraries/Components/Button.js | 86 +- .../Libraries/Components/Keyboard/Keyboard.js | 4 +- .../Components/Pressable/Pressable.js | 150 ++- .../Components/ScrollView/ScrollView.js | 40 +- .../ScrollView/ScrollViewNativeComponent.js | 4 + .../Components/StatusBar/StatusBar.js | 4 +- .../RCTMultilineTextInputNativeComponent.js | 2 +- .../RCTSingelineTextInputNativeComponent.js | 2 +- .../TextInput/RCTTextInputViewConfig.js | 15 + .../Components/TextInput/TextInput.flow.js | 150 ++- .../Components/TextInput/TextInput.js | 186 +++- .../TextInput/TextInputNativeCommands.js | 14 +- .../Components/TextInput/TextInputState.js | 4 +- .../Components/Touchable/Touchable.js | 5 + .../Components/Touchable/TouchableBounce.js | 30 +- .../Touchable/TouchableHighlight.js | 32 +- .../Touchable/TouchableNativeFeedback.js | 14 + .../Components/Touchable/TouchableOpacity.js | 56 +- .../Touchable/TouchableWithoutFeedback.js | 54 +- .../Libraries/Components/View/DraggedType.js | 21 + .../View/ReactNativeStyleAttributes.js | 1 + .../View/ReactNativeViewAttributes.js | 17 + .../Components/View/ViewAccessibility.js | 3 +- .../Components/View/ViewPropTypes.js | 162 +++- .../Libraries/Core/NativeExceptionsManager.js | 6 +- .../react-native/Libraries/Core/setUpAlert.js | 1 + .../DevToolsSettingsManager.macos.js | 14 + .../EventEmitter/NativeEventEmitter.js | 2 +- .../react-native/Libraries/Image/Image.ios.js | 3 + .../Libraries/Image/Image.macos.js | 15 + .../Libraries/Image/ImageProps.js | 5 + .../Libraries/Image/RCTAnimatedImage.h | 8 +- .../Libraries/Image/RCTAnimatedImage.mm | 11 + .../Image/RCTDisplayWeakRefreshable.h | 6 +- .../Image/RCTDisplayWeakRefreshable.mm | 6 +- .../Libraries/Image/RCTImageBlurUtils.h | 2 +- .../Libraries/Image/RCTImageBlurUtils.mm | 20 +- .../Libraries/Image/RCTImageCache.h | 2 +- .../Libraries/Image/RCTImageCache.mm | 4 + .../Libraries/Image/RCTImageDataDecoder.h | 2 +- .../Libraries/Image/RCTImageEditingManager.mm | 6 +- .../Libraries/Image/RCTImageLoader.h | 3 +- .../Libraries/Image/RCTImageLoader.mm | 67 +- .../Libraries/Image/RCTImageLoaderProtocol.h | 3 +- .../RCTImageLoaderWithAttributionProtocol.h | 4 +- .../Libraries/Image/RCTImageStoreManager.h | 2 +- .../Libraries/Image/RCTImageStoreManager.mm | 6 +- .../Libraries/Image/RCTImageURLLoader.h | 2 +- .../Image/RCTImageURLLoaderWithAttribution.h | 4 +- .../Libraries/Image/RCTImageUtils.h | 7 +- .../Libraries/Image/RCTImageUtils.mm | 48 +- .../Libraries/Image/RCTImageView.h | 5 +- .../Libraries/Image/RCTImageView.mm | 163 +++- .../Libraries/Image/RCTImageViewManager.mm | 4 +- .../Libraries/Image/RCTResizeMode.h | 7 + .../Libraries/Image/RCTUIImageViewAnimated.h | 3 +- .../Libraries/Image/RCTUIImageViewAnimated.mm | 23 +- .../Libraries/Image/nativeImageSource.js | 2 + .../Libraries/Inspector/NetworkOverlay.js | 2 +- .../react-native/Libraries/Linking/Linking.js | 6 +- .../Libraries/LinkingIOS/RCTLinkingManager.h | 9 +- .../LinkingIOS/macos/RCTLinkingManager.mm | 123 +++ .../Libraries/Lists/FillRateHelper.js | 6 +- .../react-native/Libraries/Lists/FlatList.js | 45 +- .../Libraries/Lists/SectionList.js | 4 +- .../Libraries/Lists/SectionListModern.js | 4 +- .../Libraries/Lists/ViewabilityHelper.js | 6 +- .../Libraries/Lists/VirtualizeUtils.js | 4 +- .../Libraries/Lists/VirtualizedList.js | 6 +- .../Libraries/Lists/VirtualizedListContext.js | 4 +- .../Libraries/Lists/VirtualizedSectionList.js | 6 +- .../LogBox/UI/LogBoxInspectorCodeFrame.js | 16 +- .../LogBox/UI/LogBoxInspectorHeader.js | 1 + .../LogBox/UI/LogBoxInspectorReactFrames.js | 16 +- .../LogBox/UI/LogBoxInspectorStackFrame.js | 8 +- .../react-native/Libraries/Modal/Modal.js | 2 +- .../Drivers/RCTDecayAnimation.mm | 2 +- .../Drivers/RCTFrameAnimation.mm | 2 +- .../Drivers/RCTSpringAnimation.mm | 2 +- .../Nodes/RCTInterpolationAnimatedNode.mm | 4 +- .../Nodes/RCTValueAnimatedNode.h | 2 +- .../NativeAnimation/RCTAnimationUtils.h | 3 +- .../NativeAnimation/RCTAnimationUtils.mm | 2 +- .../RCTNativeAnimatedModule.mm | 4 +- .../RCTNativeAnimatedNodesManager.h | 3 +- .../RCTNativeAnimatedNodesManager.mm | 6 +- .../NativeComponent/BaseViewConfig.macos.js | 90 ++ .../NativeComponent/ViewConfigIgnore.js | 3 +- .../NativeModules/specs/NativeDevSettings.js | 3 + .../Network/RCTFileRequestHandler.mm | 4 + .../Libraries/Network/RCTNetworking.macos.js | 15 + .../components/DebugInstructions.js | 7 + .../components/ReloadInstructions.js | 8 + .../Libraries/Pressability/HoverState.js | 4 + .../Libraries/Pressability/Pressability.js | 72 +- .../PushNotificationIOS.js | 7 +- .../RCTPushNotificationManager.h | 9 + .../RCTPushNotificationManager.mm | 178 +++- .../Libraries/ReactNative/PaperUIManager.js | 2 +- .../Libraries/Settings/RCTSettingsManager.mm | 31 + .../Libraries/Settings/Settings.macos.js | 15 + .../PlatformColorValueTypes.macos.js | 169 ++++ .../PlatformColorValueTypesMacOS.js | 44 + .../PlatformColorValueTypesMacOS.macos.js | 52 ++ .../Libraries/StyleSheet/StyleSheetTypes.js | 27 + .../Text/BaseText/RCTBaseTextViewManager.mm | 9 +- .../Libraries/Text/RCTConvert+Text.h | 16 + .../Libraries/Text/RCTConvert+Text.mm | 2 + .../Libraries/Text/RCTTextAttributes.h | 22 +- .../Libraries/Text/RCTTextAttributes.mm | 46 +- .../Libraries/Text/RCTTextUIKit.h | 50 + .../Text/RawText/RCTRawTextViewManager.mm | 4 +- .../Text/Text/NSTextStorage+FontScaling.h | 2 +- .../Text/Text/NSTextStorage+FontScaling.m | 2 +- .../Libraries/Text/Text/RCTDynamicTypeRamp.h | 2 + .../Libraries/Text/Text/RCTDynamicTypeRamp.mm | 2 + .../Libraries/Text/Text/RCTTextShadowView.mm | 32 +- .../Libraries/Text/Text/RCTTextView.h | 9 +- .../Libraries/Text/Text/RCTTextView.mm | 290 +++++- .../Libraries/Text/Text/RCTTextViewManager.mm | 8 +- .../Multiline/RCTMultilineTextInputView.h | 8 + .../Multiline/RCTMultilineTextInputView.mm | 144 ++- .../RCTMultilineTextInputViewManager.mm | 18 +- .../Text/TextInput/Multiline/RCTUITextView.h | 27 +- .../Text/TextInput/Multiline/RCTUITextView.mm | 345 ++++++- .../TextInput/RCTBackedTextInputDelegate.h | 24 +- .../RCTBackedTextInputDelegateAdapter.h | 13 +- .../RCTBackedTextInputDelegateAdapter.mm | 202 +++- .../RCTBackedTextInputViewProtocol.h | 45 +- .../TextInput/RCTBaseTextInputShadowView.mm | 19 +- .../Text/TextInput/RCTBaseTextInputView.h | 24 +- .../Text/TextInput/RCTBaseTextInputView.mm | 381 +++++++- .../TextInput/RCTBaseTextInputViewManager.mm | 75 +- .../TextInput/RCTInputAccessoryShadowView.mm | 2 + .../Text/TextInput/RCTInputAccessoryView.h | 4 +- .../Text/TextInput/RCTInputAccessoryView.mm | 6 +- .../TextInput/RCTInputAccessoryViewContent.h | 4 +- .../TextInput/RCTInputAccessoryViewContent.mm | 15 +- .../TextInput/RCTInputAccessoryViewManager.mm | 2 +- .../Singleline/RCTSinglelineTextInputView.mm | 65 +- .../RCTSinglelineTextInputViewManager.mm | 4 +- .../TextInput/Singleline/RCTUITextField.h | 40 +- .../TextInput/Singleline/RCTUITextField.mm | 429 ++++++++- .../Singleline/macOS/RCTUISecureTextField.h | 12 + .../Singleline/macOS/RCTUISecureTextField.m | 12 + .../Libraries/Text/TextNativeComponent.js | 1 + .../react-native/Libraries/Text/TextProps.js | 24 + .../Text/VirtualText/RCTVirtualTextView.h | 4 +- .../VirtualText/RCTVirtualTextViewManager.mm | 2 +- .../Libraries/Types/CoreEventTypes.js | 26 + .../Libraries/Utilities/Appearance.js | 4 +- .../Utilities/BackHandler.android.js | 2 + .../Libraries/Utilities/BackHandler.macos.js | 18 + .../Libraries/Utilities/DevSettings.js | 2 +- .../Libraries/Utilities/HMRClient.js | 4 +- .../Libraries/Utilities/LoadingView.macos.js | 15 + .../Utilities/NativePlatformConstantsMacOS.js | 31 + .../Libraries/Utilities/Platform.macos.js | 74 ++ .../Utilities/ReactNativeTestTools.js | 2 +- .../Libraries/WebSocket/WebSocket.js | 4 +- .../WebSocket/WebSocketInterceptor.js | 4 +- .../Wrapper/Example/RCTWrapperExampleView.h | 2 +- .../Example/RCTWrapperExampleViewController.h | 2 +- .../RCTWrapperReactRootViewController.h | 6 +- .../Example/RCTWrapperReactRootViewManager.h | 2 +- .../Libraries/Wrapper/RCTWrapper.h | 6 +- .../Libraries/Wrapper/RCTWrapperShadowView.h | 2 +- .../Libraries/Wrapper/RCTWrapperView.h | 14 +- .../RCTWrapperViewControllerHostingView.h | 16 +- packages/react-native/React/Base/RCTBridge.h | 2 +- packages/react-native/React/Base/RCTBridge.mm | 1 + .../react-native/React/Base/RCTBridgeModule.h | 6 +- .../react-native/React/Base/RCTBridgeProxy.mm | 2 +- .../React/Base/RCTBundleManager.h | 2 +- .../react-native/React/Base/RCTConstants.h | 4 + .../react-native/React/Base/RCTConstants.m | 8 + packages/react-native/React/Base/RCTConvert.h | 23 +- packages/react-native/React/Base/RCTConvert.m | 356 ++++++- .../react-native/React/Base/RCTDisplayLink.m | 13 +- .../React/Base/RCTEventDispatcherProtocol.h | 2 +- .../React/Base/RCTFocusChangeEvent.h | 20 + .../React/Base/RCTFocusChangeEvent.m | 30 + .../react-native/React/Base/RCTFrameUpdate.h | 5 +- .../react-native/React/Base/RCTFrameUpdate.m | 4 +- .../react-native/React/Base/RCTJSThread.h | 2 +- .../React/Base/RCTJavaScriptLoader.h | 2 +- .../react-native/React/Base/RCTKeyCommands.h | 2 + .../react-native/React/Base/RCTKeyCommands.m | 2 + .../React/Base/RCTPlatformDisplayLink.h | 41 + .../React/Base/RCTRedBoxSetEnabled.m | 2 +- .../React/Base/RCTReloadCommand.m | 6 +- .../React/Base/RCTRootContentView.h | 2 +- .../React/Base/RCTRootContentView.m | 53 +- .../react-native/React/Base/RCTRootView.h | 12 +- .../react-native/React/Base/RCTRootView.m | 95 +- .../react-native/React/Base/RCTTouchHandler.h | 14 +- .../react-native/React/Base/RCTTouchHandler.m | 305 +++++- packages/react-native/React/Base/RCTUIKit.h | 607 ++++++++++++ packages/react-native/React/Base/RCTUtils.h | 21 +- packages/react-native/React/Base/RCTUtils.m | 131 ++- .../React/Base/RCTUtilsUIOverride.h | 2 +- .../React/Base/RCTUtilsUIOverride.m | 1 + .../react-native/React/Base/RCTViewRegistry.m | 6 +- .../React/Base/Surface/RCTSurfaceDelegate.h | 2 +- .../React/Base/Surface/RCTSurfaceProtocol.h | 2 +- .../RCTSurfaceRootShadowViewDelegate.h | 2 +- .../React/Base/Surface/RCTSurfaceRootView.h | 2 +- .../React/Base/Surface/RCTSurfaceStage.h | 2 +- .../Base/Surface/RCTSurfaceView+Internal.h | 2 +- .../React/Base/Surface/RCTSurfaceView.h | 4 +- .../React/Base/Surface/RCTSurfaceView.mm | 7 + .../RCTSurfaceHostingProxyRootView.h | 8 +- .../RCTSurfaceHostingProxyRootView.mm | 12 +- .../RCTSurfaceHostingView.h | 6 +- .../RCTSurfaceHostingView.mm | 31 +- .../RCTSurfaceSizeMeasureMode.h | 2 +- .../RCTSurfaceSizeMeasureMode.mm | 2 +- .../React/Base/macOS/RCTPlatform.m | 42 + .../React/Base/macOS/RCTPlatformDisplayLink.m | 165 ++++ .../react-native/React/Base/macOS/RCTUIKit.m | 860 +++++++++++++++++ .../React/CoreModules/CoreModulesPlugins.mm | 4 + .../CoreModules/RCTAccessibilityManager.h | 1 + .../CoreModules/RCTAccessibilityManager.mm | 3 + .../React/CoreModules/RCTActionSheetManager.h | 2 +- .../CoreModules/RCTActionSheetManager.mm | 189 +++- .../RCTAddressSanitizerCrashManager.h | 12 + .../RCTAddressSanitizerCrashManager.mm | 18 + .../React/CoreModules/RCTAlertController.h | 8 +- .../React/CoreModules/RCTAlertController.mm | 4 + .../React/CoreModules/RCTAlertManager.h | 2 +- .../React/CoreModules/RCTAlertManager.mm | 111 ++- .../React/CoreModules/RCTAppState.mm | 17 +- .../React/CoreModules/RCTAppearance.h | 6 +- .../React/CoreModules/RCTAppearance.mm | 49 + .../React/CoreModules/RCTClipboard.mm | 13 +- .../React/CoreModules/RCTDevLoadingView.mm | 84 +- .../React/CoreModules/RCTDevMenu.h | 9 +- .../React/CoreModules/RCTDevMenu.mm | 95 +- .../React/CoreModules/RCTDevSettings.h | 15 + .../React/CoreModules/RCTDevSettings.mm | 42 +- .../React/CoreModules/RCTDeviceInfo.h | 4 +- .../React/CoreModules/RCTDeviceInfo.mm | 31 +- .../React/CoreModules/RCTEventDispatcher.h | 2 +- .../React/CoreModules/RCTEventDispatcher.mm | 2 +- .../React/CoreModules/RCTFPSGraph.h | 6 +- .../React/CoreModules/RCTFPSGraph.mm | 4 +- .../React/CoreModules/RCTKeyboardObserver.mm | 10 +- .../React/CoreModules/RCTLogBox.h | 2 +- .../React/CoreModules/RCTLogBox.mm | 12 + .../React/CoreModules/RCTLogBoxView.h | 19 +- .../React/CoreModules/RCTLogBoxView.mm | 88 ++ .../React/CoreModules/RCTPerfMonitor.mm | 2 +- .../React/CoreModules/RCTPlatform.mm | 2 +- .../React/CoreModules/RCTRedBox.h | 2 +- .../React/CoreModules/RCTRedBox.mm | 383 +++++++- .../React/CoreModules/RCTStatusBarManager.h | 4 +- .../React/CoreModules/RCTStatusBarManager.mm | 7 + .../React/CoreModules/RCTTiming.mm | 6 + .../React/CoreModules/RCTWebSocketModule.mm | 59 +- .../React/CxxBridge/RCTCxxBridge.mm | 14 +- .../DevSupport/RCTDevLoadingViewProtocol.h | 4 +- .../DevSupport/RCTInspectorDevServerHelper.h | 3 +- .../DevSupport/RCTInspectorDevServerHelper.mm | 18 +- .../RCTActivityIndicatorViewComponentView.h | 2 +- .../RCTActivityIndicatorViewComponentView.mm | 5 +- .../Image/RCTImageComponentView.mm | 7 + .../RCTInputAccessoryComponentView.h | 2 +- .../RCTInputAccessoryComponentView.mm | 20 +- .../RCTInputAccessoryContentView.h | 4 +- .../RCTInputAccessoryContentView.mm | 6 +- ...RCTLegacyViewManagerInteropComponentView.h | 2 +- ...CTLegacyViewManagerInteropComponentView.mm | 16 +- ...gacyViewManagerInteropCoordinatorAdapter.h | 2 +- ...acyViewManagerInteropCoordinatorAdapter.mm | 2 +- .../Modal/RCTFabricModalHostViewController.h | 4 +- .../Modal/RCTFabricModalHostViewController.mm | 2 + .../Modal/RCTModalHostViewComponentView.h | 2 + .../Modal/RCTModalHostViewComponentView.mm | 12 +- .../Root/RCTRootComponentView.h | 2 +- .../RCTSafeAreaViewComponentView.h | 2 +- .../RCTSafeAreaViewComponentView.mm | 25 +- .../RCTCustomPullToRefreshViewProtocol.h | 2 +- .../ScrollView/RCTEnhancedScrollView.h | 8 +- .../ScrollView/RCTEnhancedScrollView.mm | 29 +- .../RCTPullToRefreshViewComponentView.h | 2 +- .../RCTPullToRefreshViewComponentView.mm | 16 + .../ScrollView/RCTScrollViewComponentView.h | 10 +- .../ScrollView/RCTScrollViewComponentView.mm | 104 ++- .../Switch/RCTSwitchComponentView.h | 2 +- .../Switch/RCTSwitchComponentView.mm | 19 +- .../Text/RCTAccessibilityElement.h | 6 +- .../Text/RCTAccessibilityElement.mm | 4 +- ...TParagraphComponentAccessibilityProvider.h | 6 +- ...ParagraphComponentAccessibilityProvider.mm | 8 +- .../Text/RCTParagraphComponentView.h | 2 +- .../Text/RCTParagraphComponentView.mm | 25 +- .../TextInput/RCTTextInputComponentView.h | 2 +- .../TextInput/RCTTextInputComponentView.mm | 122 ++- .../TextInput/RCTTextInputUtils.h | 19 +- .../TextInput/RCTTextInputUtils.mm | 22 +- .../RCTUnimplementedNativeComponentView.h | 3 +- .../RCTUnimplementedNativeComponentView.mm | 14 +- .../RCTUnimplementedViewComponentView.h | 2 +- .../RCTUnimplementedViewComponentView.mm | 14 +- .../View/RCTViewComponentView.h | 8 +- .../View/RCTViewComponentView.mm | 76 +- .../RCTComponentViewClassDescriptor.h | 2 +- .../Mounting/RCTComponentViewDescriptor.h | 5 +- .../Fabric/Mounting/RCTComponentViewFactory.h | 2 +- .../Mounting/RCTComponentViewProtocol.h | 9 +- .../Mounting/RCTComponentViewRegistry.h | 4 +- .../Mounting/RCTComponentViewRegistry.mm | 12 +- .../Fabric/Mounting/RCTMountingManager.h | 6 +- .../Fabric/Mounting/RCTMountingManager.mm | 20 +- .../Mounting/RCTMountingManagerDelegate.h | 2 +- .../RCTMountingTransactionObserving.h | 2 +- .../Mounting/UIView+ComponentViewProtocol.h | 13 +- .../Mounting/UIView+ComponentViewProtocol.mm | 28 +- .../React/Fabric/RCTConversions.h | 14 +- .../React/Fabric/RCTImageResponseDelegate.h | 2 +- .../React/Fabric/RCTLocalizationProvider.h | 2 +- .../react-native/React/Fabric/RCTScheduler.h | 2 +- .../React/Fabric/RCTSurfacePointerHandler.h | 6 +- .../React/Fabric/RCTSurfacePointerHandler.mm | 216 ++++- .../React/Fabric/RCTSurfacePresenter.h | 4 +- .../React/Fabric/RCTSurfacePresenter.mm | 16 +- .../Fabric/RCTSurfacePresenterBridgeAdapter.h | 2 +- .../React/Fabric/RCTSurfaceRegistry.h | 2 +- .../React/Fabric/RCTSurfaceTouchHandler.h | 6 +- .../React/Fabric/RCTSurfaceTouchHandler.mm | 223 ++++- .../RCTTouchableComponentViewProtocol.h | 2 +- .../React/Fabric/Surface/RCTFabricSurface.mm | 2 + .../Fabric/Utils/RCTGenericDelegateSplitter.h | 2 +- .../React/Fabric/Utils/RCTReactTaggedView.h | 10 +- .../React/Fabric/Utils/RCTReactTaggedView.mm | 8 +- .../Modules/MacOS/RCTAccessibilityManager.m | 166 ++++ .../React/Modules/RCTEventEmitter.h | 14 +- .../react-native/React/Modules/RCTI18nUtil.m | 2 +- .../React/Modules/RCTLayoutAnimation.h | 2 +- .../React/Modules/RCTLayoutAnimation.m | 56 ++ .../React/Modules/RCTLayoutAnimationGroup.h | 2 +- .../RCTRedBoxExtraDataViewController.h | 3 + .../RCTRedBoxExtraDataViewController.m | 2 + .../React/Modules/RCTSurfacePresenterStub.h | 2 +- .../react-native/React/Modules/RCTUIManager.h | 25 +- .../react-native/React/Modules/RCTUIManager.m | 211 +++-- .../Modules/RCTUIManagerObserverCoordinator.h | 2 +- .../React/Modules/RCTUIManagerUtils.h | 1 + .../react-native/React/Profiler/RCTProfile.m | 28 +- .../react-native/React/UIUtils/RCTUIUtils.h | 9 +- .../react-native/React/UIUtils/RCTUIUtils.m | 32 + .../React/Views/RCTActivityIndicatorView.h | 12 +- .../React/Views/RCTActivityIndicatorView.m | 110 +++ .../Views/RCTActivityIndicatorViewManager.m | 8 +- .../React/Views/RCTAnimationType.h | 4 + .../React/Views/RCTAutoInsetsProtocol.h | 2 +- .../React/Views/RCTBorderDrawing.h | 6 +- .../React/Views/RCTBorderDrawing.m | 89 +- .../React/Views/RCTComponentData.h | 6 +- .../React/Views/RCTComponentData.m | 6 +- packages/react-native/React/Views/RCTCursor.h | 39 + packages/react-native/React/Views/RCTCursor.m | 105 +++ packages/react-native/React/Views/RCTFont.h | 2 +- packages/react-native/React/Views/RCTFont.mm | 17 + .../react-native/React/Views/RCTHandledKey.h | 40 + .../react-native/React/Views/RCTHandledKey.m | 145 +++ packages/react-native/React/Views/RCTLayout.h | 2 +- .../React/Views/RCTModalHostView.h | 4 +- .../React/Views/RCTModalHostView.m | 2 + .../React/Views/RCTModalHostViewController.h | 2 + .../React/Views/RCTModalHostViewController.m | 2 + .../React/Views/RCTModalHostViewManager.h | 2 + .../React/Views/RCTModalHostViewManager.m | 2 + .../React/Views/RCTModalManager.h | 2 +- .../React/Views/RCTSegmentedControl.h | 10 +- .../React/Views/RCTSegmentedControl.m | 70 +- .../React/Views/RCTSegmentedControlManager.m | 4 +- .../React/Views/RCTShadowView+Internal.h | 2 +- .../React/Views/RCTShadowView+Layout.h | 2 +- .../react-native/React/Views/RCTShadowView.h | 7 +- .../react-native/React/Views/RCTShadowView.m | 12 +- packages/react-native/React/Views/RCTSwitch.h | 4 +- .../React/Views/RCTSwitchManager.m | 19 +- packages/react-native/React/Views/RCTView.h | 92 +- packages/react-native/React/Views/RCTView.m | 881 +++++++++++++++++- .../React/Views/RCTViewKeyboardEvent.h | 17 + .../React/Views/RCTViewKeyboardEvent.m | 85 ++ .../react-native/React/Views/RCTViewManager.h | 25 +- .../react-native/React/Views/RCTViewManager.m | 150 ++- .../react-native/React/Views/RCTViewUtils.h | 4 +- .../react-native/React/Views/RCTViewUtils.m | 4 +- .../React/Views/RCTWrapperViewController.h | 4 +- .../React/Views/RCTWrapperViewController.m | 2 + .../Views/RefreshControl/RCTRefreshControl.h | 4 +- .../Views/RefreshControl/RCTRefreshControl.m | 2 + .../RefreshControl/RCTRefreshControlManager.m | 2 + .../RefreshControl/RCTRefreshableProtocol.h | 2 +- .../Views/SafeAreaView/RCTSafeAreaView.h | 2 +- .../Views/SafeAreaView/RCTSafeAreaView.m | 4 + .../SafeAreaView/RCTSafeAreaViewLocalData.h | 2 +- .../SafeAreaView/RCTSafeAreaViewManager.m | 2 + .../MacOS/RCTScrollContentLocalData.h | 27 + .../MacOS/RCTScrollContentLocalData.m | 26 + .../ScrollView/RCTScrollContentShadowView.h | 2 +- .../ScrollView/RCTScrollContentShadowView.m | 18 + .../Views/ScrollView/RCTScrollContentView.h | 6 +- .../Views/ScrollView/RCTScrollContentView.m | 45 + .../ScrollView/RCTScrollContentViewManager.m | 2 + .../React/Views/ScrollView/RCTScrollEvent.m | 8 +- .../React/Views/ScrollView/RCTScrollView.h | 26 +- .../React/Views/ScrollView/RCTScrollView.m | 493 ++++++++-- .../Views/ScrollView/RCTScrollViewManager.h | 2 + .../Views/ScrollView/RCTScrollViewManager.m | 48 +- .../Views/ScrollView/RCTScrollableProtocol.h | 8 +- .../react-native/React/Views/UIView+Private.h | 8 +- .../react-native/React/Views/UIView+React.h | 26 +- .../react-native/React/Views/UIView+React.m | 138 ++- .../ios/ReactCommon/RCTSampleLegacyModule.h | 2 +- .../ios/ReactCommon/RCTSampleTurboModule.mm | 6 +- .../RCTLegacyViewManagerInteropCoordinator.h | 10 +- .../renderer/components/view/BaseTouch.cpp | 14 + .../renderer/components/view/BaseTouch.h | 26 + .../graphics/RCTPlatformColorUtils.mm | 21 +- .../renderer/imagemanager/RCTImageManager.h | 2 +- .../RCTImagePrimitivesConversions.h | 2 +- .../imagemanager/RCTSyncImageManager.h | 2 +- .../NSTextStorage+FontScaling.h | 2 +- .../RCTAttributedTextUtils.h | 2 +- .../RCTAttributedTextUtils.mm | 24 +- .../textlayoutmanager/RCTFontProperties.h | 2 +- .../renderer/textlayoutmanager/RCTFontUtils.h | 2 +- .../textlayoutmanager/RCTFontUtils.mm | 4 + .../textlayoutmanager/RCTTextLayoutManager.h | 2 +- .../textlayoutmanager/RCTTextLayoutManager.mm | 14 +- .../RCTTextPrimitivesConversions.h | 16 +- .../ios/ReactCommon/ObjCTimerRegistry.h | 2 +- .../platform/ios/ReactCommon/RCTInstance.h | 2 +- .../platform/ios/ReactCommon/RCTInstance.mm | 2 +- packages/react-native/index.js | 12 + .../ios/RNTLegacyView.h | 4 +- .../ios/RNTLegacyView.mm | 2 +- .../ios/RNTMyLegacyNativeViewManager.mm | 18 +- .../ios/RNTMyNativeViewComponentView.h | 4 +- .../ios/RNTMyNativeViewComponentView.mm | 20 +- .../ios/RNTMyNativeViewManager.mm | 19 +- .../NativeModuleExample/Screenshot.mm | 37 +- packages/rn-tester/RNTester/AppDelegate.h | 2 +- packages/rn-tester/RNTester/AppDelegate.mm | 41 +- .../FlexibleSizeExampleView.h | 2 +- .../FlexibleSizeExampleView.mm | 36 +- .../UpdatePropertiesExampleView.h | 2 +- .../UpdatePropertiesExampleView.mm | 19 +- packages/rn-tester/RNTester/main.m | 11 +- packages/rn-tester/js/RNTesterApp.ios.js | 51 +- packages/rn-tester/js/RNTesterApp.macos.js | 16 + .../js/components/ListExampleShared.js | 32 +- .../js/components/RNTPressableRow.js | 10 +- .../rn-tester/js/components/RNTesterBlock.js | 12 +- .../js/components/RNTesterModuleContainer.js | 3 +- .../js/components/RNTesterModuleList.js | 25 +- .../js/examples/ASAN/ASANCrashExample.js | 37 + .../Accessibility/AccessibilityExample.js | 54 +- .../AccessibilityShowMenu.js | 66 ++ .../js/examples/Border/BorderExample.js | 1 + .../js/examples/FlatList/FlatList-basic.js | 47 +- .../FocusEventsExample/FocusEventsExample.js | 290 ++++++ .../js/examples/FocusOnMount/FocusOnMount.js | 53 ++ .../js/examples/FocusRing/FocusRingExample.js | 72 ++ .../js/examples/GhostText/GhostText.js | 256 +++++ .../js/examples/Image/ImageExample.js | 11 +- .../KeyboardEventsExample.js | 258 +++++ .../js/examples/Layout/LayoutEventsExample.js | 8 +- .../PlatformColor/PlatformColorExample.js | 247 ++++- .../js/examples/Pressable/PressableExample.js | 6 + .../examples/ScrollView/ScrollViewExample.js | 81 ++ .../ScrollViewIndicatorInsetsExample.macos.js | 114 +++ .../js/examples/Snapshot/SnapshotExample.js | 16 +- .../js/examples/Text/TextExample.ios.js | 108 ++- .../TextInput/TextInputExample.ios.js | 268 +++++- .../TextInput/TextInputSharedExamples.js | 91 +- .../TimePicker/TimePickerAndroidExample.js | 125 +++ .../js/examples/Tooltip/TooltipExample.js | 97 ++ .../js/examples/Touchable/TouchableExample.js | 127 +++ .../TurboModule/TurboModuleExampleCommon.js | 3 +- packages/rn-tester/js/types/RNTesterTypes.js | 11 +- .../rn-tester/js/utils/RNTesterList.ios.js | 65 +- .../rn-tester/js/utils/RNTesterList.macos.js | 16 + .../Lists/VirtualizedList.js | 178 +++- .../Lists/VirtualizedListCellRenderer.js | 5 + .../Lists/VirtualizedListProps.js | 52 ++ 507 files changed, 16569 insertions(+), 1447 deletions(-) create mode 100644 packages/react-native/Libraries/AddressSanitizerCrash/AddressSanitizerCrash.js create mode 100644 packages/react-native/Libraries/AddressSanitizerCrash/NativeAddressSanitizerCrash.js create mode 100644 packages/react-native/Libraries/Alert/RCTAlertManager.macos.js create mode 100644 packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js create mode 100644 packages/react-native/Libraries/Components/View/DraggedType.js create mode 100644 packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.macos.js create mode 100644 packages/react-native/Libraries/Image/Image.macos.js create mode 100644 packages/react-native/Libraries/LinkingIOS/macos/RCTLinkingManager.mm create mode 100644 packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js create mode 100644 packages/react-native/Libraries/Network/RCTNetworking.macos.js create mode 100644 packages/react-native/Libraries/Settings/Settings.macos.js create mode 100644 packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js create mode 100644 packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js create mode 100644 packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js create mode 100644 packages/react-native/Libraries/Text/RCTTextUIKit.h create mode 100644 packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h create mode 100644 packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m create mode 100644 packages/react-native/Libraries/Utilities/BackHandler.macos.js create mode 100644 packages/react-native/Libraries/Utilities/LoadingView.macos.js create mode 100644 packages/react-native/Libraries/Utilities/NativePlatformConstantsMacOS.js create mode 100644 packages/react-native/Libraries/Utilities/Platform.macos.js create mode 100644 packages/react-native/React/Base/RCTFocusChangeEvent.h create mode 100644 packages/react-native/React/Base/RCTFocusChangeEvent.m create mode 100644 packages/react-native/React/Base/RCTPlatformDisplayLink.h create mode 100644 packages/react-native/React/Base/RCTUIKit.h create mode 100644 packages/react-native/React/Base/macOS/RCTPlatform.m create mode 100644 packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m create mode 100644 packages/react-native/React/Base/macOS/RCTUIKit.m create mode 100644 packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.h create mode 100644 packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.mm create mode 100644 packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m create mode 100644 packages/react-native/React/Views/RCTCursor.h create mode 100644 packages/react-native/React/Views/RCTCursor.m create mode 100644 packages/react-native/React/Views/RCTHandledKey.h create mode 100644 packages/react-native/React/Views/RCTHandledKey.m create mode 100644 packages/react-native/React/Views/RCTViewKeyboardEvent.h create mode 100644 packages/react-native/React/Views/RCTViewKeyboardEvent.m create mode 100644 packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.h create mode 100644 packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m create mode 100644 packages/rn-tester/js/RNTesterApp.macos.js create mode 100644 packages/rn-tester/js/examples/ASAN/ASANCrashExample.js create mode 100644 packages/rn-tester/js/examples/AccessibilityShowMenu/AccessibilityShowMenu.js create mode 100644 packages/rn-tester/js/examples/FocusEventsExample/FocusEventsExample.js create mode 100644 packages/rn-tester/js/examples/FocusOnMount/FocusOnMount.js create mode 100644 packages/rn-tester/js/examples/FocusRing/FocusRingExample.js create mode 100644 packages/rn-tester/js/examples/GhostText/GhostText.js create mode 100644 packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js create mode 100644 packages/rn-tester/js/examples/ScrollView/ScrollViewIndicatorInsetsExample.macos.js create mode 100644 packages/rn-tester/js/examples/TimePicker/TimePickerAndroidExample.js create mode 100644 packages/rn-tester/js/examples/Tooltip/TooltipExample.js create mode 100644 packages/rn-tester/js/utils/RNTesterList.macos.js diff --git a/packages/react-native/Libraries/AddressSanitizerCrash/AddressSanitizerCrash.js b/packages/react-native/Libraries/AddressSanitizerCrash/AddressSanitizerCrash.js new file mode 100644 index 00000000000000..06790e7bc25a7b --- /dev/null +++ b/packages/react-native/Libraries/AddressSanitizerCrash/AddressSanitizerCrash.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import NativeAddressSanitizerCrash from './NativeAddressSanitizerCrash'; + +module.exports = NativeAddressSanitizerCrash; diff --git a/packages/react-native/Libraries/AddressSanitizerCrash/NativeAddressSanitizerCrash.js b/packages/react-native/Libraries/AddressSanitizerCrash/NativeAddressSanitizerCrash.js new file mode 100644 index 00000000000000..e8e54586ad6e20 --- /dev/null +++ b/packages/react-native/Libraries/AddressSanitizerCrash/NativeAddressSanitizerCrash.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {TurboModule} from '../TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule {} + +export default (TurboModuleRegistry.get( + 'RCTAddressSanitizerCrash', +): ?Spec); diff --git a/packages/react-native/Libraries/Alert/Alert.js b/packages/react-native/Libraries/Alert/Alert.js index a6a0ccbfecc5dc..c9084d6302a636 100644 --- a/packages/react-native/Libraries/Alert/Alert.js +++ b/packages/react-native/Libraries/Alert/Alert.js @@ -26,11 +26,22 @@ export type Buttons = Array<{ style?: AlertButtonStyle, ... }>; +// [macOS +export type DefaultInputsArray = Array<{ + default?: string, + placeholder?: string, + style?: AlertButtonStyle, +}>; +// macOS] type Options = { cancelable?: ?boolean, userInterfaceStyle?: 'unspecified' | 'light' | 'dark', onDismiss?: ?() => void, + // [macOS + modal?: ?boolean, + critical?: ?boolean, + // macOS] ... }; @@ -56,6 +67,18 @@ class Alert { undefined, options, ); + // [macOS + } else if (Platform.OS === 'macos') { + Alert.promptMacOS( + title, + message, + buttons, + 'default', + undefined, + options?.modal, + options?.critical, + ); + // macOS] } else if (Platform.OS === 'android') { const NativeDialogManagerAndroid = require('../NativeModules/specs/NativeDialogManagerAndroid').default; @@ -167,8 +190,103 @@ class Alert { cb && cb(value); }, ); + // [macOS + } else if (Platform.OS === 'macos') { + const defaultInputs = [{default: defaultValue}]; + Alert.promptMacOS(title, message, callbackOrButtons, type, defaultInputs); + } + // macOS] + } + + // [macOS + /** + * Create and display a prompt to enter some text. + * @static + * @method promptMacOS + * @param title The dialog's title. + * @param message An optional message that appears above the text + * input. + * @param callbackOrButtons This optional argument should + * be either a single-argument function or an array of buttons. If passed + * a function, it will be called with the prompt's value when the user + * taps 'OK'. + * + * If passed an array of button configurations, each button should include + * a `text` key, as well as optional `onPress` key (see + * example). + * @param type This configures the text input. One of 'plain-text', + * 'secure-text' or 'login-password'. + * @param defaultInputs This optional argument should be an array of couple + * default value - placeholder for the input fields. + * @param modal The alert can be optionally run as an app-modal dialog, instead + * of the default presentation as a sheet. + * @param critical This optional argument should be used when it's needed to + * warn the user about severe consequences of an impending event + * (such as deleting a file). + * + * @example Example with custom buttons + * + * AlertMacOS.promptMacOS( + * 'Enter password', + * 'Enter your password to claim your $1.5B in lottery winnings', + * [ + * {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'}, + * {text: 'OK', onPress: password => console.log('OK Pressed, password: ' + password)}, + * ], + * 'secure-text' + * ); + * + * @example Example with the default button and a custom callback + * + * AlertMacOS.prompt( + * 'Update username', + * null, + * text => console.log("Your username is "+text), + * null, + * 'default' + * ); + */ + static promptMacOS( + title: ?string, + message?: ?string, + callbackOrButtons?: ?((text: string) => void) | Buttons, + type?: ?AlertType = 'plain-text', + defaultInputs?: DefaultInputsArray, + modal?: ?boolean, + critical?: ?boolean, + ): void { + let callbacks: Array = []; + const buttons = []; + if (typeof callbackOrButtons === 'function') { + callbacks = [callbackOrButtons]; + } else if (callbackOrButtons instanceof Array) { + callbackOrButtons.forEach((btn, index) => { + callbacks[index] = btn.onPress; + if (btn.text || index < (callbackOrButtons || []).length - 1) { + const btnDef: {[number]: string} = {}; + btnDef[index] = btn.text || ''; + buttons.push(btnDef); + } + }); } + + RCTAlertManager.alertWithArgs( + { + title: title || undefined, + message: message || undefined, + buttons, + type: type || undefined, + defaultInputs, + modal: modal || undefined, + critical: critical || undefined, + }, + (id, value) => { + const cb = callbacks[id]; + cb && cb(value); + }, + ); } + // macOS] } module.exports = Alert; diff --git a/packages/react-native/Libraries/Alert/NativeAlertManager.js b/packages/react-native/Libraries/Alert/NativeAlertManager.js index 7f7b1cf6a9b684..9bd3836bb4d720 100644 --- a/packages/react-native/Libraries/Alert/NativeAlertManager.js +++ b/packages/react-native/Libraries/Alert/NativeAlertManager.js @@ -23,6 +23,11 @@ export type Args = {| preferredButtonKey?: string, keyboardType?: string, userInterfaceStyle?: string, + // [macOS + defaultInputs?: Array, + modal?: ?boolean, + critical?: ?boolean, + // macOS] |}; export interface Spec extends TurboModule { diff --git a/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js b/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js new file mode 100644 index 00000000000000..81c3416dcf36b1 --- /dev/null +++ b/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const alertWithArgs = require('./RCTAlertManager.ios'); + +module.exports = alertWithArgs; diff --git a/packages/react-native/Libraries/Animated/AnimatedMock.js b/packages/react-native/Libraries/Animated/AnimatedMock.js index c509c830f7d075..790a41658e1678 100644 --- a/packages/react-native/Libraries/Animated/AnimatedMock.js +++ b/packages/react-native/Libraries/Animated/AnimatedMock.js @@ -98,7 +98,13 @@ const spring = function ( return { ...emptyAnimation, start: mockAnimationStart((callback?: ?EndCallback): void => { - anyValue.setValue(config.toValue); + // [macOS setValue can't handle AnimatedNodes + if (config.toValue instanceof AnimatedNode) { + anyValue.setValue(config.toValue.__getValue()); + } else { + // macOS] + anyValue.setValue(config.toValue); + } callback?.({finished: true}); }), }; diff --git a/packages/react-native/Libraries/Animated/NativeAnimatedHelper.js b/packages/react-native/Libraries/Animated/NativeAnimatedHelper.js index a96287de26ddd8..ef8943b9866fef 100644 --- a/packages/react-native/Libraries/Animated/NativeAnimatedHelper.js +++ b/packages/react-native/Libraries/Animated/NativeAnimatedHelper.js @@ -600,7 +600,9 @@ export default { nativeEventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeAnimatedModule, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativeAnimatedModule, ); } return nativeEventEmitter; diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h index c4ee6066af2bc6..fabb691c700bfb 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] @class RCTBridge; @protocol RCTBridgeDelegate; @@ -51,10 +51,13 @@ (const facebook::react::ObjCTurboModule::InitParams &)params * - (id)getModuleInstanceFromClass:(Class)moduleClass */ +#if !TARGET_OS_OSX // [macOS] @interface RCTAppDelegate : UIResponder - +#else // [macOS +@interface RCTAppDelegate : NSResponder +#endif // macOS] /// The window object, used to render the UViewControllers -@property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) RCTPlatformWindow *window; // [macOS] @property (nonatomic, strong) RCTBridge *bridge; @property (nonatomic, strong) NSString *moduleName; @property (nonatomic, strong) NSDictionary *initialProps; @@ -83,9 +86,9 @@ * * @returns: a UIView properly configured with a bridge for React Native. */ -- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge - moduleName:(NSString *)moduleName - initProps:(NSDictionary *)initProps; +- (RCTPlatformView *)createRootViewWithBridge:(RCTBridge *)bridge // [macOS] + moduleName:(NSString *)moduleName + initProps:(NSDictionary *)initProps; /** * It creates the RootViewController. @@ -104,7 +107,7 @@ * * @return: void */ -- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController; +- (void)setRootView:(RCTPlatformView *)rootView toRootViewController:(UIViewController *)rootViewController; // [macOS] /// This method controls whether the App will use RuntimeScheduler. Only applicable in the legacy architecture. /// diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm index 348355bae359dd..2ff3f336a61739 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm @@ -76,8 +76,15 @@ - (instancetype)init } #endif +#if !TARGET_OS_OSX // [macOS] - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { +#else // [macOS +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + NSApplication *application = [notification object]; + NSDictionary *launchOptions = [notification userInfo]; +#endif // macOS] BOOL enableTM = NO; BOOL enableBridgeless = NO; #if RCT_NEW_ARCH_ENABLED @@ -87,7 +94,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( RCTAppSetupPrepareApp(application, enableTM); - UIView *rootView; + RCTPlatformView *rootView; // [macOS] if (enableBridgeless) { #if RCT_NEW_ARCH_ENABLED @@ -125,6 +132,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSDictionary *initProps = [self prepareInitialProps]; rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps:initProps]; } +#if !TARGET_OS_OSX // [macOS] self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [self createRootViewController]; [self setRootView:rootView toRootViewController:rootViewController]; @@ -133,6 +141,21 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [self.window makeKeyAndVisible]; return YES; +#else // [macOS + NSRect frame = NSMakeRect(0,0,1024,768); + self.window = [[NSWindow alloc] initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable + backing:NSBackingStoreBuffered + defer:NO]; + self.window.title = self.moduleName; + self.window.autorecalculatesKeyViewLoop = YES; + NSViewController *rootViewController = [NSViewController new]; + rootViewController.view = rootView; + rootView.frame = frame; + self.window.contentViewController = rootViewController; + [self.window makeKeyAndOrderFront:self]; + [self.window center]; +#endif // macOS] } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge @@ -160,7 +183,7 @@ - (RCTBridge *)createBridgeWithDelegate:(id)delegate launchOp return [[RCTBridge alloc] initWithDelegate:delegate launchOptions:launchOptions]; } -- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge +- (RCTPlatformView *)createRootViewWithBridge:(RCTBridge *)bridge // [macOS] moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps { @@ -168,19 +191,30 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge #if RCT_NEW_ARCH_ENABLED enableFabric = self.fabricEnabled; #endif - UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric); + RCTPlatformView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric); // [macOS] +#if !TARGET_OS_OSX // [macOS] rootView.backgroundColor = [UIColor systemBackgroundColor]; +#else // [macOS + rootView.layer.backgroundColor = [[NSColor windowBackgroundColor] CGColor]; +#endif // macOS] return rootView; } +#if !TARGET_OS_OSX // [macOS] - (UIViewController *)createRootViewController { return [UIViewController new]; } +#else // [macOS +- (NSViewController *)createRootViewController +{ + return [NSViewController new]; +} +#endif // macOS] -- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController +- (void)setRootView:(RCTPlatformView *)rootView toRootViewController:(UIViewController *)rootViewController // [macOS] { rootViewController.view = rootView; } @@ -191,6 +225,7 @@ - (BOOL)runtimeSchedulerEnabled } #pragma mark - UISceneDelegate +#if !TARGET_OS_OSX // [macOS] - (void)windowScene:(UIWindowScene *)windowScene didUpdateCoordinateSpace:(id)previousCoordinateSpace interfaceOrientation:(UIInterfaceOrientation)previousInterfaceOrientation @@ -198,6 +233,7 @@ - (void)windowScene:(UIWindowScene *)windowScene { [[NSNotificationCenter defaultCenter] postNotificationName:RCTRootViewFrameDidChangeNotification object:self]; } +#endif // [macOS] #pragma mark - RCTCxxBridgeDelegate - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.h b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.h index e1c69078409caf..0037caa0151a13 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.h +++ b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.h @@ -54,7 +54,7 @@ std::unique_ptr RCTAppSetupJsExecutorFactory RCT_EXTERN_C_BEGIN void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled); -UIView *RCTAppSetupDefaultRootView( +RCTUIView *RCTAppSetupDefaultRootView( // [macOS] RCTBridge *bridge, NSString *moduleName, NSDictionary *initialProperties, diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm index 4d69042a7d49d7..ad0fb3b9a56783 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm @@ -58,13 +58,15 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled) #endif #if DEBUG +#if !TARGET_OS_OSX // [macOS] // Disable idle timer in dev builds to avoid putting application in background and complicating // Metro reconnection logic. Users only need this when running the application using our CLI tooling. application.idleTimerDisabled = YES; +#endif // macOS] #endif } -UIView * +RCTUIView * // [macOS] RCTAppSetupDefaultRootView(RCTBridge *bridge, NSString *moduleName, NSDictionary *initialProperties, BOOL fabricEnabled) { #if RCT_NEW_ARCH_ENABLED diff --git a/packages/react-native/Libraries/AppDelegate/RCTLegacyInteropComponents.mm b/packages/react-native/Libraries/AppDelegate/RCTLegacyInteropComponents.mm index a330712cae3a7c..f15a903bca474c 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTLegacyInteropComponents.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTLegacyInteropComponents.mm @@ -1,3 +1,4 @@ + /* * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -11,7 +12,10 @@ @implementation RCTLegacyInteropComponents + (NSArray *)legacyInteropComponents { - return @[ @"RNTMyLegacyNativeView", @"RNTMyNativeView" ]; + return @[ + @"RNTMyLegacyNativeView", + @"RNTMyNativeView" + ]; } @end diff --git a/packages/react-native/Libraries/AppState/AppState.js b/packages/react-native/Libraries/AppState/AppState.js index f82afa0f614245..6ec9a262bedfea 100644 --- a/packages/react-native/Libraries/AppState/AppState.js +++ b/packages/react-native/Libraries/AppState/AppState.js @@ -51,7 +51,9 @@ class AppState { new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeAppState, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativeAppState, ); this._emitter = emitter; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index e552e8103cf407..64c2a67a5af0a5 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -17,7 +17,7 @@ import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy'; import Platform from '../../Utilities/Platform'; import legacySendAccessibilityEvent from './legacySendAccessibilityEvent'; import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo'; -import NativeAccessibilityManagerIOS from './NativeAccessibilityManager'; +import NativeAccessibilityManagerApple from './NativeAccessibilityManager'; // [macOS] // Events that are only supported on Android. type AccessibilityEventDefinitionsAndroid = { @@ -33,9 +33,17 @@ type AccessibilityEventDefinitionsIOS = { reduceTransparencyChanged: [boolean], }; +// [macOS +// Events that are only supported on macOS. +type AccessibilityEventDefinitionsMacOS = { + highContrastChanged: [boolean], // [macOS] highContrastChanged is used on macOS +}; +// macOS] + type AccessibilityEventDefinitions = { ...AccessibilityEventDefinitionsAndroid, ...AccessibilityEventDefinitionsIOS, + ...AccessibilityEventDefinitionsMacOS, // [macOS] change: [boolean], // screenReaderChanged reduceMotionChanged: [boolean], screenReaderChanged: [boolean], @@ -59,6 +67,7 @@ const EventNames: Map< ['boldTextChanged', 'boldTextChanged'], ['change', 'screenReaderChanged'], ['grayscaleChanged', 'grayscaleChanged'], + ['highContrastChanged', 'highContrastChanged'], // [macOS] ['invertColorsChanged', 'invertColorsChanged'], ['reduceMotionChanged', 'reduceMotionChanged'], ['reduceTransparencyChanged', 'reduceTransparencyChanged'], @@ -84,12 +93,11 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo#isBoldTextEnabled */ isBoldTextEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { + // [macOS rework logic to return Promise.resolve(false) on macOS + if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentBoldTextState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentBoldTextState( resolve, reject, ); @@ -97,7 +105,10 @@ const AccessibilityInfo = { reject(null); } }); + } else { + return Promise.resolve(false); } + // macOS] }, /** @@ -109,12 +120,11 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo#isGrayscaleEnabled */ isGrayscaleEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { + // [macOS rework logic to return Promise.resolve(false) on macOS + if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentGrayscaleState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentGrayscaleState( resolve, reject, ); @@ -122,8 +132,33 @@ const AccessibilityInfo = { reject(null); } }); + } else { + return Promise.resolve(false); + } + // macOS] + }, + + // [macOS + /** + * macOS only + */ + isHighContrastEnabled: function (): Promise { + if (Platform.OS === 'macos') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple) { + NativeAccessibilityManagerApple.getCurrentHighContrastState( + resolve, + reject, + ); + } else { + reject(reject); + } + }); + } else { + return Promise.resolve(false); } }, + // macOS] /** * Query whether inverted colors are currently enabled. @@ -134,12 +169,11 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo#isInvertColorsEnabled */ isInvertColorsEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { + // [macOS rework logic to return Promise.resolve(false) on macOS + if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentInvertColorsState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentInvertColorsState( resolve, reject, ); @@ -147,7 +181,10 @@ const AccessibilityInfo = { reject(null); } }); + } else { + return Promise.resolve(false); } + // macOS] }, /** @@ -167,8 +204,8 @@ const AccessibilityInfo = { reject(null); } } else { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentReduceMotionState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentReduceMotionState( resolve, reject, ); @@ -193,10 +230,11 @@ const AccessibilityInfo = { return Promise.resolve(false); } else { if ( - NativeAccessibilityManagerIOS?.getCurrentPrefersCrossFadeTransitionsState != + NativeAccessibilityManagerApple?.getCurrentPrefersCrossFadeTransitionsState != // [macOS] null ) { - NativeAccessibilityManagerIOS.getCurrentPrefersCrossFadeTransitionsState( + // [macOS] + NativeAccessibilityManagerApple.getCurrentPrefersCrossFadeTransitionsState( resolve, reject, ); @@ -216,12 +254,11 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo#isReduceTransparencyEnabled */ isReduceTransparencyEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { + // [macOS rework logic to return Promise.resolve(false) on macOS + if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentReduceTransparencyState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentReduceTransparencyState( resolve, reject, ); @@ -229,7 +266,10 @@ const AccessibilityInfo = { reject(null); } }); + } else { + return Promise.resolve(false); } + // macOS] }, /** @@ -249,8 +289,8 @@ const AccessibilityInfo = { reject(null); } } else { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentVoiceOverState( + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentVoiceOverState( resolve, reject, ); @@ -367,7 +407,7 @@ const AccessibilityInfo = { if (Platform.OS === 'android') { NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement); } else { - NativeAccessibilityManagerIOS?.announceForAccessibility(announcement); + NativeAccessibilityManagerApple?.announceForAccessibility(announcement); // [macOS] } }, @@ -384,14 +424,18 @@ const AccessibilityInfo = { if (Platform.OS === 'android') { NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement); } else { - if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) { - NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions( + // [macOS NativeAccessibilityManagerApple -> NativeAccessibilityManagerApple + if ( + NativeAccessibilityManagerApple?.announceForAccessibilityWithOptions + ) { + NativeAccessibilityManagerApple?.announceForAccessibilityWithOptions( announcement, options, ); } else { - NativeAccessibilityManagerIOS?.announceForAccessibility(announcement); + NativeAccessibilityManagerApple?.announceForAccessibility(announcement); } + // macOS] } }, diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js b/packages/react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js index f815b18c97ba2f..35e54a2340021c 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js @@ -21,6 +21,12 @@ export interface Spec extends TurboModule { onSuccess: (isGrayscaleEnabled: boolean) => void, onError: (error: Object) => void, ) => void; + // [macOS + +getCurrentHighContrastState: ( + onSuccess: (isHighContrastEnabled: boolean) => void, + onError: (error: Object) => void, + ) => void; + // macOS] +getCurrentInvertColorsState: ( onSuccess: (isInvertColorsEnabled: boolean) => void, onError: (error: Object) => void, diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js new file mode 100644 index 00000000000000..cdeb829bbb2113 --- /dev/null +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const legacySendAccessibilityEvent = require('./legacySendAccessibilityEvent.ios'); + +module.exports = legacySendAccessibilityEvent; diff --git a/packages/react-native/Libraries/Components/Button.flow.js b/packages/react-native/Libraries/Components/Button.flow.js index 777c546508d79b..1cf32db90e76dc 100644 --- a/packages/react-native/Libraries/Components/Button.flow.js +++ b/packages/react-native/Libraries/Components/Button.flow.js @@ -12,9 +12,13 @@ 'use strict'; import type {PressEvent} from '../Types/CoreEventTypes'; +import type {BlurEvent, FocusEvent, KeyEvent} from '../Types/CoreEventTypes'; // [macOS] import type { AccessibilityActionEvent, AccessibilityActionInfo, + // [macOS + AccessibilityRole, + // macOS] AccessibilityState, } from './View/ViewAccessibility'; @@ -150,6 +154,55 @@ type ButtonProps = $ReadOnly<{| importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), accessibilityHint?: ?string, accessibilityLanguage?: ?Stringish, + + // [macOS + /** + * Custom accessibility role -- otherwise we use button + */ + accessibilityRole?: ?AccessibilityRole, + + /** + * Accessibility action handlers + */ + onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, + + /** + * Handler to be called when the button receives key focus + */ + onBlur?: ?(e: BlurEvent) => void, + + /** + * Handler to be called when the button loses key focus + */ + onFocus?: ?(e: FocusEvent) => void, + + /** + * Handler to be called when a key down press is detected + */ + onKeyDown?: ?(e: KeyEvent) => void, + + /** + * Handler to be called when a key up press is detected + */ + onKeyUp?: ?(e: KeyEvent) => void, + + /* + * Array of keys to receive key down events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysUp?: ?Array, + + /* + * Specifies the Tooltip for the view + */ + tooltip?: string, + // macOS] |}>; /** diff --git a/packages/react-native/Libraries/Components/Button.js b/packages/react-native/Libraries/Components/Button.js index 2d674520c74bb8..2428cca1fdd740 100644 --- a/packages/react-native/Libraries/Components/Button.js +++ b/packages/react-native/Libraries/Components/Button.js @@ -13,10 +13,15 @@ import type {TextStyleProp, ViewStyleProp} from '../StyleSheet/StyleSheet'; import type {PressEvent} from '../Types/CoreEventTypes'; +import type {KeyEvent} from '../Types/CoreEventTypes'; // [macOS] import type {Button as ButtonType} from './Button.flow'; +import type {BlurEvent, FocusEvent} from './TextInput/TextInput'; // [macOS] import type { AccessibilityActionEvent, AccessibilityActionInfo, + // [macOS + AccessibilityRole, + // macOS] AccessibilityState, } from './View/ViewAccessibility'; @@ -127,6 +132,7 @@ type ButtonProps = $ReadOnly<{| Text to display for blindness accessibility features. */ accessibilityLabel?: ?string, + /** * Alias for accessibilityLabel https://reactnative.dev/docs/view#accessibilitylabel * https://github.com/facebook/react-native/issues/34424 @@ -144,6 +150,55 @@ type ButtonProps = $ReadOnly<{| */ testID?: ?string, + // [macOS + /** + * Custom accessibility role -- otherwise we use button + */ + accessibilityRole?: ?AccessibilityRole, + + /** + * Accessibility action handlers + */ + onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, + + /** + * Handler to be called when the button receives key focus + */ + onBlur?: ?(e: BlurEvent) => void, + + /** + * Handler to be called when the button loses key focus + */ + onFocus?: ?(e: FocusEvent) => void, + + /** + * Handler to be called when a key down press is detected + */ + onKeyDown?: ?(e: KeyEvent) => void, + + /** + * Handler to be called when a key up press is detected + */ + onKeyUp?: ?(e: KeyEvent) => void, + + /* + * Array of keys to receive key down events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysUp?: ?Array, + + /* + * Specifies the Tooltip for the view + */ + tooltip?: string, + // macOS] + /** * Accessibility props. */ @@ -305,16 +360,24 @@ class Button extends React.Component { nextFocusRight, nextFocusUp, testID, + onFocus, // [macOS + onBlur, + onKeyDown, + onKeyUp, + validKeysDown, + validKeysUp, // macOS] + tooltip, // [macOS] accessible, accessibilityActions, accessibilityHint, accessibilityLanguage, + accessibilityRole, // [macOS] onAccessibilityAction, } = this.props; const buttonStyles: Array = [styles.button]; const textStyles: Array = [styles.text]; if (color) { - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { textStyles.push({color: color}); } else { buttonStyles.push({backgroundColor: color}); @@ -367,7 +430,7 @@ class Button extends React.Component { accessibilityLabel={ariaLabel || accessibilityLabel} accessibilityHint={accessibilityHint} accessibilityLanguage={accessibilityLanguage} - accessibilityRole="button" + accessibilityRole={accessibilityRole || 'button'} // [macOS] accessibilityState={_accessibilityState} importantForAccessibility={_importantForAccessibility} hasTVPreferredFocus={hasTVPreferredFocus} @@ -379,6 +442,13 @@ class Button extends React.Component { testID={testID} disabled={disabled} onPress={onPress} + onFocus={onFocus} // [macOS] + onBlur={onBlur} // [macOS] + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} + validKeysDown={validKeysDown} + validKeysUp={validKeysUp} + tooltip={tooltip} // [macOS] touchSoundDisabled={touchSoundDisabled}> @@ -399,6 +469,7 @@ const styles = StyleSheet.create({ backgroundColor: '#2196F3', borderRadius: 2, }, + macos: {}, // [macOS] }), text: { textAlign: 'center', @@ -413,6 +484,11 @@ const styles = StyleSheet.create({ color: 'white', fontWeight: '500', }, + macos: { + // [macOS + color: '#007AFF', + fontSize: 18, + }, // macOS] }), }, buttonDisabled: Platform.select({ @@ -421,11 +497,17 @@ const styles = StyleSheet.create({ elevation: 0, backgroundColor: '#dfdfdf', }, + macos: {}, // [macOS] }), textDisabled: Platform.select({ ios: { color: '#cdcdcd', }, + // [macOS + macos: { + color: '#cdcdcd', + }, + // macOS] android: { color: '#a1a1a1', }, diff --git a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js index 8aae204e97f328..5a175ec33228e5 100644 --- a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js +++ b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js @@ -110,7 +110,9 @@ class Keyboard { new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeKeyboardObserver, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativeKeyboardObserver, ); constructor() { diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index 3d7f607b26dce2..a69a08cbaadbc6 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -9,10 +9,16 @@ */ import type { + BlurEvent, + // [macOS + FocusEvent, + KeyEvent, LayoutEvent, MouseEvent, PressEvent, + // macOS] } from '../../Types/CoreEventTypes'; +import type {DraggedTypesType} from '../View/DraggedType'; // [macOS] import type { AccessibilityActionEvent, AccessibilityActionInfo, @@ -20,6 +26,7 @@ import type { AccessibilityState, AccessibilityValue, } from '../View/ViewAccessibility'; +import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS] import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; @@ -158,6 +165,123 @@ type Props = $ReadOnly<{| */ onPressOut?: ?(event: PressEvent) => mixed, + // [macOS + /** + * Called after the element is focused. + */ + onFocus?: ?(event: FocusEvent) => void, + + /** + * Called after the element loses focus. + */ + onBlur?: ?(event: BlurEvent) => void, + + /** + * Called after a key down event is detected. + */ + onKeyDown?: ?(event: KeyEvent) => void, + + /** + * Called after a key up event is detected. + */ + onKeyUp?: ?(event: KeyEvent) => void, + + /** + * When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in + * `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp` + * still have their native default behavior prevented, but the others do not. + * + * @platform macos + */ + passthroughAllKeyEvents?: ?boolean, + + /** + * Array of keys to receive key down events for. These events have their default native behavior prevented. + * + * @platform macos + */ + validKeysDown?: ?Array, + + /** + * Array of keys to receive key up events for. These events have their default native behavior prevented. + * + * @platform macos + */ + validKeysUp?: ?Array, + + /** + * Specifies whether the view should receive the mouse down event when the + * containing window is in the background. + * + * @platform macos + */ + acceptsFirstMouse?: ?boolean, + + /** + * Specifies whether clicking and dragging the view can move the window. This is useful + * to disable in Button like components like Pressable where mouse the user should still + * be able to click and drag off the view to cancel the click without accidentally moving the window. + * + * @platform macos + */ + mouseDownCanMoveWindow?: ?boolean, + + /** + * Specifies whether system focus ring should be drawn when the view has keyboard focus. + * + * @platform macos + */ + enableFocusRing?: ?boolean, + + /** + * Specifies whether the view ensures it is vibrant on top of other content. + * For more information, see the following apple documentation: + * https://developer.apple.com/documentation/appkit/nsview/1483793-allowsvibrancy + * https://developer.apple.com/documentation/appkit/nsvisualeffectview#1674177 + * + * @platform macos + */ + allowsVibrancy?: ?boolean, + + /** + * Specifies the Tooltip for the Pressable. + * @platform macos + */ + tooltip?: ?string, + + /** + * Fired when a file is dragged into the Pressable via the mouse. + * + * @platform macos + */ + onDragEnter?: (event: MouseEvent) => void, + + /** + * Fired when a file is dragged out of the Pressable via the mouse. + * + * @platform macos + */ + onDragLeave?: (event: MouseEvent) => void, + + /** + * Fired when a file is dropped on the Pressable via the mouse. + * + * @platform macos + */ + onDrop?: (event: MouseEvent) => void, + + /** + * The types of dragged files that the Pressable will accept. + * + * Possible values for `draggedTypes` are: + * + * - `'fileUrl'` + * + * @platform macos + */ + draggedTypes?: ?DraggedTypesType, + // macOS] + /** * Either view styles or a function that receives a boolean reflecting whether * the component is currently pressed and returns view styles. @@ -228,6 +352,15 @@ function Pressable(props: Props, forwardedRef): React.Node { onPress, onPressIn, onPressOut, + // [macOS + onFocus, + onBlur, + onKeyDown, + onKeyUp, + acceptsFirstMouse, + mouseDownCanMoveWindow, + enableFocusRing, + // macOS] pressRetentionOffset, style, testOnly_pressed, @@ -267,13 +400,16 @@ function Pressable(props: Props, forwardedRef): React.Node { const restPropsWithDefaults: React.ElementConfig = { ...restProps, ...android_rippleConfig?.viewProps, + acceptsFirstMouse: acceptsFirstMouse !== false && !disabled, // [macOS] + mouseDownCanMoveWindow: false, // [macOS] + enableFocusRing: enableFocusRing !== false && !disabled, accessible: accessible !== false, accessibilityViewIsModal: restProps['aria-modal'] ?? restProps.accessibilityViewIsModal, accessibilityLiveRegion, accessibilityLabel, accessibilityState: _accessibilityState, - focusable: focusable !== false, + focusable: focusable !== false && !disabled, // macOS] accessibilityValue, hitSlop, }; @@ -312,6 +448,12 @@ function Pressable(props: Props, forwardedRef): React.Node { onPressOut(event); } }, + // [macOS + onFocus, + onBlur, + onKeyDown, + onKeyUp, + // macOS] }), [ android_disableSound, @@ -328,6 +470,12 @@ function Pressable(props: Props, forwardedRef): React.Node { onPress, onPressIn, onPressOut, + // [macOS + onFocus, + onBlur, + onKeyDown, + onKeyUp, + // macOS] pressRetentionOffset, setPressed, unstable_pressDelay, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index 2e2264afedbd2b..49ce3e72a88177 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -470,11 +470,21 @@ export type Props = $ReadOnly<{| * instead of vertically in a column. The default value is false. */ horizontal?: ?boolean, + /** + * When true, the scroll view's indicator is shown in an overlay. // [macOS + * This does does not take up any content view space. + * https://developer.apple.com/documentation/appkit/nsscrollerstyle/nsscrollerstyleoverlay // macOS] + */ + hasOverlayStyleIndicator?: ?boolean, // [macOS] /** * If sticky headers should stick at the bottom instead of the top of the * ScrollView. This is usually used with inverted ScrollViews. */ invertStickyHeaders?: ?boolean, + /** + * Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere // [macOS] + */ + inverted?: ?boolean, // [macOS] /** * Determines whether the keyboard gets dismissed in response to a drag. * @@ -671,6 +681,7 @@ export type Props = $ReadOnly<{| |}>; type State = {| + contentKey: number, // [macOS] layoutHeight: ?number, |}; @@ -756,6 +767,7 @@ class ScrollView extends React.Component { _subscriptionKeyboardDidHide: ?EventSubscription = null; state: State = { + contentKey: 1, // [macOS] layoutHeight: null, }; @@ -1013,7 +1025,11 @@ class ScrollView extends React.Component { |}, animated?: boolean, // deprecated, put this inside the rect argument instead ) => { - invariant(Platform.OS === 'ios', 'zoomToRect is not implemented'); + invariant( + // [macOS + Platform.OS === 'ios' || Platform.OS === 'macos', + 'zoomToRect is not implemented', + ); // macOS] if ('animated' in rect) { this._animated = rect.animated; delete rect.animated; @@ -1147,12 +1163,22 @@ class ScrollView extends React.Component { } } + // [macOS + _handlePreferredScrollerStyleDidChange = (event: ScrollEvent) => { + this.setState({contentKey: this.state.contentKey + 1}); + }; // macOS] + + // [macOS + _handleInvertedDidChange = () => { + this.setState({contentKey: this.state.contentKey + 1}); + }; // macOS] + _handleScroll = (e: ScrollEvent) => { if (__DEV__) { if ( this.props.onScroll && this.props.scrollEventThrottle == null && - Platform.OS === 'ios' + (Platform.OS === 'ios' || Platform.OS === 'macos') // [macOS] ) { console.log( 'You specified `onScroll` on a but not ' + @@ -1738,6 +1764,8 @@ class ScrollView extends React.Component { ? false : this.props.removeClippedSubviews } + key={this.state.contentKey} // [macOS] + inverted={this.props.inverted} // [macOS] collapsable={false}> {children} @@ -1765,6 +1793,9 @@ class ScrollView extends React.Component { // Override the onContentSizeChange from props, since this event can // bubble up from TextInputs onContentSizeChange: null, + onInvertedDidChange: this._handleInvertedDidChange, // [macOS] + onPreferredScrollerStyleDidChange: + this._handlePreferredScrollerStyleDidChange, // [macOS] onLayout: this._handleLayout, onMomentumScrollBegin: this._handleMomentumScrollBegin, onMomentumScrollEnd: this._handleMomentumScrollEnd, @@ -1801,6 +1832,11 @@ class ScrollView extends React.Component { this.props.pagingEnabled === true && this.props.snapToInterval == null && this.props.snapToOffsets == null, + // [macOS + macos: + this.props.pagingEnabled === true && + this.props.snapToInterval == null && + this.props.snapToOffsets == null, // macOS] // on Android, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work android: this.props.pagingEnabled === true || diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index 69ebcef0d56971..ace4b8bcdf4c9b 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -162,6 +162,10 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = onMomentumScrollBegin: true, onScrollToTop: true, onScroll: true, + // [macOS + onInvertedDidChange: true, + onPreferredScrollerStyleDidChange: true, + // macOS] }), }, }; diff --git a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js index 2ac3d1f6b09dad..01ca3a1d27dcb7 100644 --- a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js +++ b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js @@ -28,11 +28,11 @@ export type StatusBarStyle = $Keys<{ /** * Dark background, white texts and icons */ - 'light-content': string, + 'light-content': ColorValue, // [macOS] /** * Light background, dark texts and icons */ - 'dark-content': string, + 'dark-content': ColorValue, // [macOS] ... }>; diff --git a/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js index ccccc928a05c19..2c96ead11b89dc 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js @@ -23,7 +23,7 @@ type NativeType = HostComponent; type NativeCommands = TextInputNativeCommands; export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['focus', 'blur', 'setTextAndSelection'], + supportedCommands: ['focus', 'blur', 'setTextAndSelection', 'setGhostText'], // [macOS] }); export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { diff --git a/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js index 8ec1f49441c4b5..c14e92102cb911 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js @@ -23,7 +23,7 @@ type NativeType = HostComponent; type NativeCommands = TextInputNativeCommands; export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['focus', 'blur', 'setTextAndSelection'], + supportedCommands: ['focus', 'blur', 'setTextAndSelection', 'setGhostText'], // [macOS] }); export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 88d3cc8fe756e5..df3437a0abd9fa 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -157,6 +157,15 @@ const RCTTextInputViewConfig = { autoFocus: true, lineBreakStrategyIOS: true, smartInsertDelete: true, + // [macOS + clearTextOnSubmit: true, + grammarCheck: true, + hideVerticalScrollIndicator: true, + pastedTypes: true, + submitKeyEvents: true, + tooltip: true, + cursorColor: {process: require('../../StyleSheet/processColor').default}, + // macOS] ...ConditionallyIgnoredEventHandlers({ onChange: true, onSelectionChange: true, @@ -165,6 +174,12 @@ const RCTTextInputViewConfig = { onChangeSync: true, onKeyPressSync: true, onTextInput: true, + // [macOS + onPaste: true, + onAutoCorrectChange: true, + onSpellCheckChange: true, + onGrammarCheckChange: true, + // macOS] }), }, }; diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 0eb8f578d65879..bfa14f7c8147c5 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -94,16 +94,66 @@ export type EditingEvent = SyntheticEvent< |}>, >; +// [macOS macOS-only +export type SettingChangeEvent = SyntheticEvent< + $ReadOnly<{| + enabled: boolean, + |}>, +>; + +export type PasteEvent = SyntheticEvent< + $ReadOnly<{| + dataTransfer: {| + files: $ReadOnlyArray<{| + height: number, + size: number, + type: string, + uri: string, + width: number, + |}>, + items: $ReadOnlyArray<{| + kind: string, + type: string, + |}>, + types: $ReadOnlyArray, + |}, + |}>, +>; + +export type SubmitKeyEvent = $ReadOnly<{| + key: string, + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + functionKey?: ?boolean, +|}>; +// macOS] + +// [macOS type DataDetectorTypesType = + // iOS+macOS | 'phoneNumber' | 'link' | 'address' | 'calendarEvent' + // iOS-only | 'trackingNumber' | 'flightNumber' | 'lookupSuggestion' | 'none' - | 'all'; + | 'all' + // [macOS macOS-only + | 'ortography' + | 'spelling' + | 'grammar' + | 'quote' + | 'dash' + | 'replacement' + | 'correction' + | 'regularExpression' + | 'transitInformation'; + // macOS] export type KeyboardType = // Cross Platform @@ -308,7 +358,7 @@ type IOSProps = $ReadOnly<{| /** * Set line break strategy on iOS. - * @platform ios + * @platform ios macos */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), @@ -323,6 +373,95 @@ type IOSProps = $ReadOnly<{| smartInsertDelete?: ?boolean, |}>; +// [macOS +type MacOSProps = $ReadOnly<{| + /** + * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. + * + * @platform macos + */ + clearTextOnSubmit?: ?boolean, + + /** + * If `false`, disables grammar-check. + * + * @platform macos + */ + grammarCheck?: ?boolean, + + /** + * If `true`, hide vertical scrollbar on the underlying multiline scrollview + * The default value is `false`. + * + * @platform macos + */ + hideVerticalScrollIndicator?: ?boolean, + + /** + * Fired when a supported element is pasted + * + * @platform macos + */ + onPaste?: (event: PasteEvent) => void, + + /** + * Callback that is called when the text input's autoCorrect setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onAutoCorrectChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Callback that is called when the text input's spellCheck setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onSpellCheckChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Callback that is called when the text input's grammarCheck setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onGrammarCheckChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Enables Paste support for certain types of pasted types + * + * Possible values for `pastedTypes` are: + * + * - `'fileUrl'` + * - `'image'` + * - `'string'` + * + * @platform macos + */ + pastedTypes?: PastedTypesType, + + /** + * Configures keys that can be used to submit editing for the TextInput. Defaults to 'Enter' key. + * @platform macos + */ + submitKeyEvents?: ?$ReadOnlyArray, + + /** + * Specifies the tooltip. + * + * @platform macos + */ + tooltip?: ?string, +|}>; +// macOS] + type AndroidProps = $ReadOnly<{| /** * When provided it will set the color of the cursor (or "caret") in the component. @@ -409,10 +548,14 @@ type AndroidProps = $ReadOnly<{| underlineColorAndroid?: ?ColorValue, |}>; +export type PasteType = 'fileUrl' | 'image' | 'string'; // [macOS] +export type PastedTypesType = PasteType | $ReadOnlyArray; // [macOS] + export type Props = $ReadOnly<{| ...$Diff>, ...IOSProps, ...AndroidProps, + ...MacOSProps, // [macOS] /** * Can tell `TextInput` to automatically capitalize certain characters. @@ -743,7 +886,7 @@ export type Props = $ReadOnly<{| /** * Callback that is called when the text input is focused. */ - onFocus?: ?(e: FocusEvent) => mixed, + onFocus?: ?(e: FocusEvent) => void, // [macOS] /** * Callback that is called when a key is pressed. @@ -950,6 +1093,7 @@ type ImperativeMethods = $ReadOnly<{| isFocused: () => boolean, getNativeRef: () => ?React.ElementRef>, setSelection: (start: number, end: number) => void, + setGhostText: (ghostText: ?string) => void, // [macOS] |}>; /** diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index f0416dce6c603a..1d44c99b30ef07 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -40,6 +40,7 @@ type TextInputInstance = React.ElementRef> & { +isFocused: () => boolean, +getNativeRef: () => ?React.ElementRef>, +setSelection: (start: number, end: number) => void, + +setGhostText: (ghostText: ?string) => void, // [macOS] }; let AndroidTextInput; @@ -53,7 +54,7 @@ if (Platform.OS === 'android') { AndroidTextInput = require('./AndroidTextInputNativeComponent').default; AndroidTextInputCommands = require('./AndroidTextInputNativeComponent').Commands; -} else if (Platform.OS === 'ios') { +} else if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { RCTSinglelineTextInputView = require('./RCTSingelineTextInputNativeComponent').default; RCTSinglelineTextInputNativeCommands = @@ -132,16 +133,66 @@ export type EditingEvent = SyntheticEvent< |}>, >; +// [macOS macOS-only +export type SettingChangeEvent = SyntheticEvent< + $ReadOnly<{| + enabled: boolean, + |}>, +>; + +export type PasteEvent = SyntheticEvent< + $ReadOnly<{| + dataTransfer: {| + files: $ReadOnlyArray<{| + height: number, + size: number, + type: string, + uri: string, + width: number, + |}>, + items: $ReadOnlyArray<{| + kind: string, + type: string, + |}>, + types: $ReadOnlyArray, + |}, + |}>, +>; + +export type SubmitKeyEvent = $ReadOnly<{| + key: string, + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + functionKey?: ?boolean, +|}>; +// macOS] + +// [macOS type DataDetectorTypesType = + // iOS+macOS | 'phoneNumber' | 'link' | 'address' | 'calendarEvent' + // iOS-only | 'trackingNumber' | 'flightNumber' | 'lookupSuggestion' | 'none' - | 'all'; + | 'all' + // [macOS macOS-only + | 'ortography' + | 'spelling' + | 'grammar' + | 'quote' + | 'dash' + | 'replacement' + | 'correction' + | 'regularExpression' + | 'transitInformation'; + // macOS] export type KeyboardType = // Cross Platform @@ -352,7 +403,7 @@ type IOSProps = $ReadOnly<{| /** * Set line break strategy on iOS. - * @platform ios + * @platform ios macos */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), @@ -367,12 +418,102 @@ type IOSProps = $ReadOnly<{| smartInsertDelete?: ?boolean, |}>; +// [macOS +type MacOSProps = $ReadOnly<{| + /** + * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. + * + * @platform macos + */ + clearTextOnSubmit?: ?boolean, + + /** + * If `false`, disables grammar-check. + * + * @platform macos + */ + grammarCheck?: ?boolean, + + /** + * If `true`, hide vertical scrollbar on the underlying multiline scrollview + * The default value is `false`. + * + * @platform macos + */ + hideVerticalScrollIndicator?: ?boolean, + + /** + * Fired when a supported element is pasted + * + * @platform macos + */ + onPaste?: (event: PasteEvent) => void, + + /** + * Callback that is called when the text input's autoCorrect setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onAutoCorrectChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Callback that is called when the text input's spellCheck setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onSpellCheckChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Callback that is called when the text input's grammarCheck setting changes. + * This will be called with + * `{ nativeEvent: { enabled } }`. + * Does only work with 'multiline={true}'. + * + * @platform macos + */ + onGrammarCheckChange?: ?(e: SettingChangeEvent) => mixed, + + /** + * Enables Paste support for certain types of pasted types + * + * Possible values for `pastedTypes` are: + * + * - `'fileUrl'` + * - `'image'` + * - `'string'` + * + * @platform macos + */ + pastedTypes?: PastedTypesType, + + /** + * Configures keys that can be used to submit editing for the TextInput. Defaults to 'Enter' key. + * @platform macos + */ + submitKeyEvents?: ?$ReadOnlyArray, + + /** + * Specifies the tooltip. + * + * @platform macos + */ + tooltip?: ?string, +|}>; +// macOS] + type AndroidProps = $ReadOnly<{| /** * When provided it will set the color of the cursor (or "caret") in the component. * Unlike the behavior of `selectionColor` the cursor color will be set independently * from the color of the text selection box. - * @platform android + // [macOS] + * @platform android macos */ cursorColor?: ?ColorValue, @@ -453,9 +594,13 @@ type AndroidProps = $ReadOnly<{| underlineColorAndroid?: ?ColorValue, |}>; +export type PasteType = 'fileUrl' | 'image' | 'string'; // [macOS] +export type PastedTypesType = PasteType | $ReadOnlyArray; // [macOS] + export type Props = $ReadOnly<{| ...$Diff>, ...IOSProps, + ...MacOSProps, // [macOS] ...AndroidProps, /** @@ -785,7 +930,7 @@ export type Props = $ReadOnly<{| /** * Callback that is called when the text input is focused. */ - onFocus?: ?(e: FocusEvent) => mixed, + onFocus?: ?(e: FocusEvent) => void, // [macOS] /** * Callback that is called when a key is pressed. @@ -1099,6 +1244,19 @@ const emptyFunctionThatReturnsTrue = () => true; * in AndroidManifest.xml ( https://developer.android.com/guide/topics/manifest/activity-element.html ) * or control this param programmatically with native code. * + * + * The following values work on macOS only: + * + * - `'ortography'` + * - `'spelling'` + * - `'grammar'` + * - `'quote'` + * - `'dash'` + * - `'replacement'` + * - `'correction'` + * - `'regularExpression'` + * - `'transitInformation'` + * */ function InternalTextInput(props: Props): React.Node { const { @@ -1272,6 +1430,13 @@ function InternalTextInput(props: Props): React.Node { ); } }, + // [macOS + setGhostText(ghostText: ?string): void { + if (inputRef.current != null) { + viewCommands.setGhostText(inputRef.current, ghostText); + } + }, + // macOS] }); } }, @@ -1408,7 +1573,11 @@ function InternalTextInput(props: Props): React.Node { }, onPressIn: onPressIn, onPressOut: onPressOut, - cancelable: Platform.OS === 'ios' ? !rejectResponderTermination : null, + // [macOS] + cancelable: + Platform.OS === 'ios' || Platform.OS === 'macos' + ? !rejectResponderTermination + : null, }), [ editable, @@ -1452,7 +1621,8 @@ function InternalTextInput(props: Props): React.Node { // $FlowFixMe[underconstrained-implicit-instantiation] let style = flattenStyle(props.style); - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos') { + // [macOS] const RCTTextInputView = props.multiline === true ? RCTMultilineTextInputView @@ -1484,6 +1654,8 @@ function InternalTextInput(props: Props): React.Node { onChangeSync={useOnChangeSync === true ? _onChangeSync : null} onContentSizeChange={props.onContentSizeChange} onFocus={_onFocus} + onKeyDown={props.onKeyDown} // [macOS] + onKeyUp={props.onKeyUp} // [macOS] onScroll={_onScroll} onSelectionChange={_onSelectionChange} onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue} diff --git a/packages/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js b/packages/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js index e64262ae37dea8..0c45ee1ea08b53 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js @@ -22,8 +22,20 @@ export interface TextInputNativeCommands { start: Int32, end: Int32, ) => void; + // [macOS + // NYI on Android + +setGhostText: ( + viewRef: React.ElementRef, + value: ?string, // in theory this is nullable + ) => void; + // macOS] } -const supportedCommands = ['focus', 'blur', 'setTextAndSelection']; +const supportedCommands = [ + 'focus', + 'blur', + 'setTextAndSelection', + 'setGhostText', +]; // [macOS] export default supportedCommands; diff --git a/packages/react-native/Libraries/Components/TextInput/TextInputState.js b/packages/react-native/Libraries/Components/TextInput/TextInputState.js index 9a381437c9f1d4..1fe8a152b57f5c 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInputState.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInputState.js @@ -113,7 +113,7 @@ function focusTextInput(textField: ?ComponentRef) { return; } focusInput(textField); - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { // This isn't necessarily a single line text input // But commands don't actually care as long as the thing being passed in // actually has a command with that name. So this should work with single @@ -144,7 +144,7 @@ function blurTextInput(textField: ?ComponentRef) { if (currentlyFocusedInputRef === textField && textField != null) { blurInput(textField); - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { // This isn't necessarily a single line text input // But commands don't actually care as long as the thing being passed in // actually has a command with that name. So this should work with single diff --git a/packages/react-native/Libraries/Components/Touchable/Touchable.js b/packages/react-native/Libraries/Components/Touchable/Touchable.js index 50c9b8f6a14040..2a97ad708a1652 100644 --- a/packages/react-native/Libraries/Components/Touchable/Touchable.js +++ b/packages/react-native/Libraries/Components/Touchable/Touchable.js @@ -22,13 +22,18 @@ import Position from './Position'; import * as React from 'react'; const extractSingleTouch = (nativeEvent: { + +altKey?: ?boolean, // [macOS] + +button?: ?number, // [macOS] +changedTouches: $ReadOnlyArray, + +ctrlKey?: ?boolean, // [macOS] +force?: number, +identifier: number, +locationX: number, +locationY: number, + +metaKey?: ?boolean, // [macOS] +pageX: number, +pageY: number, + +shiftKey?: ?boolean, // [macOS] +target: ?number, +timestamp: number, +touches: $ReadOnlyArray, diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js b/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js index 4fcbe801e16051..1f9d8900b4a948 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js @@ -130,8 +130,13 @@ class TouchableBounce extends React.Component { render(): React.Node { // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. - const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = - this.state.pressability.getEventHandlers(); + const { + onBlur, + onFocus, + onMouseEnter, + onMouseLeave, + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); // [macOS] const accessibilityLiveRegion = this.props['aria-live'] === 'off' ? 'none' @@ -184,11 +189,26 @@ class TouchableBounce extends React.Component { nativeID={this.props.id ?? this.props.nativeID} testID={this.props.testID} hitSlop={this.props.hitSlop} - focusable={ - this.props.focusable !== false && - this.props.onPress !== undefined && + // [macOS + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } + enableFocusRing={ + (this.props.enableFocusRing === undefined || + this.props.enableFocusRing === true) && !this.props.disabled } + focusable={this.props.focusable !== false && !this.props.disabled} + tooltip={this.props.tooltip} + onMouseEnter={this.props.onMouseEnter} + onMouseLeave={this.props.onMouseLeave} + onDragEnter={this.props.onDragEnter} + onDragLeave={this.props.onDragLeave} + onDrop={this.props.onDrop} + onFocus={this.props.onFocus} + onBlur={this.props.onBlur} + draggedTypes={this.props.draggedTypes} + // macOS] ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> {this.props.children} diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js index 998a7d02f3b45a..95a5b0843d2dc2 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -281,8 +281,13 @@ class TouchableHighlight extends React.Component { // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. - const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = - this.state.pressability.getEventHandlers(); + const { + onBlur, + onFocus, + onMouseEnter, // [macOS] + onMouseLeave, // [macOS] + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); const accessibilityState = this.props.disabled != null @@ -342,10 +347,31 @@ class TouchableHighlight extends React.Component { nextFocusRight={this.props.nextFocusRight} nextFocusUp={this.props.nextFocusUp} focusable={ - this.props.focusable !== false && this.props.onPress !== undefined + this.props.focusable !== false && + this.props.onPress !== undefined && + !this.props.disabled // [macOS] } nativeID={this.props.id ?? this.props.nativeID} testID={this.props.testID} + // [macOS + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } + enableFocusRing={ + (this.props.enableFocusRing === undefined || + this.props.enableFocusRing === true) && + !this.props.disabled + } + tooltip={this.props.tooltip} + onMouseEnter={this.props.onMouseEnter} + onMouseLeave={this.props.onMouseLeave} + onDragEnter={this.props.onDragEnter} + onDragLeave={this.props.onDragLeave} + onDrop={this.props.onDrop} + onFocus={this.props.onFocus} + onBlur={this.props.onBlur} + draggedTypes={this.props.draggedTypes} + // macOS] ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> {React.cloneElement(child, { diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js index 17dfed376cb686..18d84f20f8a170 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -78,6 +78,18 @@ type Props = $ReadOnly<{| */ nextFocusUp?: ?number, + /* + * Array of keys to receive key down events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysUp?: ?Array, + /** * Set to true to add the ripple effect to the foreground of the view, instead * of the background. This is useful if one of your child views has a @@ -330,6 +342,8 @@ class TouchableNativeFeedback extends React.Component { nextFocusUp: this.props.nextFocusUp, onLayout: this.props.onLayout, testID: this.props.testID, + validKeysDown: this.props.validKeysDown, + validKeysUp: this.props.validKeysUp, }, ...children, ); diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index 95b3787905c082..20675e0d748eed 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -38,6 +38,20 @@ type Props = $ReadOnly<{| style?: ?ViewStyleProp, hostRef?: ?React.Ref, + + // [macOS + /* + * Array of keys to receive key down events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysUp?: ?Array, + // macOS] |}>; type State = $ReadOnly<{| @@ -164,6 +178,18 @@ class TouchableOpacity extends React.Component { this.props.onFocus(event); } }, + onKeyDown: event => { + if (this.props.onKeyDown != null) { + this.props.onKeyDown(event); + } + }, + onKeyUp: event => { + if (this.props.onKeyUp != null) { + this.props.onKeyUp(event); + } + }, + validKeysDown: this.props.validKeysDown, + validKeysUp: this.props.validKeysUp, onLongPress: this.props.onLongPress, onPress: this.props.onPress, onPressIn: event => { @@ -215,8 +241,13 @@ class TouchableOpacity extends React.Component { render(): React.Node { // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. - const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = - this.state.pressability.getEventHandlers(); + const { + onBlur, + onFocus, + onMouseEnter, // [macOS] + onMouseLeave, // [macOS] + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); let _accessibilityState = { busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy, @@ -286,9 +317,26 @@ class TouchableOpacity extends React.Component { nextFocusUp={this.props.nextFocusUp} hasTVPreferredFocus={this.props.hasTVPreferredFocus} hitSlop={this.props.hitSlop} - focusable={ - this.props.focusable !== false && this.props.onPress !== undefined + // [macOS + acceptsFirstMouse={ + this.props.acceptsFirstMouse !== false && !this.props.disabled + } + enableFocusRing={ + (this.props.enableFocusRing === undefined || + this.props.enableFocusRing === true) && + !this.props.disabled } + focusable={this.props.focusable !== false && !this.props.disabled} + tooltip={this.props.tooltip} + onMouseEnter={this.props.onMouseEnter} + onMouseLeave={this.props.onMouseLeave} + onDragEnter={this.props.onDragEnter} + onDragLeave={this.props.onDragLeave} + onDrop={this.props.onDrop} + onFocus={this.props.onFocus} + onBlur={this.props.onBlur} + draggedTypes={this.props.draggedTypes} + // macOS] ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> {this.props.children} diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 8d6a4787640baa..2babdceab6a52c 100755 --- a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -19,10 +19,16 @@ import type {EdgeInsetsOrSizeProp} from '../../StyleSheet/EdgeInsetsPropType'; import type { BlurEvent, FocusEvent, + KeyEvent, LayoutEvent, + MouseEvent, PressEvent, + // [macOS] } from '../../Types/CoreEventTypes'; +// [macOS +import type {DraggedTypesType} from '../View/DraggedType'; +// macOS] import View from '../../Components/View/View'; import Pressability, { type PressabilityConfig, @@ -72,13 +78,28 @@ type Props = $ReadOnly<{| importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), nativeID?: ?string, onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, - onBlur?: ?(event: BlurEvent) => mixed, - onFocus?: ?(event: FocusEvent) => mixed, + onBlur?: ?(event: BlurEvent) => void, // [macOS] + onFocus?: ?(event: FocusEvent) => void, // [macOS] onLayout?: ?(event: LayoutEvent) => mixed, onLongPress?: ?(event: PressEvent) => mixed, onPress?: ?(event: PressEvent) => mixed, onPressIn?: ?(event: PressEvent) => mixed, onPressOut?: ?(event: PressEvent) => mixed, + // [macOS + acceptsFirstMouse?: ?boolean, + enableFocusRing?: ?boolean, + tooltip?: ?string, + onMouseEnter?: (event: MouseEvent) => void, + onMouseLeave?: (event: MouseEvent) => void, + onDragEnter?: (event: MouseEvent) => void, + onDragLeave?: (event: MouseEvent) => void, + onDrop?: (event: MouseEvent) => void, + draggedTypes?: ?DraggedTypesType, + onKeyDown?: ?(event: KeyEvent) => void, + onKeyUp?: ?(event: KeyEvent) => void, + validKeysDown?: ?Array, + validKeysUp?: ?Array, + // macOS] pressRetentionOffset?: ?EdgeInsetsOrSizeProp, rejectResponderTermination?: ?boolean, testID?: ?string, @@ -111,7 +132,16 @@ const PASSTHROUGH_PROPS = [ 'onAccessibilityAction', 'onBlur', 'onFocus', + 'validKeysDown', + 'validKeysUp', 'onLayout', + 'onMouseEnter', // [macOS + 'onMouseLeave', + 'onDragEnter', + 'onDragLeave', + 'onDrop', + 'draggedTypes', + 'tooltip', // macOS] 'testID', ]; @@ -147,8 +177,13 @@ class TouchableWithoutFeedback extends React.Component { // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. - const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = - this.state.pressability.getEventHandlers(); + const { + onBlur, + onFocus, + onMouseEnter, // [macOS] + onMouseLeave, // [macOS] + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); const elementProps: {[string]: mixed, ...} = { ...eventHandlersWithoutBlurAndFocus, @@ -162,7 +197,12 @@ class TouchableWithoutFeedback extends React.Component { : _accessibilityState, focusable: this.props.focusable !== false && this.props.onPress !== undefined, - + // [macOS + acceptsFirstMouse: + this.props.acceptsFirstMouse !== false && !this.props.disabled, + enableFocusRing: + this.props.enableFocusRing !== false && !this.props.disabled, + // macOS] accessibilityElementsHidden: this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden, importantForAccessibility: @@ -213,6 +253,10 @@ function createPressabilityConfig({ android_disableSound: props.touchSoundDisabled, onBlur: props.onBlur, onFocus: props.onFocus, + onKeyDown: props.onKeyDown, + onKeyUp: props.onKeyUp, + validKeysDown: props.validKeysDown, + validKeysUp: props.validKeysUp, onLongPress: props.onLongPress, onPress: props.onPress, onPressIn: props.onPressIn, diff --git a/packages/react-native/Libraries/Components/View/DraggedType.js b/packages/react-native/Libraries/Components/View/DraggedType.js new file mode 100644 index 00000000000000..dc02cf4cb6c02d --- /dev/null +++ b/packages/react-native/Libraries/Components/View/DraggedType.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +// [macOS] + +'use strict'; + +export type DraggedType = 'fileUrl'; + +export type DraggedTypesType = DraggedType | $ReadOnlyArray; + +module.exports = { + DraggedTypes: ['fileUrl'], +}; diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 79dbeddb741e39..de77b65aaba7f5 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -144,6 +144,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderTopLeftRadius: true, borderTopRightRadius: true, borderTopStartRadius: true, + cursor: true, opacity: true, pointerEvents: true, diff --git a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index e27df43205f540..963accabe90216 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -36,6 +36,23 @@ const UIView = { needsOffscreenAlphaCompositing: true, style: ReactNativeStyleAttributes, role: true, + // [macOS + acceptsFirstMouse: true, + mouseDownCanMoveWindow: true, + enableFocusRing: true, + focusable: true, + onMouseEnter: true, + onMouseLeave: true, + onDragEnter: true, + onDragLeave: true, + onDrop: true, + onKeyDown: true, + onKeyUp: true, + passthroughAllKeyEvents: true, + validKeysDown: true, + validKeysUp: true, + draggedTypes: true, + // macOS] }; const RCTView = { diff --git a/packages/react-native/Libraries/Components/View/ViewAccessibility.js b/packages/react-native/Libraries/Components/View/ViewAccessibility.js index 18da75effcb1d7..07fdaa6a8e59dc 100644 --- a/packages/react-native/Libraries/Components/View/ViewAccessibility.js +++ b/packages/react-native/Libraries/Components/View/ViewAccessibility.js @@ -53,7 +53,8 @@ export type AccessibilityRole = | 'webview' | 'drawerlayout' | 'slidingdrawer' - | 'iconmenu'; + | 'iconmenu' + | 'menubutton'; // [macOS] // Role types for web export type Role = diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 1ead9ba053a51a..bf0a4d724df999 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -15,12 +15,17 @@ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import type { BlurEvent, FocusEvent, + // [macOS] + KeyEvent, Layout, LayoutEvent, MouseEvent, PointerEvent, PressEvent, + ScrollEvent, + // [macOS] } from '../../Types/CoreEventTypes'; +import type {DraggedTypesType} from '../View/DraggedType'; // [macOS] import type { AccessibilityActionEvent, AccessibilityActionInfo, @@ -50,6 +55,20 @@ type DirectEventProps = $ReadOnly<{| */ onAccessibilityTap?: ?() => mixed, + // [macOS + /** + * This event is fired when the scrollView's inverted property changes. + * @platform macos + */ + onInvertedDidChange?: ?() => mixed, + + /** + * This event is fired when the system's preferred scroller style changes. + * The `preferredScrollerStyle` key will be `legacy` or `overlay`. + */ + onPreferredScrollerStyleDidChange?: ?(event: ScrollEvent) => mixed, + // macOS] + /** * Invoked on mount and layout changes with: * @@ -80,6 +99,62 @@ type DirectEventProps = $ReadOnly<{| onAccessibilityEscape?: ?() => mixed, |}>; +// [macOS +/** + * Represents a key that could be passed to `validKeysDown` and `validKeysUp`. + * + * `key` is the actual key, such as "a", or one of the special values: + * "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + * "Backspace", "Delete", "Home", "End", "PageUp", "PageDown". + * + * The rest are modifiers that when absent mean false. + * + * @platform macos + */ +export type HandledKeyboardEvent = $ReadOnly<{| + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + key: string, +|}>; + +export type KeyboardEventProps = $ReadOnly<{| + /** + * Called after a key down event is detected. + */ + onKeyDown?: ?(event: KeyEvent) => void, + + /** + * Called after a key up event is detected. + */ + onKeyUp?: ?(event: KeyEvent) => void, + + /** + * When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in + * `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp` + * are still removed from the event queue, but the others are not. + * + * @platform macos + */ + passthroughAllKeyEvents?: ?boolean, + + /** + * Array of keys to receive key down events for. These events have their default native behavior prevented. + * + * @platform macos + */ + validKeysDown?: ?Array, + + /** + * Array of keys to receive key up events for. These events have their default native behavior prevented. + * + * @platform macos + */ + validKeysUp?: ?Array, +|}>; +// macOS] + type MouseEventProps = $ReadOnly<{| onMouseEnter?: ?(event: MouseEvent) => void, onMouseLeave?: ?(event: MouseEvent) => void, @@ -365,7 +440,7 @@ type AndroidViewProps = $ReadOnly<{| /** * Whether this `View` should be focusable with a non-touch input device, eg. receive focus with a hardware keyboard. * - * @platform android + * @platform android macos */ focusable?: boolean, @@ -438,6 +513,89 @@ type IOSViewProps = $ReadOnly<{| shouldRasterizeIOS?: ?boolean, |}>; +// [macOS +type MacOSViewProps = $ReadOnly<{| + /** + * Fired when a file is dragged into the view via the mouse. + * + * @platform macos + */ + onDragEnter?: (event: MouseEvent) => void, + + /** + * Fired when a file is dragged out of the view via the mouse. + * + * @platform macos + */ + onDragLeave?: (event: MouseEvent) => void, + + /** + * Fired when an element is dropped on a valid drop target + * + * @platform macos + */ + onDrop?: (event: MouseEvent) => void, + + /** + * Specifies the Tooltip for the view + * @platform macos + */ + tooltip?: ?string, + + /** + * Specifies whether the view should receive the mouse down event when the + * containing window is in the background. + * + * @platform macos + */ + acceptsFirstMouse?: ?boolean, + + /** + * Specifies whether clicking and dragging the view can move the window. This is useful + * to disable in Button like components like Pressable where mouse the user should still + * be able to click and drag off the view to cancel the click without accidentally moving the window. + * + * @platform macos + */ + mouseDownCanMoveWindow?: ?boolean, + + /** + * Specifies whether the view ensures it is vibrant on top of other content. + * For more information, see the following apple documentation: + * https://developer.apple.com/documentation/appkit/nsview/1483793-allowsvibrancy + * https://developer.apple.com/documentation/appkit/nsvisualeffectview#1674177 + * + * @platform macos + */ + allowsVibrancy?: ?boolean, + + /** + * Specifies whether system focus ring should be drawn when the view has keyboard focus. + * + * @platform macos + */ + enableFocusRing?: ?boolean, + + /** + * The types of dragged files that the view will accept. + * + * Possible values for `draggedTypes` are: + * + * - `'fileUrl'` + * + * @platform macos + */ + draggedTypes?: ?DraggedTypesType, + + /** + * Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere + * + * @platform macos + */ + inverted?: ?boolean, +|}>; +// macOS] + export type ViewProps = $ReadOnly<{| ...DirectEventProps, ...GestureResponderEventProps, @@ -445,8 +603,10 @@ export type ViewProps = $ReadOnly<{| ...PointerEventProps, ...FocusEventProps, ...TouchEventProps, + ...KeyboardEventProps, // [macOS] ...AndroidViewProps, ...IOSViewProps, + ...MacOSViewProps, // [macOS] children?: Node, style?: ?ViewStyleProp, diff --git a/packages/react-native/Libraries/Core/NativeExceptionsManager.js b/packages/react-native/Libraries/Core/NativeExceptionsManager.js index aac86e07edad66..311879395f5d7b 100644 --- a/packages/react-native/Libraries/Core/NativeExceptionsManager.js +++ b/packages/react-native/Libraries/Core/NativeExceptionsManager.js @@ -82,7 +82,11 @@ const ExceptionsManager = { NativeModule.updateExceptionMessage(message, stack, exceptionId); }, dismissRedbox(): void { - if (Platform.OS !== 'ios' && NativeModule.dismissRedbox) { + if ( + Platform.OS !== 'ios' && + Platform.OS !== 'macos' /* [macOS] */ && + NativeModule.dismissRedbox + ) { // TODO(T53311281): This is a noop on iOS now. Implement it. NativeModule.dismissRedbox(); } diff --git a/packages/react-native/Libraries/Core/setUpAlert.js b/packages/react-native/Libraries/Core/setUpAlert.js index 777f7f85fbecbe..5f5d0ac18581ee 100644 --- a/packages/react-native/Libraries/Core/setUpAlert.js +++ b/packages/react-native/Libraries/Core/setUpAlert.js @@ -18,6 +18,7 @@ if (!global.alert) { global.alert = function (text: string) { // Require Alert on demand. Requiring it too early can lead to issues // with things like Platform not being fully initialized. + // @flow // [macOS] require('../Alert/Alert').alert('Alert', '' + text); }; } diff --git a/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.macos.js b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.macos.js new file mode 100644 index 00000000000000..96bbe349a32968 --- /dev/null +++ b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.macos.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// [macOS] + +// $FlowFixMe[prop-missing] Share the iOS file +export {DevToolsSettingsManager} from './DevToolsSettingsManager.ios'; diff --git a/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js index f30a69df7a4f43..8a72f639b2a463 100644 --- a/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js @@ -42,7 +42,7 @@ export default class NativeEventEmitter _nativeModule: ?NativeModule; constructor(nativeModule: ?NativeModule) { - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { invariant( nativeModule != null, '`new NativeEventEmitter()` requires a non-null argument.', diff --git a/packages/react-native/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js index ea3dcb972aa81d..69e02e9cee9081 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -158,12 +158,15 @@ let BaseImage: AbstractImageIOS = React.forwardRef((props, forwardedRef) => { }; const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel; + const accessibilityRole = props.accessibilityRole || 'image'; + return ( {analyticTag => { return ( +#import // [macOS] @protocol RCTAnimatedImage @property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount; @@ -17,5 +17,9 @@ @end @interface RCTAnimatedImage : UIImage - +// [macOS +// This is a known initializer for UIImage, but needs to be exposed publicly for macOS since +// this is not a known initializer for NSImage +- (nullable instancetype)initWithData:(NSData *)data scale:(CGFloat)scale; +// macOS] @end diff --git a/packages/react-native/Libraries/Image/RCTAnimatedImage.mm b/packages/react-native/Libraries/Image/RCTAnimatedImage.mm index e4dfc284c62d8e..e4f3f363660b88 100644 --- a/packages/react-native/Libraries/Image/RCTAnimatedImage.mm +++ b/packages/react-native/Libraries/Image/RCTAnimatedImage.mm @@ -42,6 +42,7 @@ - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale _imageSource = imageSource; +#if !TARGET_OS_OSX // [macOS] // grab image at the first index UIImage *image = [self animatedImageFrameAtIndex:0]; if (!image) { @@ -53,6 +54,9 @@ - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#else // [macOS + self = [super initWithData:data]; +#endif // macOS] } return self; @@ -150,7 +154,11 @@ - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index if (!imageRef) { return nil; } +#if !TARGET_OS_OSX // [macOS] UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp]; +#else // [macOS + UIImage *image = [[NSImage alloc] initWithCGImage:imageRef size:CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef))]; +#endif // macOS] CGImageRelease(imageRef); return image; } @@ -170,6 +178,9 @@ - (void)dealloc CFRelease(_imageSource); _imageSource = NULL; } +#if !TARGET_OS_OSX // [macOS] + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif // [macOS] } @end diff --git a/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.h b/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.h index d285c0c181926c..95c4a324280fc1 100644 --- a/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.h +++ b/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.h @@ -8,9 +8,11 @@ #import #import +#import // [macOS] + @protocol RCTDisplayRefreshable -- (void)displayDidRefresh:(CADisplayLink *)displayLink; +- (void)displayDidRefresh:(RCTPlatformDisplayLink *)displayLink; // [macOS] @end @@ -18,6 +20,6 @@ @property (nonatomic, weak) id refreshable; -+ (CADisplayLink *)displayLinkWithWeakRefreshable:(id)refreshable; ++ (RCTPlatformDisplayLink *)displayLinkWithWeakRefreshable:(id)refreshable; // [macOS] @end diff --git a/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.mm b/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.mm index 436f624d2679ac..c34b274692afd4 100644 --- a/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.mm +++ b/packages/react-native/Libraries/Image/RCTDisplayWeakRefreshable.mm @@ -9,10 +9,10 @@ @implementation RCTDisplayWeakRefreshable -+ (CADisplayLink *)displayLinkWithWeakRefreshable:(id)refreshable ++ (RCTPlatformDisplayLink *)displayLinkWithWeakRefreshable:(id)refreshable // [macOS] { RCTDisplayWeakRefreshable *target = [[RCTDisplayWeakRefreshable alloc] initWithRefreshable:refreshable]; - return [CADisplayLink displayLinkWithTarget:target selector:@selector(displayDidRefresh:)]; + return [RCTPlatformDisplayLink displayLinkWithTarget:target selector:@selector(displayDidRefresh:)]; // [macOS] } - (instancetype)initWithRefreshable:(id)refreshable @@ -23,7 +23,7 @@ - (instancetype)initWithRefreshable:(id)refreshable return self; } -- (void)displayDidRefresh:(CADisplayLink *)displayLink +- (void)displayDidRefresh:(RCTPlatformDisplayLink *)displayLink // [macOS] { [_refreshable displayDidRefresh:displayLink]; } diff --git a/packages/react-native/Libraries/Image/RCTImageBlurUtils.h b/packages/react-native/Libraries/Image/RCTImageBlurUtils.h index 4edcf3f2fa2659..3ee3e93e534e59 100644 --- a/packages/react-native/Libraries/Image/RCTImageBlurUtils.h +++ b/packages/react-native/Libraries/Image/RCTImageBlurUtils.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] #import diff --git a/packages/react-native/Libraries/Image/RCTImageBlurUtils.mm b/packages/react-native/Libraries/Image/RCTImageBlurUtils.mm index e5a17309473b12..889cb5ae629c1b 100644 --- a/packages/react-native/Libraries/Image/RCTImageBlurUtils.mm +++ b/packages/react-native/Libraries/Image/RCTImageBlurUtils.mm @@ -7,11 +7,16 @@ #import +#import // [macOS] +#import // [macOS] + UIImage *RCTBlurredImageWithRadius(UIImage *inputImage, CGFloat radius) { - CGImageRef imageRef = inputImage.CGImage; - CGFloat imageScale = inputImage.scale; + CGImageRef imageRef = UIImageGetCGImageRef(inputImage); // [macOS] + CGFloat imageScale = UIImageGetScale(inputImage); // [macOS] +#if !TARGET_OS_OSX // [macOS] UIImageOrientation imageOrientation = inputImage.imageOrientation; +#endif // [macOS] // Image must be nonzero size if (CGImageGetWidth(imageRef) * CGImageGetHeight(imageRef) == 0) { @@ -20,6 +25,7 @@ // convert to ARGB if it isn't if (CGImageGetBitsPerPixel(imageRef) != 32 || !((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) { +#if !TARGET_OS_OSX // [macOS] UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat]; rendererFormat.scale = inputImage.scale; UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:inputImage.size @@ -28,6 +34,12 @@ imageRef = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) { [inputImage drawAtPoint:CGPointZero]; }].CGImage; +#else // [macOS + UIGraphicsBeginImageContextWithOptions(inputImage.size, NO, imageScale); + [inputImage drawAtPoint:CGPointZero fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; + imageRef = (CGImageRef)CFAutorelease(CGBitmapContextCreateImage(UIGraphicsGetCurrentContext())); + UIGraphicsEndImageContext(); +#endif // macOS] } vImage_Buffer buffer1, buffer2; @@ -92,7 +104,11 @@ // create image from context imageRef = CGBitmapContextCreateImage(ctx); +#if !TARGET_OS_OSX // [macOS] UIImage *outputImage = [UIImage imageWithCGImage:imageRef scale:imageScale orientation:imageOrientation]; +#else // [macOS + NSImage *outputImage = [[NSImage alloc] initWithCGImage:imageRef size:inputImage.size]; +#endif // macOS] CGImageRelease(imageRef); CGContextRelease(ctx); free(buffer1.data); diff --git a/packages/react-native/Libraries/Image/RCTImageCache.h b/packages/react-native/Libraries/Image/RCTImageCache.h index 3f0997690b388f..038eb2293d8f8b 100644 --- a/packages/react-native/Libraries/Image/RCTImageCache.h +++ b/packages/react-native/Libraries/Image/RCTImageCache.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] #import diff --git a/packages/react-native/Libraries/Image/RCTImageCache.mm b/packages/react-native/Libraries/Image/RCTImageCache.mm index 5fa3e14ea5a817..947bf378b99fbe 100644 --- a/packages/react-native/Libraries/Image/RCTImageCache.mm +++ b/packages/react-native/Libraries/Image/RCTImageCache.mm @@ -46,6 +46,7 @@ - (instancetype)init _decodedImageCache.totalCostLimit = RCTImageCacheTotalCostLimit; _cacheStaleTimes = [NSMutableDictionary new]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearCache) name:UIApplicationDidReceiveMemoryWarningNotification @@ -54,11 +55,13 @@ - (instancetype)init selector:@selector(clearCache) name:UIApplicationWillResignActiveNotification object:nil]; +#endif // [macOS] } return self; } +#if !TARGET_OS_OSX // [macOS] - (void)clearCache { [_decodedImageCache removeAllObjects]; @@ -66,6 +69,7 @@ - (void)clearCache [_cacheStaleTimes removeAllObjects]; } } +#endif // [macOS] - (void)addImageToCache:(UIImage *)image forKey:(NSString *)cacheKey { diff --git a/packages/react-native/Libraries/Image/RCTImageDataDecoder.h b/packages/react-native/Libraries/Image/RCTImageDataDecoder.h index 1f52b4a07e7f5a..03658f705955cc 100644 --- a/packages/react-native/Libraries/Image/RCTImageDataDecoder.h +++ b/packages/react-native/Libraries/Image/RCTImageDataDecoder.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/Image/RCTImageEditingManager.mm b/packages/react-native/Libraries/Image/RCTImageEditingManager.mm index 9fe7f2238d22f6..5ac292455cf67a 100644 --- a/packages/react-native/Libraries/Image/RCTImageEditingManager.mm +++ b/packages/react-native/Libraries/Image/RCTImageEditingManager.mm @@ -15,7 +15,7 @@ #import #import #import -#import +#import // [macOS] #import "RCTImagePlugins.h" @@ -70,7 +70,7 @@ @implementation RCTImageEditingManager CGSize targetSize = rect.size; CGRect targetRect = {{-rect.origin.x, -rect.origin.y}, image.size}; CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetRect); - UIImage *croppedImage = RCTTransformImage(image, targetSize, image.scale, transform); + UIImage *croppedImage = RCTTransformImage(image, targetSize, UIImageGetScale(image), transform); // [macOS] // Scale image if (cropDataCopy.displaySize()) { @@ -81,7 +81,7 @@ @implementation RCTImageEditingManager RCTResizeMode resizeMode = [RCTConvert RCTResizeMode:cropDataCopy.resizeMode() ?: @"contain"]; targetRect = RCTTargetRect(croppedImage.size, targetSize, 1, resizeMode); transform = RCTTransformFromTargetRect(croppedImage.size, targetRect); - croppedImage = RCTTransformImage(croppedImage, targetSize, image.scale, transform); + croppedImage = RCTTransformImage(croppedImage, targetSize, UIImageGetScale(image), transform); // [macOS] } // Store image diff --git a/packages/react-native/Libraries/Image/RCTImageLoader.h b/packages/react-native/Libraries/Image/RCTImageLoader.h index ae9dc8069743e3..0d9a04c31650c8 100644 --- a/packages/react-native/Libraries/Image/RCTImageLoader.h +++ b/packages/react-native/Libraries/Image/RCTImageLoader.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -24,6 +24,7 @@ - (instancetype)initWithRedirectDelegate:(id)redirectDelegate loadersProvider:(NSArray> * (^)(RCTModuleRegistry *))getLoaders decodersProvider:(NSArray> * (^)(RCTModuleRegistry *))getDecoders; +- (NSInteger)activeTasks; // [macOS] @end /** diff --git a/packages/react-native/Libraries/Image/RCTImageLoader.mm b/packages/react-native/Libraries/Image/RCTImageLoader.mm index 09317d99c2d653..e694c2bf0f2ddd 100644 --- a/packages/react-native/Libraries/Image/RCTImageLoader.mm +++ b/packages/react-native/Libraries/Image/RCTImageLoader.mm @@ -14,6 +14,7 @@ #import #import #import +#import // [macOS] Expose DevSettings in release builds #import #import #import @@ -40,10 +41,50 @@ void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled) static NSInteger RCTImageBytesForImage(UIImage *image) { - NSInteger singleImageBytes = (NSInteger)(image.size.width * image.size.height * image.scale * image.scale * 4); + CGFloat imageScale = 1.0; +#if !TARGET_OS_OSX // [macOS] no .scale prop on NSImage + imageScale = image.scale; +#endif // [macOS] + NSInteger singleImageBytes = (NSInteger)(image.size.width * image.size.height * imageScale * imageScale * 4); +#if !TARGET_OS_OSX // [macOS] return image.images ? image.images.count * singleImageBytes : singleImageBytes; +#else // [macOS + return singleImageBytes; +#endif // macOS] } +#if TARGET_OS_OSX // [macOS + +/** + * Github #1611 - We can't depend on RCTUIKit here, because this file's podspec (React-RCTImage) doesn't + * take a dependency on RCTUIKit's pod `React-Core`. Let's just copy the methods we want to shim here + */ + +static NSData *NSImageDataForFileType(NSImage *image, NSBitmapImageFileType fileType, NSDictionary *properties) +{ + RCTAssert(image.representations.count == 1, @"Expected only a single representation since UIImage only supports one."); + + NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; + if (![imageRep isKindOfClass:[NSBitmapImageRep class]]) { + RCTAssert([imageRep isKindOfClass:[NSBitmapImageRep class]], @"We need an NSBitmapImageRep to create an image."); + return nil; + } + + return [imageRep representationUsingType:fileType properties:properties]; +} + + +NSData *UIImagePNGRepresentation(NSImage *image) { + return NSImageDataForFileType(image, NSBitmapImageFileTypePNG, @{}); +} + +NSData *UIImageJPEGRepresentation(NSImage *image, CGFloat compressionQuality) { + return NSImageDataForFileType(image, + NSBitmapImageFileTypeJPEG, + @{NSImageCompressionFactor: @(compressionQuality)}); +} +#endif // macOS] + static uint64_t monotonicTimeGetCurrentNanoseconds(void) { static struct mach_timebase_info tb_info = {0}; @@ -466,6 +507,12 @@ - (void)dequeueTasks }); } +// [macOS +- (NSInteger)activeTasks { + return _activeTasks; +} +// macOS] + /** * This returns either an image, or raw image data, depending on the loading * path taken. This is useful if you want to skip decoding, e.g. when preloading @@ -878,7 +925,7 @@ - (BOOL)shouldEnablePerfLoggingForRequestUrl:(NSURL *)url return NO; } -- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(UIView *)imageView +- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(RCTUIView *)imageView // [macOS] { if (!loaderRequest || !imageView) { return; @@ -968,8 +1015,8 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data // Decompress the image data (this may be CPU and memory intensive) UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode); -#if RCT_DEV - CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale); +#if !TARGET_OS_OSX && RCT_DEV // [macOS] + CGSize imagePixelSize = RCTSizeInPixels(image.size, UIImageGetScale(image)); // [macOS] CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale()); if (imagePixelSize.width * imagePixelSize.height > screenPixelSize.width * screenPixelSize.height) { RCTLogInfo( @@ -1062,9 +1109,15 @@ - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)ima } } else { UIImage *image = imageOrData; +#if !TARGET_OS_OSX // [macOS] + CGFloat imageScale = image.scale; +#else // [macOS + // Trust -[NSImage size] on macOS since an image is a collection of representations instead of a thin wrapper around a CGImage + CGFloat imageScale = 1.0; +#endif // macOS] size = (CGSize){ - image.size.width * image.scale, - image.size.height * image.scale, + image.size.width * imageScale, // [macOS] + image.size.height * imageScale, // [macOS] }; } callback(error, size); @@ -1157,7 +1210,7 @@ - (id)sendRequest:(NSURLRequest *)request withDelegate:(id +#import // [macOS] #import #import @@ -129,7 +129,6 @@ typedef NS_ENUM(NSInteger, RCTImageLoaderPriority) { RCTImageLoaderPriorityImmed * protocol. This method should be called in bridgeDidInitializeModule. */ - (void)setImageCache:(id)cache; - @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h b/packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h index 14997865e50dae..7795544e331ad2 100644 --- a/packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h +++ b/packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -36,7 +36,7 @@ RCT_EXTERN void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled); /** * Image instrumentation - start tracking the on-screen visibility of the native image view. */ -- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(UIView *)imageView; +- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(RCTUIView *)imageView; // [macOS] /** * Image instrumentation - notify that the request was cancelled. diff --git a/packages/react-native/Libraries/Image/RCTImageStoreManager.h b/packages/react-native/Libraries/Image/RCTImageStoreManager.h index 61f324d51cd4e8..707b5cf996ecd8 100644 --- a/packages/react-native/Libraries/Image/RCTImageStoreManager.h +++ b/packages/react-native/Libraries/Image/RCTImageStoreManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/Image/RCTImageStoreManager.mm b/packages/react-native/Libraries/Image/RCTImageStoreManager.mm index 242c42cce8a0e5..3b151604b8c3fd 100644 --- a/packages/react-native/Libraries/Image/RCTImageStoreManager.mm +++ b/packages/react-native/Libraries/Image/RCTImageStoreManager.mm @@ -12,7 +12,9 @@ #import #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] #import #import #import @@ -228,7 +230,7 @@ - (UIImage *)imageForTag:(NSString *)imageTag dispatch_sync(_methodQueue, ^{ imageData = self->_store[imageTag]; }); - return [UIImage imageWithData:imageData]; + return UIImageWithData(imageData); // [macOS] } - (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block @@ -238,7 +240,7 @@ - (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image)) NSData *imageData = self->_store[imageTag]; dispatch_async(dispatch_get_main_queue(), ^{ // imageWithData: is not thread-safe, so we can't do this on methodQueue - block([UIImage imageWithData:imageData]); + block(UIImageWithData(imageData)); // [macOS] }); }); } diff --git a/packages/react-native/Libraries/Image/RCTImageURLLoader.h b/packages/react-native/Libraries/Image/RCTImageURLLoader.h index b58b11f7d69152..755e1adc5ae561 100644 --- a/packages/react-native/Libraries/Image/RCTImageURLLoader.h +++ b/packages/react-native/Libraries/Image/RCTImageURLLoader.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h b/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h index 87b59f0843c6cf..401ca3660e5f7d 100644 --- a/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h +++ b/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h @@ -9,6 +9,8 @@ #import #import +#import // [macOS] + // TODO (T61325135): Remove C++ checks #ifdef __cplusplus namespace facebook::react { @@ -63,7 +65,7 @@ struct ImageURLLoaderAttribution { /** * Image instrumentation - start tracking the on-screen visibility of the native image view. */ -- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(UIView *)imageView; +- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(RCTUIView *)imageView; // [macOS] /** * Image instrumentation - notify that the request was destroyed. diff --git a/packages/react-native/Libraries/Image/RCTImageUtils.h b/packages/react-native/Libraries/Image/RCTImageUtils.h index b430e7a5b9a740..b42ad50663a0f4 100644 --- a/packages/react-native/Libraries/Image/RCTImageUtils.h +++ b/packages/react-native/Libraries/Image/RCTImageUtils.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -91,4 +91,9 @@ RCTTransformImage(UIImage *image, CGSize destSize, CGFloat destScale, CGAffineTr */ RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); +/* + * Return YES if image has an alpha component + */ +RCT_EXTERN BOOL RCTUIImageHasAlpha(UIImage *image); // [macOS] + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Image/RCTImageUtils.mm b/packages/react-native/Libraries/Image/RCTImageUtils.mm index 38fbd6bad434af..1a80c1e652498e 100644 --- a/packages/react-native/Libraries/Image/RCTImageUtils.mm +++ b/packages/react-native/Libraries/Image/RCTImageUtils.mm @@ -10,7 +10,9 @@ #import #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] #import #import @@ -30,6 +32,7 @@ static CGSize RCTCeilSize(CGSize size, CGFloat scale) return (CGSize){RCTCeilValue(size.width, scale), RCTCeilValue(size.height, scale)}; } +#if !TARGET_OS_OSX // [macOS] static CGImagePropertyOrientation CGImagePropertyOrientationFromUIImageOrientation(UIImageOrientation imageOrientation) { // see https://stackoverflow.com/a/6699649/496389 @@ -54,6 +57,7 @@ static CGImagePropertyOrientation CGImagePropertyOrientationFromUIImageOrientati return kCGImagePropertyOrientationUp; } } +#endif // [macOS] CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, CGFloat destScale, RCTResizeMode resizeMode) { @@ -278,7 +282,11 @@ BOOL RCTUpscalingRequired( destScale = 1; } } else if (!destScale) { +#if !TARGET_OS_OSX // [macOS] destScale = RCTScreenScale(); +#else // [macOS + destScale = 1.0; // It's not possible to derive the correct scale on macOS, but it's not necessary for NSImage anyway +#endif // macOS] } if (resizeMode == RCTResizeModeStretch) { @@ -308,7 +316,11 @@ BOOL RCTUpscalingRequired( } // Return image +#if !TARGET_OS_OSX // [macOS] UIImage *image = [UIImage imageWithCGImage:imageRef scale:destScale orientation:UIImageOrientationUp]; +#else // [macOS + NSImage *image = [[NSImage alloc] initWithCGImage:imageRef size:targetSize]; +#endif // macOS] CGImageRelease(imageRef); return image; } @@ -326,15 +338,22 @@ BOOL RCTUpscalingRequired( NSData *__nullable RCTGetImageData(UIImage *image, float quality) { +#if !TARGET_OS_OSX // [macOS] CGImageRef cgImage = image.CGImage; +#else // [macOS + CGImageRef cgImage = [image CGImageForProposedRect:NULL context:NULL hints:NULL]; +#endif // macOS] if (!cgImage) { return NULL; } NSMutableDictionary *properties = [[NSMutableDictionary alloc] initWithDictionary:@{ +#if !TARGET_OS_OSX // [macOS] (id)kCGImagePropertyOrientation : @(CGImagePropertyOrientationFromUIImageOrientation(image.imageOrientation)) +#endif // [macOS] }]; CGImageDestinationRef destination; CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0); + if (RCTImageHasAlpha(cgImage)) { // get png data destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL); @@ -362,7 +381,8 @@ BOOL RCTUpscalingRequired( return nil; } - BOOL opaque = !RCTImageHasAlpha(image.CGImage); + BOOL opaque = !RCTUIImageHasAlpha(image); // [macOS] +#if !TARGET_OS_OSX // [macOS] UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat]; rendererFormat.opaque = opaque; rendererFormat.scale = destScale; @@ -372,6 +392,15 @@ BOOL RCTUpscalingRequired( CGContextConcatCTM(context.CGContext, transform); [image drawAtPoint:CGPointZero]; }]; +#else // [macOS + UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale); + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + CGContextConcatCTM(currentContext, transform); + [image drawAtPoint:CGPointZero fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return result; +#endif // macOS] } BOOL RCTImageHasAlpha(CGImageRef image) @@ -385,3 +414,20 @@ BOOL RCTImageHasAlpha(CGImageRef image) return YES; } } + +#if !TARGET_OS_OSX // [macOS] +BOOL RCTUIImageHasAlpha(UIImage *image) +{ + return RCTImageHasAlpha(image.CGImage); +} +#else // [macOS +BOOL RCTUIImageHasAlpha(UIImage *image) +{ + for (NSImageRep *imageRep in image.representations) { + if (imageRep.hasAlpha) { + return YES; + } + } + return NO; +} +#endif // macOS] diff --git a/packages/react-native/Libraries/Image/RCTImageView.h b/packages/react-native/Libraries/Image/RCTImageView.h index da7f860228c6bb..d18b6cca4f1c2f 100644 --- a/packages/react-native/Libraries/Image/RCTImageView.h +++ b/packages/react-native/Libraries/Image/RCTImageView.h @@ -7,7 +7,7 @@ #import #import -#import +#import // [macOS] @class RCTBridge; @class RCTImageSource; @@ -24,4 +24,7 @@ @property (nonatomic, assign) RCTResizeMode resizeMode; @property (nonatomic, copy) NSString *internal_analyticTag; +#if TARGET_OS_OSX // [macOS +@property (nonatomic, copy) NSColor *tintColor; +#endif // macOS] @end diff --git a/packages/react-native/Libraries/Image/RCTImageView.mm b/packages/react-native/Libraries/Image/RCTImageView.mm index 3b3878fccb6495..2f8f4e08603848 100644 --- a/packages/react-native/Libraries/Image/RCTImageView.mm +++ b/packages/react-native/Libraries/Image/RCTImageView.mm @@ -33,6 +33,56 @@ static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSi heightMultiplier > upscaleThreshold || heightMultiplier < downscaleThreshold; } +#if TARGET_OS_OSX // [macOS +/** + * Implements macOS equivalent behavior of UIViewContentModeScaleAspectFill. + * Used for RCTResizeModeCover support. + */ +static NSImage *RCTFillImagePreservingAspectRatio(NSImage *originalImage, NSSize targetSize, CGFloat windowScale) +{ + RCTAssertParam(originalImage); + if (!originalImage) { + return nil; + } + + NSSize originalImageSize = originalImage.size; + if (NSEqualSizes(originalImageSize, NSZeroSize) || + NSEqualSizes(originalImageSize, targetSize) || + [[originalImage representations] count] == 0) { + return originalImage; + } + + CGFloat scaleX = targetSize.width / originalImageSize.width; + CGFloat scaleY = targetSize.height / originalImageSize.height; + CGFloat scale = 1.0; + + if (scaleX < scaleY) { + // clamped width + scale = scaleY; + } + else { + // clamped height + scale = scaleX; + } + + NSSize newSize = NSMakeSize(RCTRoundPixelValue(originalImageSize.width * scale, windowScale), + RCTRoundPixelValue(originalImageSize.height * scale, windowScale)); + NSImage *newImage = [[NSImage alloc] initWithSize:newSize]; + + for (NSImageRep *imageRep in [originalImage representations]) { + NSImageRep *newImageRep = [imageRep copy]; + NSSize newImageRepSize = NSMakeSize(RCTRoundPixelValue(imageRep.size.width * scale, windowScale), + RCTRoundPixelValue(imageRep.size.height * scale, windowScale)); + + newImageRep.size = newImageRepSize; + + [newImage addRepresentation:newImageRep]; + } + + return newImage; +} +#endif // macOS] + /** * See RCTConvert (ImageSource). We want to send down the source as a similar * JSON parameter. @@ -77,6 +127,8 @@ @implementation RCTImageView { // Whether the latest change of props requires the image to be reloaded BOOL _needsReload; + UIImage *_image; // [macOS] + RCTUIImageViewAnimated *_imageView; RCTImageURLLoaderRequest *_loaderRequest; @@ -84,12 +136,20 @@ @implementation RCTImageView { - (instancetype)initWithBridge:(RCTBridge *)bridge { +#if !TARGET_OS_OSX // [macOS] if ((self = [super initWithFrame:CGRectZero])) { +#else // [macOS + if ((self = [super initWithFrame:NSZeroRect])) { +#endif // macOS] _bridge = bridge; +#if TARGET_OS_OSX // [macOS + self.wantsLayer = YES; +#endif // macOS] _imageView = [RCTUIImageViewAnimated new]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview:_imageView]; +#if !TARGET_OS_OSX // [macOS] NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(clearImageIfDetached) @@ -104,6 +164,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge name:UISceneDidEnterBackgroundNotification object:nil]; +#endif // [macOS] } return self; } @@ -122,16 +183,38 @@ - (void)updateWithImage:(UIImage *)image } // Apply rendering mode +#if !TARGET_OS_OSX // [macOS] if (_renderingMode != image.renderingMode) { image = [image imageWithRenderingMode:_renderingMode]; } +#endif // [macOS] if (_resizeMode == RCTResizeModeRepeat) { +#if !TARGET_OS_OSX // [macOS] image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeTile]; +#else // [macOS + image.capInsets = _capInsets; + image.resizingMode = NSImageResizingModeTile; + } else if (_resizeMode == RCTResizeModeCover) { + if (!NSEqualSizes(self.bounds.size, NSZeroSize)) { + image = RCTFillImagePreservingAspectRatio(image, self.bounds.size, self.window.backingScaleFactor ?: 1.0); + } +#endif // macOS] } else if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _capInsets)) { // Applying capInsets of 0 will switch the "resizingMode" of the image to "tile" which is undesired +#if !TARGET_OS_OSX // [macOS] image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeStretch]; +#else // [macOS + image.capInsets = _capInsets; + image.resizingMode = NSImageResizingModeStretch; +#endif // macOS] + } + +#if TARGET_OS_OSX // [macOS + if ((_renderingMode == UIImageRenderingModeAlwaysTemplate) != [image isTemplate]) { + [image setTemplate:(_renderingMode == UIImageRenderingModeAlwaysTemplate)]; } +#endif // macOS] // Apply trilinear filtering to smooth out missized images _imageView.layer.minificationFilter = kCAFilterTrilinear; @@ -144,13 +227,14 @@ - (void)setImage:(UIImage *)image { image = image ?: _defaultImage; if (image != self.image) { + _image = image; // [macOS] [self updateWithImage:image]; } } - (UIImage *)image { - return _imageView.image; + return _image ?: _imageView.image; // [macOS] } - (void)setBlurRadius:(CGFloat)blurRadius @@ -200,9 +284,21 @@ - (void)setResizeMode:(RCTResizeMode)resizeMode if (_resizeMode == RCTResizeModeRepeat) { // Repeat resize mode is handled by the UIImage. Use scale to fill // so the repeated image fills the UIImageView. +#if !TARGET_OS_OSX // [macOS] _imageView.contentMode = UIViewContentModeScaleToFill; +#else // [macOS + _imageView.imageScaling = NSImageScaleAxesIndependently; +#endif // macOS] } else { +#if !TARGET_OS_OSX // [macOS] _imageView.contentMode = (UIViewContentMode)resizeMode; +#else // [macOS + // This relies on having previously resampled the image to a size that exceeds the image view. + if (resizeMode == RCTResizeModeCover) { + resizeMode = RCTResizeModeCenter; + } + _imageView.imageScaling = (NSImageScaling)resizeMode; +#endif // macOS] } if ([self shouldReloadImageSourceAfterResize]) { @@ -237,6 +333,7 @@ - (void)cancelAndClearImageLoad } } +#if !TARGET_OS_OSX // [macOS] - (void)clearImageIfDetached { if (!self.window) { @@ -245,6 +342,7 @@ - (void)clearImageIfDetached _imageSource = nil; } } +#endif // [macOS] - (BOOL)hasMultipleSources { @@ -262,7 +360,11 @@ - (RCTImageSource *)imageSourceForSize:(CGSize)size return nil; } +#if !TARGET_OS_OSX // [macOS] const CGFloat scale = RCTScreenScale(); +#else // [macOS + const CGFloat scale = self.window != nil ? self.window.backingScaleFactor : [NSScreen mainScreen].backingScaleFactor; +#endif // macOS] const CGFloat targetImagePixels = size.width * size.height * scale * scale; RCTImageSource *bestSource = nil; @@ -324,7 +426,11 @@ - (void)reloadImage }; CGSize imageSize = self.bounds.size; +#if !TARGET_OS_OSX // [macOS] CGFloat imageScale = RCTScreenScale(); +#else // [macOS + CGFloat imageScale = self.window != nil ? self.window.backingScaleFactor : [NSScreen mainScreen].backingScaleFactor; +#endif // macOS] if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) { // Don't resize images that use capInsets imageSize = CGSizeZero; @@ -446,13 +552,31 @@ - (void)reactSetFrame:(CGRect)frame [self reloadImage]; } else if ([self shouldReloadImageSourceAfterResize]) { CGSize imageSize = self.image.size; - CGFloat imageScale = self.image.scale; - CGSize idealSize = - RCTTargetSize(imageSize, imageScale, frame.size, RCTScreenScale(), (RCTResizeMode)self.contentMode, YES); - + CGFloat imageScale = UIImageGetScale(self.image); // [macOS] +#if !TARGET_OS_OSX // [macOS] + CGFloat windowScale = RCTScreenScale(); + RCTResizeMode resizeMode = (RCTResizeMode)_imageView.contentMode; // [macOS] +#else // [macOS + CGFloat windowScale = self.window != nil ? self.window.backingScaleFactor : [NSScreen mainScreen].backingScaleFactor; + RCTResizeMode resizeMode = self.resizeMode; + + // self.contentMode on iOS is translated to RCTResizeModeRepeat in -setResizeMode: + if (resizeMode == RCTResizeModeRepeat) { + resizeMode = RCTResizeModeStretch; + } +#endif // macOS] + CGSize idealSize = RCTTargetSize(imageSize, imageScale, frame.size, windowScale, + resizeMode, YES); // macOS] // Don't reload if the current image or target image size is close enough - if (!RCTShouldReloadImageForSizeChange(imageSize, idealSize) || - !RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) { + if ((!RCTShouldReloadImageForSizeChange(imageSize, idealSize) || + !RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) // [macOS +#if TARGET_OS_OSX + // Since macOS doen't support UIViewContentModeScaleAspectFill, we have to manually resample the image + // If we're in cover mode we need to ensure that the image is re-sampled to the correct size when the container size (shrinking + // being the most obvious case) otherwise we will end up in a state an image will not properly scale inside its container + && (resizeMode != RCTResizeModeCover || (imageSize.width == idealSize.width && imageSize.height == idealSize.height)) +#endif + ) { // macOS] return; } @@ -480,6 +604,9 @@ - (void)didSetProps:(NSArray *)changedProps } } +#if TARGET_OS_OSX // [macOS +#define didMoveToWindow viewDidMoveToWindow +#endif - (void)didMoveToWindow { [super didMoveToWindow]; @@ -495,6 +622,28 @@ - (void)didMoveToWindow [self reloadImage]; } } + +#if TARGET_OS_OSX // [macOS +- (void)viewDidChangeBackingProperties +{ + [self reloadImage]; +} + +- (RCTPlatformView *)reactAccessibilityElement +{ + return _imageView; +} + +- (NSColor *)tintColor +{ + return _imageView.contentTintColor; +} + +- (void)setTintColor:(NSColor *)tintColor +{ + _imageView.contentTintColor = tintColor; +} +#endif // macOS] - (void)dealloc { diff --git a/packages/react-native/Libraries/Image/RCTImageViewManager.mm b/packages/react-native/Libraries/Image/RCTImageViewManager.mm index 19ecaf7ee2c938..a2a990d2a472b2 100644 --- a/packages/react-native/Libraries/Image/RCTImageViewManager.mm +++ b/packages/react-native/Libraries/Image/RCTImageViewManager.mm @@ -7,7 +7,7 @@ #import -#import +#import // [macOS] #import #import @@ -25,7 +25,7 @@ - (RCTShadowView *)shadowView return [RCTImageShadowView new]; } -- (UIView *)view +- (RCTPlatformView *)view // [macOS] { return [[RCTImageView alloc] initWithBridge:self.bridge]; } diff --git a/packages/react-native/Libraries/Image/RCTResizeMode.h b/packages/react-native/Libraries/Image/RCTResizeMode.h index 38d1f6e486e429..60809b3d8376f4 100644 --- a/packages/react-native/Libraries/Image/RCTResizeMode.h +++ b/packages/react-native/Libraries/Image/RCTResizeMode.h @@ -8,10 +8,17 @@ #import typedef NS_ENUM(NSInteger, RCTResizeMode) { +#if !TARGET_OS_OSX // [macOS] RCTResizeModeCover = UIViewContentModeScaleAspectFill, RCTResizeModeContain = UIViewContentModeScaleAspectFit, RCTResizeModeStretch = UIViewContentModeScaleToFill, RCTResizeModeCenter = UIViewContentModeCenter, +#else // [macOS + RCTResizeModeCover = -2, // Not supported by NSImageView + RCTResizeModeContain = NSImageScaleProportionallyUpOrDown, + RCTResizeModeStretch = NSImageScaleAxesIndependently, + RCTResizeModeCenter = NSImageScaleNone, // assumes NSImageAlignmentCenter +#endif // macOS] RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values. }; diff --git a/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h index 1215e21b2d641b..9a8924467370e3 100644 --- a/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h +++ b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h @@ -8,6 +8,5 @@ #import #import -@interface RCTUIImageViewAnimated : UIImageView - +@interface RCTUIImageViewAnimated : RCTUIImageView // [macOS] @end diff --git a/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.mm b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.mm index aa0abfe6489417..ce209e06a4487e 100644 --- a/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.mm +++ b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.mm @@ -23,7 +23,6 @@ static NSUInteger RCTDeviceFreeMemory(void) vm_size_t page_size; vm_statistics_data_t vm_stat; kern_return_t kern; - kern = host_page_size(host_port, &page_size); if (kern != KERN_SUCCESS) return 0; @@ -49,7 +48,9 @@ @interface RCTUIImageViewAnimated () @property (nonatomic, strong) NSOperationQueue *fetchQueue; @property (nonatomic, strong) dispatch_semaphore_t lock; @property (nonatomic, assign) CGFloat animatedImageScale; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong) CADisplayLink *displayLink; +#endif // [macOS] @end @@ -59,10 +60,12 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.lock = dispatch_semaphore_create(1); +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif // [macOS] } return self; } @@ -93,12 +96,12 @@ - (void)setImage:(UIImage *)image return; } +#if !TARGET_OS_OSX // [macOS] [self stop]; [self resetAnimatedImage]; if ([image respondsToSelector:@selector(animatedImageFrameAtIndex:)]) { NSUInteger animatedImageFrameCount = ((UIImage *)image).animatedImageFrameCount; - // In case frame count is 0, there is no reason to continue. if (animatedImageFrameCount == 0) { return; @@ -110,7 +113,7 @@ - (void)setImage:(UIImage *)image // Get the current frame and loop count. self.totalLoopCount = self.animatedImage.animatedImageLoopCount; - self.animatedImageScale = image.scale; + self.animatedImageScale = UIImageGetScale(image); // [macOS] self.currentFrame = image; @@ -124,11 +127,14 @@ - (void)setImage:(UIImage *)image if ([self paused]) { [self start]; } - + [self.layer setNeedsDisplay]; } else { super.image = image; } +#else // [macOS + [super setImage:image]; +#endif // macOS] } #pragma mark - Private @@ -150,6 +156,7 @@ - (NSOperationQueue *)fetchQueue return _frameBuffer; } +#if !TARGET_OS_OSX // [macOS] - (CADisplayLink *)displayLink { // We only need a displayLink in the case of animated images, so short-circuit this code and don't create one for most @@ -198,7 +205,6 @@ - (void)displayDidRefresh:(CADisplayLink *)displayLink NSUInteger totalFrameCount = self.totalFrameCount; NSUInteger currentFrameIndex = self.currentFrameIndex; NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; - // Check if we have the frame buffer firstly to improve performance if (!self.bufferMiss) { // Then check if timestamp is reached @@ -216,7 +222,6 @@ - (void)displayDidRefresh:(CADisplayLink *)displayLink self.currentTime = nextDuration; } } - // Update the current frame UIImage *currentFrame; UIImage *fetchFrame; @@ -243,7 +248,6 @@ - (void)displayDidRefresh:(CADisplayLink *)displayLink } else { self.bufferMiss = YES; } - // Update the loop count when last frame rendered if (nextFrameIndex == 0 && !self.bufferMiss) { // Update the loop count @@ -255,7 +259,6 @@ - (void)displayDidRefresh:(CADisplayLink *)displayLink return; } } - // Check if we should prefetch next frame or current frame NSUInteger fetchFrameIndex; if (self.bufferMiss) { @@ -265,7 +268,6 @@ - (void)displayDidRefresh:(CADisplayLink *)displayLink // Or, most cases, the decode speed is faster than render speed, we fetch next frame fetchFrameIndex = nextFrameIndex; } - if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { // Prefetch next frame in background queue UIImage *animatedImage = self.animatedImage; @@ -308,13 +310,11 @@ - (void)calculateMaxBufferCount NSUInteger free = RCTDeviceFreeMemory(); max = MIN((double)total * 0.2, (double)free * 0.6); } - NSUInteger maxBufferCount = (double)max / (double)bytes; if (!maxBufferCount) { // At least 1 frame maxBufferCount = 1; } - self.maxBufferCount = maxBufferCount; } @@ -343,5 +343,6 @@ - (void)didReceiveMemoryWarning:(NSNotification *)notification dispatch_semaphore_signal(self.lock); }]; } +#endif // [macOS] @end diff --git a/packages/react-native/Libraries/Image/nativeImageSource.js b/packages/react-native/Libraries/Image/nativeImageSource.js index 7785b56786f8bd..c44f665e83b47a 100644 --- a/packages/react-native/Libraries/Image/nativeImageSource.js +++ b/packages/react-native/Libraries/Image/nativeImageSource.js @@ -15,6 +15,7 @@ import Platform from '../Utilities/Platform'; type NativeImageSourceSpec = $ReadOnly<{| android?: string, ios?: string, + macos?: string, // [macOS] default?: string, // For more details on width and height, see @@ -44,6 +45,7 @@ function nativeImageSource(spec: NativeImageSourceSpec): ImageURISource { android: spec.android, default: spec.default, ios: spec.ios, + macos: spec.macos, // [macOS] }); if (uri == null) { console.warn( diff --git a/packages/react-native/Libraries/Inspector/NetworkOverlay.js b/packages/react-native/Libraries/Inspector/NetworkOverlay.js index 1e4d63f27fd9d2..a9348a352adeac 100644 --- a/packages/react-native/Libraries/Inspector/NetworkOverlay.js +++ b/packages/react-native/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '@react-native/virtualized-lists'; +import type {RenderItemProps} from '@react-native-mac/virtualized-lists'; // [macOS] const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); diff --git a/packages/react-native/Libraries/Linking/Linking.js b/packages/react-native/Libraries/Linking/Linking.js index 76d187988ff6e2..14f9f415bf4145 100644 --- a/packages/react-native/Libraries/Linking/Linking.js +++ b/packages/react-native/Libraries/Linking/Linking.js @@ -29,7 +29,11 @@ type LinkingEventDefinitions = { */ class Linking extends NativeEventEmitter { constructor() { - super(Platform.OS === 'ios' ? nullthrows(NativeLinkingManager) : undefined); + super( + Platform.OS === 'ios' || Platform.OS === 'macos' // [macOS] + ? nullthrows(NativeLinkingManager) + : undefined, + ); } /** diff --git a/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h b/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h index eff3b0c5461422..5c00f19dd976e9 100644 --- a/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h +++ b/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h @@ -6,11 +6,14 @@ */ #import -#import +#import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] @interface RCTLinkingManager : RCTEventEmitter +#if !TARGET_OS_OSX // [macOS] + (BOOL)application:(nonnull UIApplication *)app openURL:(nonnull NSURL *)URL options:(nonnull NSDictionary *)options; @@ -23,5 +26,9 @@ + (BOOL)application:(nonnull UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> *_Nullable))restorationHandler; +#else // [macOS ++ (void)getUrlEventHandler:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; ++ (void)setAlwaysForegroundLastWindow:(BOOL)alwaysForeground; +#endif // macOS] @end diff --git a/packages/react-native/Libraries/LinkingIOS/macos/RCTLinkingManager.mm b/packages/react-native/Libraries/LinkingIOS/macos/RCTLinkingManager.mm new file mode 100644 index 00000000000000..6b0108d8da3fc6 --- /dev/null +++ b/packages/react-native/Libraries/LinkingIOS/macos/RCTLinkingManager.mm @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "RCTLinkingManager.h" + +#import + +#import +#import +#import + +#import "RCTLinkingPlugins.h" + +NSString *const RCTOpenURLNotification = @"RCTOpenURLNotification"; + +static NSString *initialURL = nil; +static BOOL moduleInitalized = NO; +static BOOL alwaysForegroundLastWindow = YES; + +static void postNotificationWithURL(NSString *url, id sender) +{ + NSDictionary *payload = @{@"url": url}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTOpenURLNotification + object:sender + userInfo:payload]; +} + +@implementation RCTLinkingManager + +RCT_EXPORT_MODULE() + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (void)startObserving +{ + moduleInitalized = YES; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleOpenURLNotification:) + name:RCTOpenURLNotification + object:nil]; +} + +- (void)stopObserving +{ + moduleInitalized = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (NSArray *)supportedEvents +{ + return @[@"url"]; +} + ++ (void)setAlwaysForegroundLastWindow:(BOOL)alwaysForeground +{ + alwaysForegroundLastWindow = alwaysForeground; +} + ++ (void)getUrlEventHandler:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent +{ + // extract url value from the event + NSString* url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + // If the application was launched via URL, this handler will be called before + // the module is initialized by the bridge. Store the initial URL, becase we are not listening to the notification yet. + if (!moduleInitalized && initialURL == nil) { + initialURL = url; + } + + postNotificationWithURL(url, self); +} + +- (void)handleOpenURLNotification:(NSNotification *)notification +{ + // Activate app, because [NSApp mainWindow] returns nil when the app is hidden and another app is maximized + [NSApp activateIgnoringOtherApps:YES]; + // foreground top level window + if (alwaysForegroundLastWindow) { + NSWindow *lastWindow = [[NSApp windows] lastObject]; + [lastWindow makeKeyAndOrderFront:nil]; + } + [self sendEventWithName:@"url" body:notification.userInfo]; +} + +RCT_EXPORT_METHOD(openURL:(NSURL *)URL + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + BOOL result = [[NSWorkspace sharedWorkspace] openURL:URL]; + if (result) { + resolve(@YES); + } else { + reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unable to open URL: %@", URL], nil); + } +} + +RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL + resolve:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) +{ + resolve(@YES); +} + +RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) +{ + resolve(RCTNullIfNil(initialURL)); +} +@end + +Class RCTLinkingManagerCls(void) { + return RCTLinkingManager.class; +} diff --git a/packages/react-native/Libraries/Lists/FillRateHelper.js b/packages/react-native/Libraries/Lists/FillRateHelper.js index 141fe98eb067bb..c94ca9916f1562 100644 --- a/packages/react-native/Libraries/Lists/FillRateHelper.js +++ b/packages/react-native/Libraries/Lists/FillRateHelper.js @@ -10,10 +10,10 @@ 'use strict'; -import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; +import {typeof FillRateHelper as FillRateHelperType} from '@react-native-mac/virtualized-lists'; // [macOS] const FillRateHelper: FillRateHelperType = - require('@react-native/virtualized-lists').FillRateHelper; + require('@react-native-mac/virtualized-lists').FillRateHelper; // [macOS] -export type {FillRateInfo} from '@react-native/virtualized-lists'; +export type {FillRateInfo} from '@react-native-mac/virtualized-lists'; // [macOS] module.exports = FillRateHelper; diff --git a/packages/react-native/Libraries/Lists/FlatList.js b/packages/react-native/Libraries/Lists/FlatList.js index 1b2ee1707c10a5..7c2480d6c17eba 100644 --- a/packages/react-native/Libraries/Lists/FlatList.js +++ b/packages/react-native/Libraries/Lists/FlatList.js @@ -15,13 +15,13 @@ import type { RenderItemType, ViewabilityConfigCallbackPair, ViewToken, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; import { VirtualizedList, keyExtractor as defaultKeyExtractor, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] import memoizeOne from 'memoize-one'; const View = require('../Components/View/View'); @@ -66,11 +66,28 @@ type OptionalProps = {| * your use-case. */ renderItem?: ?RenderItemType, - /** * Optional custom style for multi-item rows generated when numColumns > 1. */ columnWrapperStyle?: ViewStyleProp, + // [macOS + /** + * Allows you to 'select' a row using arrow keys. The selected row will have the prop `isSelected` + * passed in as true to it's renderItem / ListItemComponent. You can also imperatively select a row + * using the `selectRowAtIndex` method. You can set the initially selected row using the + * `initialSelectedIndex` prop. + * Keyboard Behavior: + * - ArrowUp: Select row above current selected row + * - ArrowDown: Select row below current selected row + * - Option+ArrowUp: Select the first row + * - Opton+ArrowDown: Select the last 'realized' row + * - Home: Scroll to top of list + * - End: Scroll to end of list + * + * @platform macos + */ + enableSelectionOnKeyPress?: ?boolean, + // macOS] /** * A marker property for telling the list to re-render (since it implements `PureComponent`). If * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the @@ -116,8 +133,14 @@ type OptionalProps = {| * `getItemLayout` to be implemented. */ initialScrollIndex?: ?number, + // [macOS /** - * Reverses the direction of scroll. Uses scale transforms of -1. + * The initially selected row, if `enableSelectionOnKeyPress` is set. + */ + initialSelectedIndex?: ?number, + // macOS] + /** + * Reverses the direction of scroll. Uses native inversion on macOS and scale transforms of -1 elsewhere */ inverted?: ?boolean, /** @@ -384,6 +407,19 @@ class FlatList extends React.PureComponent, void> { } } + // [macOS + /** + * Move selection to the specified index + * + * @platform macos + */ + selectRowAtIndex(index: number) { + if (this._listRef) { + this._listRef.selectRowAtIndex(index); + } + } + // macOS] + /** * Provides a handle to the underlying scroll responder. */ @@ -650,6 +686,7 @@ class FlatList extends React.PureComponent, void> { // $FlowFixMe[incompatible-call] item: it, index: index * cols + kk, + isSelected: info.isSelected, // [macOS] separators: info.separators, }); return element != null ? ( diff --git a/packages/react-native/Libraries/Lists/SectionList.js b/packages/react-native/Libraries/Lists/SectionList.js index 6177b07d58dec3..51457bb64f9686 100644 --- a/packages/react-native/Libraries/Lists/SectionList.js +++ b/packages/react-native/Libraries/Lists/SectionList.js @@ -15,10 +15,10 @@ import type { ScrollToLocationParamsType, SectionBase as _SectionBase, VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import {VirtualizedSectionList} from '@react-native-mac/virtualized-lists'; // [macOS] import * as React from 'react'; type Item = any; diff --git a/packages/react-native/Libraries/Lists/SectionListModern.js b/packages/react-native/Libraries/Lists/SectionListModern.js index d9676f106f8cfc..07f41c0e46d6c9 100644 --- a/packages/react-native/Libraries/Lists/SectionListModern.js +++ b/packages/react-native/Libraries/Lists/SectionListModern.js @@ -15,11 +15,11 @@ import type { ScrollToLocationParamsType, SectionBase as _SectionBase, VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] import type {AbstractComponent, Element, ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import {VirtualizedSectionList} from '@react-native-mac/virtualized-lists'; // [macOS] import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; diff --git a/packages/react-native/Libraries/Lists/ViewabilityHelper.js b/packages/react-native/Libraries/Lists/ViewabilityHelper.js index c7dedfdf496b93..3b7dd04f0ada0e 100644 --- a/packages/react-native/Libraries/Lists/ViewabilityHelper.js +++ b/packages/react-native/Libraries/Lists/ViewabilityHelper.js @@ -14,11 +14,11 @@ export type { ViewToken, ViewabilityConfig, ViewabilityConfigCallbackPair, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] -import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; +import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native-mac/virtualized-lists'; // [macOS] const ViewabilityHelper: ViewabilityHelperType = - require('@react-native/virtualized-lists').ViewabilityHelper; + require('@react-native-mac/virtualized-lists').ViewabilityHelper; // [macOS] module.exports = ViewabilityHelper; diff --git a/packages/react-native/Libraries/Lists/VirtualizeUtils.js b/packages/react-native/Libraries/Lists/VirtualizeUtils.js index 535d25b3abc538..e57587cc068c1c 100644 --- a/packages/react-native/Libraries/Lists/VirtualizeUtils.js +++ b/packages/react-native/Libraries/Lists/VirtualizeUtils.js @@ -10,9 +10,9 @@ 'use strict'; -import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; +import {typeof keyExtractor as KeyExtractorType} from '@react-native-mac/virtualized-lists'; // [macOS] const keyExtractor: KeyExtractorType = - require('@react-native/virtualized-lists').keyExtractor; + require('@react-native-mac/virtualized-lists').keyExtractor; // [macOS] module.exports = {keyExtractor}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedList.js b/packages/react-native/Libraries/Lists/VirtualizedList.js index 2488b1e5e37f57..0efad220f7c76b 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedList.js +++ b/packages/react-native/Libraries/Lists/VirtualizedList.js @@ -10,14 +10,14 @@ 'use strict'; -import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; +import {typeof VirtualizedList as VirtualizedListType} from '@react-native-mac/virtualized-lists'; // [macOS] const VirtualizedList: VirtualizedListType = - require('@react-native/virtualized-lists').VirtualizedList; + require('@react-native-mac/virtualized-lists').VirtualizedList; // [macOS] export type { RenderItemProps, RenderItemType, Separators, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] module.exports = VirtualizedList; diff --git a/packages/react-native/Libraries/Lists/VirtualizedListContext.js b/packages/react-native/Libraries/Lists/VirtualizedListContext.js index 5686ccf372286c..83417f56d07550 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedListContext.js +++ b/packages/react-native/Libraries/Lists/VirtualizedListContext.js @@ -10,9 +10,9 @@ 'use strict'; -import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; +import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native-mac/virtualized-lists'; // [macOS] const VirtualizedListContextResetter: VirtualizedListContextResetterType = - require('@react-native/virtualized-lists').VirtualizedListContextResetter; + require('@react-native-mac/virtualized-lists').VirtualizedListContextResetter; // [macOS] module.exports = {VirtualizedListContextResetter}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedSectionList.js b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js index 242dfe34c6b231..15adbae4794ee6 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedSectionList.js +++ b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js @@ -10,13 +10,13 @@ 'use strict'; -import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; +import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native-mac/virtualized-lists'; // [macOS] const VirtualizedSectionList: VirtualizedSectionListType = - require('@react-native/virtualized-lists').VirtualizedSectionList; + require('@react-native-mac/virtualized-lists').VirtualizedSectionList; // [macOS] export type { SectionBase, ScrollToLocationParamsType, -} from '@react-native/virtualized-lists'; +} from '@react-native-mac/virtualized-lists'; // [macOS] module.exports = VirtualizedSectionList; diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js index eff4443bb897cc..17b1f3bc6ffb22 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js @@ -143,7 +143,13 @@ const styles = StyleSheet.create({ fontSize: 12, includeFontPadding: false, lineHeight: 20, - fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}), + // [macOS + fontFamily: Platform.select({ + android: 'monospace', + ios: 'Menlo', + macos: 'Menlo', + }), + // macOS] }, fileText: { color: LogBoxStyle.getTextColor(0.5), @@ -152,7 +158,13 @@ const styles = StyleSheet.create({ fontSize: 12, includeFontPadding: false, lineHeight: 16, - fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}), + // [macOS + fontFamily: Platform.select({ + android: 'monospace', + ios: 'Menlo', + macos: 'Menlo', + }), + // macOS] }, }); diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js index 1f578db82f0f26..afa5ce18a03aee 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js @@ -146,6 +146,7 @@ const styles = StyleSheet.create({ height: Platform.select({ android: 48, ios: 44, + macos: 44, // [macOS] }), }, title: { diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js index 4c1cf9b3c1c482..f53c583c33b2cb 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js @@ -158,14 +158,26 @@ const componentStyles = StyleSheet.create({ paddingRight: 10, }, frameName: { - fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}), + // [macOS + fontFamily: Platform.select({ + android: 'monospace', + ios: 'Menlo', + macos: 'Menlo', + }), + // macOS] color: LogBoxStyle.getTextColor(1), fontSize: 14, includeFontPadding: false, lineHeight: 18, }, bracket: { - fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}), + // [macOS + fontFamily: Platform.select({ + android: 'monospace', + ios: 'Menlo', + macos: 'Menlo', + }), + // macOS] color: LogBoxStyle.getTextColor(0.4), fontSize: 14, fontWeight: '500', diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js index 98326f9d0d7349..552bc21c9fd2c5 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js @@ -88,7 +88,13 @@ const styles = StyleSheet.create({ includeFontPadding: false, lineHeight: 18, fontWeight: '400', - fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}), + // [macOS + fontFamily: Platform.select({ + android: 'monospace', + ios: 'Menlo', + macos: 'Menlo', + }), + // macOS] }, location: { color: LogBoxStyle.getTextColor(0.8), diff --git a/packages/react-native/Libraries/Modal/Modal.js b/packages/react-native/Libraries/Modal/Modal.js index 9750d2e5be31d3..0d4902e2030271 100644 --- a/packages/react-native/Libraries/Modal/Modal.js +++ b/packages/react-native/Libraries/Modal/Modal.js @@ -17,7 +17,7 @@ import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; -import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; +import {VirtualizedListContextResetter} from '@react-native-mac/virtualized-lists'; // [macOS] const ScrollView = require('../Components/ScrollView/ScrollView'); const View = require('../Components/View/View'); diff --git a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.mm b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.mm index b4aec0c33288fe..7facdd8d2bb3fc 100644 --- a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.mm +++ b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.mm @@ -8,7 +8,7 @@ #import #import -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.mm b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.mm index 1de5cb13eb8ffa..dfb1b81ba853b0 100644 --- a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.mm +++ b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.mm @@ -7,7 +7,7 @@ #import -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.mm b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.mm index 640107df09c287..22fb8779b94c1b 100644 --- a/packages/react-native/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.mm +++ b/packages/react-native/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.mm @@ -7,7 +7,7 @@ #import -#import +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.mm b/packages/react-native/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.mm index 78ee190dee0cd4..2887f97b5bcdd2 100644 --- a/packages/react-native/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.mm +++ b/packages/react-native/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.mm @@ -105,8 +105,8 @@ - (instancetype)initWithTag:(NSNumber *)tag config:(NSDictionary for (id value in outputRangeConfig) { switch (_outputType) { case RCTInterpolationOutputColor: { - UIColor *color = [RCTConvert UIColor:value]; - [outputRange addObject:color ? color : [UIColor whiteColor]]; + RCTUIColor *color = [RCTConvert UIColor:value]; // [macOS] + [outputRange addObject:color ? color : [RCTUIColor whiteColor]]; // [macOS] break; } case RCTInterpolationOutputString: diff --git a/packages/react-native/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h b/packages/react-native/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h index 84d220bb7b0dda..89db2321a82d58 100644 --- a/packages/react-native/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h +++ b/packages/react-native/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTAnimatedNode.h" diff --git a/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.h b/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.h index 77152f4a5773de..7ac9b660b5e69d 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.h +++ b/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.h @@ -9,6 +9,7 @@ #import #import +#import // [macOS] RCT_EXTERN NSString *const EXTRAPOLATE_TYPE_IDENTITY; RCT_EXTERN NSString *const EXTRAPOLATE_TYPE_CLAMP; @@ -33,7 +34,7 @@ RCT_EXTERN CGFloat RCTInterpolateValueInRange( NSString *extrapolateRight); RCT_EXTERN uint32_t -RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange); +RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange); // [macOS] // Represents a color as a int32_t. RGB components are assumed to be in [0-255] range and alpha in [0-1] range RCT_EXTERN uint32_t RCTColorFromComponents(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha); diff --git a/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.mm b/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.mm index 90cfe304d6f68a..94662918ded67c 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.mm @@ -84,7 +84,7 @@ CGFloat RCTInterpolateValueInRange( return RCTInterpolateValue(value, inputMin, inputMax, outputMin, outputMax, extrapolateLeft, extrapolateRight); } -uint32_t RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange) +uint32_t RCTInterpolateColorInRange(CGFloat value, NSArray *inputRange, NSArray *outputRange) // [macOS] { NSUInteger rangeIndex = RCTFindIndexOfNearestValue(value, inputRange); CGFloat inputMin = inputRange[rangeIndex].doubleValue; diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm index b444ecca8e8040..15708742205e87 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm @@ -350,12 +350,12 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)uiManager _operations = [NSMutableArray new]; [uiManager - prependUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { + prependUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { // [macOS] for (AnimatedOperation operation in preOperations) { operation(self->_nodesManager); } }]; - [uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { + [uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { // [macOS] for (AnimatedOperation operation in operations) { operation(self->_nodesManager); } diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index 932c873eb6cf13..703778cf77325f 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h @@ -8,6 +8,7 @@ #import #import #import +#import // [macOS] #import #import @@ -22,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateAnimations; -- (void)stepAnimations:(CADisplayLink *)displaylink; +- (void)stepAnimations:(RCTPlatformDisplayLink *)displaylink; // [macOS] - (BOOL)isNodeManagedByFabric:(NSNumber *)tag; diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm index b3669766ea0746..7d15a32fd79334 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm @@ -53,7 +53,7 @@ @implementation RCTNativeAnimatedNodesManager { // there will be only one driver per mapping so all code code should be optimized around that. NSMutableDictionary *> *_eventDrivers; NSMutableSet> *_activeAnimations; - CADisplayLink *_displayLink; + RCTPlatformDisplayLink *_displayLink; // [macOS] } - (instancetype)initWithBridge:(nullable RCTBridge *)bridge @@ -431,7 +431,7 @@ - (void)stopListeningToAnimatedNodeValue:(NSNumber *)tag - (void)startAnimationLoopIfNeeded { if (!_displayLink && _activeAnimations.count > 0) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(stepAnimations:)]; + _displayLink = [RCTPlatformDisplayLink displayLinkWithTarget:self selector:@selector(stepAnimations:)]; // [macOS] [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } } @@ -451,7 +451,7 @@ - (void)stopAnimationLoop } } -- (void)stepAnimations:(CADisplayLink *)displaylink +- (void)stepAnimations:(RCTPlatformDisplayLink *)displaylink // [macOS] { NSTimeInterval time = displaylink.timestamp; for (id animationDriver in _activeAnimations) { diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js new file mode 100644 index 00000000000000..3d2b949e0c8607 --- /dev/null +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +import type {PartialViewConfigWithoutName} from './PlatformBaseViewConfig'; + +/* $FlowFixMe allow macOS to share iOS file */ +import PlatformBaseViewConfigIos from './BaseViewConfig.ios'; +import {ConditionallyIgnoredEventHandlers} from './ViewConfigIgnore'; + +const bubblingEventTypes = { + ...PlatformBaseViewConfigIos.bubblingEventTypes, +}; + +const directEventTypes = { + ...PlatformBaseViewConfigIos.directEventTypes, + topDragEnter: { + registrationName: 'onDragEnter', + }, + topDragLeave: { + registrationName: 'onDragLeave', + }, + topDrop: { + registrationName: 'onDrop', + }, + topKeyUp: { + registrationName: 'onKeyUp', + }, + topKeyDown: { + registrationName: 'onKeyDown', + }, + topMouseEnter: { + registrationName: 'onMouseEnter', + }, + topMouseLeave: { + registrationName: 'onMouseLeave', + }, +}; + +const validAttributesForNonEventProps = { + acceptsFirstMouse: true, + accessibilityTraits: true, + allowsVibrancy: true, + cursor: true, + draggedTypes: true, + enableFocusRing: true, + tooltip: true, + passthroughAllKeyEvents: true, + validKeysDown: true, + validKeysUp: true, + mouseDownCanMoveWindow: true, +}; + +// Props for bubbling and direct events +const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ + onBlur: true, + onDragEnter: true, + onDragLeave: true, + onDrop: true, + onFocus: true, + onKeyDown: true, + onKeyUp: true, + onMouseEnter: true, + onMouseLeave: true, +}); + +/** + * On macOS, view managers define all of a component's props. + * All view managers extend RCTViewManager, and RCTViewManager declares these props. + */ +const PlatformBaseViewConfigMacOS: PartialViewConfigWithoutName = { + bubblingEventTypes, + directEventTypes, + validAttributes: { + ...PlatformBaseViewConfigIos.validAttributes, + ...validAttributesForNonEventProps, + // $FlowFixMe[exponential-spread] + ...validAttributesForEventProps, + }, +}; + +export default PlatformBaseViewConfigMacOS; diff --git a/packages/react-native/Libraries/NativeComponent/ViewConfigIgnore.js b/packages/react-native/Libraries/NativeComponent/ViewConfigIgnore.js index 99a067ec35d8e2..589574f05195a0 100644 --- a/packages/react-native/Libraries/NativeComponent/ViewConfigIgnore.js +++ b/packages/react-native/Libraries/NativeComponent/ViewConfigIgnore.js @@ -37,7 +37,8 @@ export function DynamicallyInjectedByGestureHandler(object: T): T { export function ConditionallyIgnoredEventHandlers( value: T, ): T | void { - if (Platform.OS === 'ios') { + // [macOS] + if (Platform.OS === 'ios' || Platform.OS === 'macos') { return value; } return undefined; diff --git a/packages/react-native/Libraries/NativeModules/specs/NativeDevSettings.js b/packages/react-native/Libraries/NativeModules/specs/NativeDevSettings.js index 142836e57c2028..7a717b87a7f22e 100644 --- a/packages/react-native/Libraries/NativeModules/specs/NativeDevSettings.js +++ b/packages/react-native/Libraries/NativeModules/specs/NativeDevSettings.js @@ -28,6 +28,9 @@ export interface Spec extends TurboModule { // iOS only. +setIsShakeToShowDevMenuEnabled: (enabled: boolean) => void; + + // macOS only. + +setIsSecondaryClickToShowDevMenuEnabled: (enabled: boolean) => void; // [macOS] } export default (TurboModuleRegistry.getEnforcing('DevSettings'): Spec); diff --git a/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm b/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm index 19d025c51e19df..5838e167f96d55 100644 --- a/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm +++ b/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm @@ -7,7 +7,11 @@ #import +#if !TARGET_OS_OSX // [macOS] #import +#else // [macOS +#import +#endif // macOS] #import #import diff --git a/packages/react-native/Libraries/Network/RCTNetworking.macos.js b/packages/react-native/Libraries/Network/RCTNetworking.macos.js new file mode 100644 index 00000000000000..a05a885a336bc4 --- /dev/null +++ b/packages/react-native/Libraries/Network/RCTNetworking.macos.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const RCTNetworking = require('./RCTNetworking.ios'); +module.exports = RCTNetworking; diff --git a/packages/react-native/Libraries/NewAppScreen/components/DebugInstructions.js b/packages/react-native/Libraries/NewAppScreen/components/DebugInstructions.js index 9963fe6908188f..79f6fd0dc54246 100644 --- a/packages/react-native/Libraries/NewAppScreen/components/DebugInstructions.js +++ b/packages/react-native/Libraries/NewAppScreen/components/DebugInstructions.js @@ -29,6 +29,13 @@ const DebugInstructions: () => Node = Platform.select({ Menu. ), + // [macOS + macos: () => ( + + Secondary click in this window to open the React Native debug menu. + + ), + // macOS] default: () => ( Press Cmd or Ctrl + M or{' '} diff --git a/packages/react-native/Libraries/NewAppScreen/components/ReloadInstructions.js b/packages/react-native/Libraries/NewAppScreen/components/ReloadInstructions.js index 6c1eba2fdfffff..e23a22764438c1 100644 --- a/packages/react-native/Libraries/NewAppScreen/components/ReloadInstructions.js +++ b/packages/react-native/Libraries/NewAppScreen/components/ReloadInstructions.js @@ -28,6 +28,14 @@ const ReloadInstructions: () => Node = Platform.select({ reload your app's code. ), + // [macOS + macos: () => ( + + Secondary click in this window and choose{' '} + Reload to reload your app's code. + + ), + // macOS] default: () => ( Double tap R on your keyboard to diff --git a/packages/react-native/Libraries/Pressability/HoverState.js b/packages/react-native/Libraries/Pressability/HoverState.js index ebbe9921dbac3c..8e5d6aec69e631 100644 --- a/packages/react-native/Libraries/Pressability/HoverState.js +++ b/packages/react-native/Libraries/Pressability/HoverState.js @@ -49,6 +49,10 @@ if (Platform.OS === 'web') { document.addEventListener('touchmove', disableHover, true); document.addEventListener('mousemove', enableHover, true); } + // [macOS +} else if (Platform.OS === 'macos') { + isEnabled = true; + // macOS] } export function isHoverEnabled(): boolean { diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index 24f664c27da41f..92c638cdc231a7 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -12,8 +12,10 @@ import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; import type { BlurEvent, FocusEvent, + KeyEvent, MouseEvent, PressEvent, + // [macOS] } from '../Types/CoreEventTypes'; import SoundManager from '../Components/Sound/SoundManager'; @@ -89,12 +91,34 @@ export type PressabilityConfig = $ReadOnly<{| /** * Called after the element loses focus. */ - onBlur?: ?(event: BlurEvent) => mixed, + onBlur?: ?(event: BlurEvent) => void, /** * Called after the element is focused. */ - onFocus?: ?(event: FocusEvent) => mixed, + onFocus?: ?(event: FocusEvent) => void, + + /* + * Called after a key down event is detected. + */ + onKeyDown?: ?(event: KeyEvent) => void, + + /* + * Called after a key up event is detected. + */ + onKeyUp?: ?(event: KeyEvent) => void, + + /* + * Array of keys to receive key down events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + */ + validKeysUp?: ?Array, /** * Called when the hover is activated to provide visual feedback. @@ -169,6 +193,8 @@ export type EventHandlers = $ReadOnly<{| onBlur: (event: BlurEvent) => void, onClick: (event: PressEvent) => void, onFocus: (event: FocusEvent) => void, + onKeyDown: (event: KeyEvent) => void, + onKeyUp: (event: KeyEvent) => void, onMouseEnter?: (event: MouseEvent) => void, onMouseLeave?: (event: MouseEvent) => void, onPointerEnter?: (event: PointerEvent) => void, @@ -586,6 +612,46 @@ export default class Pressability { () => this._config; } + // [macOS + const keyboardEventHandlers = { + onKeyDown: (event: KeyEvent): void => { + const {onKeyDown} = this._config; + if (onKeyDown != null) { + onKeyDown(event); + } + // Pressables on macOS should respond to the enter/return and spacebar keys. + // The keyDown event triggers a press event as well as the pressIn effect mimicking a native control behavior. + if ( + (event.nativeEvent.key === 'Enter' || + event.nativeEvent.key === ' ') && + event.defaultPrevented !== true + ) { + const {onPress, onPressIn} = this._config; + // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead + onPressIn && onPressIn(event); + // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead + onPress && onPress(event); + } + }, + onKeyUp: (event: KeyEvent): void => { + const {onKeyUp} = this._config; + if (onKeyUp != null) { + onKeyUp(event); + } + // The keyUp event triggers the pressOut effect. + if ( + (event.nativeEvent.key === 'Enter' || + event.nativeEvent.key === ' ') && + event.defaultPrevented !== true + ) { + const {onPressOut} = this._config; + // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead + onPressOut && onPressOut(event); + } + }, + }; + // macOS] + if ( ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover() ) { @@ -634,6 +700,7 @@ export default class Pressability { ...focusEventHandlers, ...responderEventHandlers, ...hoverPointerEvents, + ...keyboardEventHandlers, // [macOS] }; } else { const mouseEventHandlers = @@ -686,6 +753,7 @@ export default class Pressability { ...focusEventHandlers, ...responderEventHandlers, ...mouseEventHandlers, + ...keyboardEventHandlers, // [macOS] }; } } diff --git a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js index 5422d02e32e2f0..91c7e02c3e4c2e 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -43,7 +43,9 @@ const PushNotificationEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativePushNotificationManagerIOS, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativePushNotificationManagerIOS, ); const _notifHandlers = new Map(); @@ -472,7 +474,8 @@ class PushNotificationIOS { if ( !this._isRemote || !this._notificationId || - this._remoteNotificationCompleteCallbackCalled + this._remoteNotificationCompleteCallbackCalled || + Platform.OS === 'macos' // [macOS] ) { return; } diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index 778db415b5875c..fc912b4bef5cb8 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -11,15 +11,24 @@ extern NSString *const RCTRemoteNotificationReceived; @interface RCTPushNotificationManager : RCTEventEmitter +#if !TARGET_OS_OSX // [macOS] typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result); +#endif // [macOS] #if !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_OSX // [macOS] + (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; +#endif // [macOS] + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)didReceiveRemoteNotification:(NSDictionary *)notification; +#if !TARGET_OS_OSX // [macOS] + (void)didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(RCTRemoteNotificationCallback)completionHandler; + (void)didReceiveLocalNotification:(UILocalNotification *)notification; +#endif // [macOS] +#if TARGET_OS_OSX // [macOS ++ (void)didReceiveUserNotification:(NSUserNotification *)notification; +#endif // macOS] + (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; #endif diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 0d586d8e65dd99..7d575e2fe01de7 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -48,6 +48,7 @@ @interface RCTPushNotificationManager () @implementation RCTConvert (UILocalNotification) +#if !TARGET_OS_OSX // [macOS] + (UILocalNotification *)UILocalNotification:(id)json { NSDictionary *details = [self NSDictionary:json]; @@ -68,7 +69,44 @@ + (UILocalNotification *)UILocalNotification:(id)json } return notification; } +#else // [macOS ++ (NSUserNotification *)NSUserNotification:(id)json +{ + NSDictionary *details = [self NSDictionary:json]; + BOOL isSilent = [RCTConvert BOOL:details[@"isSilent"]]; + NSUserNotification *notification = [NSUserNotification new]; + notification.deliveryDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; + notification.informativeText = [RCTConvert NSString:details[@"alertBody"]]; + NSString *title = [RCTConvert NSString:details[@"alertTitle"]]; + if (title) { + notification.title = title; + } + NSString *actionButtonTitle = [RCTConvert NSString:details[@"alertAction"]]; + if (actionButtonTitle) { + notification.actionButtonTitle = actionButtonTitle; + } + notification.userInfo = [RCTConvert NSDictionary:details[@"userInfo"]]; + + NSCalendarUnit calendarUnit = [RCTConvert NSCalendarUnit:details[@"repeatInterval"]]; + if (calendarUnit > 0) { + NSDateComponents *dateComponents = [NSDateComponents new]; + [dateComponents setValue:1 forComponent:calendarUnit]; + notification.deliveryRepeatInterval = dateComponents; + } + if (!isSilent) { + notification.soundName = [RCTConvert NSString:details[@"soundName"]] ?: NSUserNotificationDefaultSoundName; + } + + NSString *identifier = [RCTConvert NSString:details[@"identifier"]]; + if (identifier == nil) { + identifier = [[NSUUID UUID] UUIDString]; + } + notification.identifier = identifier; + return notification; +} +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] RCT_ENUM_CONVERTER( UIBackgroundFetchResult, (@{ @@ -78,6 +116,7 @@ + (UILocalNotification *)UILocalNotification:(id)json }), UIBackgroundFetchResultNoData, integerValue) +#endif // [macOS] @end #else @@ -87,7 +126,7 @@ @interface RCTPushNotificationManager () @implementation RCTPushNotificationManager -#if !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_UIKITFORMAC && !TARGET_OS_OSX // [macOS] static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification) { @@ -132,6 +171,26 @@ @implementation RCTPushNotificationManager } #endif // TARGET_OS_UIKITFORMAC +#if TARGET_OS_OSX // [macOS + +static NSDictionary *RCTFormatUserNotification(NSUserNotification *notification) +{ + NSMutableDictionary *formattedUserNotification = [NSMutableDictionary dictionary]; + if (notification.deliveryDate) { + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notification.deliveryDate]; + formattedUserNotification[@"fireDate"] = fireDateString; + } + formattedUserNotification[@"alertAction"] = RCTNullIfNil(notification.actionButtonTitle); + formattedUserNotification[@"alertBody"] = RCTNullIfNil(notification.informativeText); + formattedUserNotification[@"soundName"] = RCTNullIfNil(notification.soundName); + formattedUserNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(notification.userInfo)); + formattedUserNotification[@"remote"] = @(notification.isRemote); + formattedUserNotification[@"identifier"] = notification.identifier; + return formattedUserNotification; +} +#endif // macOS] RCT_EXPORT_MODULE() @@ -176,9 +235,11 @@ - (void)stopObserving ]; } +#if !TARGET_OS_OSX // [macOS] + (void)didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings *)notificationSettings { } +#endif // [macOS] + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { @@ -208,6 +269,7 @@ + (void)didReceiveRemoteNotification:(NSDictionary *)notification userInfo:userInfo]; } +#if !TARGET_OS_OSX // [macOS] + (void)didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(RCTRemoteNotificationCallback)completionHandler { @@ -224,6 +286,19 @@ + (void)didReceiveLocalNotification:(UILocalNotification *)notification userInfo:RCTFormatLocalNotification(notification)]; } +#else // [macOS + ++ (void)didReceiveUserNotification:(NSUserNotification *)notification +{ + NSString *notificationName = notification.isRemote ? RCTRemoteNotificationReceived : kLocalNotificationReceived; + NSDictionary *userInfo = notification.isRemote ? @{@"notification": notification.userInfo} : RCTFormatUserNotification(notification); + [[NSNotificationCenter defaultCenter] postNotificationName:notificationName + object:self + userInfo:userInfo]; +} + +#endif // macOS] + - (void)handleLocalNotificationReceived:(NSNotification *)notification { [self sendEventWithName:@"localNotificationReceived" body:notification.userInfo]; @@ -233,10 +308,13 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification { NSMutableDictionary *remoteNotification = [NSMutableDictionary dictionaryWithDictionary:notification.userInfo[@"notification"]]; +#if !TARGET_OS_OSX // [macOS] RCTRemoteNotificationCallback completionHandler = notification.userInfo[@"completionHandler"]; +#endif // [macOS] NSString *notificationId = [[NSUUID UUID] UUIDString]; remoteNotification[@"notificationId"] = notificationId; remoteNotification[@"remote"] = @YES; +#if !TARGET_OS_OSX // [macOS] if (completionHandler) { if (!self.remoteNotificationCallbacks) { // Lazy initialization @@ -244,6 +322,7 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification } self.remoteNotificationCallbacks[notificationId] = completionHandler; } +#endif // [macOS] [self sendEventWithName:@"remoteNotificationReceived" body:remoteNotification]; } @@ -264,6 +343,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification [self sendEventWithName:@"remoteNotificationRegistrationError" body:errorDetails]; } +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(onFinishRemoteNotification : (NSString *)notificationId fetchResult : (NSString *)fetchResult) { UIBackgroundFetchResult result = [RCTConvert UIBackgroundFetchResult:fetchResult]; @@ -275,13 +355,20 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification completionHandler(result); [self.remoteNotificationCallbacks removeObjectForKey:notificationId]; } +#endif // [macOS] /** * Update the application icon badge number on the home screen */ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber : (double)number) { +#if !TARGET_OS_OSX // [macOS] RCTSharedApplication().applicationIconBadgeNumber = number; +#else // [macOS + NSDockTile *tile = [NSApp dockTile]; + tile.showsApplicationBadge = number > 0; + tile.badgeLabel = number > 0 ? [NSString stringWithFormat:@"%.0lf", number] : nil; +#endif // macOS] } /** @@ -289,7 +376,11 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification */ RCT_EXPORT_METHOD(getApplicationIconBadgeNumber : (RCTResponseSenderBlock)callback) { +#if !TARGET_OS_OSX // [macOS] callback(@[ @(RCTSharedApplication().applicationIconBadgeNumber) ]); +#else // [macOS + callback(@[ @([NSApp dockTile].badgeLabel.integerValue) ]); +#endif // macOS] } RCT_EXPORT_METHOD(requestPermissions @@ -297,6 +388,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { reject( kErrorUnableToRequestPermissions, @@ -304,10 +396,12 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCTErrorWithMessage(@"Requesting push notifications is currently unavailable in an app extension")); return; } +#endif // [macOS] // Add a listener to make sure that startObserving has been called [self addListener:@"remoteNotificationsRegistered"]; +#if !TARGET_OS_OSX // [macOS UIUserNotificationType types = UIUserNotificationTypeNone; if (permissions.alert()) { @@ -319,7 +413,19 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification if (permissions.sound()) { types |= UIUserNotificationTypeSound; } +#else + NSRemoteNotificationType types = NSRemoteNotificationTypeNone; + if (permissions.alert()) { + types |= NSRemoteNotificationTypeAlert; + } + if (permissions.badge()) { + types |= NSRemoteNotificationTypeBadge; + } + if (permissions.sound()) { + types |= NSRemoteNotificationTypeSound; + } +#endif // macOS] [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:types completionHandler:^(BOOL granted, NSError *_Nullable error) { @@ -345,10 +451,12 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification RCT_EXPORT_METHOD(checkPermissions : (RCTResponseSenderBlock)callback) { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { callback(@[ RCTSettingsDictForUNNotificationSettings(NO, NO, NO, NO, NO, NO, UNAuthorizationStatusNotDetermined) ]); return; } +#endif // [macOS] [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *_Nonnull settings) { @@ -388,6 +496,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification }; } +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(presentLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSMutableDictionary *notificationDict = [NSMutableDictionary new]; @@ -411,7 +520,14 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } [RCTSharedApplication() presentLocalNotificationNow:[RCTConvert UILocalNotification:notificationDict]]; } +#else // [macOS +RCT_EXPORT_METHOD(presentLocalNotification:(NSUserNotification *)notification) +{ + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; +} +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_METHOD(scheduleLocalNotification : (JS::NativePushNotificationManagerIOS::Notification &)notification) { NSMutableDictionary *notificationDict = [NSMutableDictionary new]; @@ -435,15 +551,31 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } [RCTSharedApplication() scheduleLocalNotification:[RCTConvert UILocalNotification:notificationDict]]; } +#else // [macOS +RCT_EXPORT_METHOD(scheduleLocalNotification:(NSUserNotification *)notification) +{ + [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; +} +#endif // macOS] RCT_EXPORT_METHOD(cancelAllLocalNotifications) { +#if !TARGET_OS_OSX // [macOS] [RCTSharedApplication() cancelAllLocalNotifications]; +#else // [macOS + for (NSUserNotification *notif in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { + [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notif]; + } +#endif // macOS] } RCT_EXPORT_METHOD(cancelLocalNotifications : (NSDictionary *)userInfo) { +#if !TARGET_OS_OSX // [macOS] for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { +#else // [macOS + for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { +#endif // macOS] __block BOOL matchesAll = YES; NSDictionary *notificationInfo = notification.userInfo; // Note: we do this with a loop instead of just `isEqualToDictionary:` @@ -456,9 +588,15 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification *stop = YES; } }]; +#if !TARGET_OS_OSX // [macOS] if (matchesAll) { [RCTSharedApplication() cancelLocalNotification:notification]; } +#else // [macOS + if ([notification.identifier isEqualToString:userInfo[@"identifier"]] || matchesAll) { + [[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification:notification]; + } +#endif // macOS] } } @@ -466,6 +604,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject) { +#if !TARGET_OS_OSX // [macOS] NSMutableDictionary *initialNotification = [self.bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] mutableCopy]; @@ -480,32 +619,62 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } else { resolve((id)kCFNull); } +#else // [macOS + NSUserNotification *initialNotification = self.bridge.launchOptions[NSApplicationLaunchUserNotificationKey]; + if (initialNotification) { + resolve(RCTFormatUserNotification(initialNotification)); + } else { + resolve((id)kCFNull); + } +#endif // macOS] } RCT_EXPORT_METHOD(getScheduledLocalNotifications : (RCTResponseSenderBlock)callback) { +#if !TARGET_OS_OSX // [macOS] NSArray *scheduledLocalNotifications = RCTSharedApplication().scheduledLocalNotifications; +#endif // [macOS] NSMutableArray *formattedScheduledLocalNotifications = [NSMutableArray new]; +#if !TARGET_OS_OSX // [macOS] for (UILocalNotification *notification in scheduledLocalNotifications) { [formattedScheduledLocalNotifications addObject:RCTFormatLocalNotification(notification)]; } +#else // [macOS + for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications) { + [formattedScheduledLocalNotifications addObject:RCTFormatUserNotification(notification)]; + } +#endif // macOS] callback(@[ formattedScheduledLocalNotifications ]); } RCT_EXPORT_METHOD(removeAllDeliveredNotifications) { +#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeAllDeliveredNotifications]; +#else // [macOS + [[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications]; +#endif // macOS] } RCT_EXPORT_METHOD(removeDeliveredNotifications : (NSArray *)identifiers) { +#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center removeDeliveredNotificationsWithIdentifiers:identifiers]; +#else // [macOS + NSArray *notificationsToRemove = [[NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSUserNotification* evaluatedObject, NSDictionary * _Nullable bindings) { + return [identifiers containsObject:evaluatedObject.identifier]; + }]]; + for (NSUserNotification *notification in notificationsToRemove) { + [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:notification]; + } +#endif // macOS] } RCT_EXPORT_METHOD(getDeliveredNotifications : (RCTResponseSenderBlock)callback) { +#if !TARGET_OS_OSX // [macOS] UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray *_Nonnull notifications) { NSMutableArray *formattedNotifications = [NSMutableArray new]; @@ -515,6 +684,13 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification } callback(@[ formattedNotifications ]); }]; +#else // [macOS + NSMutableArray *formattedNotifications = [NSMutableArray new]; + for (NSUserNotification *notification in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications) { + [formattedNotifications addObject:RCTFormatUserNotification(notification)]; + } + callback(@[formattedNotifications]); +#endif // macOS] } RCT_EXPORT_METHOD(getAuthorizationStatus : (RCTResponseSenderBlock)callback) diff --git a/packages/react-native/Libraries/ReactNative/PaperUIManager.js b/packages/react-native/Libraries/ReactNative/PaperUIManager.js index 2b1efffb456fb0..2f1e9b809826a8 100644 --- a/packages/react-native/Libraries/ReactNative/PaperUIManager.js +++ b/packages/react-native/Libraries/ReactNative/PaperUIManager.js @@ -155,7 +155,7 @@ function lazifyViewManagerConfig(viewName: string) { * only needed for iOS, which puts the constants in the ViewManager * namespace instead of UIManager, unlike Android. */ -if (Platform.OS === 'ios') { +if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { Object.keys(getConstants()).forEach(viewName => { lazifyViewManagerConfig(viewName); }); diff --git a/packages/react-native/Libraries/Settings/RCTSettingsManager.mm b/packages/react-native/Libraries/Settings/RCTSettingsManager.mm index e0f37c9a6de341..bb477520f1ed77 100644 --- a/packages/react-native/Libraries/Settings/RCTSettingsManager.mm +++ b/packages/react-native/Libraries/Settings/RCTSettingsManager.mm @@ -21,6 +21,10 @@ @interface RCTSettingsManager () @implementation RCTSettingsManager { BOOL _ignoringUpdates; NSUserDefaults *_defaults; + +#if TARGET_OS_OSX // [macOS + BOOL _isListeningForUpdates; +#endif // macOS] } @synthesize moduleRegistry = _moduleRegistry; @@ -42,10 +46,12 @@ - (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults if ((self = [super init])) { _defaults = defaults; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:_defaults]; +#endif // [macOS] } return self; } @@ -109,6 +115,31 @@ - (void)userDefaultsDidChange:(NSNotification *)note _ignoringUpdates = NO; } +#if TARGET_OS_OSX // [macOS +/** + * Enable or disable monitoring of changes to NSUserDefaults + */ +RCT_EXPORT_METHOD(setIsMonitoringEnabled:(BOOL)isEnabled) +{ + if (isEnabled) { + if (!_isListeningForUpdates) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(userDefaultsDidChange:) + name:NSUserDefaultsDidChangeNotification + object:_defaults]; + _isListeningForUpdates = YES; + } + } + else + { + if (_isListeningForUpdates) { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + _isListeningForUpdates = NO; + } + } +} +#endif // macOS] + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/packages/react-native/Libraries/Settings/Settings.macos.js b/packages/react-native/Libraries/Settings/Settings.macos.js new file mode 100644 index 00000000000000..3a97e107580975 --- /dev/null +++ b/packages/react-native/Libraries/Settings/Settings.macos.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const Settings = require('./Settings.ios'); +module.exports = Settings; diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js new file mode 100644 index 00000000000000..29ae835410baf6 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] +import type {ProcessedColorValue} from './processColor'; +import type {ColorValue, NativeColorValue} from './StyleSheet'; + +/** The actual type of the opaque NativeColorValue on macOS platform */ +type LocalNativeColorValue = { + semantic?: Array, + dynamic?: { + light: ?(ColorValue | ProcessedColorValue), + dark: ?(ColorValue | ProcessedColorValue), + highContrastLight?: ?(ColorValue | ProcessedColorValue), + highContrastDark?: ?(ColorValue | ProcessedColorValue), + }, + colorWithSystemEffect?: { + baseColor: ?(ColorValue | ProcessedColorValue), + systemEffect: SystemEffectMacOSPrivate, + }, +}; + +export const PlatformColor = (...names: Array): ColorValue => { + // $FlowExpectedError[incompatible-return] LocalNativeColorValue is the macOS LocalNativeColorValue type + return ({semantic: names}: LocalNativeColorValue); +}; + +export type SystemEffectMacOSPrivate = + | 'none' + | 'pressed' + | 'deepPressed' + | 'disabled' + | 'rollover'; + +export const ColorWithSystemEffectMacOSPrivate = ( + color: ColorValue, + effect: SystemEffectMacOSPrivate, +): ColorValue => { + return ({ + colorWithSystemEffect: { + baseColor: color, + systemEffect: effect, + }, + /* $FlowExpectedError[incompatible-return] + * LocalNativeColorValue is the actual type of the opaque NativeColorValue on macOS platform */ + }: LocalNativeColorValue); +}; + +export type DynamicColorMacOSTuplePrivate = { + light: ColorValue, + dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, +}; + +export const DynamicColorMacOSPrivate = ( + tuple: DynamicColorMacOSTuplePrivate, +): ColorValue => { + return ({ + dynamic: { + light: tuple.light, + dark: tuple.dark, + highContrastLight: tuple.highContrastLight, + highContrastDark: tuple.highContrastDark, + }, + /* $FlowExpectedError[incompatible-return] + * LocalNativeColorValue is the actual type of the opaque NativeColorValue on macOS platform */ + }: LocalNativeColorValue); +}; + +const _normalizeColorObject = ( + color: LocalNativeColorValue, +): ?LocalNativeColorValue => { + if ('semantic' in color) { + // a macOS semantic color + return color; + } else if ('dynamic' in color && color.dynamic !== undefined) { + const normalizeColor = require('./normalizeColor'); + + // a dynamic, appearance aware color + const dynamic = color.dynamic; + const dynamicColor: LocalNativeColorValue = { + dynamic: { + // $FlowFixMe[incompatible-use] + light: normalizeColor(dynamic.light), + // $FlowFixMe[incompatible-use] + dark: normalizeColor(dynamic.dark), + // $FlowFixMe[incompatible-use] + highContrastLight: normalizeColor(dynamic.highContrastLight), + // $FlowFixMe[incompatible-use] + highContrastDark: normalizeColor(dynamic.highContrastDark), + }, + }; + return dynamicColor; + } else if ( + 'colorWithSystemEffect' in color && + color.colorWithSystemEffect != null + ) { + const normalizeColor = require('./normalizeColor'); + const colorWithSystemEffect = color.colorWithSystemEffect; + const colorObject: LocalNativeColorValue = { + colorWithSystemEffect: { + // $FlowFixMe[incompatible-use] + baseColor: normalizeColor(colorWithSystemEffect.baseColor), + // $FlowFixMe[incompatible-use] + systemEffect: colorWithSystemEffect.systemEffect, + }, + }; + return colorObject; + } + return null; +}; + +export const normalizeColorObject: ( + color: NativeColorValue, + /* $FlowExpectedError[incompatible-type] + * LocalNativeColorValue is the actual type of the opaque NativeColorValue on macOS platform */ +) => ?ProcessedColorValue = _normalizeColorObject; + +const _processColorObject = ( + color: LocalNativeColorValue, +): ?LocalNativeColorValue => { + if ('dynamic' in color && color.dynamic != null) { + const processColor = require('./processColor').default; + const dynamic = color.dynamic; + const dynamicColor: LocalNativeColorValue = { + dynamic: { + // $FlowFixMe[incompatible-use] + light: processColor(dynamic.light), + // $FlowFixMe[incompatible-use] + dark: processColor(dynamic.dark), + // $FlowFixMe[incompatible-use] + highContrastLight: processColor(dynamic.highContrastLight), + // $FlowFixMe[incompatible-use] + highContrastDark: processColor(dynamic.highContrastDark), + }, + }; + return dynamicColor; + } else if ( + 'colorWithSystemEffect' in color && + color.colorWithSystemEffect != null + ) { + const processColor = require('./processColor').default; + const colorWithSystemEffect = color.colorWithSystemEffect; + const colorObject: LocalNativeColorValue = { + colorWithSystemEffect: { + // $FlowFixMe[incompatible-use] + baseColor: processColor(colorWithSystemEffect.baseColor), + // $FlowFixMe[incompatible-use] + systemEffect: colorWithSystemEffect.systemEffect, + }, + }; + return colorObject; + } + return color; +}; + +export const processColorObject: ( + color: NativeColorValue, + /* $FlowExpectedError[incompatible-type] + * LocalNativeColorValue is the actual type of the opaque NativeColorValue on iOS platform */ +) => ?NativeColorValue = _processColorObject; diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js new file mode 100644 index 00000000000000..9b9506e8a45f2f --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +'use strict'; + +import type {ColorValue} from './StyleSheet'; + +export type DynamicColorMacOSTuple = { + light: ColorValue, + dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, +}; + +export const DynamicColorMacOS = ( + tuple: DynamicColorMacOSTuple, +): ColorValue => { + throw new Error('DynamicColorMacOS is not available on this platform.'); +}; + +export type SystemEffectMacOS = + | 'none' + | 'pressed' + | 'deepPressed' + | 'disabled' + | 'rollover'; + +export const ColorWithSystemEffectMacOS = ( + color: ColorValue, + effect: SystemEffectMacOS, +): ColorValue => { + throw new Error( + 'ColorWithSystemEffectMacOS is not available on this platform.', + ); +}; diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js new file mode 100644 index 00000000000000..d0890a335d4903 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +'use strict'; + +import type {ColorValue} from './StyleSheet'; + +import { + ColorWithSystemEffectMacOSPrivate, + DynamicColorMacOSPrivate, +} from './PlatformColorValueTypes.macos'; + +export type DynamicColorMacOSTuple = { + light: ColorValue, + dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, +}; + +export const DynamicColorMacOS = ( + tuple: DynamicColorMacOSTuple, +): ColorValue => { + return DynamicColorMacOSPrivate({ + light: tuple.light, + dark: tuple.dark, + highContrastLight: tuple.highContrastLight, + highContrastDark: tuple.highContrastDark, + }); +}; + +export type SystemEffectMacOS = + | 'none' + | 'pressed' + | 'deepPressed' + | 'disabled' + | 'rollover'; + +export const ColorWithSystemEffectMacOS = ( + color: ColorValue, + effect: SystemEffectMacOS, +): ColorValue => { + return ColorWithSystemEffectMacOSPrivate(color, effect); +}; diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index fb2db1c4c60e31..27d3f868257431 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -20,6 +20,31 @@ import type { } from './private/_StyleSheetTypesOverrides'; import type {____TransformStyle_Internal} from './private/_TransformStyle'; +// [macOS +export type CursorValue = ?( + | 'alias' + | 'auto' + | 'col-resize' + | 'context-menu' + | 'copy' + | 'crosshair' + | 'default' + | 'disappearing-item' + | 'e-resize' + | 'grab' + | 'grabbing' + | 'n-resize' + | 'no-drop' + | 'not-allowed' + | 'pointer' + | 'row-resize' + | 's-resize' + | 'text' + | 'vertical-text' + | 'w-resize' +); +// macOS] + declare export opaque type NativeColorValue; export type ____ColorValue_Internal = null | string | number | NativeColorValue; export type ColorArrayValue = null | $ReadOnlyArray<____ColorValue_Internal>; @@ -727,6 +752,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ opacity?: AnimatableNumericValue, elevation?: number, pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only', + cursor?: CursorValue, // [macOS] }>; export type ____ViewStyle_Internal = $ReadOnly<{ @@ -849,6 +875,7 @@ export type ____TextStyle_InternalCore = $ReadOnly<{ userSelect?: 'auto' | 'text' | 'none' | 'contain' | 'all', verticalAlign?: 'auto' | 'top' | 'bottom' | 'middle', writingDirection?: 'auto' | 'ltr' | 'rtl', + cursor?: CursorValue, // [macOS] }>; export type ____TextStyle_Internal = $ReadOnly<{ diff --git a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm index ece68768c40251..427a4254e77d0a 100644 --- a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm +++ b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm @@ -6,12 +6,15 @@ */ #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] @implementation RCTBaseTextViewManager RCT_EXPORT_MODULE(RCTBaseText) -- (UIView *)view +- (RCTUIView *)view // [macOS] { RCTAssert(NO, @"The `-[RCTBaseTextViewManager view]` property must be overridden in subclass."); return nil; @@ -56,4 +59,8 @@ - (RCTShadowView *)shadowView RCT_REMAP_SHADOW_PROPERTY(isHighlighted, textAttributes.isHighlighted, BOOL) RCT_REMAP_SHADOW_PROPERTY(textTransform, textAttributes.textTransform, RCTTextTransform) +#if TARGET_OS_OSX // [macOS +RCT_REMAP_SHADOW_PROPERTY(cursor, textAttributes.cursor, RCTCursor) +#endif // macOS] + @end diff --git a/packages/react-native/Libraries/Text/RCTConvert+Text.h b/packages/react-native/Libraries/Text/RCTConvert+Text.h index 4425cc2ccfa8e3..852f71abc89408 100644 --- a/packages/react-native/Libraries/Text/RCTConvert+Text.h +++ b/packages/react-native/Libraries/Text/RCTConvert+Text.h @@ -11,12 +11,28 @@ NS_ASSUME_NONNULL_BEGIN +#if TARGET_OS_OSX // [macOS +typedef enum UITextAutocorrectionType : NSInteger { + UITextAutocorrectionTypeDefault, + UITextAutocorrectionTypeNo, + UITextAutocorrectionTypeYes, +} UITextAutocorrectionType; + +typedef enum UITextSpellCheckingType : NSInteger { + UITextSpellCheckingTypeDefault, + UITextSpellCheckingTypeNo, + UITextSpellCheckingTypeYes, +} UITextSpellCheckingType; +#endif // macOS] + @interface RCTConvert (Text) + (UITextAutocorrectionType)UITextAutocorrectionType:(nullable id)json; + (UITextSpellCheckingType)UITextSpellCheckingType:(nullable id)json; + (RCTTextTransform)RCTTextTransform:(nullable id)json; +#if !TARGET_OS_OSX // [macOS] + (UITextSmartInsertDeleteType)UITextSmartInsertDeleteType:(nullable id)json; +#endif // [macOS] @end diff --git a/packages/react-native/Libraries/Text/RCTConvert+Text.mm b/packages/react-native/Libraries/Text/RCTConvert+Text.mm index 3ab3cc656d0b63..cec9a0fb1b7495 100644 --- a/packages/react-native/Libraries/Text/RCTConvert+Text.mm +++ b/packages/react-native/Libraries/Text/RCTConvert+Text.mm @@ -34,11 +34,13 @@ + (UITextSpellCheckingType)UITextSpellCheckingType:(id)json RCTTextTransformUndefined, integerValue) +#if !TARGET_OS_OSX // [macOS] + (UITextSmartInsertDeleteType)UITextSmartInsertDeleteType:(id)json { return json == nil ? UITextSmartInsertDeleteTypeDefault : [RCTConvert BOOL:json] ? UITextSmartInsertDeleteTypeYes : UITextSmartInsertDeleteTypeNo; } +#endif// macOS] @end diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.h b/packages/react-native/Libraries/Text/RCTTextAttributes.h index 22fb646d434940..518d0535e26f50 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -5,13 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import #import "RCTTextTransform.h" +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + NS_ASSUME_NONNULL_BEGIN extern NSString *const RCTTextAttributesIsHighlightedAttributeName; @@ -25,8 +29,8 @@ extern NSString *const RCTTextAttributesTagAttributeName; @interface RCTTextAttributes : NSObject // Color -@property (nonatomic, strong, nullable) UIColor *foregroundColor; -@property (nonatomic, strong, nullable) UIColor *backgroundColor; +@property (nonatomic, strong, nullable) RCTUIColor *foregroundColor; // [macOS] +@property (nonatomic, strong, nullable) RCTUIColor *backgroundColor; // [macOS] @property (nonatomic, assign) CGFloat opacity; // Font @property (nonatomic, copy, nullable) NSString *fontFamily; @@ -45,19 +49,23 @@ extern NSString *const RCTTextAttributesTagAttributeName; @property (nonatomic, assign) NSWritingDirection baseWritingDirection; @property (nonatomic, assign) NSLineBreakStrategy lineBreakStrategy; // Decoration -@property (nonatomic, strong, nullable) UIColor *textDecorationColor; +@property (nonatomic, strong, nullable) RCTUIColor *textDecorationColor; // [macOS] @property (nonatomic, assign) NSUnderlineStyle textDecorationStyle; @property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine; // Shadow @property (nonatomic, assign) CGSize textShadowOffset; @property (nonatomic, assign) CGFloat textShadowRadius; -@property (nonatomic, strong, nullable) UIColor *textShadowColor; +@property (nonatomic, strong, nullable) RCTUIColor *textShadowColor; // [macOS] // Special @property (nonatomic, assign) BOOL isHighlighted; @property (nonatomic, strong, nullable) NSNumber *tag; @property (nonatomic, assign) UIUserInterfaceLayoutDirection layoutDirection; @property (nonatomic, assign) RCTTextTransform textTransform; +#if TARGET_OS_OSX // [macOS +@property (nonatomic, assign) RCTCursor cursor; +#endif // macOS] + #pragma mark - Inheritance - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes; @@ -87,8 +95,8 @@ extern NSString *const RCTTextAttributesTagAttributeName; /** * Foreground and background colors with opacity and right defaults. */ -- (UIColor *)effectiveForegroundColor; -- (UIColor *)effectiveBackgroundColor; +- (RCTUIColor *)effectiveForegroundColor; // [macOS] +- (RCTUIColor *)effectiveBackgroundColor; // [macOS] /** * Text transformed per 'none', 'uppercase', 'lowercase', 'capitalize' diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.mm b/packages/react-native/Libraries/Text/RCTTextAttributes.mm index c8323388ce684b..9bfdb93cc9a82c 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -11,11 +11,26 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + NSString *const RCTTextAttributesIsHighlightedAttributeName = @"RCTTextAttributesIsHighlightedAttributeName"; NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttributeName"; @implementation RCTTextAttributes +// [macOS ++ (RCTUIColor *)defaultForegroundColor +{ + if (@available(iOS 13.0, *)) { + return [RCTUIColor labelColor]; + } else { + return [RCTUIColor blackColor]; + } +} +// macOS] + - (instancetype)init { if (self = [super init]) { @@ -31,6 +46,10 @@ - (instancetype)init _textShadowRadius = NAN; _opacity = NAN; _textTransform = RCTTextTransformUndefined; + _foregroundColor = [RCTTextAttributes defaultForegroundColor]; // [macOS] +#if TARGET_OS_OSX // [macOS + _cursor = RCTCursorAuto; +#endif // macOS] } return self; @@ -43,7 +62,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes // We will address this in the future. // Color - _foregroundColor = textAttributes->_foregroundColor ?: _foregroundColor; + _foregroundColor = textAttributes->_foregroundColor == [RCTTextAttributes defaultForegroundColor] ? _foregroundColor : textAttributes->_foregroundColor; _backgroundColor = textAttributes->_backgroundColor ?: _backgroundColor; _opacity = !isnan(textAttributes->_opacity) ? (isnan(_opacity) ? 1.0 : _opacity) * textAttributes->_opacity : _opacity; @@ -95,6 +114,9 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes : _layoutDirection; _textTransform = textAttributes->_textTransform != RCTTextTransformUndefined ? textAttributes->_textTransform : _textTransform; +#if TARGET_OS_OSX // [macOS + _cursor = textAttributes->_cursor != RCTCursorAuto ? textAttributes->_cursor : _cursor; +#endif // macOS] } - (NSParagraphStyle *)effectiveParagraphStyle @@ -122,7 +144,7 @@ - (NSParagraphStyle *)effectiveParagraphStyle } if (_lineBreakStrategy != NSLineBreakStrategyNone) { - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] paragraphStyle.lineBreakStrategy = _lineBreakStrategy; isParagraphStyleUsed = YES; } @@ -153,7 +175,7 @@ - (NSParagraphStyle *)effectiveParagraphStyle } // Colors - UIColor *effectiveForegroundColor = self.effectiveForegroundColor; + RCTUIColor *effectiveForegroundColor = self.effectiveForegroundColor; // [macOS] if (_foregroundColor || !isnan(_opacity)) { attributes[NSForegroundColorAttributeName] = effectiveForegroundColor; @@ -211,6 +233,12 @@ - (NSParagraphStyle *)effectiveParagraphStyle attributes[RCTTextAttributesTagAttributeName] = _tag; } +#if TARGET_OS_OSX // [macOS + if (_cursor != RCTCursorAuto) { + attributes[NSCursorAttributeName] = [RCTConvert NSCursor:_cursor]; + } +#endif // macOS] + return [attributes copy]; } @@ -232,12 +260,14 @@ - (CGFloat)effectiveFontSizeMultiplier if (fontScalingEnabled) { CGFloat fontSizeMultiplier = !isnan(_fontSizeMultiplier) ? _fontSizeMultiplier : 1.0; +#if !TARGET_OS_OSX // [macOS] if (_dynamicTypeRamp != RCTDynamicTypeRampUndefined) { UIFontMetrics *fontMetrics = RCTUIFontMetricsForDynamicTypeRamp(_dynamicTypeRamp); // Using a specific font size reduces rounding errors from -scaledValueForValue: CGFloat requestedSize = isnan(_fontSize) ? RCTBaseSizeForDynamicTypeRamp(_dynamicTypeRamp) : _fontSize; fontSizeMultiplier = [fontMetrics scaledValueForValue:requestedSize] / requestedSize; } +#endif // [macOS] CGFloat maxFontSizeMultiplier = !isnan(_maxFontSizeMultiplier) ? _maxFontSizeMultiplier : 0.0; return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier; } else { @@ -245,9 +275,9 @@ - (CGFloat)effectiveFontSizeMultiplier } } -- (UIColor *)effectiveForegroundColor +- (RCTUIColor *)effectiveForegroundColor // [macOS] { - UIColor *effectiveForegroundColor = _foregroundColor ?: [UIColor blackColor]; + RCTUIColor *effectiveForegroundColor = _foregroundColor ?: [RCTUIColor blackColor]; // [macOS] if (!isnan(_opacity)) { effectiveForegroundColor = @@ -257,16 +287,16 @@ - (UIColor *)effectiveForegroundColor return effectiveForegroundColor; } -- (UIColor *)effectiveBackgroundColor +- (RCTUIColor *)effectiveBackgroundColor // [macOS] { - UIColor *effectiveBackgroundColor = _backgroundColor; // ?: [[UIColor whiteColor] colorWithAlphaComponent:0]; + RCTUIColor *effectiveBackgroundColor = _backgroundColor; // ?: [[UIColor whiteColor] colorWithAlphaComponent:0]; // [macOS] if (effectiveBackgroundColor && !isnan(_opacity)) { effectiveBackgroundColor = [effectiveBackgroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * _opacity]; } - return effectiveBackgroundColor ?: [UIColor clearColor]; + return effectiveBackgroundColor ?: [RCTUIColor clearColor]; // [macOS] } static NSString *capitalizeText(NSString *text) diff --git a/packages/react-native/Libraries/Text/RCTTextUIKit.h b/packages/react-native/Libraries/Text/RCTTextUIKit.h new file mode 100644 index 00000000000000..34c0a703926fc2 --- /dev/null +++ b/packages/react-native/Libraries/Text/RCTTextUIKit.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + // [macOS] + +#include +#if !TARGET_OS_OSX +#import + +UIKIT_STATIC_INLINE BOOL RCTTextSelectionEqual(UITextRange *range1, UITextRange *range2) +{ + return [range1 isEqual:range2]; +} + +#else +#import + +NS_INLINE BOOL RCTTextSelectionEqual(NSRange range1, NSRange range2) +{ + return NSEqualRanges(range1, range2); +} + +// +// semantically equivalent constants +// + +// UITextView.h/NSTextView.h +#define UITextViewTextDidChangeNotification NSTextDidChangeNotification +#define UITextFieldTextDidChangeNotification NSControlTextDidChangeNotification + +// +// functionally equivalent types +// + +// These types have the same purpose but may differ semantically. Use with care! + +// UITextField + +#define UITextField NSTextField +#define UITextFieldDelegate NSTextFieldDelegate + +// UITextView +#define UITextView NSTextView +#define UITextViewDelegate NSTextViewDelegate + +#endif diff --git a/packages/react-native/Libraries/Text/RawText/RCTRawTextViewManager.mm b/packages/react-native/Libraries/Text/RawText/RCTRawTextViewManager.mm index f872536ac4c062..d9e349eefbb17e 100644 --- a/packages/react-native/Libraries/Text/RawText/RCTRawTextViewManager.mm +++ b/packages/react-native/Libraries/Text/RawText/RCTRawTextViewManager.mm @@ -13,9 +13,9 @@ @implementation RCTRawTextViewManager RCT_EXPORT_MODULE(RCTRawText) -- (UIView *)view +- (RCTUIView *)view // [macOS] { - return [UIView new]; + return [RCTUIView new]; // [macOS] } - (RCTShadowView *)shadowView diff --git a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.h b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.h index a87d67d192305c..5fa455d4bba336 100644 --- a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.h +++ b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @interface NSTextStorage (FontScaling) diff --git a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m index ee391a20f6c271..692d2f3d8c3d26 100644 --- a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m +++ b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m @@ -108,7 +108,7 @@ - (void)scaleFontSizeWithRatio:(CGFloat)ratio CGFloat fontSize = MAX(MIN(font.pointSize * ratio, maximumFontSize), minimumFontSize); - [self addAttribute:NSFontAttributeName value:[font fontWithSize:fontSize] range:range]; + [self addAttribute:NSFontAttributeName value:UIFontWithSize(font, fontSize) /* [macOS] */ range:range]; }]; [self endEditing]; diff --git a/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.h b/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.h index 0a34a5a8a17332..360ccc75d18228 100644 --- a/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.h +++ b/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.h @@ -30,8 +30,10 @@ typedef NS_ENUM(NSInteger, RCTDynamicTypeRamp) { @end +#if !TARGET_OS_OSX // [macOS] /// Generates a `UIFontMetrics` instance representing a particular Dynamic Type ramp. UIFontMetrics *_Nonnull RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp); /// The "reference" size for a particular font scale ramp, equal to a text element's size under default text size /// settings. CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp); +#endif // [macOS] diff --git a/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.mm b/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.mm index be97a50a61f405..88354bc5904e4a 100644 --- a/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.mm +++ b/packages/react-native/Libraries/Text/Text/RCTDynamicTypeRamp.mm @@ -29,6 +29,7 @@ @implementation RCTConvert (DynamicTypeRamp) @end +#if !TARGET_OS_OSX // [macOS] UIFontMetrics *RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp) { static NSDictionary *mapping; @@ -80,3 +81,4 @@ CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp) mapping[@(dynamicTypeRamp)] ?: @17; // Default to body size if we don't recognize the specified ramp return CGFLOAT_IS_DOUBLE ? [baseSize doubleValue] : [baseSize floatValue]; } +#endif // [macOS] diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm index 5afa863ad0600c..8d3acd8f29bc0b 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm @@ -96,16 +96,16 @@ - (void)uiManagerWillPerformMounting [descendantViewTags addObject:shadowView.reactTag]; }]; - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTTextView *textView = (RCTTextView *)viewRegistry[tag]; if (!textView) { return; } - NSMutableArray *descendantViews = [NSMutableArray arrayWithCapacity:descendantViewTags.count]; + NSMutableArray *descendantViews = [NSMutableArray arrayWithCapacity:descendantViewTags.count]; // [macOS] [descendantViewTags enumerateObjectsUsingBlock:^(NSNumber *_Nonnull descendantViewTag, NSUInteger index, BOOL *_Nonnull stop) { - UIView *descendantView = viewRegistry[descendantViewTag]; + RCTPlatformView *descendantView = viewRegistry[descendantViewTag]; // [macOS] if (!descendantView) { return; } @@ -141,7 +141,7 @@ - (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText return; } - __block CGFloat maximumFontLineHeight = 0; + __block CGFloat maximumFontLineHeight = 0; [attributedText enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedText.length) @@ -151,8 +151,8 @@ - (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText return; } - if (maximumFontLineHeight <= font.lineHeight) { - maximumFontLineHeight = font.lineHeight; + if (maximumFontLineHeight <= UIFontLineHeight(font)) { // [macOS] + maximumFontLineHeight = UIFontLineHeight(font); // [macOS] } }]; @@ -163,8 +163,8 @@ - (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0; [attributedText addAttribute:NSBaselineOffsetAttributeName - value:@(baseLineOffset) - range:NSMakeRange(0, attributedText.length)]; + value:@(baseLineOffset) + range:NSMakeRange(0, attributedText.length)]; } - (NSAttributedString *)attributedTextWithMeasuredAttachmentsThatFitSize:(CGSize)size @@ -202,7 +202,7 @@ - (NSAttributedString *)attributedTextWithMeasuredAttachmentsThatFitSize:(CGSize - (NSTextStorage *)textStorageAndLayoutManagerThatFitsSize:(CGSize)size exclusiveOwnership:(BOOL)exclusiveOwnership { - NSValue *key = [NSValue valueWithCGSize:size]; + NSValue *key = NSValueWithCGSize(size); // [macOS] NSTextStorage *cachedTextStorage = [_cachedTextStorages objectForKey:key]; if (cachedTextStorage) { @@ -288,10 +288,19 @@ - (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; CGRect frame = { +#if !TARGET_OS_OSX // [macOS] {RCTRoundPixelValue(glyphRect.origin.x), RCTRoundPixelValue( glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender)}, +#else // [macOS + {RCTRoundPixelValue(glyphRect.origin.x, [self scale]), + RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender, [self scale])}, +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] {RCTRoundPixelValue(attachmentSize.width), RCTRoundPixelValue(attachmentSize.height)}}; +#else // [macOS + {RCTRoundPixelValue(attachmentSize.width, [self scale]), RCTRoundPixelValue(attachmentSize.height, [self scale])}}; +#endif // macOS] NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location]; @@ -396,7 +405,12 @@ static YGSize RCTTextShadowViewMeasure( } size = (CGSize){ +#if !TARGET_OS_OSX // [macOS] MIN(RCTCeilPixelValue(size.width), maximumSize.width), MIN(RCTCeilPixelValue(size.height), maximumSize.height)}; +#else // [macOS + MIN(RCTCeilPixelValue(size.width, shadowTextView.scale), maximumSize.width), + MIN(RCTCeilPixelValue(size.height, shadowTextView.scale), maximumSize.height)}; +#endif // macOS] // Adding epsilon value illuminates problems with converting values from // `double` to `float`, and then rounding them to pixel grid in Yoga. diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.h b/packages/react-native/Libraries/Text/Text/RCTTextView.h index 4858dd8cf406c9..3de368b73b6bba 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.h +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.h @@ -6,18 +6,21 @@ */ #import +#import // [macOS] -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN -@interface RCTTextView : UIView +@interface RCTTextView : RCTUIView // [macOS] + +- (instancetype)initWithEventDispatcher:(id)eventDispatcher; // [macOS] @property (nonatomic, assign) BOOL selectable; - (void)setTextStorage:(NSTextStorage *)textStorage contentFrame:(CGRect)contentFrame - descendantViews:(NSArray *)descendantViews; + descendantViews:(NSArray *)descendantViews; // [macOS] /** * (Experimental and unused for Paper) Pointer event handlers. diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index 3f64e313ab58f1..4e5afc435ec77d 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -7,40 +7,104 @@ #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] +#import // [macOS] #import #import +#import // [macOS] #import +#import + +#import + +#if TARGET_OS_OSX // [macOS + +// We are managing the key view loop using the RCTTextView. +// Disable key view for backed NSTextView so we don't get double focus. +@interface RCTUnfocusableTextView : NSTextView +@end + +@implementation RCTUnfocusableTextView + +- (BOOL)canBecomeKeyView +{ + return NO; +} + +@end + +@interface RCTTextView () +@end + +#endif // macOS] #import @implementation RCTTextView { CAShapeLayer *_highlightLayer; +#if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; +#else // [macOS + NSString * _accessibilityLabel; + NSTextView *_textView; +#endif // macOS] - NSArray *_Nullable _descendantViews; + id _eventDispatcher; // [macOS] + NSArray *_Nullable _descendantViews; // [macOS] NSTextStorage *_Nullable _textStorage; CGRect _contentFrame; } +// [macOS +- (instancetype)initWithEventDispatcher:(id)eventDispatcher +{ + if ((self = [self initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + } + return self; +} +// macOS] + - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { +#if !TARGET_OS_OSX // [macOS] self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; self.opaque = NO; - self.contentMode = UIViewContentModeRedraw; +#else // [macOS + self.accessibilityRole = NSAccessibilityStaticTextRole; + // Fix blurry text on non-retina displays. + self.canDrawSubviewsIntoLayer = YES; + // The NSTextView is responsible for drawing text and managing selection. + _textView = [[RCTUnfocusableTextView alloc] initWithFrame:self.bounds]; + _textView.delegate = self; + _textView.usesFontPanel = NO; + _textView.drawsBackground = NO; + _textView.linkTextAttributes = @{}; + _textView.editable = NO; + _textView.selectable = NO; + _textView.verticallyResizable = NO; + _textView.layoutManager.usesFontLeading = NO; + _textStorage = _textView.textStorage; + [self addSubview:_textView]; +#endif // macOS] + RCTUIViewSetContentModeRedraw(self); // [macOS] } return self; } +#if DEBUG // [macOS] description is a debug-only feature - (NSString *)description { NSString *stringToAppend = [NSString stringWithFormat:@" reactTag: %@; text: %@", self.reactTag, _textStorage.string]; return [[super description] stringByAppendingString:stringToAppend]; } +#endif // [macOS] - (void)setSelectable:(BOOL)selectable { @@ -50,13 +114,21 @@ - (void)setSelectable:(BOOL)selectable _selectable = selectable; +#if !TARGET_OS_OSX // [macOS] if (_selectable) { [self enableContextMenu]; } else { [self disableContextMenu]; } +#else // [macOS + _textView.selectable = _selectable; + if (_selectable) { + [self setFocusable:YES]; + } +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] - (void)reactSetFrame:(CGRect)frame { // Text looks super weird if its frame is animated. @@ -65,6 +137,7 @@ - (void)reactSetFrame:(CGRect)frame [super reactSetFrame:frame]; }]; } +#endif // [macOS] - (void)didUpdateReactSubviews { @@ -73,19 +146,47 @@ - (void)didUpdateReactSubviews - (void)setTextStorage:(NSTextStorage *)textStorage contentFrame:(CGRect)contentFrame - descendantViews:(NSArray *)descendantViews + descendantViews:(NSArray *)descendantViews // [macOS] { + // This lets the textView own its text storage on macOS + // We update and replace the text container `_textView.textStorage.attributedString` when text/layout changes +#if !TARGET_OS_OSX // [macOS] _textStorage = textStorage; +#endif // [macOS] + _contentFrame = contentFrame; +#if TARGET_OS_OSX // [macOS + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [_textView replaceTextContainer:textContainer]; + + // On macOS AppKit can throw an uncaught exception + // (-[NSConcretePointerArray pointerAtIndex:]: attempt to access pointer at index ...) + // during the dealloc of NSLayoutManager. The textStorage and its + // associated NSLayoutManager dealloc later in an autorelease pool. + // Manually removing the layout managers from textStorage prior to release + // works around this issue in AppKit. + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + _textView.minSize = contentFrame.size; + _textView.maxSize = contentFrame.size; + _textView.frame = contentFrame; + _textView.textStorage.attributedString = textStorage; +#endif // macOS] + // FIXME: Optimize this. - for (UIView *view in _descendantViews) { + for (RCTUIView *view in _descendantViews) { // [macOS] [view removeFromSuperview]; } _descendantViews = descendantViews; - for (UIView *view in descendantViews) { + for (RCTUIView *view in descendantViews) { // [macOS] [self addSubview:view]; } @@ -95,6 +196,13 @@ - (void)setTextStorage:(NSTextStorage *)textStorage - (void)drawRect:(CGRect)rect { [super drawRect:rect]; + + // For iOS, UITextView api is not used for legacy performance reasons. A custom draw implementation is used instead. + // On desktop, we use NSTextView to access api's for arbitrary selection, custom cursors etc... +#if TARGET_OS_OSX // [macOS + return; +#endif // macOS] + if (!_textStorage) { return; } @@ -131,11 +239,17 @@ - (void)drawRect:(CGRect)rect withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect enclosingRect, __unused BOOL *anotherStop) { - UIBezierPath *path = [UIBezierPath - bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) - cornerRadius:2]; + // [macOS + UIBezierPath *path = UIBezierPathWithRoundedRect( + CGRectInset(enclosingRect, -2, -2), + 2); + // [macOS] if (highlightPath) { +#if !TARGET_OS_OSX // [macOS] [highlightPath appendPath:path]; +#else // [macOS + [highlightPath appendBezierPath:path]; +#endif // macOS] } else { highlightPath = path; } @@ -145,11 +259,11 @@ - (void)drawRect:(CGRect)rect if (highlightPath) { if (!_highlightLayer) { _highlightLayer = [CAShapeLayer layer]; - _highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor; + _highlightLayer.fillColor = [RCTUIColor colorWithWhite:0 alpha:0.25].CGColor; // [macOS] [self.layer addSublayer:_highlightLayer]; } _highlightLayer.position = _contentFrame.origin; - _highlightLayer.path = highlightPath.CGPath; + _highlightLayer.path = UIBezierPathCreateCGPathRef(highlightPath); // [macOS] } else { [_highlightLayer removeFromSuperlayer]; _highlightLayer = nil; @@ -198,6 +312,7 @@ - (void)didMoveToWindow #pragma mark - Accessibility +#if !TARGET_OS_OSX // [macOS] - (NSString *)accessibilityLabel { NSString *superAccessibilityLabel = [super accessibilityLabel]; @@ -206,9 +321,32 @@ - (NSString *)accessibilityLabel } return _textStorage.string; } +#else // [macOS + +// This code is here to cover for a mismatch in the what accessibilityLabels and accessibilityValues mean in iOS versus macOS. +// In macOS a text element will always read its accessibilityValue, but will only read it's accessibilityLabel if it's value is set. +// In iOS a text element will only read it's accessibilityValue if it has no accessibilityLabel, and will always read its accessibilityLabel. +// This code replicates the expected behavior in macOS by: +// 1) Setting the accessibilityValue = the react-native accessibilityLabel prop if one exists and setting it equal to the text's contents otherwise. +// 2) Making sure that its accessibilityLabel is always nil, so that it doesn't read out the label twice. + +- (void)setAccessibilityLabel:(NSString *)label +{ + _accessibilityLabel = [label copy]; +} + +- (NSString *)accessibilityValue +{ + if (_accessibilityLabel) { + return _accessibilityLabel; + } + return _textStorage.string; +} +#endif // macOS] #pragma mark - Context Menu +#if !TARGET_OS_OSX // [macOS] - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self @@ -240,12 +378,132 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture [menuController setMenuVisible:YES animated:YES]; #endif } +#else // [macOS +- (NSView *)hitTest:(NSPoint)point +{ + // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). + NSView *hitView = [super hitTest:point]; + + NSEventType eventType = NSApp.currentEvent.type; + BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; + BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; + BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; + BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent; + + return isTextViewClick ? self : hitView; +} + +- (void)rightMouseDown:(NSEvent *)event +{ + + if (self.selectable == NO) { + [super rightMouseDown:event]; + return; + } + + [[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event]; + [_textView rightMouseDown:event]; +} + +- (void)mouseDown:(NSEvent *)event +{ + if (!self.selectable) { + [super mouseDown:event]; + return; + } + + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; + + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; + } + + if (shouldForward) { + NSView *contentView = self.window.contentView; + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil]; + + // Start selection if we're still selectable and hit-testable. + if (self.selectable && [contentView hitTest:point] == self) { + [[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event]; + [self.window makeFirstResponder:_textView]; + [_textView mouseDown:event]; + } + } else { + // Clear selection for single clicks. + _textView.selectedRange = NSMakeRange(NSNotFound, 0); + } +} +#endif // macOS] + +#pragma mark - Selection + +#if TARGET_OS_OSX // [macOS +- (void)textDidEndEditing:(NSNotification *)notification +{ + _textView.selectedRange = NSMakeRange(NSNotFound, 0); +} +#endif // macOS] + +#pragma mark - Responder chain + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canBecomeFirstResponder { return _selectable; } +#else // [macOS +- (BOOL)canBecomeKeyView +{ + return self.focusable; +} + +- (void)drawFocusRingMask { + if (self.focusable && self.enableFocusRing) { + NSRectFill([self bounds]); + } +} + +- (NSRect)focusRingMaskBounds { + return [self bounds]; +} + +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + // If we've gained focus, notify listeners + [_eventDispatcher sendEvent:[RCTFocusChangeEvent focusEventWithReactTag:self.reactTag]]; + + return YES; +} + +- (BOOL)resignFirstResponder +{ + // Don't relinquish first responder while selecting text. + if (_selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { + return NO; + } + + return [super resignFirstResponder]; +} +- (BOOL)canBecomeFirstResponder +{ + return self.focusable; +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (_selectable && action == @selector(copy:)) { @@ -254,16 +512,19 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return [self.nextResponder canPerformAction:action withSender:sender]; } +#endif // [macOS] + +#pragma mark - Copy/Paste - (void)copy:(id)sender { NSAttributedString *attributedText = _textStorage; - NSMutableDictionary *item = [NSMutableDictionary new]; - NSData *rtf = [attributedText dataFromRange:NSMakeRange(0, attributedText.length) documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType} error:nil]; +#if !TARGET_OS_OSX // [macOS] + NSMutableDictionary *item = [NSMutableDictionary new]; // [macOS] if (rtf) { [item setObject:rtf forKey:(id)kUTTypeFlatRTFD]; @@ -273,6 +534,11 @@ - (void)copy:(id)sender UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[ item ]; +#else // [macOS + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setData:rtf forType:NSPasteboardTypeRTFD]; +#endif // macOS] } @end diff --git a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm index 7fec0b7b58b010..6afe1b7a288bee 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm @@ -42,23 +42,27 @@ - (void)setBridge:(RCTBridge *)bridge [bridge.uiManager.observerCoordinator addObserver:self]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDidUpdateMultiplierNotification) name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification" object:[bridge moduleForName:@"AccessibilityManager" lazilyLoadIfNecessary:YES]]; +#endif // [macOS] } -- (UIView *)view +- (RCTUIView *)view // [macOS] { - return [RCTTextView new]; + return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; // [macOS] } - (RCTShadowView *)shadowView { RCTTextShadowView *shadowView = [[RCTTextShadowView alloc] initWithBridge:self.bridge]; +#if !TARGET_OS_OSX // [macOS] shadowView.textAttributes.fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager"] valueForKey:@"multiplier"] floatValue]; +#endif // [macOS] [_shadowViews addObject:shadowView]; return shadowView; } diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.h index bd183de3a5953b..bcfa573bfbcf46 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.h +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.h @@ -11,6 +11,14 @@ NS_ASSUME_NONNULL_BEGIN @interface RCTMultilineTextInputView : RCTBaseTextInputView +#if TARGET_OS_OSX // [macOS + +@property (nonatomic, assign) BOOL scrollEnabled; + +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; +@property (nonatomic, assign) BOOL hideVerticalScrollIndicator; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.mm index e3a7c6be60eb16..7bc2c53c52c857 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.mm @@ -12,6 +12,10 @@ #import @implementation RCTMultilineTextInputView { +#if TARGET_OS_OSX // [macOS + RCTUIScrollView *_scrollView; + RCTClipView *_clipView; +#endif // macOS] RCTUITextView *_backedTextInputView; } @@ -20,25 +24,143 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge if (self = [super initWithBridge:bridge]) { _backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds]; _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#if TARGET_OS_OSX // [macOS + self.hideVerticalScrollIndicator = NO; + _scrollView = [[RCTUIScrollView alloc] initWithFrame:self.bounds]; + _scrollView.backgroundColor = [RCTUIColor clearColor]; + _scrollView.drawsBackground = NO; + _scrollView.borderType = NSNoBorder; + _scrollView.hasHorizontalRuler = NO; + _scrollView.hasVerticalRuler = NO; + _scrollView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [_scrollView setHasVerticalScroller:YES]; + + _clipView = [[RCTClipView alloc] initWithFrame:_scrollView.frame]; + [_scrollView setContentView:_clipView]; + + _backedTextInputView.verticallyResizable = YES; + _backedTextInputView.horizontallyResizable = YES; + _backedTextInputView.textContainer.containerSize = NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX); + _backedTextInputView.textContainer.widthTracksTextView = YES; +#endif // macOS] _backedTextInputView.textInputDelegate = self; +#if !TARGET_OS_OSX // [macOS] [self addSubview:_backedTextInputView]; +#else // [macOS + _scrollView.documentView = _backedTextInputView; + _scrollView.contentView.postsBoundsChangedNotifications = YES; + // Enable focus ring by default + _scrollView.enableFocusRing = YES; + [self addSubview:_scrollView]; + + // a register for those notifications on the content view. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(boundDidChange:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; +#endif // macOS] } return self; } +#if TARGET_OS_OSX // [macOS +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +#endif // macOS] + - (id)backedTextInputView { return _backedTextInputView; } +#if TARGET_OS_OSX // [macOS +- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets +{ + [super setReactPaddingInsets:reactPaddingInsets]; + // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInsets` on mac. + ((RCTUITextView*)self.backedTextInputView).textContainerInsets = reactPaddingInsets; + [self setNeedsLayout]; +} + +- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets +{ + [super setReactBorderInsets:reactBorderInsets]; + // We apply `borderInsets` as `_scrollView` layout offset on mac. + _scrollView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets); + [self setNeedsLayout]; +} + +- (void)setEnableFocusRing:(BOOL)enableFocusRing +{ + [super setEnableFocusRing:enableFocusRing]; + if ([_scrollView respondsToSelector:@selector(setEnableFocusRing:)]) { + [_scrollView setEnableFocusRing:enableFocusRing]; + } +} + +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes +{ + [_backedTextInputView setReadablePasteBoardTypes:readablePasteboardTypes]; +} + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + if (scrollEnabled) { + _scrollView.scrollEnabled = YES; + [_clipView setConstrainScrolling:NO]; + } else { + _scrollView.scrollEnabled = NO; + [_clipView setConstrainScrolling:YES]; + } +} + +- (BOOL)scrollEnabled +{ + return _scrollView.isScrollEnabled; +} + +- (BOOL)shouldShowVerticalScrollbar +{ + // Hide vertical scrollbar if explicity set to NO + if (self.hideVerticalScrollIndicator) { + return NO; + } + + // Hide vertical scrollbar if attributed text overflows view + CGSize textViewSize = [_backedTextInputView intrinsicContentSize]; + NSClipView *clipView = (NSClipView *)_scrollView.contentView; + if (textViewSize.height > clipView.bounds.size.height) { + return YES; + }; + + return NO; +} + +- (void)textInputDidChange +{ + [super textInputDidChange]; + + [_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + [super setAttributedText:attributedText]; + + [_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]]; +} + +#endif // macOS] + #pragma mark - UIScrollViewDelegate -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { RCTDirectEventBlock onScroll = self.onScroll; - if (onScroll) { CGPoint contentOffset = scrollView.contentOffset; CGSize contentSize = scrollView.contentSize; @@ -60,4 +182,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView } } +#if TARGET_OS_OSX // [macOS + +#pragma mark - Notification handling + +- (void)boundDidChange:(NSNotification*)NSNotification +{ + [self scrollViewDidScroll:_scrollView]; +} + +#pragma mark - NSResponder chain + +- (BOOL)acceptsFirstResponder +{ + return _backedTextInputView.acceptsFirstResponder; +} + +#endif // macOS] + @end diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm index e729bb7a222a78..477f4545e0107e 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm @@ -7,18 +7,32 @@ #import #import +#import // [macOS] @implementation RCTMultilineTextInputViewManager RCT_EXPORT_MODULE() -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; } #pragma mark - Multiline (aka TextView) specific properties -RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) // [macOS] +RCT_REMAP_OSX_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.enabledTextCheckingTypes, NSTextCheckingTypes) // [macOS] + +#if TARGET_OS_OSX // [macOS +RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(hideVerticalScrollIndicator, BOOL) +RCT_CUSTOM_VIEW_PROPERTY(pastedTypes, NSArray*, RCTUITextView) +{ + NSArray *types = json ? [RCTConvert NSPasteboardTypeArray:json] : nil; + if ([view respondsToSelector:@selector(setReadablePasteBoardTypes:)]) { + [view setReadablePasteBoardTypes: types]; + } +} +#endif // macOS] @end diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h index 205f9943262add..4774333941aeb8 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] + +#import "RCTTextUIKit.h" // [macOS] #import #import @@ -23,21 +25,42 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id textInputDelegate; @property (nonatomic, assign) BOOL contextMenuHidden; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign, readonly) BOOL textWasPasted; +#else // [macOS +@property (nonatomic, assign) BOOL textWasPasted; +#endif // macOS] @property (nonatomic, assign, readonly) BOOL dictationRecognizing; @property (nonatomic, copy, nullable) NSString *placeholder; -@property (nonatomic, strong, nullable) UIColor *placeholderColor; +@property (nonatomic, strong, nullable) RCTUIColor *placeholderColor; // [macOS] @property (nonatomic, assign) CGFloat preferredMaxLayoutWidth; +#if !TARGET_OS_OSX // [macOS] // The `clearButtonMode` property actually is not supported yet; // it's declared here only to conform to the interface. @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; +#endif // [macOS] @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; +#if TARGET_OS_OSX // [macOS +@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, strong, nullable) RCTUIColor *selectionColor; +@property (nonatomic, strong, nullable) RCTUIColor *cursorColor; +@property (nonatomic, assign) UIEdgeInsets textContainerInsets; +@property (nonatomic, copy) NSString *text; +@property (nonatomic, assign) NSTextAlignment textAlignment; +@property (nonatomic, copy, nullable) NSAttributedString *attributedText; +@property (nonatomic, assign) CGFloat pointScaleFactor; +- (NSSize)sizeThatFits:(NSSize)size; +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; +#endif // macOS] + +@property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 582b49c1ef4f4b..38b16cb8c07471 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -14,21 +14,33 @@ #import @implementation RCTUITextView { +#if !TARGET_OS_OSX // [macOS] UILabel *_placeholderView; UITextView *_detachedTextView; +#endif // [macOS] RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; +#if TARGET_OS_OSX // [macOS + NSArray *_readablePasteboardTypes; +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] static UIFont *defaultPlaceholderFont(void) { return [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; } +#else // [macOS +static NSFont *defaultPlaceholderFont(void) +{ + return [NSFont systemFontOfSize:[NSFont systemFontSize]]; +} +#endif // macOS] -static UIColor *defaultPlaceholderColor(void) +static RCTUIColor *defaultPlaceholderColor(void) // [macOS] { // Default placeholder color from UITextField. - return [UIColor placeholderTextColor]; + return [RCTUIColor placeholderTextColor]; // [macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -38,19 +50,33 @@ - (instancetype)initWithFrame:(CGRect)frame selector:@selector(textDidChange) name:UITextViewTextDidChangeNotification object:self]; - +#if !TARGET_OS_OSX // [macOS] _placeholderView = [[UILabel alloc] initWithFrame:self.bounds]; _placeholderView.isAccessibilityElement = NO; _placeholderView.numberOfLines = 0; [self addSubview:_placeholderView]; +#else // [macOS + // Fix blurry text on non-retina displays. + self.canDrawSubviewsIntoLayer = YES; + self.allowsUndo = YES; +#endif // macOS] _textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self]; - self.backgroundColor = [UIColor clearColor]; - self.textColor = [UIColor blackColor]; + self.backgroundColor = [RCTUIColor clearColor]; // [macOS] + self.textColor = [RCTUIColor blackColor]; // [macOS] // This line actually removes 5pt (default value) left and right padding in UITextView. +#if !TARGET_OS_OSX // [macOS] self.textContainer.lineFragmentPadding = 0; +#else // [macOS + // macOS has a bug where setting this to 0 will cause the scroll view to scroll to top when + // inserting a newline at the bottom of a NSTextView when it has more rows than can be displayed + // on screen. + self.textContainer.lineFragmentPadding = 1; +#endif //macOS] +#if !TARGET_OS_OSX // [macOS] self.scrollsToTop = NO; +#endif // [macOS] self.scrollEnabled = YES; } @@ -104,12 +130,117 @@ - (void)setPlaceholder:(NSString *)placeholder [self _updatePlaceholder]; } -- (void)setPlaceholderColor:(UIColor *)placeholderColor +- (void)setPlaceholderColor:(RCTUIColor *)placeholderColor // [macOS] { _placeholderColor = placeholderColor; [self _updatePlaceholder]; } +#if TARGET_OS_OSX // [macOS +- (void)toggleAutomaticSpellingCorrection:(id)sender +{ + self.automaticSpellingCorrectionEnabled = !self.isAutomaticSpellingCorrectionEnabled; + [_textInputDelegate automaticSpellingCorrectionDidChange:self.isAutomaticSpellingCorrectionEnabled]; +} + +- (void)toggleContinuousSpellChecking:(id)sender +{ + self.continuousSpellCheckingEnabled = !self.isContinuousSpellCheckingEnabled; + [_textInputDelegate continuousSpellCheckingDidChange:self.isContinuousSpellCheckingEnabled]; +} + +- (void)toggleGrammarChecking:(id)sender +{ + self.grammarCheckingEnabled = !self.isGrammarCheckingEnabled; + [_textInputDelegate grammarCheckingDidChange:self.isGrammarCheckingEnabled]; +} + +- (void)setSelectionColor:(RCTUIColor *)selectionColor +{ + NSMutableDictionary *selectTextAttributes = self.selectedTextAttributes.mutableCopy; + selectTextAttributes[NSBackgroundColorAttributeName] = selectionColor ?: [NSColor selectedControlColor]; + self.selectedTextAttributes = selectTextAttributes.copy; +} + +- (RCTUIColor*)selectionColor +{ + return (RCTUIColor*)self.selectedTextAttributes[NSBackgroundColorAttributeName]; +} + +- (void)setCursorColor:(NSColor *)cursorColor +{ + _cursorColor = cursorColor; + self.insertionPointColor = cursorColor; +} + +- (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)checkingType +{ + [super setEnabledTextCheckingTypes:checkingType]; + self.automaticDataDetectionEnabled = checkingType != 0; +} + +- (NSTextAlignment)textAlignment +{ + return self.alignment; +} + +- (NSString*)text +{ + return self.string; +} + +- (void)setText:(NSString *)text +{ + self.string = text; +} + +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes +{ + _readablePasteboardTypes = readablePasteboardTypes; +} + +- (void)setTypingAttributes:(__unused NSDictionary *)typingAttributes +{ + // Prevent NSTextView from changing its own typing attributes out from under us. + [super setTypingAttributes:_defaultTextAttributes]; +} + +- (NSAttributedString*)attributedText +{ + return self.textStorage; +} + +- (BOOL)becomeFirstResponder +{ + BOOL success = [super becomeFirstResponder]; + + if (success) { + id textInputDelegate = [self textInputDelegate]; + if ([textInputDelegate respondsToSelector:@selector(textInputDidBeginEditing)]) { + [textInputDelegate textInputDidBeginEditing]; + } + } + + return success; +} + +- (BOOL)resignFirstResponder +{ + if (self.selectable) { + self.selectedRange = NSMakeRange(NSNotFound, 0); + } + + BOOL success = [super resignFirstResponder]; + + if (success) { + // Break undo coalescing when losing focus. + [self breakUndoCoalescing]; + } + + return success; +} +#endif // macOS] + - (void)setDefaultTextAttributes:(NSDictionary *)defaultTextAttributes { if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) { @@ -142,17 +273,35 @@ - (void)setFont:(UIFont *)font - (void)setTextAlignment:(NSTextAlignment)textAlignment { +#if !TARGET_OS_OSX // [macOS] [super setTextAlignment:textAlignment]; _placeholderView.textAlignment = textAlignment; +#else // [macOS + self.alignment = textAlignment; + [self setNeedsDisplay:YES]; +#endif // macOS] } - (void)setAttributedText:(NSAttributedString *)attributedText { +#if !TARGET_OS_OSX // [macOS] [super setAttributedText:attributedText]; +#else // [macOS + // Break undo coalescing when the text is changed by JS (e.g. autocomplete). + [self breakUndoCoalescing]; + // Avoid Exception thrown while executing UI block: *** -[NSBigMutableString replaceCharactersInRange:withString:]: nil argument + [self.textStorage setAttributedString:attributedText ?: [NSAttributedString new]]; +#endif // macOS] [self textDidChange]; } +#pragma mark - Overrides + +#if !TARGET_OS_OSX // [macOS] - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate +#else // [macOS +- (void)setSelectedTextRange:(NSRange)selectedTextRange notifyDelegate:(BOOL)notifyDelegate +#endif // macOS] { if (!notifyDelegate) { // We have to notify an adapter that following selection change was initiated programmatically, @@ -160,13 +309,65 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange]; } +#if !TARGET_OS_OSX // [macOS] [super setSelectedTextRange:selectedTextRange]; +#else // [macOS + [super setSelectedRange:selectedTextRange]; +#endif // macOS] +} + +#if TARGET_OS_OSX // [macOS +- (NSRange)selectedTextRange +{ + return [super selectedRange]; +} + +- (NSDragOperation)draggingEntered:(id )draggingInfo +{ + NSDragOperation dragOperation = [self.textInputDelegate textInputDraggingEntered:draggingInfo]; + NSDragOperation superOperation = [super draggingEntered:draggingInfo]; + // The delegate's operation should take precedence. + return dragOperation != NSDragOperationNone ? dragOperation : superOperation; +} + +- (void)draggingExited:(id)draggingInfo +{ + [self.textInputDelegate textInputDraggingExited:draggingInfo]; + [super draggingExited:draggingInfo]; +} + +- (BOOL)performDragOperation:(id)draggingInfo +{ + if ([self.textInputDelegate textInputShouldHandleDragOperation:draggingInfo]) { + return [super performDragOperation:draggingInfo]; + } + return YES; +} +- (NSArray *)readablePasteboardTypes +{ + return _readablePasteboardTypes ? _readablePasteboardTypes : [super readablePasteboardTypes]; } +// Remove the default touchbar that comes with NSTextView, since the actions that come with the +// default touchbar are currently not supported by RCTUITextView +- (NSTouchBar *)makeTouchBar +{ + return nil; +} + +#endif // macOS] + - (void)paste:(id)sender { - _textWasPasted = YES; - [super paste:sender]; +#if TARGET_OS_OSX // [macOS + if ([self.textInputDelegate textInputShouldHandlePaste:self]) + { +#endif // macOS] + _textWasPasted = YES; + [super paste:sender]; +#if TARGET_OS_OSX // [macOS + } +#endif // macOS] } // Turn off scroll animation to fix flaky scrolling. @@ -178,15 +379,69 @@ - (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated } #endif +#if TARGET_OS_OSX // [macOS + +#pragma mark - Placeholder + +- (NSAttributedString*)placeholderTextAttributedString +{ + if (self.placeholder == nil) { + return nil; + } + return [[NSAttributedString alloc] initWithString:self.placeholder attributes:[self _placeholderTextAttributes]]; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + if (self.text.length == 0 && self.placeholder) { + NSAttributedString *attributedPlaceholderString = self.placeholderTextAttributedString; + + if (attributedPlaceholderString) { + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedPlaceholderString]; + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:self.textContainer.containerSize]; + NSLayoutManager *layoutManager = [NSLayoutManager new]; + + textContainer.lineFragmentPadding = self.textContainer.lineFragmentPadding; + [layoutManager addTextContainer:textContainer]; + [textStorage addLayoutManager:layoutManager]; + + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:self.textContainerOrigin]; + } + } +} + +#pragma mark - Text Insets + +- (void)setTextContainerInsets:(UIEdgeInsets)textContainerInsets +{ + // NSTextView has a NSSize textContainerInset property + // UITextview has a UIEdgeInsets textContainerInset property + // RCTUITextView mac only has a UIEdgeInsets textContainerInsets property + // UI/NSTextField do NOT have textContainerInset properties + _textContainerInsets = textContainerInsets; + super.textContainerInset = NSMakeSize(MIN(textContainerInsets.left, textContainerInsets.right), MIN(textContainerInsets.top, textContainerInsets.bottom)); + + if (self.shouldDrawInsertionPoint) { + [self updateInsertionPointStateAndRestartTimer:NO]; + } +} + +#endif // macOS] + - (void)selectAll:(id)sender { [super selectAll:sender]; +#if !TARGET_OS_OSX // [macOS] // `selectAll:` does not work for UITextView when it's being called inside UITextView's delegate methods. dispatch_async(dispatch_get_main_queue(), ^{ UITextRange *selectionRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument]; [self setSelectedTextRange:selectionRange notifyDelegate:NO]; }); +#endif // [macOS] } #pragma mark - Layout @@ -199,8 +454,13 @@ - (CGFloat)preferredMaxLayoutWidth - (CGSize)placeholderSize { +#if !TARGET_OS_OSX // [macOS] UIEdgeInsets textContainerInset = self.textContainerInset; +#else // [macOS + UIEdgeInsets textContainerInset = self.textContainerInsets; +#endif // macOS] NSString *placeholder = self.placeholder ?: @""; +#if !TARGET_OS_OSX // [macOS] CGSize maxPlaceholderSize = CGSizeMake(UIEdgeInsetsInsetRect(self.bounds, textContainerInset).size.width, CGFLOAT_MAX); CGSize placeholderSize = [placeholder boundingRectWithSize:maxPlaceholderSize @@ -209,6 +469,11 @@ - (CGSize)placeholderSize context:nil] .size; placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height)); +#else // [macOS + CGFloat scale = _pointScaleFactor ?: self.window.backingScaleFactor; + CGSize placeholderSize = [placeholder sizeWithAttributes:[self _placeholderTextAttributes]]; + placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width, scale), RCTCeilPixelValue(placeholderSize.height, scale)); +#endif // macOS] placeholderSize.width += textContainerInset.left + textContainerInset.right; placeholderSize.height += textContainerInset.top + textContainerInset.bottom; // Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does). @@ -217,14 +482,20 @@ - (CGSize)placeholderSize - (CGSize)contentSize { +#if !TARGET_OS_OSX // [macOS] CGSize contentSize = super.contentSize; CGSize placeholderSize = _placeholderView.isHidden ? CGSizeZero : self.placeholderSize; +#else // [macOS + CGSize contentSize = super.intrinsicContentSize; + CGSize placeholderSize = self.placeholderSize; +#endif // macOS] // When a text input is empty, it actually displays a placeholder. // So, we have to consider `placeholderSize` as a minimum `contentSize`. // Returning size DOES contain `textContainerInset` (aka `padding`). return CGSizeMake(MAX(contentSize.width, placeholderSize.width), MAX(contentSize.height, placeholderSize.height)); } +#if !TARGET_OS_OSX // [macOS] - (void)layoutSubviews { [super layoutSubviews]; @@ -234,6 +505,7 @@ - (void)layoutSubviews textFrame.size.height = MIN(placeholderHeight, textFrame.size.height); _placeholderView.frame = textFrame; } +#endif // [macOS] - (CGSize)intrinsicContentSize { @@ -244,7 +516,13 @@ - (CGSize)intrinsicContentSize - (CGSize)sizeThatFits:(CGSize)size { // Returned fitting size depends on text size and placeholder size. +#if !TARGET_OS_OSX // [macOS] CGSize textSize = [super sizeThatFits:size]; +#else // [macOS + [self.layoutManager glyphRangeForTextContainer:self.textContainer]; + NSRect rect = [self.layoutManager usedRectForTextContainer:self.textContainer]; + CGSize textSize = CGSizeMake(MIN(rect.size.width, size.width), rect.size.height); +#endif // macOS] CGSize placeholderSize = self.placeholderSize; // Returning size DOES contain `textContainerInset` (aka `padding`). return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height)); @@ -252,6 +530,7 @@ - (CGSize)sizeThatFits:(CGSize)size #pragma mark - Context Menu +#if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (_contextMenuHidden) { @@ -273,19 +552,67 @@ - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL) [super removeDictationResultPlaceholder:placeholder willInsertResult:willInsertResult]; _dictationRecognizing = NO; } +#endif // [macOS] #pragma mark - Placeholder - (void)_invalidatePlaceholderVisibility { +#if !TARGET_OS_OSX // [macOS] BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0; _placeholderView.hidden = !isVisible; +#else // [macOS + [self setNeedsDisplay:YES]; +#endif // macOS] +} + +#if !TARGET_OS_OSX // [macOS] +- (void)deleteBackward { + id textInputDelegate = [self textInputDelegate]; + if ([textInputDelegate textInputShouldHandleDeleteBackward:self]) { + [super deleteBackward]; + } +} +#else // [macOS +- (BOOL)performKeyEquivalent:(NSEvent *)event { + if (!self.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + return YES; + } + + return [super performKeyEquivalent:event]; +} + +- (void)keyDown:(NSEvent *)event { + // If has marked text, handle by native and return + // Do this check before textInputShouldHandleKeyEvent as that one attempts to send the event to JS + if (self.hasMarkedText) { + [super keyDown:event]; + return; + } + + // textInputShouldHandleKeyEvent represents if native should handle the event instead of JS. + // textInputShouldHandleKeyEvent also sends keyDown event to JS internally, so we only call this once + if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + [super keyDown:event]; + [self.textInputDelegate submitOnKeyDownIfNeeded:event]; + } +} + +- (void)keyUp:(NSEvent *)event { + if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + [super keyUp:event]; + } } +#endif // macOS] - (void)_updatePlaceholder { +#if !TARGET_OS_OSX // [macOS] _placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder ?: @"" attributes:[self _placeholderTextAttributes]]; +#else // [macOS + [self setNeedsDisplay:YES]; +#endif // macOS] [self _invalidatePlaceholderVisibility]; } @@ -304,6 +631,7 @@ - (void)_updatePlaceholder } #pragma mark - Caret Manipulation +#if !TARGET_OS_OSX // [macOS] - (CGRect)caretRectForPosition:(UITextPosition *)position { @@ -313,6 +641,7 @@ - (CGRect)caretRectForPosition:(UITextPosition *)position return [super caretRectForPosition:position]; } +#endif // [macOS] #pragma mark - Utility Methods diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index 7187177b69d400..f90f11c91810cd 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @protocol RCTBackedTextInputViewProtocol; @@ -25,6 +25,13 @@ NS_ASSUME_NONNULL_BEGIN // Dismisses keyboard if true - (void)textInputDidReturn; +#if TARGET_OS_OSX // [macOS +- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled; +- (void)continuousSpellCheckingDidChange:(BOOL)enabled; +- (void)grammarCheckingDidChange:(BOOL)enabled; +- (void)submitOnKeyDownIfNeeded:(NSEvent *)event; +#endif // macOS] + - (BOOL)textInputShouldSubmitOnReturn; // Checks whether to submit when return is pressed and emits an event if true. /* @@ -37,9 +44,20 @@ NS_ASSUME_NONNULL_BEGIN - (void)textInputDidChangeSelection; -@optional +- (BOOL)textInputShouldHandleDeleteBackward:(id)sender; // Return `YES` to have the deleteBackward event handled normally. Return `NO` to disallow it and handle it yourself. // [macOS] +#if TARGET_OS_OSX // [macOS +- (BOOL)textInputShouldHandleDeleteForward:(id)sender; // Return `YES` to have the deleteForward event handled normally. Return `NO` to disallow it and handle it yourself. +- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event; // Return `YES` to have the key event handled normally. Return `NO` to disallow it and handle it yourself. +- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key; +- (NSDragOperation)textInputDraggingEntered:(id)draggingInfo; +- (void)textInputDraggingExited:(id)draggingInfo; +- (BOOL)textInputShouldHandleDragOperation:(id)draggingInfo; +- (void)textInputDidCancel; // Handle `Escape` key press. +- (BOOL)textInputShouldHandlePaste:(id)sender; // Return `YES` to have the paste event handled normally. Return `NO` to disallow it and handle it yourself. +#endif // macOS] -- (void)scrollViewDidScroll:(UIScrollView *)scrollView; +@optional +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView; // [macOS] @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h index f1c32e6e33bb05..bf4b24c1bd99e7 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTBackedTextInputDelegate.h" #import "RCTBackedTextInputViewProtocol.h" @@ -14,11 +14,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) +@protocol RCTBackedTextInputViewProtocol; // [macOS] +@protocol RCTBackedTextInputDelegate; // [macOS] + @interface RCTBackedTextFieldDelegateAdapter : NSObject - (instancetype)initWithTextField:(UITextField *)backedTextInputView; +#if !TARGET_OS_OSX // [macOS] - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; +#else // [macOS +- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(NSRange)textRange; +#endif // macOS] - (void)selectedTextRangeWasSet; @end @@ -29,7 +36,11 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithTextView:(UITextView *)backedTextInputView; +#if !TARGET_OS_OSX // [macOS] - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; +#else // [macOS +- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(NSRange)textRange; +#endif // macOS] @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm index 9dca6a5567d9a0..aab2d70e9e7650 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -6,18 +6,31 @@ */ #import +#import "RCTBackedTextInputViewProtocol.h" // [macOS +#import "RCTBackedTextInputDelegate.h" +#import "../RCTTextUIKit.h" // macOS] #pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext; -@interface RCTBackedTextFieldDelegateAdapter () +@interface RCTBackedTextFieldDelegateAdapter () +#if !TARGET_OS_OSX // [macOS] + +#else // [macOS + +#endif // macOS] + @end @implementation RCTBackedTextFieldDelegateAdapter { __weak UITextField *_backedTextInputView; BOOL _textDidChangeIsComing; +#if !TARGET_OS_OSX // [macOS] UITextRange *_previousSelectedTextRange; +#else // [macOS + NSRange _previousSelectedTextRange; +#endif // macOS] } - (instancetype)initWithTextField:(UITextField *)backedTextInputView @@ -26,12 +39,14 @@ - (instancetype)initWithTextField:(UITextField * _backedTextInputView = backedTextInputView; backedTextInputView.delegate = self; +#if !TARGET_OS_OSX // [macOS] [_backedTextInputView addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; [_backedTextInputView addTarget:self action:@selector(textFieldDidEndEditingOnExit) forControlEvents:UIControlEventEditingDidEndOnExit]; +#endif // [macOS] } return self; @@ -39,8 +54,10 @@ - (instancetype)initWithTextField:(UITextField * - (void)dealloc { +#if !TARGET_OS_OSX // [macOS] [_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingChanged]; [_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingDidEndOnExit]; +#endif // [macOS] } #pragma mark - UITextFieldDelegate @@ -91,12 +108,14 @@ - (BOOL)textField:(__unused UITextField *)textField [attributedString replaceCharactersInRange:range withString:newText]; [_backedTextInputView setAttributedText:[attributedString copy]]; +#if !TARGET_OS_OSX // [macOS] // Setting selection to the end of the replaced text. UITextPosition *position = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:(range.location + newText.length)]; [_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position] notifyDelegate:YES]; +#endif // [macOS] [self textFieldDidChange]; return NO; @@ -137,7 +156,11 @@ - (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField #pragma mark - Public Interface +#if !TARGET_OS_OSX // [macOS] - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange +#else // [macOS +- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(NSRange)textRange +#endif // macOS] { _previousSelectedTextRange = textRange; } @@ -151,14 +174,90 @@ - (void)selectedTextRangeWasSet - (void)textFieldProbablyDidChangeSelection { - if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { + if (RCTTextSelectionEqual([_backedTextInputView selectedTextRange], _previousSelectedTextRange)) { // [macOS] return; } - _previousSelectedTextRange = _backedTextInputView.selectedTextRange; + _previousSelectedTextRange = [_backedTextInputView selectedTextRange]; // [macOS] setter not defined for mac [_backedTextInputView.textInputDelegate textInputDidChangeSelection]; } +#if TARGET_OS_OSX // [macOS + +#pragma mark - NSTextFieldDelegate + +- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor +{ + return [self textFieldShouldEndEditing:_backedTextInputView]; +} + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector +{ + id textInputDelegate = [_backedTextInputView textInputDelegate]; + BOOL commandHandled = NO; + // enter/return + if (commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:)) { + [self textFieldDidEndEditingOnExit]; + if ([textInputDelegate textInputShouldSubmitOnReturn]) { + [[_backedTextInputView window] makeFirstResponder:nil]; + } + commandHandled = YES; + //backspace + } else if (commandSelector == @selector(deleteBackward:)) { + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]) { + commandHandled = YES; + } else { + [self keyboardInputShouldDelete:_backedTextInputView]; + } + //deleteForward + } else if (commandSelector == @selector(deleteForward:)) { + id textInputDelegate = [_backedTextInputView textInputDelegate]; + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteForward:_backedTextInputView]) { + commandHandled = YES; + } else { + [self keyboardInputShouldDelete:_backedTextInputView]; + } + //paste + } else if (commandSelector == @selector(paste:)) { + id textInputDelegate = [_backedTextInputView textInputDelegate]; + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandlePaste:_backedTextInputView]) { + commandHandled = YES; + } else { + _backedTextInputView.textWasPasted = YES; + } + //escape + } else if (commandSelector == @selector(cancelOperation:)) { + [textInputDelegate textInputDidCancel]; + if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) { + [[_backedTextInputView window] makeFirstResponder:nil]; + } + commandHandled = YES; +} + + return commandHandled; +} + +- (void)textFieldBeginEditing:(NSTextField *)textField +{ + [self textFieldDidBeginEditing:_backedTextInputView]; +} + +- (void)textFieldDidChange:(NSTextField *)textField +{ + [self textFieldDidChange]; +} + +- (void)textFieldEndEditing:(NSTextField *)textField +{ + [self textFieldDidEndEditing:_backedTextInputView]; +} + +- (void)textFieldDidChangeSelection:(NSTextField *)textField +{ + [self selectedTextRangeWasSet]; +} +#endif // macOS] + @end #pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView) @@ -167,11 +266,20 @@ @interface RCTBackedTextViewDelegateAdapter () @end @implementation RCTBackedTextViewDelegateAdapter { +#if !TARGET_OS_OSX // [macOS] __weak UITextView *_backedTextInputView; +#else // [macOS + __unsafe_unretained UITextView *_backedTextInputView; +#endif // macOS] NSAttributedString *_lastStringStateWasUpdatedWith; BOOL _ignoreNextTextInputCall; BOOL _textDidChangeIsComing; +#if !TARGET_OS_OSX // [macOS] UITextRange *_previousSelectedTextRange; +#else // [macOS + NSRange _previousSelectedTextRange; + NSUndoManager *_undoManager; +#endif // macOS] } - (instancetype)initWithTextView:(UITextView *)backedTextInputView @@ -215,6 +323,7 @@ - (void)textViewDidEndEditing:(__unused UITextView *)textView - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { +#if !TARGET_OS_OSX // [macOS] // Custom implementation of `textInputShouldReturn` and `textInputDidReturn` pair for `UITextView`. if (!_backedTextInputView.textWasPasted && [text isEqualToString:@"\n"]) { const BOOL shouldSubmit = [_backedTextInputView.textInputDelegate textInputShouldSubmitOnReturn]; @@ -227,6 +336,7 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang return NO; } } +#endif // [macOS] NSString *newText = [_backedTextInputView.textInputDelegate textInputShouldChangeText:text inRange:range]; @@ -243,11 +353,13 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang [attributedString replaceCharactersInRange:range withString:newText]; [_backedTextInputView setAttributedText:[attributedString copy]]; +#if !TARGET_OS_OSX // [macOS] // Setting selection to the end of the replaced text. UITextPosition *position = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:(range.location + newText.length)]; [_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position] notifyDelegate:YES]; +#endif // [macOS] [self textViewDidChange:_backedTextInputView]; @@ -264,11 +376,16 @@ - (void)textViewDidChange:(__unused UITextView *)textView [_backedTextInputView.textInputDelegate textInputDidChange]; } +#if !TARGET_OS_OSX // [macOS] + - (void)textViewDidChangeSelection:(__unused UITextView *)textView { if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { [self textViewDidChange:_backedTextInputView]; - _ignoreNextTextInputCall = YES; + + if (![_backedTextInputView isGhostTextChanging]) { // [macOS] + _ignoreNextTextInputCall = YES; + } // [macOS] } _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText; [self textViewProbablyDidChangeSelection]; @@ -283,9 +400,82 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView } } +#endif // [macOS] + +#if TARGET_OS_OSX // [macOS + +#pragma mark - NSTextViewDelegate + +- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(nullable NSString *)replacementString +{ + return [self textView:textView shouldChangeTextInRange:affectedCharRange replacementText:replacementString]; +} + +- (void)textViewDidChangeSelection:(NSNotification *)notification +{ + [self textViewProbablyDidChangeSelection]; +} + +- (void)textDidBeginEditing:(NSNotification *)notification +{ + [self textViewDidBeginEditing:_backedTextInputView]; +} + +- (void)textDidChange:(NSNotification *)notification +{ + [self textViewDidChange:_backedTextInputView]; +} + +- (void)textDidEndEditing:(NSNotification *)notification +{ + [self textViewDidEndEditing:_backedTextInputView]; +} + +- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector +{ + BOOL commandHandled = NO; + id textInputDelegate = [_backedTextInputView textInputDelegate]; + // enter/return + if ((commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:))) { + if ([textInputDelegate textInputShouldSubmitOnReturn]) { + [_backedTextInputView.window makeFirstResponder:nil]; + commandHandled = YES; + } + //backspace + } else if (commandSelector == @selector(deleteBackward:)) { + commandHandled = textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]; + //deleteForward + } else if (commandSelector == @selector(deleteForward:)) { + commandHandled = textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteForward:_backedTextInputView]; + //escape + } else if (commandSelector == @selector(cancelOperation:)) { + [textInputDelegate textInputDidCancel]; + if (![textInputDelegate hasValidKeyDownOrValidKeyUp:@"Escape"]) { + [[_backedTextInputView window] makeFirstResponder:nil]; + } + commandHandled = YES; + + } + + return commandHandled; +} + +- (NSUndoManager *)undoManagerForTextView:(NSTextView *)textView { + if (!_undoManager) { + _undoManager = [NSUndoManager new]; + } + return _undoManager; +} + +#endif // macOS] + #pragma mark - Public Interface +#if !TARGET_OS_OSX // [macOS] - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange +#else // [macOS +- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(NSRange)textRange +#endif // macOS] { _previousSelectedTextRange = textRange; } @@ -294,11 +484,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex - (void)textViewProbablyDidChangeSelection { - if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) { + if (RCTTextSelectionEqual([_backedTextInputView selectedTextRange], _previousSelectedTextRange)) { // [macOS] return; } - _previousSelectedTextRange = _backedTextInputView.selectedTextRange; + _previousSelectedTextRange = [_backedTextInputView selectedTextRange]; // [macOS] [_backedTextInputView.textInputDelegate textInputDidChangeSelection]; } diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index a8719ecd4d0165..6807acc06235fc 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -5,23 +5,46 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] + +#if TARGET_OS_OSX // [macOS +NS_ASSUME_NONNULL_BEGIN +@protocol RCTUITextFieldDelegate +@optional +- (BOOL)textField:(NSTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string; // return NO to not change text +- (void)textFieldBeginEditing:(NSTextField *)textField; +- (void)textFieldDidChange:(NSTextField *)textField; +- (void)textFieldEndEditing:(NSTextField *)textField; +- (void)textFieldDidChangeSelection:(NSTextField *)textField; +@end +NS_ASSUME_NONNULL_END +#endif // macOS] @protocol RCTBackedTextInputDelegate; @class RCTTextAttributes; NS_ASSUME_NONNULL_BEGIN +#if !TARGET_OS_OSX // [macOS] @protocol RCTBackedTextInputViewProtocol +#else // [macOS +@protocol RCTBackedTextInputViewProtocol +#endif // macOS] @property (nonatomic, copy, nullable) NSAttributedString *attributedText; @property (nonatomic, copy, nullable) NSString *placeholder; -@property (nonatomic, strong, nullable) UIColor *placeholderColor; +@property (nonatomic, strong, nullable) RCTUIColor *placeholderColor; // [macOS] +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign, readonly) BOOL textWasPasted; +#else // [macOS +@property (nonatomic, assign) BOOL textWasPasted; +#endif // macOS] @property (nonatomic, assign, readonly) BOOL dictationRecognizing; @property (nonatomic, assign) UIEdgeInsets textContainerInset; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong, nullable) UIView *inputAccessoryView; @property (nonatomic, strong, nullable) UIView *inputView; +#endif // [macOS] @property (nonatomic, weak, nullable) id textInputDelegate; @property (nonatomic, readonly) CGSize contentSize; @property (nonatomic, strong, nullable) NSDictionary *defaultTextAttributes; @@ -29,26 +52,44 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, getter=isEditable) BOOL editable; @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL enablesReturnKeyAutomatically; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; +#endif // [macOS] @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; @property (nonatomic, assign, readonly) CGFloat zoomScale; @property (nonatomic, assign, readonly) CGPoint contentOffset; @property (nonatomic, assign, readonly) UIEdgeInsets contentInset; +#if TARGET_OS_OSX // [macOS +@property (nonatomic, assign) CGFloat pointScaleFactor; +#endif // macOS] // This protocol disallows direct access to `selectedTextRange` property because // unwise usage of it can break the `delegate` behavior. So, we always have to // explicitly specify should `delegate` be notified about the change or not. // If the change was initiated programmatically, we must NOT notify the delegate. // If the change was a result of user actions (like typing or touches), we MUST notify the delegate. +#if !TARGET_OS_OSX // [macOS] - (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE; - (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate; +#else // [macOS +- (NSRange)selectedTextRange; +- (void)setSelectedTextRange:(NSRange)selectedTextRange NS_UNAVAILABLE; +- (void)setSelectedTextRange:(NSRange)selectedTextRange notifyDelegate:(BOOL)notifyDelegate; +#endif // macOS] + +#if TARGET_OS_OSX // [macOS +// UITextInput method for OSX +- (CGSize)sizeThatFits:(CGSize)size; +#endif // macOS] // This protocol disallows direct access to `text` property because // unwise usage of it can break the `attributeText` behavior. // Use `attributedText.string` instead. @property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE; +@property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm index 1f06b79070aa54..da48b69bc1c6d3 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm @@ -169,7 +169,7 @@ - (void)uiManagerWillPerformMounting NSNumber *tag = self.reactTag; - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTBaseTextInputView *baseTextInputView = (RCTBaseTextInputView *)viewRegistry[tag]; if (!baseTextInputView) { return; @@ -222,7 +222,19 @@ - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximu if (!_textStorage) { _textContainer = [NSTextContainer new]; +#if !TARGET_OS_OSX // [macOS] _textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. +#else // [macOS + // macOS has a bug in multiline where setting the real text view's lineFragmentPadding to 0 will + // cause the scroll view to scroll to top when inserting a newline at the bottom of + // a NSTextView when it has more rows than can be displayed on screen. The shadow needs to match + // the NSTextView that it is tracking. + if (_maximumNumberOfLines != 1) { + _textContainer.lineFragmentPadding = 1; + } else { + _textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. + } +#endif // macOS] _layoutManager = [NSLayoutManager new]; [_layoutManager addTextContainer:_textContainer]; _textStorage = [NSTextStorage new]; @@ -236,8 +248,13 @@ - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximu CGSize size = [_layoutManager usedRectForTextContainer:_textContainer].size; return (CGSize){ +#if !TARGET_OS_OSX // [macOS] MAX(minimumSize.width, MIN(RCTCeilPixelValue(size.width), maximumSize.width)), MAX(minimumSize.height, MIN(RCTCeilPixelValue(size.height), maximumSize.height))}; +#else // [macOS + MAX(minimumSize.width, MIN(RCTCeilPixelValue(size.width, [self scale]), maximumSize.width)), + MAX(minimumSize.height, MIN(RCTCeilPixelValue(size.height, [self scale]), maximumSize.height))}; +#endif // macOS] } - (CGFloat)lastBaselineForSize:(CGSize)size diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h index 209947de9b4aaa..ac88357fb9a93e 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @@ -26,8 +26,12 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; -@property (nonatomic, readonly) UIView *backedTextInputView; +@property (nonatomic, readonly) RCTUIView *backedTextInputView; // [macOS] +/** + Whether this text input ignores the `textAttributes` property. Defaults to `NO`. If set to `YES`, the value of `textAttributes` will be ignored in favor of standard text input behavior. + */ +@property (nonatomic) BOOL ignoresTextAttributes; // [macOS] @property (nonatomic, strong, nullable) RCTTextAttributes *textAttributes; @property (nonatomic, assign) UIEdgeInsets reactPaddingInsets; @property (nonatomic, assign) UIEdgeInsets reactBorderInsets; @@ -35,10 +39,19 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; @property (nonatomic, copy, nullable) RCTDirectEventBlock onSelectionChange; @property (nonatomic, copy, nullable) RCTDirectEventBlock onChange; +@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; // [macOS] @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; @property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput; @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; - +#if TARGET_OS_OSX // [macOS +@property (nonatomic, copy, nullable) RCTBubblingEventBlock onAutoCorrectChange; +@property (nonatomic, copy, nullable) RCTBubblingEventBlock onSpellCheckChange; +@property (nonatomic, copy, nullable) RCTBubblingEventBlock onGrammarCheckChange; +@property (nonatomic, assign) BOOL clearTextOnSubmit; +@property (nonatomic, copy, nullable) RCTDirectEventBlock onSubmitEditing; +@property (nonatomic, copy) NSArray *submitKeyEvents; +@property (nonatomic, strong, nullable) RCTUIColor *cursorColor; +#endif // macOS] @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign, readonly) NSInteger nativeEventCount; @property (nonatomic, assign) BOOL autoFocus; @@ -49,9 +62,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) RCTTextSelection *selection; @property (nonatomic, strong, nullable) NSNumber *maxLength; @property (nonatomic, copy, nullable) NSAttributedString *attributedText; +@property (nonatomic, copy) NSString *predictedText; // [macOS] @property (nonatomic, copy) NSString *inputAccessoryViewID; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign) UIKeyboardType keyboardType; @property (nonatomic, assign) BOOL showSoftInputOnFocus; +#endif // [macOS] + +@property (nonatomic, copy, nullable) NSString *ghostText; // [macOS] /** Sets selection intext input if both start and end are within range of the text input. diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index b0d71dcd3508bb..c7b66f0cfb892d 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -15,10 +15,14 @@ #import #import +#import // [macOS] #import #import #import #import +#import // [macOS] +#import "../RCTTextUIKit.h" // [macOS] +#import // [macOS] /** Native iOS text field bottom keyboard offset amount */ static const CGFloat kSingleLineKeyboardBottomOffset = 15.0; @@ -26,11 +30,15 @@ @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; __weak id _eventDispatcher; + + NSInteger _ghostTextPosition; // [macOS] only valid if _ghostText != nil + BOOL _hasInputAccessoryView; - NSString *_Nullable _predictedText; + // [macOS] remove explicit _predictedText ivar declaration BOOL _didMoveToWindow; } +#if !TARGET_OS_OSX // [macOS] - (void)reactUpdateResponderOffsetForScrollView:(RCTScrollView *)scrollView { if (![self isDescendantOfView:scrollView]) { @@ -54,12 +62,13 @@ - (void)reactUpdateResponderOffsetForScrollView:(RCTScrollView *)scrollView } scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil]; } +#endif // [macOS] - (instancetype)initWithBridge:(RCTBridge *)bridge { RCTAssertParam(bridge); - if (self = [super initWithFrame:CGRectZero]) { + if (self = [super initWithEventDispatcher:bridge.eventDispatcher]) { // [macOS] _bridge = bridge; _eventDispatcher = bridge.eventDispatcher; } @@ -69,9 +78,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge RCT_NOT_IMPLEMENTED(-(instancetype)init) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)decoder) -RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) -- (UIView *)backedTextInputView + +- (RCTUIView *)backedTextInputView // [macOS] { RCTAssert(NO, @"-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass."); return nil; @@ -94,30 +103,36 @@ - (void)setTextAttributes:(RCTTextAttributes *)textAttributes - (void)enforceTextAttributesIfNeeded { - id backedTextInputView = self.backedTextInputView; + if (![self ignoresTextAttributes]) { // [macOS] + id backedTextInputView = self.backedTextInputView; NSDictionary *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy]; if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) { - [textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName]; + [textAttributes setValue:[RCTUIColor blackColor] forKey:NSForegroundColorAttributeName]; // [macOS] } - backedTextInputView.defaultTextAttributes = textAttributes; + backedTextInputView.defaultTextAttributes = textAttributes; + } // [macOS] } - (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets { _reactPaddingInsets = reactPaddingInsets; +#if !TARGET_OS_OSX // [macOS] // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`. self.backedTextInputView.textContainerInset = reactPaddingInsets; [self setNeedsLayout]; +#endif // [macOS] } - (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets { _reactBorderInsets = reactBorderInsets; +#if !TARGET_OS_OSX // [macOS] // We apply `borderInsets` as `backedTextInputView` layout offset. self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets); [self setNeedsLayout]; +#endif // [macOS] } - (NSAttributedString *)attributedText @@ -147,6 +162,7 @@ - (BOOL)textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTex } }]; +#if !TARGET_OS_OSX // [macOS] BOOL shouldFallbackDictation = [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"]; if (@available(iOS 16.0, *)) { shouldFallbackDictation = self.backedTextInputView.dictationRecognizing; @@ -155,6 +171,12 @@ - (BOOL)textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTex BOOL shouldFallbackToBareTextComparison = shouldFallbackDictation || [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] || self.backedTextInputView.markedTextRange || self.backedTextInputView.isSecureTextEntry || +#else // [macOS + BOOL shouldFallbackToBareTextComparison = + // There are multiple Korean input sources (2-Set, 3-Set, etc). Check substring instead instead + [[[self.backedTextInputView inputContext] selectedKeyboardInputSource] containsString:@"com.apple.inputmethod.Korean"] || + [self.backedTextInputView hasMarkedText] || [self.backedTextInputView isKindOfClass:[NSSecureTextField class]] || +#endif // macOS] fontHasBeenUpdatedBySystem; if (shouldFallbackToBareTextComparison) { @@ -170,7 +192,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText BOOL textNeedsUpdate = NO; // Remove tag attribute to ensure correct attributed string comparison. NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy]; - NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy]; + NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy] ?: [NSMutableAttributedString new]; [backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName range:NSMakeRange(0, backedTextInputViewTextCopy.length)]; @@ -180,12 +202,23 @@ - (void)setAttributedText:(NSAttributedString *)attributedText textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO); - if (eventLag == 0 && textNeedsUpdate) { + if ((eventLag == 0 || self.backedTextInputView.ghostTextChanging) && textNeedsUpdate) { // [macOS] +#if !TARGET_OS_OSX // [macOS] UITextRange *selection = self.backedTextInputView.selectedTextRange; - NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length; +#else // [macOS + NSRange selection = [self.backedTextInputView selectedTextRange]; +#endif // macOS] + NSAttributedString *oldAttributedText = [self.backedTextInputView.attributedText copy]; + NSInteger oldTextLength = oldAttributedText.string.length; + + [self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) { + strongSelf.attributedText = oldAttributedText; + [strongSelf textInputDidChange]; + }]; self.backedTextInputView.attributedText = attributedText; +#if !TARGET_OS_OSX // [macOS] if (selection.empty) { // Maintaining a cursor position relative to the end of the old text. NSInteger offsetStart = [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument @@ -196,8 +229,18 @@ - (void)setAttributedText:(NSAttributedString *)attributedText [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument offset:newOffset]; [self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position] - notifyDelegate:YES]; + notifyDelegate:!self.backedTextInputView.ghostTextChanging]; // [macOS] + } +#else // [macOS + if (selection.length == 0) { + // Maintaining a cursor position relative to the end of the old text. + NSInteger start = selection.location; + NSInteger offsetFromEnd = oldTextLength - start; + NSInteger newOffset = self.backedTextInputView.attributedText.length - offsetFromEnd; + [self.backedTextInputView setSelectedTextRange:NSMakeRange(newOffset, 0) + notifyDelegate:!self.backedTextInputView.ghostTextChanging]; } +#endif // macOS] [self updateLocalData]; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { @@ -211,12 +254,18 @@ - (void)setAttributedText:(NSAttributedString *)attributedText - (RCTTextSelection *)selection { id backedTextInputView = self.backedTextInputView; +#if !TARGET_OS_OSX // [macOS] UITextRange *selectedTextRange = backedTextInputView.selectedTextRange; return [[RCTTextSelection new] initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start] end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]]; +#else // [macOS + NSRange selectedTextRange = backedTextInputView.selectedTextRange; + return [[RCTTextSelection new] initWithStart:selectedTextRange.location + end:selectedTextRange.location + selectedTextRange.length]; +#endif // macOS] } - (void)setSelection:(RCTTextSelection *)selection @@ -227,15 +276,23 @@ - (void)setSelection:(RCTTextSelection *)selection id backedTextInputView = self.backedTextInputView; +#if !TARGET_OS_OSX // [macOS] UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange; UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.start]; UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.end]; UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end]; - +#else // [macOS + NSRange previousSelectedTextRange = backedTextInputView.selectedTextRange; + NSInteger start = MIN(selection.start, selection.end); + NSInteger end = MAX(selection.start, selection.end); + NSInteger length = end - selection.start; + NSRange selectedTextRange = NSMakeRange(start, length); +#endif // macOS] + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) { + if (eventLag == 0 && !RCTTextSelectionEqual(previousSelectedTextRange, selectedTextRange)) { // [macOS] [backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO]; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLog( @@ -247,6 +304,7 @@ - (void)setSelection:(RCTTextSelection *)selection - (void)setSelectionStart:(NSInteger)start selectionEnd:(NSInteger)end { +#if !TARGET_OS_OSX // [macOS] UITextPosition *startPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument offset:start]; UITextPosition *endPosition = @@ -255,10 +313,16 @@ - (void)setSelectionStart:(NSInteger)start selectionEnd:(NSInteger)end UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; [self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO]; } +#else // [macOS + NSInteger startPosition = MIN(start, end); + NSInteger endPosition = MAX(start, end); + [self.backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:NO]; +#endif // macOS] } - (void)setTextContentType:(NSString *)type { +#if !TARGET_OS_OSX // [macOS] static dispatch_once_t onceToken; static NSDictionary *contentTypeMap; @@ -321,8 +385,10 @@ - (void)setTextContentType:(NSString *)type // Setting textContentType to an empty string will disable any // default behaviour, like the autofill bar for password inputs self.backedTextInputView.textContentType = contentTypeMap[type] ?: type; +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] - (void)setPasswordRules:(NSString *)descriptor { self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor]; @@ -363,6 +429,7 @@ - (void)setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus self.backedTextInputView.inputView = [UIView new]; } } +#endif // [macOS] #pragma mark - RCTBackedTextInputDelegate @@ -378,7 +445,13 @@ - (void)textInputDidBeginEditing } if (_selectTextOnFocus) { - [self.backedTextInputView selectAll:nil]; + if ([self.backedTextInputView respondsToSelector:@selector(selectAll:)]) { + [self.backedTextInputView selectAll:nil]; + } +#if TARGET_OS_OSX // [macOS + } else { + [self.backedTextInputView setSelectedTextRange:NSMakeRange(NSNotFound, 0) notifyDelegate:NO]; +#endif // macOS] } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus @@ -395,6 +468,8 @@ - (BOOL)textInputShouldEndEditing - (void)textInputDidEndEditing { + self.ghostText = nil; // [macOS] + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:[self.backedTextInputView.attributedText.string copy] @@ -408,6 +483,69 @@ - (void)textInputDidEndEditing eventCount:_nativeEventCount]; } +#if TARGET_OS_OSX // [macOS +- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled +{ + if (_onAutoCorrectChange) { + _onAutoCorrectChange(@{@"enabled": [NSNumber numberWithBool:enabled]}); + } +} + +- (void)continuousSpellCheckingDidChange:(BOOL)enabled +{ + if (_onSpellCheckChange) { + _onSpellCheckChange(@{@"enabled": [NSNumber numberWithBool:enabled]}); + } +} + +- (void)grammarCheckingDidChange:(BOOL)enabled +{ + if (_onGrammarCheckChange) { + _onGrammarCheckChange(@{@"enabled": [NSNumber numberWithBool:enabled]}); + } +} + +- (void)submitOnKeyDownIfNeeded:(NSEvent *)event +{ + NSDictionary *currentKeyboardEvent = [RCTViewKeyboardEvent bodyFromEvent:event]; + // Enter is the default clearTextOnSubmit key + BOOL shouldSubmit = NO; + if (!_submitKeyEvents) { + shouldSubmit = [currentKeyboardEvent[@"key"] isEqualToString:@"Enter"] + && ![currentKeyboardEvent[@"altKey"] boolValue] + && ![currentKeyboardEvent[@"shiftKey"] boolValue] + && ![currentKeyboardEvent[@"ctrlKey"] boolValue] + && ![currentKeyboardEvent[@"metaKey"] boolValue] + && ![currentKeyboardEvent[@"functionKey"] boolValue]; // Default clearTextOnSubmit key + } else { + for (NSDictionary *submitKeyEvent in _submitKeyEvents) { + if ( + [submitKeyEvent[@"key"] isEqualToString:currentKeyboardEvent[@"key"]] && + [submitKeyEvent[@"altKey"] boolValue] == [currentKeyboardEvent[@"altKey"] boolValue] && + [submitKeyEvent[@"shiftKey"] boolValue] == [currentKeyboardEvent[@"shiftKey"] boolValue] && + [submitKeyEvent[@"ctrlKey"] boolValue]== [currentKeyboardEvent[@"ctrlKey"] boolValue] && + [submitKeyEvent[@"metaKey"] boolValue]== [currentKeyboardEvent[@"metaKey"] boolValue] && + [submitKeyEvent[@"functionKey"] boolValue]== [currentKeyboardEvent[@"functionKey"] boolValue] + ) { + shouldSubmit = YES; + break; + } + } + } + + if (shouldSubmit) { + if (_onSubmitEditing) { + _onSubmitEditing(@{}); + } + + if (_clearTextOnSubmit) { + self.backedTextInputView.attributedText = [NSAttributedString new]; + [self.backedTextInputView.textInputDelegate textInputDidChange]; + } + } +} +#endif // macOS] + - (BOOL)textInputShouldSubmitOnReturn { const BOOL shouldSubmit = @@ -441,6 +579,8 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range { id backedTextInputView = self.backedTextInputView; + self.ghostText = nil; // [macOS] + if (!backedTextInputView.textWasPasted) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress reactTag:self.reactTag @@ -477,15 +617,20 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range [newAttributedText replaceCharactersInRange:range withString:limitedString]; } backedTextInputView.attributedText = newAttributedText; - _predictedText = newAttributedText.string; + [self setPredictedText:newAttributedText.string]; // [macOS] // Collapse selection at end of insert to match normal paste behavior. +#if !TARGET_OS_OSX // [macOS] UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:(range.location + allowedLength)]; [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] notifyDelegate:YES]; - +#else // [macOS + [backedTextInputView setSelectedTextRange:NSMakeRange(range.location + allowedLength, 0) + notifyDelegate:YES]; +#endif // macOS] + [self textInputDidChange]; } @@ -502,11 +647,11 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range withString:text]; } - if (_onTextInput) { + if (_onTextInput && !self.backedTextInputView.ghostTextChanging) { // [macOS] _onTextInput(@{ // We copy the string here because if it's a mutable string it may get released before we stop using it on a // different thread, causing a crash. - @"text" : [text copy], + @"text" : [text copy] ?: @"", // [macOS] fall back to empty string if text is nil @"previousText" : previousText, @"range" : @{@"start" : @(range.location), @"end" : @(range.location + range.length)}, @"eventCount" : @(_nativeEventCount), @@ -528,27 +673,32 @@ - (void)textInputDidChange // update the mismatched range. NSRange currentRange; NSRange predictionRange; - if (findMismatch(backedTextInputView.attributedText.string, _predictedText, ¤tRange, &predictionRange)) { + if (findMismatch(backedTextInputView.attributedText.string, [self predictedText], ¤tRange, &predictionRange)) { // [macOS] NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange]; [self textInputShouldChangeText:replacement inRange:predictionRange]; // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it. [self textInputDidChangeSelection]; + [self setPredictedText:backedTextInputView.attributedText.string]; // [macOS] } - _nativeEventCount++; + if (!self.backedTextInputView.ghostTextChanging) { // [macOS] + _nativeEventCount++; - if (_onChange) { - _onChange(@{ - @"text" : [self.attributedText.string copy], - @"target" : self.reactTag, - @"eventCount" : @(_nativeEventCount), - }); - } + if (_onChange) { + _onChange(@{ + @"text" : [self.attributedText.string copy], + @"target" : self.reactTag, + @"eventCount" : @(_nativeEventCount), + }); + } + } // [macOS] } - (void)textInputDidChangeSelection { - if (!_onSelectionChange) { + self.ghostText = nil; // [macOS] + + if (!_onSelectionChange || self.backedTextInputView.ghostTextChanging) { // [macOS] return; } @@ -562,6 +712,76 @@ - (void)textInputDidChangeSelection }); } +// [macOS +- (BOOL)textInputShouldHandleDeleteBackward:(__unused id)sender { + return YES; +} +// macOS] + +#if TARGET_OS_OSX // [macOS +- (BOOL)textInputShouldHandleDeleteForward:(__unused id)sender { + return YES; +} + +- (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key { + return [RCTHandledKey key:key matchesFilter:self.validKeysDown] + || [RCTHandledKey key:key matchesFilter:self.validKeysUp]; +} + +- (NSDragOperation)textInputDraggingEntered:(id)draggingInfo +{ + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + return [self draggingEntered:draggingInfo]; + } + return NSDragOperationNone; +} + +- (void)textInputDraggingExited:(id)draggingInfo +{ + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self draggingExited:draggingInfo]; + } +} + +- (BOOL)textInputShouldHandleDragOperation:(id)draggingInfo +{ + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self performDragOperation:draggingInfo]; + return NO; + } + + return YES; +} + +- (void)textInputDidCancel { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress + reactTag:self.reactTag + text:nil + key:@"Escape" + eventCount:_nativeEventCount]; + [self textInputDidEndEditing]; +} + +- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event { + return ![self handleKeyboardEvent:event]; +} + +- (BOOL)textInputShouldHandlePaste:(__unused id)sender +{ + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + NSArray* pastedTypes = ((RCTUITextView*) self.backedTextInputView).readablePasteboardTypes; + + // If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste + if (_onPaste && fileType != nil && [pastedTypes containsObject:fileType]) { + _onPaste([self dataTransferInfoFromPasteboard:pasteboard]); + } + + // Only allow pasting text. + return fileType == nil; +} +#endif // macOS] + - (void)updateLocalData { [self enforceTextAttributesIfNeeded]; @@ -601,7 +821,7 @@ - (CGSize)sizeThatFits:(CGSize)size #pragma mark - Accessibility -- (UIView *)reactAccessibilityElement +- (RCTUIView *)reactAccessibilityElement // [macOS] { return self.backedTextInputView; } @@ -642,6 +862,7 @@ - (void)didSetProps:(NSArray *)changedProps - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID { +#if !TARGET_OS_OSX // [macOS] __weak RCTBaseTextInputView *weakSelf = self; [_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) { @@ -656,10 +877,12 @@ - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID } } }]; +#endif // [macOS] } - (void)setDefaultInputAccessoryView { +#if !TARGET_OS_OSX // [macOS] UIView *textInputView = self.backedTextInputView; UIKeyboardType keyboardType = textInputView.keyboardType; @@ -691,8 +914,10 @@ - (void)setDefaultInputAccessoryView textInputView.inputAccessoryView = nil; } [self reloadInputViewsIfNecessary]; +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] - (void)reloadInputViewsIfNecessary { // We have to call `reloadInputViews` for focused text inputs to update an accessory view. @@ -709,6 +934,93 @@ - (void)handleInputAccessoryDoneButton [self.backedTextInputView endEditing:YES]; } } +#endif // [macOS] + +// [macOS + +- (NSDictionary *)ghostTextAttributes +{ + RCTUIView *backedTextInputView = self.backedTextInputView; + NSMutableDictionary *textAttributes = + [backedTextInputView.defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new]; + + if (@available(iOS 13.0, *)) { + [textAttributes setValue:backedTextInputView.placeholderColor ?: [RCTUIColor placeholderTextColor] + forKey:NSForegroundColorAttributeName]; + } else { + if (backedTextInputView.placeholderColor) { + [textAttributes setValue:backedTextInputView.placeholderColor forKey:NSForegroundColorAttributeName]; + } else { + [textAttributes removeObjectForKey:NSForegroundColorAttributeName]; + } + } + + return textAttributes; +} + +- (void)setGhostText:(NSString *)ghostText { + RCTTextSelection *selection = self.selection; + NSString *newGhostText = ghostText.length > 0 ? ghostText : nil; + + if (selection.start != selection.end) { + newGhostText = nil; + } + + if ((_ghostText == nil && newGhostText == nil) || [_ghostText isEqual:newGhostText]) { + return; + } + + if (self.backedTextInputView.ghostTextChanging) { + // look out for nested callbacks -- this can happen for example when selection changes in response to + // attributed text changing. Such callbacks are initiated by Apple, or we could suppress this other ways. + return; + } + + self.backedTextInputView.ghostTextChanging = YES; + + if (_ghostText != nil) { + BOOL shouldDeleteGhostText = YES; + NSRange ghostTextRange = NSMakeRange(_ghostTextPosition, _ghostText.length); + NSMutableAttributedString *attributedString = [self.attributedText mutableCopy]; + + if ([attributedString length] < NSMaxRange(ghostTextRange)) { + RCTAssert(false, @"Ghost text not fully present in text view text"); + shouldDeleteGhostText = NO; + } + + NSString *actualGhostText = shouldDeleteGhostText + ? [[attributedString attributedSubstringFromRange:ghostTextRange] string] + : nil; + + if (![actualGhostText isEqual:_ghostText]) { + RCTAssert(false, @"Ghost text does not match text view text"); + shouldDeleteGhostText = NO; + } + + if (shouldDeleteGhostText) { + [attributedString deleteCharactersInRange:ghostTextRange]; + self.attributedText = attributedString; + [self setSelectionStart:selection.start selectionEnd:selection.end]; + } + } + + _ghostText = [newGhostText copy]; + _ghostTextPosition = selection.start; + + if (_ghostText != nil) { + NSMutableAttributedString *attributedString = [self.attributedText mutableCopy]; + NSAttributedString *ghostAttributedString = [[NSAttributedString alloc] initWithString:_ghostText + attributes:self.ghostTextAttributes]; + + [attributedString insertAttributedString:ghostAttributedString atIndex:_ghostTextPosition]; + self.attributedText = attributedString; + [self setSelectionStart:_ghostTextPosition selectionEnd:_ghostTextPosition]; + } + + self.backedTextInputView.ghostTextChanging = NO; +} + +// macOS] #pragma mark - Helpers @@ -741,4 +1053,15 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, return YES; } +#if TARGET_OS_OSX // [macOS + +#pragma mark - NSResponder chain + +- (BOOL)canBecomeKeyView +{ + return NO; // Enclosed backedTextInputView can become the key view +} + +#endif // macOS] + @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm index a19b55569e8d71..3dc2359f3ed8f6 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -18,7 +18,10 @@ #import #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] +#import // [macOS] @interface RCTBaseTextInputViewManager () @@ -32,27 +35,30 @@ @implementation RCTBaseTextInputViewManager { #pragma mark - Unified properties -RCT_REMAP_VIEW_PROPERTY(autoCapitalize, backedTextInputView.autocapitalizationType, UITextAutocapitalizationType) -RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType) +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(autoCapitalize, backedTextInputView.autocapitalizationType, UITextAutocapitalizationType) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType) // [macOS] +RCT_REMAP_OSX_VIEW_PROPERTY(autoCorrect, backedTextInputView.automaticSpellingCorrectionEnabled, BOOL) // [macOS] RCT_REMAP_VIEW_PROPERTY(contextMenuHidden, backedTextInputView.contextMenuHidden, BOOL) RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL) -RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL) -RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance) +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance) // [macOS] RCT_REMAP_VIEW_PROPERTY(placeholder, backedTextInputView.placeholder, NSString) RCT_REMAP_VIEW_PROPERTY(placeholderTextColor, backedTextInputView.placeholderColor, UIColor) -RCT_REMAP_VIEW_PROPERTY(returnKeyType, backedTextInputView.returnKeyType, UIReturnKeyType) -RCT_REMAP_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor) -RCT_REMAP_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType) -RCT_REMAP_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL) -RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode) -RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL) -RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL) -RCT_REMAP_VIEW_PROPERTY(smartInsertDelete, backedTextInputView.smartInsertDeleteType, UITextSmartInsertDeleteType) +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(returnKeyType, backedTextInputView.returnKeyType, UIReturnKeyType) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor) // [macOS] +RCT_REMAP_OSX_VIEW_PROPERTY(selectionColor, backedTextInputView.selectionColor, UIColor) // [macOS] +RCT_REMAP_OSX_VIEW_PROPERTY(grammarCheck, backedTextInputView.grammarCheckingEnabled, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType)// [macOS] +RCT_REMAP_OSX_VIEW_PROPERTY(spellCheck, backedTextInputView.continuousSpellCheckingEnabled, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(smartInsertDelete, backedTextInputView.smartInsertDeleteType, UITextSmartInsertDeleteType) // [macOS] RCT_EXPORT_VIEW_PROPERTY(autoFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(submitBehavior, NSString) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) -RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) -RCT_EXPORT_VIEW_PROPERTY(showSoftInputOnFocus, BOOL) +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(keyboardType, backedTextInputView.keyboardType, UIKeyboardType) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(showSoftInputOnFocus, BOOL) // [macOS] RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection) @@ -60,10 +66,24 @@ @implementation RCTBaseTextInputViewManager { RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString) RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString) +#if TARGET_OS_OSX // [macOS +RCT_EXPORT_VIEW_PROPERTY(onAutoCorrectChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onSpellCheckChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onGrammarCheckChange, RCTBubblingEventBlock); + +// Specifically for clearing text on enter key press +RCT_EXPORT_VIEW_PROPERTY(clearTextOnSubmit, BOOL); +RCT_EXPORT_VIEW_PROPERTY(onSubmitEditing, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(submitKeyEvents, NSArray); + +RCT_REMAP_VIEW_PROPERTY(cursorColor, backedTextInputView.cursorColor, UIColor) +#endif // macOS] + RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) @@ -73,7 +93,7 @@ @implementation RCTBaseTextInputViewManager { RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString) RCT_EXPORT_SHADOW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock) -RCT_CUSTOM_VIEW_PROPERTY(multiline, BOOL, UIView) +RCT_CUSTOM_VIEW_PROPERTY(multiline, BOOL, RCTUIView) // [macOS] { // No op. // This View Manager doesn't use this prop but it must be exposed here via ViewConfig to enable Fabric component use @@ -83,9 +103,11 @@ @implementation RCTBaseTextInputViewManager { - (RCTShadowView *)shadowView { RCTBaseTextInputShadowView *shadowView = [[RCTBaseTextInputShadowView alloc] initWithBridge:self.bridge]; +#if !TARGET_OS_OSX // [macOS] shadowView.textAttributes.fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager" lazilyLoadIfNecessary:YES] valueForKey:@"multiplier"] floatValue]; +#endif // [macOS] [_shadowViews addObject:shadowView]; return shadowView; } @@ -98,25 +120,27 @@ - (void)setBridge:(RCTBridge *)bridge [bridge.uiManager.observerCoordinator addObserver:self]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDidUpdateMultiplierNotification) name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification" object:[bridge moduleForName:@"AccessibilityManager" lazilyLoadIfNecessary:YES]]; +#endif // [macOS] } RCT_EXPORT_METHOD(focus : (nonnull NSNumber *)viewTag) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[viewTag]; + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[viewTag]; // [macOS] [view reactFocus]; }]; } RCT_EXPORT_METHOD(blur : (nonnull NSNumber *)viewTag) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[viewTag]; + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[viewTag]; // [macOS] [view reactBlur]; }]; } @@ -128,7 +152,7 @@ - (void)setBridge:(RCTBridge *)bridge : (NSInteger)start end : (NSInteger)end) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTBaseTextInputView *view = (RCTBaseTextInputView *)viewRegistry[viewTag]; NSInteger eventLag = view.nativeEventCount - mostRecentEventCount; if (eventLag != 0) { @@ -148,6 +172,17 @@ - (void)setBridge:(RCTBridge *)bridge }]; } +// [macOS +RCT_EXPORT_METHOD(setGhostText + :(nonnull NSNumber *)reactTag + :(NSString *)text) { + + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [(RCTBaseTextInputView *)viewRegistry[reactTag] setGhostText:text]; + }]; +} +// macOS] + #pragma mark - RCTUIManagerObserver - (void)uiManagerWillPerformMounting:(__unused RCTUIManager *)uiManager diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryShadowView.mm b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryShadowView.mm index ebe514e037f486..5a78c4a01730f2 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryShadowView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryShadowView.mm @@ -14,7 +14,9 @@ @implementation RCTInputAccessoryShadowView - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; +#if !TARGET_OS_OSX // [macOS] subview.width = (YGValue){static_cast(RCTScreenSize().width), YGUnitPoint}; +#endif // [macOS] } @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.h b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.h index f86370ce5cb9cb..1c07cc81a0ac39 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.h @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @class RCTBridge; @class RCTInputAccessoryViewContent; -@interface RCTInputAccessoryView : UIView +@interface RCTInputAccessoryView : RCTUIView // [macOS] - (instancetype)initWithBridge:(RCTBridge *)bridge; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.mm b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.mm index f41263651b75f5..bc169262b62062 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryView.mm @@ -16,7 +16,7 @@ @interface RCTInputAccessoryView () // Overriding `inputAccessoryView` to `readwrite`. -@property (nonatomic, readwrite, retain) UIView *inputAccessoryView; +@property (nonatomic, readwrite, retain) RCTUIView *inputAccessoryView; // [macOS] @end @@ -49,13 +49,13 @@ - (void)reactSetFrame:(CGRect)frame } } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index +- (void)insertReactSubview:(RCTUIView *)subview atIndex:(NSInteger)index // [macOS] { [super insertReactSubview:subview atIndex:index]; [_inputAccessoryView insertReactSubview:subview atIndex:index]; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTUIView *)subview // [macOS] { [super removeReactSubview:subview]; [_inputAccessoryView removeReactSubview:subview]; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.h b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.h index e07b07d785b4fc..e7a03df0523690 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.h @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] -@interface RCTInputAccessoryViewContent : UIView +@interface RCTInputAccessoryViewContent : RCTUIView // [macOS] @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.mm b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.mm index 7fd8808c14fc4d..74fd5d51aed00f 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewContent.mm @@ -10,14 +10,14 @@ #import @implementation RCTInputAccessoryViewContent { - UIView *_safeAreaContainer; + RCTUIView *_safeAreaContainer; // [macOS] NSLayoutConstraint *_heightConstraint; } - (instancetype)init { if (self = [super init]) { - _safeAreaContainer = [UIView new]; + _safeAreaContainer = [RCTUIView new]; // [macOS] [self addSubview:_safeAreaContainer]; // Use autolayout to position the view properly and take into account @@ -29,10 +29,17 @@ - (instancetype)init _heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0]; _heightConstraint.active = YES; +#if !TARGET_OS_OSX // [macOS] [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES; [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES; [_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor].active = YES; [_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor].active = YES; +#else // [macOS + [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES; + [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES; + [_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = YES; + [_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = YES; +#endif // macOS] } return self; } @@ -56,13 +63,13 @@ - (void)reactSetFrame:(CGRect)frame [self layoutIfNeeded]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index +- (void)insertReactSubview:(RCTUIView *)subview atIndex:(NSInteger)index // [macOS] { [super insertReactSubview:subview atIndex:index]; [_safeAreaContainer insertSubview:subview atIndex:index]; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTUIView *)subview // [macOS] { [super removeReactSubview:subview]; [subview removeFromSuperview]; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewManager.mm b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewManager.mm index 39ba1564754af6..8ab7f2b20cd61f 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTInputAccessoryViewManager.mm @@ -19,7 +19,7 @@ + (BOOL)requiresMainQueueSetup return NO; } -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [[RCTInputAccessoryView alloc] initWithBridge:self.bridge]; } diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.mm index eeb1569321078e..9973da7611fc92 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.mm @@ -9,10 +9,14 @@ #import -#import +#include +#if TARGET_OS_OSX // [macOS +#include +#endif // macOS] @implementation RCTSinglelineTextInputView { RCTUITextField *_backedTextInputView; + BOOL _useSecureTextField; // [macOS] } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -23,6 +27,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds]; _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#if TARGET_OS_OSX // [macOS + _backedTextInputView.cell.scrollable = YES; + _backedTextInputView.cell.usesSingleLineMode = YES; +#endif // macOS] _backedTextInputView.textInputDelegate = self; [self addSubview:_backedTextInputView]; @@ -36,4 +44,59 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return _backedTextInputView; } +#if TARGET_OS_OSX // [macOS +- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets +{ + [super setReactPaddingInsets:reactPaddingInsets]; + // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInsets` on mac. + ((RCTUITextField*)self.backedTextInputView).textContainerInset = reactPaddingInsets; + [self setNeedsLayout]; +} + +- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets +{ + [super setReactBorderInsets:reactBorderInsets]; + // We apply `borderInsets` as `backedTextInputView`'s layout offset on mac. + ((RCTUITextField*)self.backedTextInputView).frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets); + [self setNeedsLayout]; +} + +- (void)setUseSecureTextField:(BOOL)useSecureTextField { + if (_useSecureTextField != useSecureTextField) { + _useSecureTextField = useSecureTextField; + RCTUITextField *previousTextField = _backedTextInputView; + if (useSecureTextField) { + _backedTextInputView = (RCTUITextField *)[[RCTUISecureTextField alloc] initWithFrame:self.bounds]; + } else { + _backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds]; + } + _backedTextInputView.accessibilityElement = previousTextField.accessibilityElement; + _backedTextInputView.accessibilityHelp = previousTextField.accessibilityHelp; + _backedTextInputView.accessibilityIdentifier = previousTextField.accessibilityIdentifier; + _backedTextInputView.accessibilityLabel = previousTextField.accessibilityLabel; + _backedTextInputView.accessibilityRole = previousTextField.accessibilityRole; + _backedTextInputView.caretHidden = previousTextField.caretHidden; + _backedTextInputView.contextMenuHidden = previousTextField.contextMenuHidden; + _backedTextInputView.defaultTextAttributes = previousTextField.defaultTextAttributes; + _backedTextInputView.editable = previousTextField.editable; + _backedTextInputView.placeholder = previousTextField.placeholder; + _backedTextInputView.placeholderColor = previousTextField.placeholderColor; + _backedTextInputView.selectionColor = previousTextField.selectionColor; + _backedTextInputView.textAlignment = previousTextField.textAlignment; + _backedTextInputView.textContainerInset = previousTextField.textContainerInset; + _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _backedTextInputView.textInputDelegate = self; + _backedTextInputView.text = previousTextField.text; + [self replaceSubview:previousTextField with:_backedTextInputView]; + } +} + +- (void)setEnableFocusRing:(BOOL)enableFocusRing { + [super setEnableFocusRing:enableFocusRing]; + if ([_backedTextInputView respondsToSelector:@selector(setEnableFocusRing:)]) { + [_backedTextInputView setEnableFocusRing:enableFocusRing]; + } +} +#endif // macOS] + @end diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm index 413ac42238783a..e8b9b7b0cb9dbd 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm @@ -23,9 +23,11 @@ - (RCTShadowView *)shadowView return shadowView; } -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [[RCTSinglelineTextInputView alloc] initWithBridge:self.bridge]; } +RCT_REMAP_OSX_VIEW_PROPERTY(secureTextEntry, useSecureTextField, BOOL) // [macOS] + @end diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h index 91f8eb087acf87..ae55ec1c8b0683 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] + +#import "RCTTextUIKit.h" // [macOS] #import #import @@ -15,7 +17,16 @@ NS_ASSUME_NONNULL_BEGIN /* * Just regular UITextField... but much better! */ +#if !TARGET_OS_OSX // [macOS] @interface RCTUITextField : UITextField +#else // [macOS +#if RCT_SUBCLASS_SECURETEXTFIELD +@interface RCTUISecureTextField : NSSecureTextField +#else +@interface RCTUITextField : NSTextField +#endif // RCT_SUBCLASS_SECURETEXTFIELD +#endif // macOS] + - (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; @@ -23,17 +34,42 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL contextMenuHidden; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign, readonly) BOOL textWasPasted; +#else // [macOS +@property (nonatomic, assign) BOOL textWasPasted; +#endif // macOS] @property (nonatomic, assign, readonly) BOOL dictationRecognizing; -@property (nonatomic, strong, nullable) UIColor *placeholderColor; +@property (nonatomic, strong, nullable) RCTUIColor *placeholderColor; // [macOS] @property (nonatomic, assign) UIEdgeInsets textContainerInset; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign, getter=isEditable) BOOL editable; +#else // [macOS +@property (assign, getter=isEditable) BOOL editable; +#endif // macOS] @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; @property (nonatomic, assign, readonly) CGFloat zoomScale; @property (nonatomic, assign, readonly) CGPoint contentOffset; @property (nonatomic, assign, readonly) UIEdgeInsets contentInset; +#if TARGET_OS_OSX // [macOS +@property (nonatomic, copy, nullable) NSString *text; +@property (nonatomic, copy, nullable) NSAttributedString *attributedText; +@property (nonatomic, copy) NSDictionary *defaultTextAttributes; +@property (nonatomic, assign) NSTextAlignment textAlignment; +@property (nonatomic, getter=isAutomaticTextReplacementEnabled) BOOL automaticTextReplacementEnabled; +@property (nonatomic, getter=isAutomaticSpellingCorrectionEnabled) BOOL automaticSpellingCorrectionEnabled; +@property (nonatomic, getter=isContinuousSpellCheckingEnabled) BOOL continuousSpellCheckingEnabled; +@property (nonatomic, getter=isGrammarCheckingEnabled) BOOL grammarCheckingEnabled; +@property (nonatomic, assign) BOOL enableFocusRing; +@property (nonatomic, strong, nullable) RCTUIColor *selectionColor; +@property (weak, nullable) id delegate; +@property (nonatomic, assign) CGFloat pointScaleFactor; +#endif // macOS] + +@property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index 4d0afd97ae682a..07a386846763b5 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -8,23 +8,112 @@ #import #import +#import // [macOS] #import #import #import +#import // [macOS] + +#if TARGET_OS_OSX // [macOS + +#if RCT_SUBCLASS_SECURETEXTFIELD +#define RCTUITextFieldCell RCTUISecureTextFieldCell +@interface RCTUISecureTextFieldCell : NSSecureTextFieldCell +#else +@interface RCTUITextFieldCell : NSTextFieldCell +#endif + +@property (nonatomic, assign) UIEdgeInsets textContainerInset; +@property (nonatomic, getter=isAutomaticTextReplacementEnabled) BOOL automaticTextReplacementEnabled; +@property (nonatomic, getter=isAutomaticSpellingCorrectionEnabled) BOOL automaticSpellingCorrectionEnabled; +@property (nonatomic, getter=isContinuousSpellCheckingEnabled) BOOL continuousSpellCheckingEnabled; +@property (nonatomic, getter=isGrammarCheckingEnabled) BOOL grammarCheckingEnabled; +@property (nonatomic, strong, nullable) RCTUIColor *selectionColor; +@property (nonatomic, strong, nullable) RCTUIColor *insertionPointColor; + +@end + +@implementation RCTUITextFieldCell + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + _textContainerInset = textContainerInset; +} + +- (NSRect)titleRectForBounds:(NSRect)rect +{ + return UIEdgeInsetsInsetRect([super titleRectForBounds:rect], self.textContainerInset); +} + +- (void)editWithFrame:(NSRect)rect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)delegate event:(NSEvent *)event +{ + [super editWithFrame:[self titleRectForBounds:rect] inView:controlView editor:textObj delegate:delegate event:event]; +} + +- (void)selectWithFrame:(NSRect)rect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)delegate start:(NSInteger)selStart length:(NSInteger)selLength +{ + [super selectWithFrame:[self titleRectForBounds:rect] inView:controlView editor:textObj delegate:delegate start:selStart length:selLength]; +} + +- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + if (self.drawsBackground) { + if (self.backgroundColor && self.backgroundColor.alphaComponent > 0) { + + [self.backgroundColor set]; + NSRectFill(cellFrame); + } + } + + [super drawInteriorWithFrame:[self titleRectForBounds:cellFrame] inView:controlView]; +} + +- (NSText *)setUpFieldEditorAttributes:(NSText *)textObj +{ + NSTextView *fieldEditor = (NSTextView *)[super setUpFieldEditorAttributes:textObj]; + fieldEditor.automaticSpellingCorrectionEnabled = self.isAutomaticSpellingCorrectionEnabled; + fieldEditor.automaticTextReplacementEnabled = self.isAutomaticTextReplacementEnabled; + fieldEditor.continuousSpellCheckingEnabled = self.isContinuousSpellCheckingEnabled; + fieldEditor.grammarCheckingEnabled = self.isGrammarCheckingEnabled; + NSMutableDictionary *selectTextAttributes = fieldEditor.selectedTextAttributes.mutableCopy; + selectTextAttributes[NSBackgroundColorAttributeName] = self.selectionColor ?: [NSColor selectedControlColor]; + fieldEditor.selectedTextAttributes = selectTextAttributes; + fieldEditor.insertionPointColor = self.insertionPointColor ?: [NSColor textColor]; + return fieldEditor; +} + +@end +#endif // macOS] + +#ifdef RCT_SUBCLASS_SECURETEXTFIELD +@implementation RCTUISecureTextField { +#else @implementation RCTUITextField { +#endif RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; } +#if TARGET_OS_OSX // [macOS +@dynamic delegate; +#endif // macOS] + - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_textDidChange) name:UITextFieldTextDidChangeNotification object:self]; +#if TARGET_OS_OSX // [macOS + [self setBordered:NO]; + [self setAllowsEditingTextAttributes:YES]; + [self setBackgroundColor:[NSColor clearColor]]; +#endif // macOS] + _textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self]; _scrollEnabled = YES; } @@ -35,11 +124,35 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)_textDidChange { _textWasPasted = NO; +#if TARGET_OS_OSX // [macOS + [self setAttributedText:[[NSAttributedString alloc] initWithString:[self text] + attributes:[self defaultTextAttributes]]]; + if([[self text] length] == 0) { + self.font = [[self defaultTextAttributes] objectForKey:NSFontAttributeName]; + } +#endif // macOS] +} + +#if TARGET_OS_OSX // [macOS +- (BOOL)hasMarkedText +{ + return ((NSTextView *)self.currentEditor).hasMarkedText; +} + +- (NSArray *)validAttributesForMarkedText +{ + return ((NSTextView *)self.currentEditor).validAttributesForMarkedText; } +#endif // macOS] + #pragma mark - Accessibility +#if !TARGET_OS_OSX // [macOS] - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement +#else // [macOS +- (void)setAccessibilityElement:(BOOL)isAccessibilityElement +#endif // macOS] { // UITextField is accessible by default (some nested views are) and disabling that is not supported. // On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view @@ -51,17 +164,161 @@ - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { + if (UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset)) { + return; + } + _textContainerInset = textContainerInset; +#if !TARGET_OS_OSX // [macOS] [self setNeedsLayout]; +#else // [macOS + ((RCTUITextFieldCell*)self.cell).textContainerInset = _textContainerInset; + + if (self.currentEditor) { + NSRange selectedRange = self.currentEditor.selectedRange; + + // Relocate the NSTextView without changing the selection. + [self.cell selectWithFrame:self.bounds + inView:self + editor:self.currentEditor + delegate:self + start:selectedRange.location + length:selectedRange.length]; + } +#endif // macOS] +} + +#if TARGET_OS_OSX // [macOS + ++ (Class)cellClass +{ + return RCTUITextFieldCell.class; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + self.attributedStringValue = attributedText; +} + +- (NSAttributedString *)attributedText +{ + return self.attributedStringValue; +} + +- (void)setText:(NSString *)text +{ + self.stringValue = text; +} + +- (NSString*)text +{ + return self.stringValue; +} + +- (void)setAutomaticTextReplacementEnabled:(BOOL)automaticTextReplacementEnabled +{ + ((RCTUITextFieldCell*)self.cell).automaticTextReplacementEnabled = automaticTextReplacementEnabled; +} + +- (BOOL)isAutomaticTextReplacementEnabled +{ + return ((RCTUITextFieldCell*)self.cell).isAutomaticTextReplacementEnabled; +} + +- (void)setAutomaticSpellingCorrectionEnabled:(BOOL)automaticSpellingCorrectionEnabled +{ + ((RCTUITextFieldCell*)self.cell).automaticSpellingCorrectionEnabled = automaticSpellingCorrectionEnabled; +} + +- (BOOL)isAutomaticSpellingCorrectionEnabled +{ + return ((RCTUITextFieldCell*)self.cell).isAutomaticSpellingCorrectionEnabled; +} + +- (void)setContinuousSpellCheckingEnabled:(BOOL)continuousSpellCheckingEnabled +{ + ((RCTUITextFieldCell*)self.cell).continuousSpellCheckingEnabled = continuousSpellCheckingEnabled; +} + +- (BOOL)isContinuousSpellCheckingEnabled +{ + return ((RCTUITextFieldCell*)self.cell).isContinuousSpellCheckingEnabled; +} + +- (void)setGrammarCheckingEnabled:(BOOL)grammarCheckingEnabled +{ + ((RCTUITextFieldCell*)self.cell).grammarCheckingEnabled = grammarCheckingEnabled; +} + +- (BOOL)isGrammarCheckingEnabled +{ + return ((RCTUITextFieldCell*)self.cell).isGrammarCheckingEnabled; +} + +- (void)setSelectionColor:(RCTUIColor *)selectionColor +{ + ((RCTUITextFieldCell*)self.cell).selectionColor = selectionColor; +} + +- (RCTUIColor*)selectionColor +{ + return ((RCTUITextFieldCell*)self.cell).selectionColor; +} + +- (void)setCursorColor:(NSColor *)cursorColor +{ + ((RCTUITextFieldCell*)self.cell).insertionPointColor = cursorColor; +} + +- (RCTUIColor*)cursorColor +{ + return ((RCTUITextFieldCell*)self.cell).insertionPointColor; } +- (void)setFont:(UIFont *)font +{ + ((RCTUITextFieldCell*)self.cell).font = font; +} + +- (UIFont *)font +{ + return ((RCTUITextFieldCell*)self.cell).font; +} + +- (void)setEnableFocusRing:(BOOL)enableFocusRing { + if (_enableFocusRing != enableFocusRing) { + _enableFocusRing = enableFocusRing; + } + + if (enableFocusRing) { + [self setFocusRingType:NSFocusRingTypeDefault]; + } else { + [self setFocusRingType:NSFocusRingTypeNone]; + } +} + +#endif // macOS] + - (void)setPlaceholder:(NSString *)placeholder { +#if !TARGET_OS_OSX // [macOS] [super setPlaceholder:placeholder]; +#else // [macOS + [super setPlaceholderString:placeholder]; +#endif // macOS] [self _updatePlaceholder]; } -- (void)setPlaceholderColor:(UIColor *)placeholderColor +- (NSString*)placeholder // [macOS +{ +#if !TARGET_OS_OSX // [macOS] + return super.placeholder; +#else + return self.placeholderAttributedString.string ?: self.placeholderString; +#endif +} // macOS] + +- (void)setPlaceholderColor:(RCTUIColor *)placeholderColor // [macOS] { _placeholderColor = placeholderColor; [self _updatePlaceholder]; @@ -74,8 +331,17 @@ - (void)setDefaultTextAttributes:(NSDictionary *)defa } _defaultTextAttributes = defaultTextAttributes; +#if !TARGET_OS_OSX // [macOS] [super setDefaultTextAttributes:defaultTextAttributes]; +#endif // [macOS] [self _updatePlaceholder]; + +#if TARGET_OS_OSX // [macOS + [self setAttributedText:[[NSAttributedString alloc] initWithString:[self text] + attributes:[self defaultTextAttributes]]]; + + self.font = [[self defaultTextAttributes] objectForKey:NSFontAttributeName]; +#endif // macOS] } - (NSDictionary *)defaultTextAttributes @@ -85,8 +351,13 @@ - (void)setDefaultTextAttributes:(NSDictionary *)defa - (void)_updatePlaceholder { +#if !TARGET_OS_OSX // [macOS] self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder ?: @"" attributes:[self _placeholderTextAttributes]]; +#else // [macOS + self.placeholderAttributedString = [[NSAttributedString alloc] initWithString:self.placeholder ?: @"" + attributes:[self _placeholderTextAttributes]]; +#endif // macOS] } - (BOOL)isEditable @@ -96,9 +367,15 @@ - (BOOL)isEditable - (void)setEditable:(BOOL)editable { +#if TARGET_OS_OSX // [macOS + // on macos the super must be called otherwise its NSTextFieldCell editable property doesn't get set. + [super setEditable:editable]; +#endif // macOS] self.enabled = editable; } +#if !TARGET_OS_OSX // [macOS] + - (void)setSecureTextEntry:(BOOL)secureTextEntry { if (self.secureTextEntry == secureTextEntry) { @@ -115,6 +392,9 @@ - (void)setSecureTextEntry:(BOOL)secureTextEntry self.attributedText = originalText; } +#endif // [macOS] + + #pragma mark - Placeholder - (NSDictionary *)_placeholderTextAttributes @@ -122,10 +402,17 @@ - (void)setSecureTextEntry:(BOOL)secureTextEntry NSMutableDictionary *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new]; - if (self.placeholderColor) { - [textAttributes setValue:self.placeholderColor forKey:NSForegroundColorAttributeName]; + // [macOS + if (@available(iOS 13.0, *)) { + [textAttributes setValue:self.placeholderColor ?: [RCTUIColor placeholderTextColor] + forKey:NSForegroundColorAttributeName]; } else { - [textAttributes removeObjectForKey:NSForegroundColorAttributeName]; + // macOS] + if (self.placeholderColor) { + [textAttributes setValue:self.placeholderColor forKey:NSForegroundColorAttributeName]; + } else { + [textAttributes removeObjectForKey:NSForegroundColorAttributeName]; + } } return textAttributes; @@ -133,6 +420,8 @@ - (void)setSecureTextEntry:(BOOL)secureTextEntry #pragma mark - Context Menu +#if !TARGET_OS_OSX // [macOS] + - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (_contextMenuHidden) { @@ -177,9 +466,60 @@ - (CGRect)editingRectForBounds:(CGRect)bounds { return [self textRectForBounds:bounds]; } + +#else // [macOS + +#pragma mark - NSTextFieldDelegate methods + +- (void)textDidChange:(NSNotification *)notification +{ + [super textDidChange:notification]; + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textFieldDidChange:)]) { + [delegate textFieldDidChange:self]; + } +} + +- (void)textDidEndEditing:(NSNotification *)notification +{ + [super textDidEndEditing:notification]; + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textFieldEndEditing:)]) { + [delegate textFieldEndEditing:self]; + } +} + +- (void)textViewDidChangeSelection:(NSNotification *)notification +{ + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textFieldDidChangeSelection:)]) { + [delegate textFieldDidChangeSelection:self]; + } +} + +- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)aRange replacementString:(NSString *)aString +{ + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { + return [delegate textField:self shouldChangeCharactersInRange:aRange replacementString:aString]; + } + return NO; +} + +- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex +{ + if (menu) { + [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; + } + + return menu; +} + +#endif // macOS] #pragma mark - Overrides +#if !TARGET_OS_OSX // [macOS] #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" // Overrides selectedTextRange setter to get notify when selectedTextRange changed. @@ -189,7 +529,44 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange [_textInputDelegateAdapter selectedTextRangeWasSet]; } #pragma clang diagnostic pop +#endif // [macOS] + +#if TARGET_OS_OSX // [macOS +- (BOOL)becomeFirstResponder +{ + BOOL isFirstResponder = [super becomeFirstResponder]; + if (isFirstResponder) { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(textFieldBeginEditing:)]) { + // The AppKit -[NSTextField textDidBeginEditing:] notification is only called when the user + // makes the first change to the text in the text field. + // The react-native -[RCTUITextFieldDelegate textFieldBeginEditing:] is intended to be + // called when the text field is focused so call it here in becomeFirstResponder. + [delegate textFieldBeginEditing:self]; + } + + NSScrollView *scrollView = [self enclosingScrollView]; + if (scrollView != nil) { + NSRect visibleRect = [[scrollView documentView] convertRect:self.frame fromView:self]; + [[scrollView documentView] scrollRectToVisible:visibleRect]; + } + } + return isFirstResponder; +} +- (BOOL)performKeyEquivalent:(NSEvent *)event +{ + // The currentEditor is NSText for historical reasons, but documented to be NSTextView. + NSTextView *currentEditor = (NSTextView *)self.currentEditor; + // The currentEditor is non-nil when focused and hasMarkedText means an IME is open. + if (currentEditor && !currentEditor.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + return YES; // Don't send currentEditor the keydown event. + } + return [super performKeyEquivalent:event]; +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate { if (!notifyDelegate) { @@ -206,6 +583,23 @@ - (void)paste:(id)sender _textWasPasted = YES; [super paste:sender]; } +#else // [macOS +- (void)setSelectedTextRange:(NSRange)selectedTextRange notifyDelegate:(BOOL)notifyDelegate +{ + if (!notifyDelegate) { + // We have to notify an adapter that following selection change was initiated programmatically, + // so the adapter must not generate a notification for it. + [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange]; + } + + [[self currentEditor] setSelectedRange:selectedTextRange]; +} + +- (NSRange)selectedTextRange +{ + return [[self currentEditor] selectedRange]; +} +#endif // macOS] #pragma mark - Layout @@ -219,8 +613,20 @@ - (CGSize)intrinsicContentSize { // Note: `placeholder` defines intrinsic size for ``. NSString *text = self.placeholder ?: @""; +#if !TARGET_OS_OSX // [macOS] CGSize size = [text sizeWithAttributes:[self _placeholderTextAttributes]]; size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)); +#else // [macOS + CGSize size = [text sizeWithAttributes:@{NSFontAttributeName: self.font}]; + CGFloat scale = _pointScaleFactor ?: self.window.backingScaleFactor; + if (scale == 0.0 && RCTRunningInTestEnvironment()) { + // When running in the test environment the view is not on screen. + // Use a scaleFactor of 1 so that the test results are machine independent. + scale = 1; + } + RCTAssert(scale != 0.0, @"Layout occurs before the view is in a window?"); + size = CGSizeMake(RCTCeilPixelValue(size.width, scale), RCTCeilPixelValue(size.height, scale)); +#endif // macOS] size.width += _textContainerInset.left + _textContainerInset.right; size.height += _textContainerInset.top + _textContainerInset.bottom; // Returning size DOES contain `textContainerInset` (aka `padding`). @@ -234,4 +640,19 @@ - (CGSize)sizeThatFits:(CGSize)size return CGSizeMake(MIN(size.width, intrinsicSize.width), MIN(size.height, intrinsicSize.height)); } +#if !TARGET_OS_OSX // [macOS] +- (void)deleteBackward { + id textInputDelegate = [self textInputDelegate]; + if ([textInputDelegate textInputShouldHandleDeleteBackward:self]) { + [super deleteBackward]; + } +} +#else // [macOS +- (void)keyUp:(NSEvent *)event { + if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + [super keyUp:event]; + } +} +#endif // macOS] + @end diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h new file mode 100644 index 00000000000000..792c6703c41d58 --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#define RCT_SUBCLASS_SECURETEXTFIELD 1 + +#include diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m new file mode 100644 index 00000000000000..610a8e39a32367 --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m @@ -0,0 +1,12 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#define RCT_SUBCLASS_SECURETEXTFIELD 1 + +#include "../RCTUITextField.mm" diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 0d5990455b5be0..cdf79e036b92e7 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -47,6 +47,7 @@ const textViewConfig = { dataDetectorType: true, android_hyphenationFrequency: true, lineBreakStrategyIOS: true, + tooltip: true, // [macOS] }, directEventTypes: { topTextLayout: { diff --git a/packages/react-native/Libraries/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index bb9348f6a55e4f..51d8d84bc1b709 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -265,4 +265,28 @@ export type TextProps = $ReadOnly<{| * See https://reactnative.dev/docs/text.html#linebreakstrategyios */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + + /** + * Specifies the Tooltip for the button view + * + * @platform macos + */ + tooltip?: ?string, + + /** + * When `true`, indicates that the text can be focused in key view loop + * By default, when `selectable={true}` the text view will be focusable unless disabled. + * + * @platform macos + */ + focusable?: ?boolean, + + /** + * Specifies whether focus ring should be drawn when the view has the first responder status. + * Only works when `focusable={true}`. + * + * @platform macos + */ + enableFocusRing?: ?boolean, + // macOS] |}>; diff --git a/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextView.h b/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextView.h index 567a4a0f9ada88..203ec732087ccc 100644 --- a/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextView.h +++ b/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextView.h @@ -7,11 +7,11 @@ #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN -@interface RCTVirtualTextView : UIView +@interface RCTVirtualTextView : RCTUIView // [macOS] /** * (Experimental and unused for Paper) Pointer event handlers. diff --git a/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextViewManager.mm b/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextViewManager.mm index 313f1103a72c1a..a3d0ed79f60fc0 100644 --- a/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextViewManager.mm +++ b/packages/react-native/Libraries/Text/VirtualText/RCTVirtualTextViewManager.mm @@ -13,7 +13,7 @@ @implementation RCTVirtualTextViewManager RCT_EXPORT_MODULE(RCTVirtualText) -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [RCTVirtualTextView new]; } diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index a0234e95f26d16..3234cb663fecb4 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -224,13 +224,18 @@ export type PointerEvent = SyntheticEvent; export type PressEvent = ResponderSyntheticEvent< $ReadOnly<{| + altKey?: ?boolean, // [macOS] + button?: ?number, // [macOS] changedTouches: $ReadOnlyArray<$PropertyType>, + ctrlKey?: ?boolean, // [macOS] force?: number, identifier: number, locationX: number, locationY: number, + metaKey?: ?boolean, // [macOS] pageX: number, pageY: number, + shiftKey?: ?boolean, // [macOS] target: ?number, timestamp: number, touches: $ReadOnlyArray<$PropertyType>, @@ -267,6 +272,7 @@ export type ScrollEvent = SyntheticEvent< |}>, zoomScale?: number, responderIgnoreScroll?: boolean, + preferredScrollerStyle?: string, // [macOS] |}>, >; @@ -282,6 +288,26 @@ export type FocusEvent = SyntheticEvent< |}>, >; +export type KeyEvent = SyntheticEvent< + $ReadOnly<{| + // Modifier keys + capsLockKey: boolean, + shiftKey: boolean, + ctrlKey: boolean, + altKey: boolean, + metaKey: boolean, + numericPadKey: boolean, + helpKey: boolean, + functionKey: boolean, + // Key options + ArrowLeft: boolean, + ArrowRight: boolean, + ArrowUp: boolean, + ArrowDown: boolean, + key: string, + |}>, +>; + export type MouseEvent = SyntheticEvent< $ReadOnly<{| clientX: number, diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index f08588774d585c..168b58319f8981 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -34,7 +34,9 @@ if (NativeAppearance) { new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeAppearance, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS Also use this parameter on macOS + ? null + : NativeAppearance, // macOS] ); nativeEventEmitter.addListener( 'appearanceChanged', diff --git a/packages/react-native/Libraries/Utilities/BackHandler.android.js b/packages/react-native/Libraries/Utilities/BackHandler.android.js index 74519f78193482..187136a7f61338 100644 --- a/packages/react-native/Libraries/Utilities/BackHandler.android.js +++ b/packages/react-native/Libraries/Utilities/BackHandler.android.js @@ -35,6 +35,8 @@ RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function () { * * iOS: Not applicable. * + * macOS: Not applicable. + * * The event subscriptions are called in reverse order (i.e. last registered subscription first), * and if one subscription returns true then subscriptions registered earlier will not be called. * diff --git a/packages/react-native/Libraries/Utilities/BackHandler.macos.js b/packages/react-native/Libraries/Utilities/BackHandler.macos.js new file mode 100644 index 00000000000000..02d7142731af0e --- /dev/null +++ b/packages/react-native/Libraries/Utilities/BackHandler.macos.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * On Apple TV, this implements back navigation using the TV remote's menu button. + * On iOS, this just implements a stub. + * + * @flow + * @format + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const BackHandler = require('./BackHandler.ios'); +module.exports = BackHandler; diff --git a/packages/react-native/Libraries/Utilities/DevSettings.js b/packages/react-native/Libraries/Utilities/DevSettings.js index af227f2582d539..9f7b4baddc368c 100644 --- a/packages/react-native/Libraries/Utilities/DevSettings.js +++ b/packages/react-native/Libraries/Utilities/DevSettings.js @@ -32,7 +32,7 @@ if (__DEV__) { const emitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeDevSettings, + Platform.OS !== 'ios' && Platform.OS !== 'macos' ? null : NativeDevSettings, // [macOS] Also use this parameter on macOS ); const subscriptions = new Map(); diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 03ae2874fff300..9732cb249e8299 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -181,7 +181,7 @@ const HMRClient: HMRClientNativeInterface = { Try the following to fix the issue: - Ensure that Metro is running and available on the same network`; - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' || Platform.OS === 'macos' /* [macOS] */) { error += ` - Ensure that the Metro URL is correctly set in AppDelegate`; } else { @@ -323,7 +323,7 @@ function flushEarlyLogs(client: MetroHMRClient) { function dismissRedbox() { if ( - Platform.OS === 'ios' && + (Platform.OS === 'ios' || Platform.OS === 'macos') /* [macOS] */ && NativeRedBox != null && NativeRedBox.dismiss != null ) { diff --git a/packages/react-native/Libraries/Utilities/LoadingView.macos.js b/packages/react-native/Libraries/Utilities/LoadingView.macos.js new file mode 100644 index 00000000000000..f2ba69cdd0130d --- /dev/null +++ b/packages/react-native/Libraries/Utilities/LoadingView.macos.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const LoadingView = require('./LoadingView.ios'); +module.exports = LoadingView; diff --git a/packages/react-native/Libraries/Utilities/NativePlatformConstantsMacOS.js b/packages/react-native/Libraries/Utilities/NativePlatformConstantsMacOS.js new file mode 100644 index 00000000000000..59c427be43ebea --- /dev/null +++ b/packages/react-native/Libraries/Utilities/NativePlatformConstantsMacOS.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import type {TurboModule} from '../TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getConstants: () => {| + isTesting: boolean, + reactNativeVersion: {| + major: number, + minor: number, + patch: number, + prerelease: ?number, + |}, + osVersion: string, + systemName: string, + |}; +} + +export default (TurboModuleRegistry.getEnforcing( + 'PlatformConstants', +): Spec); diff --git a/packages/react-native/Libraries/Utilities/Platform.macos.js b/packages/react-native/Libraries/Utilities/Platform.macos.js new file mode 100644 index 00000000000000..e79a5ea4508ab7 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/Platform.macos.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +// [macOS] + +import NativePlatformConstantsMacOS from './NativePlatformConstantsMacOS'; + +export type PlatformSelectSpec = { + default?: T, + native?: T, + macos?: T, + ... +}; + +const Platform = { + __constants: null, + OS: 'macos', + // $FlowFixMe[unsafe-getters-setters] + get Version(): string { + // $FlowFixMe[object-this-reference] + return this.constants.osVersion; + }, + // $FlowFixMe[unsafe-getters-setters] + get constants(): {| + isTesting: boolean, + osVersion: string, + reactNativeVersion: {| + major: number, + minor: number, + patch: number, + prerelease: ?number, + |}, + systemName: string, + |} { + // $FlowFixMe[object-this-reference] + if (this.__constants == null) { + // $FlowFixMe[object-this-reference] + this.__constants = NativePlatformConstantsMacOS.getConstants(); + } + // $FlowFixMe[object-this-reference] + return this.__constants; + }, + // $FlowFixMe[unsafe-getters-setters] + get isTV(): boolean { + return false; + }, + // $FlowFixMe[unsafe-getters-setters] + get isTesting(): boolean { + if (__DEV__) { + // $FlowFixMe[object-this-reference] + return this.constants.isTesting; + } + return false; + }, + select: (spec: PlatformSelectSpec): T => + // $FlowFixMe[incompatible-return] + 'macos' in spec + ? // $FlowFixMe[incompatible-return] + spec.macos + : 'native' in spec + ? // $FlowFixMe[incompatible-return] + spec.native + : // $FlowFixMe[incompatible-return] + spec.default, +}; + +module.exports = Platform; diff --git a/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js b/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js index f36e735eb2af9f..d52a7b58aa4194 100644 --- a/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js +++ b/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js @@ -16,7 +16,7 @@ const Switch = require('../Components/Switch/Switch').default; const TextInput = require('../Components/TextInput/TextInput'); const View = require('../Components/View/View'); const Text = require('../Text/Text'); -const {VirtualizedList} = require('@react-native/virtualized-lists'); +const {VirtualizedList} = require('@react-native-mac/virtualized-lists'); // [macOS] const React = require('react'); const ShallowRenderer = require('react-shallow-renderer'); const ReactTestRenderer = require('react-test-renderer'); diff --git a/packages/react-native/Libraries/WebSocket/WebSocket.js b/packages/react-native/Libraries/WebSocket/WebSocket.js index 3fdde2b3639adf..1bf3509a574c73 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocket.js +++ b/packages/react-native/Libraries/WebSocket/WebSocket.js @@ -141,7 +141,9 @@ class WebSocket extends (EventTarget(...WEBSOCKET_EVENTS): any) { this._eventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeWebSocketModule, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativeWebSocketModule, ); this._socketId = nextWebSocketId++; this._registerEvents(); diff --git a/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js b/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js index 43192fb5afa31d..b3618d58f61c58 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js +++ b/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js @@ -135,7 +135,9 @@ const WebSocketInterceptor = { eventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeWebSocketModule, + Platform.OS !== 'ios' && Platform.OS !== 'macos' // [macOS] Also use this parameter on macOS + ? null + : NativeWebSocketModule, ); WebSocketInterceptor._registerEvents(); diff --git a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleView.h b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleView.h index 3d5885cdeab65a..24e6d6183b74a2 100644 --- a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleView.h +++ b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleViewController.h b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleViewController.h index fb0f3ed35a74ae..ecc823dbdb6954 100644 --- a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleViewController.h +++ b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperExampleViewController.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewController.h b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewController.h index 4035c856a33303..ef02ba7c03df70 100644 --- a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewController.h +++ b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewController.h @@ -5,13 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @class RCTBridge; NS_ASSUME_NONNULL_BEGIN +#if !TARGET_OS_OSX // [macOS] @interface RCTWrapperReactRootViewController : UIViewController +#else +@interface RCTWrapperReactRootViewController : NSViewController +#endif // [macOS] - (instancetype)initWithBridge:(RCTBridge *)bridge; diff --git a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewManager.h b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewManager.h index 68f99f529fdd2f..2537f871e92463 100644 --- a/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewManager.h +++ b/packages/react-native/Libraries/Wrapper/Example/RCTWrapperReactRootViewManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/Libraries/Wrapper/RCTWrapper.h b/packages/react-native/Libraries/Wrapper/RCTWrapper.h index 579e73a63896a7..86a82f245d6a8c 100644 --- a/packages/react-native/Libraries/Wrapper/RCTWrapper.h +++ b/packages/react-native/Libraries/Wrapper/RCTWrapper.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -30,7 +30,7 @@ \ RCT_EXPORT_MODULE() \ \ - - (UIView *)view \ + - (RCTPlatformView *)view /* [macOS]*/ \ { \ RCTWrapperView *wrapperView = [super view]; \ wrapperView.contentView = [ClassName new]; \ @@ -56,7 +56,7 @@ \ RCT_EXPORT_MODULE() \ \ - - (UIView *)view \ + - (RCTPlatformView *)view /* [macOS] */ \ { \ RCTWrapperViewControllerHostingView *contentViewControllerHostingView = [RCTWrapperViewControllerHostingView new]; \ contentViewControllerHostingView.contentViewController = [[ClassName alloc] initWithNibName:nil bundle:nil]; \ diff --git a/packages/react-native/Libraries/Wrapper/RCTWrapperShadowView.h b/packages/react-native/Libraries/Wrapper/RCTWrapperShadowView.h index a5e969814dbb7a..28ac5d1dc3988a 100644 --- a/packages/react-native/Libraries/Wrapper/RCTWrapperShadowView.h +++ b/packages/react-native/Libraries/Wrapper/RCTWrapperShadowView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/Libraries/Wrapper/RCTWrapperView.h b/packages/react-native/Libraries/Wrapper/RCTWrapperView.h index dc46ec588b522b..3a4253c7423a5a 100644 --- a/packages/react-native/Libraries/Wrapper/RCTWrapperView.h +++ b/packages/react-native/Libraries/Wrapper/RCTWrapperView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] typedef CGSize (^RCTWrapperMeasureBlock)(CGSize minimumSize, CGSize maximumSize); @@ -13,9 +13,9 @@ typedef CGSize (^RCTWrapperMeasureBlock)(CGSize minimumSize, CGSize maximumSize) NS_ASSUME_NONNULL_BEGIN -@interface RCTWrapperView : UIView +@interface RCTWrapperView : RCTPlatformView // [macOS] -@property (nonatomic, retain, nullable) UIView *contentView; +@property (nonatomic, retain, nullable) RCTPlatformView *contentView; // [macOS] @property (nonatomic, readonly) RCTWrapperMeasureBlock measureBlock; - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; @@ -26,10 +26,10 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; -- (void)addSubview:(UIView *)view NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview NS_UNAVAILABLE; +- (void)addSubview:(RCTPlatformView *)view NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view atIndex:(NSInteger)index NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view aboveSubview:(RCTPlatformView *)siblingSubview NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view belowSubview:(RCTPlatformView *)siblingSubview NS_UNAVAILABLE; // [macOS] @end diff --git a/packages/react-native/Libraries/Wrapper/RCTWrapperViewControllerHostingView.h b/packages/react-native/Libraries/Wrapper/RCTWrapperViewControllerHostingView.h index c8dff426c3aad1..14c14a7508a063 100644 --- a/packages/react-native/Libraries/Wrapper/RCTWrapperViewControllerHostingView.h +++ b/packages/react-native/Libraries/Wrapper/RCTWrapperViewControllerHostingView.h @@ -5,20 +5,24 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN -@interface RCTWrapperViewControllerHostingView : UIView +@interface RCTWrapperViewControllerHostingView : RCTPlatformView // [macOS] +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, retain, nullable) UIViewController *contentViewController; +#else +@property (nonatomic, retain, nullable) NSViewController *contentViewController; +#endif // [macOS] #pragma mark - Restrictions -- (void)addSubview:(UIView *)view NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview NS_UNAVAILABLE; -- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview NS_UNAVAILABLE; +- (void)addSubview:(RCTPlatformView *)view NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view atIndex:(NSInteger)index NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view aboveSubview:(UIView *)siblingSubview NS_UNAVAILABLE; // [macOS] +- (void)insertSubview:(RCTPlatformView *)view belowSubview:(RCTPlatformView *)siblingSubview NS_UNAVAILABLE; // [macOS] @end diff --git a/packages/react-native/React/Base/RCTBridge.h b/packages/react-native/React/Base/RCTBridge.h index 68651edb771a6b..7d06d7672fd568 100644 --- a/packages/react-native/React/Base/RCTBridge.h +++ b/packages/react-native/React/Base/RCTBridge.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index d0598e621e22f2..117e5d0ae310e5 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -7,6 +7,7 @@ #import "RCTBridge.h" #import "RCTBridge+Private.h" +#import "RCTDevSettings.h" // [macOS] #import diff --git a/packages/react-native/React/Base/RCTBridgeModule.h b/packages/react-native/React/Base/RCTBridgeModule.h index 15d871fa76a7cd..ea34b2ef836309 100644 --- a/packages/react-native/React/Base/RCTBridgeModule.h +++ b/packages/react-native/React/Base/RCTBridgeModule.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] #import #import @@ -380,7 +380,7 @@ RCT_EXTERN_C_END - (BOOL)moduleIsInitialized:(Class)moduleClass; @end -typedef UIView * (^RCTBridgelessComponentViewProvider)(NSNumber *); +typedef RCTPlatformView * (^RCTBridgelessComponentViewProvider)(NSNumber *); // [macOS] typedef void (^RCTViewRegistryUIBlock)(RCTViewRegistry *viewRegistry); @@ -391,7 +391,7 @@ typedef void (^RCTViewRegistryUIBlock)(RCTViewRegistry *viewRegistry); - (void)setBridge:(RCTBridge *)bridge; - (void)setBridgelessComponentViewProvider:(RCTBridgelessComponentViewProvider)bridgelessComponentViewProvider; -- (UIView *)viewForReactTag:(NSNumber *)reactTag; +- (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag; // [macOS] - (void)addUIBlock:(RCTViewRegistryUIBlock)block; @end diff --git a/packages/react-native/React/Base/RCTBridgeProxy.mm b/packages/react-native/React/Base/RCTBridgeProxy.mm index e4ece263b12bad..8c1accc5788a82 100644 --- a/packages/react-native/React/Base/RCTBridgeProxy.mm +++ b/packages/react-native/React/Base/RCTBridgeProxy.mm @@ -407,7 +407,7 @@ - (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry /** * RCTViewRegistry */ -- (UIView *)viewForReactTag:(NSNumber *)reactTag +- (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag // [macOS] { [self logWarning:@"Please migrate to RCTViewRegistry: @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED." cmd:_cmd]; diff --git a/packages/react-native/React/Base/RCTBundleManager.h b/packages/react-native/React/Base/RCTBundleManager.h index 15ed245dbea80a..f58177ff7147e9 100644 --- a/packages/react-native/React/Base/RCTBundleManager.h +++ b/packages/react-native/React/Base/RCTBundleManager.h @@ -19,5 +19,5 @@ typedef void (^RCTBridgelessBundleURLSetter)(NSURL *bundleURL); andSetter:(RCTBridgelessBundleURLSetter)setter andDefaultGetter:(RCTBridgelessBundleURLGetter)defaultGetter; - (void)resetBundleURL; -@property NSURL *bundleURL; +@property (strong) NSURL *bundleURL; // [macOS] @end diff --git a/packages/react-native/React/Base/RCTConstants.h b/packages/react-native/React/Base/RCTConstants.h index 54d3a470543a57..ca9a2649cc66e4 100644 --- a/packages/react-native/React/Base/RCTConstants.h +++ b/packages/react-native/React/Base/RCTConstants.h @@ -10,7 +10,11 @@ RCT_EXTERN NSString *const RCTPlatformName; RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotification; +#if !TARGET_OS_OSX // [macOS] RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey; +#else // [macOS +RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotificationAppearanceKey; +#endif // macOS] RCT_EXTERN NSString *const RCTRootViewFrameDidChangeNotification; diff --git a/packages/react-native/React/Base/RCTConstants.m b/packages/react-native/React/Base/RCTConstants.m index 056f6095d8fc06..e375a2f0c18916 100644 --- a/packages/react-native/React/Base/RCTConstants.m +++ b/packages/react-native/React/Base/RCTConstants.m @@ -7,10 +7,18 @@ #import "RCTConstants.h" +#if !TARGET_OS_OSX // [macOS] NSString *const RCTPlatformName = @"ios"; +#else // [macOS +NSString *const RCTPlatformName = @"macos"; +#endif // macOS] NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification"; +#if !TARGET_OS_OSX // [macOS] NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey = @"traitCollection"; +#else // [macOS +NSString *const RCTUserInterfaceStyleDidChangeNotificationAppearanceKey = @"appearance"; +#endif // macOS] NSString *const RCTRootViewFrameDidChangeNotification = @"RCTRootViewFrameDidChangeNotification"; diff --git a/packages/react-native/React/Base/RCTConvert.h b/packages/react-native/React/Base/RCTConvert.h index 7a6ca764191d0d..9bb88471fe0048 100644 --- a/packages/react-native/React/Base/RCTConvert.h +++ b/packages/react-native/React/Base/RCTConvert.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] #import #import @@ -17,6 +17,8 @@ #import #import +@class RCTHandledKey; // [macOS] + /** * This class provides a collection of conversion functions for mapping * JSON objects to native types and classes. These are useful when writing @@ -63,6 +65,7 @@ typedef NSURL RCTFileURL; + (NSUnderlineStyle)NSUnderlineStyle:(id)json; + (NSWritingDirection)NSWritingDirection:(id)json; + (NSLineBreakStrategy)NSLineBreakStrategy:(id)json; +#if !TARGET_OS_OSX // [macOS] + (UITextAutocapitalizationType)UITextAutocapitalizationType:(id)json; + (UITextFieldViewMode)UITextFieldViewMode:(id)json; + (UIKeyboardType)UIKeyboardType:(id)json; @@ -79,6 +82,11 @@ typedef NSURL RCTFileURL; #if !TARGET_OS_TV + (UIBarStyle)UIBarStyle:(id)json; #endif +#endif // [macOS] + +#if TARGET_OS_OSX // [macOS ++ (NSTextCheckingTypes)NSTextCheckingTypes:(id)json; +#endif // macOS] + (CGFloat)CGFloat:(id)json; + (CGPoint)CGPoint:(id)json; @@ -91,7 +99,8 @@ typedef NSURL RCTFileURL; + (CGAffineTransform)CGAffineTransform:(id)json; -+ (UIColor *)UIColor:(id)json; ++ (RCTUIColor *)UIColor:(id)json; // [macOS] ++ (RCTUIColor *)NSColor:(id)json; // [macOS] + (CGColorRef)CGColor:(id)json CF_RETURNS_NOT_RETAINED; + (YGValue)YGValue:(id)json; @@ -103,7 +112,10 @@ typedef NSURL RCTFileURL; + (NSArray *)NSURLArray:(id)json; + (NSArray *)RCTFileURLArray:(id)json; + (NSArray *)NSNumberArray:(id)json; -+ (NSArray *)UIColorArray:(id)json; ++ (NSArray *)UIColorArray:(id)json; // [macOS] +#if TARGET_OS_OSX // [macOS ++ (NSArray *)NSPasteboardTypeArray:(id)json; +#endif // macOS] typedef NSArray CGColorArray; + (CGColorArray *)CGColorArray:(id)json; @@ -131,6 +143,11 @@ typedef BOOL css_backface_visibility_t; + (RCTBorderCurve)RCTBorderCurve:(id)json; + (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json; +#if TARGET_OS_OSX // [macOS ++ (NSString *)accessibilityRoleFromTraits:(id)json; + ++ (NSArray *)RCTHandledKeyArray:(id)json; +#endif // macOS] @end @interface RCTConvert (Deprecated) diff --git a/packages/react-native/React/Base/RCTConvert.m b/packages/react-native/React/Base/RCTConvert.m index 34822463593f8f..48978105ba1fc3 100644 --- a/packages/react-native/React/Base/RCTConvert.m +++ b/packages/react-native/React/Base/RCTConvert.m @@ -12,6 +12,7 @@ #import #import "RCTDefines.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTImageSource.h" #import "RCTParserUtils.h" #import "RCTUtils.h" @@ -376,7 +377,7 @@ + (NSLocale *)NSLocale:(id)json + (NSLineBreakStrategy)NSLineBreakStrategy:(id)json RCT_DYNAMIC { - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] static NSDictionary *mapping; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -393,6 +394,7 @@ + (NSLineBreakStrategy)NSLineBreakStrategy:(id)json RCT_DYNAMIC } } +#if !TARGET_OS_OSX // [macOS] RCT_ENUM_CONVERTER( UITextAutocapitalizationType, (@{ @@ -487,6 +489,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC UIReturnKeyDefault, integerValue) +#if !TARGET_OS_OSX // [macOS] RCT_ENUM_CONVERTER( UIUserInterfaceStyle, (@{ @@ -496,6 +499,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC }), UIUserInterfaceStyleUnspecified, integerValue) +#endif // [macOS] RCT_ENUM_CONVERTER( UIInterfaceOrientationMask, @@ -543,6 +547,23 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC }), UIBarStyleDefault, integerValue) +#else // [macOS +RCT_MULTI_ENUM_CONVERTER(NSTextCheckingTypes, (@{ + @"ortography": @(NSTextCheckingTypeOrthography), + @"spelling": @(NSTextCheckingTypeSpelling), + @"grammar": @(NSTextCheckingTypeGrammar), + @"calendarEvent": @(NSTextCheckingTypeDate), + @"address": @(NSTextCheckingTypeAddress), + @"link": @(NSTextCheckingTypeLink), + @"quote": @(NSTextCheckingTypeQuote), + @"dash": @(NSTextCheckingTypeDash), + @"replacement": @(NSTextCheckingTypeReplacement), + @"correction": @(NSTextCheckingTypeCorrection), + @"regularExpression": @(NSTextCheckingTypeRegularExpression), + @"phoneNumber": @(NSTextCheckingTypePhoneNumber), + @"transitInformation": @(NSTextCheckingTypeTransitInformation), +}), NSTextCheckingTypeOrthography, unsignedLongLongValue) +#endif // macOS] static void convertCGStruct(const char *type, NSArray *fields, CGFloat *result, id json) { @@ -651,6 +672,7 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json static NSDictionary *colorMap = nil; if (colorMap == nil) { NSMutableDictionary *map = [@{ +#if !TARGET_OS_OSX // [macOS] // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors // Label Colors @"labelColor" : @{ @@ -780,6 +802,88 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json // iOS 13.0 RCTFallbackARGB : @(0x00000000) }, +#else // [macOS + // https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors + // Label Colors + @"labelColor": @{}, // 10_10 + @"secondaryLabelColor": @{}, // 10_10 + @"tertiaryLabelColor": @{}, // 10_10 + @"quaternaryLabelColor": @{}, // 10_10 + // Text Colors + @"textColor": @{}, + @"placeholderTextColor": @{}, // 10_10 + @"selectedTextColor": @{}, + @"textBackgroundColor": @{}, + @"selectedTextBackgroundColor": @{}, + @"keyboardFocusIndicatorColor": @{}, + @"unemphasizedSelectedTextColor": @{ // 10_14 + RCTFallback: @"selectedTextColor" + }, + @"unemphasizedSelectedTextBackgroundColor": @{ // 10_14 + RCTFallback: @"textBackgroundColor" + }, + // Content Colors + @"linkColor": @{}, // 10_10 + @"separatorColor": @{ // 10_14: Replacement for +controlHighlightColor, +controlLightHighlightColor, +controlShadowColor, +controlDarkShadowColor + RCTFallback: @"gridColor" + }, + @"selectedContentBackgroundColor": @{ // 10_14: Alias for +alternateSelectedControlColor + RCTFallback: @"alternateSelectedControlColor" + }, + @"unemphasizedSelectedContentBackgroundColor": @{ // 10_14: Alias for +secondarySelectedControlColor + RCTFallback: @"secondarySelectedControlColor" + }, + // Menu Colors + @"selectedMenuItemTextColor": @{}, + // Table Colors + @"gridColor": @{}, + @"headerTextColor": @{}, + @"alternatingEvenContentBackgroundColor": @{ // 10_14: Alias for +controlAlternatingRowBackgroundColors + RCTSelector: @"alternatingContentBackgroundColors", + RCTIndex: @0, + RCTFallback: @"controlAlternatingRowBackgroundColors" + }, + @"alternatingOddContentBackgroundColor": @{ // 10_14: Alias for +controlAlternatingRowBackgroundColors + RCTSelector: @"alternatingContentBackgroundColors", + RCTIndex: @1, + RCTFallback: @"controlAlternatingRowBackgroundColors" + }, + // Control Colors + @"controlAccentColor": @{ // 10_14 + RCTFallback: @"controlColor" + }, + @"controlColor": @{}, + @"controlBackgroundColor": @{}, + @"controlTextColor": @{}, + @"disabledControlTextColor": @{}, + @"selectedControlColor": @{}, + @"selectedControlTextColor": @{}, + @"alternateSelectedControlTextColor": @{}, + @"scrubberTexturedBackgroundColor": @{}, // 10_12_2 + // Window Colors + @"windowBackgroundColor": @{}, + @"windowFrameTextColor": @{}, + @"underPageBackgroundColor": @{}, // 10_8 + // Highlights and Shadows + @"findHighlightColor": @{ // 10_13 + RCTFallback: @"highlightColor" + }, + @"highlightColor": @{}, + @"shadowColor": @{}, + // https://developer.apple.com/documentation/appkit/nscolor/standard_colors + // Standard Colors + @"systemBlueColor": @{}, // 10_10 + @"systemBrownColor": @{}, // 10_10 + @"systemGrayColor": @{}, // 10_10 + @"systemGreenColor": @{}, // 10_10 + @"systemOrangeColor": @{}, // 10_10 + @"systemPinkColor": @{}, // 10_10 + @"systemPurpleColor": @{}, // 10_10 + @"systemRedColor": @{}, // 10_10 + @"systemYellowColor": @{}, // 10_10 + // Transparent Color + @"clearColor" : @{}, +#endif // macOS] } mutableCopy]; // The color names are the Objective-C UIColor selector names, // but Swift selector names are valid as well, so make aliases. @@ -789,9 +893,10 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json RCTAssert( [objcSelector hasSuffix:RCTColorSuffix], @"A selector in the color map did not end with the suffix Color."); NSMutableDictionary *entry = [map[objcSelector] mutableCopy]; - RCTAssert([entry objectForKey:RCTSelector] == nil, @"Entry should not already have an RCTSelector"); + if ([entry objectForKey:RCTSelector] == nil) { + entry[RCTSelector] = objcSelector; + } NSString *swiftSelector = [objcSelector substringToIndex:[objcSelector length] - [RCTColorSuffix length]]; - entry[RCTSelector] = objcSelector; aliases[swiftSelector] = entry; } [map addEntriesFromDictionary:aliases]; @@ -818,13 +923,14 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json return colorMap; } +// [macOS /** Returns a UIColor based on a semantic color name. * Returns nil if the semantic color name is invalid. */ -static UIColor *RCTColorFromSemanticColorName(NSString *semanticColorName) +static RCTUIColor *RCTColorFromSemanticColorName(NSString *semanticColorName) { NSDictionary *colorMap = RCTSemanticColorsMap(); - UIColor *color = nil; + RCTUIColor *color = nil; NSDictionary *colorInfo = colorMap[semanticColorName]; if (colorInfo) { NSString *semanticColorSelector = colorInfo[RCTSelector]; @@ -832,7 +938,7 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json semanticColorSelector = semanticColorName; } SEL selector = NSSelectorFromString(semanticColorSelector); - if (![UIColor respondsToSelector:selector]) { + if (![RCTUIColor respondsToSelector:selector]) { NSNumber *fallbackRGB = colorInfo[RCTFallbackARGB]; if (fallbackRGB != nil) { RCTAssert([fallbackRGB isKindOfClass:[NSNumber class]], @"fallback ARGB is not a number"); @@ -841,12 +947,12 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json semanticColorSelector = colorInfo[RCTFallback]; selector = NSSelectorFromString(semanticColorSelector); } - RCTAssert([UIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector."); - Class klass = [UIColor class]; + RCTAssert ([RCTUIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector."); + Class klass = [RCTUIColor class]; IMP imp = [klass methodForSelector:selector]; id (*getSemanticColorObject)(id, SEL) = (void *)imp; id colorObject = getSemanticColorObject(klass, selector); - if ([colorObject isKindOfClass:[UIColor class]]) { + if ([colorObject isKindOfClass:[RCTUIColor class]]) { color = colorObject; } else if ([colorObject isKindOfClass:[NSArray class]]) { NSArray *colors = colorObject; @@ -859,6 +965,7 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json } return color; } +// macOS] /** Returns an alphabetically sorted comma separated list of the valid semantic color names */ @@ -866,10 +973,9 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json { NSMutableString *names = [NSMutableString new]; NSDictionary *colorMap = RCTSemanticColorsMap(); - NSArray *allKeys = - [[[colorMap allKeys] mutableCopy] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + NSArray *allKeys = [[[colorMap allKeys] mutableCopy] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; - for (id key in allKeys) { + for(id key in allKeys) { if ([names length]) { [names appendString:@", "]; } @@ -878,7 +984,36 @@ + (UIEdgeInsets)UIEdgeInsets:(id)json return names; } -+ (UIColor *)UIColor:(id)json +// [macOS ++ (RCTUIColor *)NSColor:(id)json +{ + return [RCTConvert UIColor:json]; +} +// macOS] + +// [macOS +#if TARGET_OS_OSX +static NSColor *RCTColorWithSystemEffect(NSColor* color, NSString *systemEffectString) { + NSColor *colorWithEffect = color; + if (systemEffectString != nil) { + if ([systemEffectString isEqualToString:@"none"]) { + colorWithEffect = [color colorWithSystemEffect:NSColorSystemEffectNone]; + } else if ([systemEffectString isEqualToString:@"pressed"]) { + colorWithEffect = [color colorWithSystemEffect:NSColorSystemEffectPressed]; + } else if ([systemEffectString isEqualToString:@"deepPressed"]) { + colorWithEffect = [color colorWithSystemEffect:NSColorSystemEffectDeepPressed]; + } else if ([systemEffectString isEqualToString:@"disabled"]) { + colorWithEffect = [color colorWithSystemEffect:NSColorSystemEffectDisabled]; + } else if ([systemEffectString isEqualToString:@"rollover"]) { + colorWithEffect = [color colorWithSystemEffect:NSColorSystemEffectRollover]; + } + } + return colorWithEffect; +} +#endif //TARGET_OS_OSX +// macOS] + ++ (RCTUIColor *)UIColor:(id)json // [macOS] { if (!json) { return nil; @@ -886,24 +1021,25 @@ + (UIColor *)UIColor:(id)json if ([json isKindOfClass:[NSArray class]]) { NSArray *components = [self NSNumberArray:json]; CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0; - return [UIColor colorWithRed:[self CGFloat:components[0]] - green:[self CGFloat:components[1]] - blue:[self CGFloat:components[2]] - alpha:alpha]; + return [RCTUIColor colorWithRed:[self CGFloat:components[0]] // [macOS] + green:[self CGFloat:components[1]] + blue:[self CGFloat:components[2]] + alpha:alpha]; } else if ([json isKindOfClass:[NSNumber class]]) { NSUInteger argb = [self NSUInteger:json]; CGFloat a = ((argb >> 24) & 0xFF) / 255.0; CGFloat r = ((argb >> 16) & 0xFF) / 255.0; CGFloat g = ((argb >> 8) & 0xFF) / 255.0; CGFloat b = (argb & 0xFF) / 255.0; - return [UIColor colorWithRed:r green:g blue:b alpha:a]; + return [RCTUIColor colorWithRed:r green:g blue:b alpha:a]; // [macOS] + } else if ([json isKindOfClass:[NSDictionary class]]) { NSDictionary *dictionary = json; id value = nil; if ((value = [dictionary objectForKey:@"semantic"])) { if ([value isKindOfClass:[NSString class]]) { NSString *semanticName = value; - UIColor *color = [UIColor colorNamed:semanticName]; + RCTUIColor *color = [RCTUIColor colorNamed:semanticName]; // [macOS] if (color != nil) { return color; } @@ -916,7 +1052,7 @@ + (UIColor *)UIColor:(id)json return color; } else if ([value isKindOfClass:[NSArray class]]) { for (id name in value) { - UIColor *color = [UIColor colorNamed:name]; + RCTUIColor *color = [RCTUIColor colorNamed:name]; // [macOS] if (color != nil) { return color; } @@ -937,14 +1073,15 @@ + (UIColor *)UIColor:(id)json } else if ((value = [dictionary objectForKey:@"dynamic"])) { NSDictionary *appearances = value; id light = [appearances objectForKey:@"light"]; - UIColor *lightColor = [RCTConvert UIColor:light]; + RCTUIColor *lightColor = [RCTConvert UIColor:light]; id dark = [appearances objectForKey:@"dark"]; - UIColor *darkColor = [RCTConvert UIColor:dark]; + RCTUIColor *darkColor = [RCTConvert UIColor:dark]; // [macOS] id highContrastLight = [appearances objectForKey:@"highContrastLight"]; - UIColor *highContrastLightColor = [RCTConvert UIColor:highContrastLight]; + RCTUIColor *highContrastLightColor = [RCTConvert UIColor:highContrastLight]; // [macOS] id highContrastDark = [appearances objectForKey:@"highContrastDark"]; - UIColor *highContrastDarkColor = [RCTConvert UIColor:highContrastDark]; + RCTUIColor *highContrastDarkColor = [RCTConvert UIColor:highContrastDark]; // [macOS] if (lightColor != nil && darkColor != nil) { +#if !TARGET_OS_OSX // [macOS] UIColor *color = [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull collection) { if (collection.userInterfaceStyle == UIUserInterfaceStyleDark) { if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastDarkColor != nil) { @@ -961,13 +1098,52 @@ + (UIColor *)UIColor:(id)json } }]; return color; +#else // [macOS + NSColor *color = [NSColor colorWithName:nil dynamicProvider:^NSColor * _Nonnull(NSAppearance * _Nonnull appearance) { + NSMutableArray *appearances = [NSMutableArray arrayWithArray:@[NSAppearanceNameAqua,NSAppearanceNameDarkAqua]]; + if (highContrastLightColor != nil) { + [appearances addObject:NSAppearanceNameAccessibilityHighContrastAqua]; + } + if (highContrastDarkColor != nil) { + [appearances addObject:NSAppearanceNameAccessibilityHighContrastDarkAqua]; + } + NSAppearanceName bestMatchingAppearance = [appearance bestMatchFromAppearancesWithNames:appearances]; + if (bestMatchingAppearance == NSAppearanceNameAqua) { + return lightColor; + } else if (bestMatchingAppearance == NSAppearanceNameDarkAqua) { + return darkColor; + } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastAqua) { + return highContrastLightColor; + } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastDarkAqua) { + return highContrastDarkColor; + } else { + RCTLogWarn(@"DynamicColorMacOS: Could not resolve current appearance. Defaulting to light."); + return lightColor; + } + }]; + return color; +#endif // macOS] } else { - RCTLogConvertError(json, @"a UIColor. Expected an iOS dynamic appearance aware color."); + RCTLogConvertError(json, @"a UIColor. Expected an apple dynamic appearance aware color."); // [macOS] return nil; } +#if TARGET_OS_OSX // [macOS + } else if((value = [dictionary objectForKey:@"colorWithSystemEffect"])) { + NSDictionary *colorWithSystemEffect = value; + id base = [colorWithSystemEffect objectForKey:@"baseColor"]; + NSColor *baseColor = [RCTConvert UIColor:base]; + NSString * systemEffectString = [colorWithSystemEffect objectForKey:@"systemEffect"]; + if (baseColor != nil && systemEffectString != nil) { + return RCTColorWithSystemEffect(baseColor, systemEffectString); + } else { + RCTLogConvertError( + json, @"a UIColor. Expected a color with a system effect string, but got something else"); + return nil; + } +#endif // macOS] } else { - RCTLogConvertError(json, @"a UIColor. Expected an iOS semantic color or dynamic appearance aware color."); + RCTLogConvertError(json, @"a UIColor. Expected an apple semantic color, dynamic appearance aware color, or color with system effect"); // [macOS] return nil; } } else { @@ -1032,7 +1208,7 @@ + (YGValue)YGValue:(id)json RCT_ARRAY_CONVERTER(NSURL) RCT_ARRAY_CONVERTER(RCTFileURL) -RCT_ARRAY_CONVERTER(UIColor) +RCT_ARRAY_CONVERTER(RCTUIColor) // [macOS] /** * This macro is used for creating converter functions for directly @@ -1065,6 +1241,39 @@ + (NSArray *)CGColorArray:(id)json return colors; } +#if TARGET_OS_OSX // [macOS ++ (NSArray *)NSPasteboardType:(id)json +{ + NSString *type = [self NSString:json]; + if (!type) { + return @[]; + } + + if ([type isEqualToString:@"fileUrl"]) { + return @[NSFilenamesPboardType]; + } else if ([type isEqualToString:@"image"]) { + return @[NSPasteboardTypePNG, NSPasteboardTypeTIFF]; + } else if ([type isEqualToString:@"string"]) { + return @[NSPasteboardTypeString]; + } + return @[]; +} + ++ (NSArray *)NSPasteboardTypeArray:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + return [RCTConvert NSPasteboardType:json]; + } else if ([json isKindOfClass:[NSArray class]]) { + NSMutableArray *mutablePasteboardTypes = [NSMutableArray new]; + for (NSString *type in json) { + [mutablePasteboardTypes addObjectsFromArray:[RCTConvert NSPasteboardType:type]]; + } + return mutablePasteboardTypes.copy; + } + return @[]; +} +#endif // macOS] + static id RCTConvertPropertyListValue(id json) { if (!json || json == (id)kCFNull) { @@ -1217,16 +1426,91 @@ + (NSPropertyList)NSPropertyList:(id)json RCT_ENUM_CONVERTER( RCTAnimationType, (@{ +#if !TARGET_OS_OSX // [macOS] @"spring" : @(RCTAnimationTypeSpring), +#endif // [macOS] @"linear" : @(RCTAnimationTypeLinear), @"easeIn" : @(RCTAnimationTypeEaseIn), @"easeOut" : @(RCTAnimationTypeEaseOut), @"easeInEaseOut" : @(RCTAnimationTypeEaseInEaseOut), +#if !TARGET_OS_OSX // [macOS] @"keyboard" : @(RCTAnimationTypeKeyboard), +#endif // [macOS] }), RCTAnimationTypeEaseInEaseOut, integerValue) +#if TARGET_OS_OSX // [macOS ++ (NSString*)accessibilityRoleFromTrait:(NSString*)trait +{ + static NSDictionary *traitOrRoleToAccessibilityRole; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + traitOrRoleToAccessibilityRole = @{ + // from https://reactnative.dev/docs/accessibility#accessibilityrole + @"adjustable": NSAccessibilitySliderRole, + @"alert": NSAccessibilityStaticTextRole, // no exact match on macOS + @"button": NSAccessibilityButtonRole, // also a legacy iOS accessibilityTraits + @"checkbox": NSAccessibilityCheckBoxRole, + @"combobox": NSAccessibilityComboBoxRole, + @"header": NSAccessibilityStaticTextRole, // no exact match on macOS + @"image": NSAccessibilityImageRole, // also a legacy iOS accessibilityTraits + @"imagebutton": NSAccessibilityButtonRole, // no exact match on macOS + @"keyboardkey": NSAccessibilityButtonRole, // no exact match on macOS + @"link": NSAccessibilityLinkRole, // also a legacy iOS accessibilityTraits + @"menu": NSAccessibilityMenuRole, + @"menubar": NSAccessibilityMenuBarRole, + @"menuitem": NSAccessibilityMenuItemRole, + @"none": NSAccessibilityUnknownRole, + @"progressbar": NSAccessibilityProgressIndicatorRole, + @"radio": NSAccessibilityRadioButtonRole, + @"radiogroup": NSAccessibilityRadioGroupRole, + @"scrollbar": NSAccessibilityScrollBarRole, + @"search": NSAccessibilityTextFieldRole, // no exact match on macOS + @"spinbutton": NSAccessibilityIncrementorRole, + @"summary": NSAccessibilityStaticTextRole, // no exact match on macOS + @"switch": NSAccessibilityCheckBoxRole, // no exact match on macOS + @"tab": NSAccessibilityButtonRole, // no exact match on macOS + @"tablist": NSAccessibilityTabGroupRole, + @"text": NSAccessibilityStaticTextRole, // also a legacy iOS accessibilityTraits + @"timer": NSAccessibilityStaticTextRole, // no exact match on macOS + @"toolbar": NSAccessibilityToolbarRole, + // Roles/traits that are macOS specific and are used by some of the core components (Lists): + @"disclosure": NSAccessibilityDisclosureTriangleRole, + @"group": NSAccessibilityGroupRole, + @"list": NSAccessibilityListRole, + @"popupbutton": NSAccessibilityPopUpButtonRole, + @"menubutton": NSAccessibilityMenuButtonRole, + @"table": NSAccessibilityTableRole, + }; + }); + + NSString *role = [traitOrRoleToAccessibilityRole valueForKey:trait]; + if (role == nil) { + role = NSAccessibilityUnknownRole; + } + return role; +} + ++ (NSString *)accessibilityRoleFromTraits:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + return [RCTConvert accessibilityRoleFromTrait:json]; + } else if ([json isKindOfClass:[NSArray class]]) { + for (NSString *trait in json) { + NSString *accessibilityRole = [RCTConvert accessibilityRoleFromTrait:trait]; + if (![accessibilityRole isEqualToString:NSAccessibilityUnknownRole]) { + return accessibilityRole; + } + } + } + return NSAccessibilityUnknownRole; +} + +RCT_ARRAY_CONVERTER(RCTHandledKey); + +#endif // macOS] + @end @interface RCTImageSource (Packager) @@ -1279,23 +1563,29 @@ + (UIImage *)UIImage:(id)json RCTLogConvertError(json, @"an image. File not found."); } } else if ([scheme isEqualToString:@"data"]) { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + image = UIImageWithData([NSData dataWithContentsOfURL:URL]); // [macOS] } else if ([scheme isEqualToString:@"http"] && imageSource.packagerAsset) { - image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + image = UIImageWithData([NSData dataWithContentsOfURL:URL]); // [macOS] } else { RCTLogConvertError(json, @"an image. Only local files or data URIs are supported."); return nil; } + CGImageRef imageRef = UIImageGetCGImageRef(image); // [macOS] +#if !TARGET_OS_OSX // [macOS] CGFloat scale = imageSource.scale; if (!scale && imageSource.size.width) { // If no scale provided, set scale to image width / source width - scale = CGImageGetWidth(image.CGImage) / imageSource.size.width; + scale = CGImageGetWidth(imageRef) / imageSource.size.width; // [macOS] } - if (scale) { - image = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; + image = [UIImage imageWithCGImage:imageRef scale:scale orientation:image.imageOrientation]; + } +#else // [macOS + if (!CGSizeEqualToSize(image.size, imageSource.size)) { + image = [[NSImage alloc] initWithCGImage:imageRef size:imageSource.size]; } +#endif // macOS] if (!CGSizeEqualToSize(imageSource.size, CGSizeZero) && !CGSizeEqualToSize(imageSource.size, image.size)) { RCTLogInfo( @@ -1310,7 +1600,7 @@ + (UIImage *)UIImage:(id)json + (CGImageRef)CGImage:(id)json { - return [self UIImage:json].CGImage; + return UIImageGetCGImageRef([self UIImage:json]); // [macOS] } @end diff --git a/packages/react-native/React/Base/RCTDisplayLink.m b/packages/react-native/React/Base/RCTDisplayLink.m index 4aed812ed7e04f..22daa07d2383bb 100644 --- a/packages/react-native/React/Base/RCTDisplayLink.m +++ b/packages/react-native/React/Base/RCTDisplayLink.m @@ -8,19 +8,24 @@ #import "RCTDisplayLink.h" #import -#import +#import "RCTPlatformDisplayLink.h" // [macOS] #import "RCTAssert.h" #import "RCTBridgeModule.h" #import "RCTFrameUpdate.h" #import "RCTModuleData.h" #import "RCTProfile.h" +#if TARGET_OS_OSX // [macOS Github#533 +// To compile in Xcode 12 beta 4 on macOS, we need to explicitly pull in the framework to get the definition for CACurrentMediaTime() +#import +#endif // macOS] + #define RCTAssertRunLoop() \ RCTAssert(_runLoop == [NSRunLoop currentRunLoop], @"This method must be called on the CADisplayLink run loop") @implementation RCTDisplayLink { - CADisplayLink *_jsDisplayLink; + RCTPlatformDisplayLink *_jsDisplayLink; // [macOS] NSMutableSet *_frameUpdateObservers; NSRunLoop *_runLoop; } @@ -29,7 +34,7 @@ - (instancetype)init { if ((self = [super init])) { _frameUpdateObservers = [NSMutableSet new]; - _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + _jsDisplayLink = [RCTPlatformDisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; // [macOS] } return self; @@ -114,7 +119,7 @@ - (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue } } -- (void)_jsThreadUpdate:(CADisplayLink *)displayLink +- (void)_jsThreadUpdate:(RCTPlatformDisplayLink *)displayLink // [macOS] { RCTAssertRunLoop(); diff --git a/packages/react-native/React/Base/RCTEventDispatcherProtocol.h b/packages/react-native/React/Base/RCTEventDispatcherProtocol.h index 170135bb64b7b9..0705309fef2f09 100644 --- a/packages/react-native/React/Base/RCTEventDispatcherProtocol.h +++ b/packages/react-native/React/Base/RCTEventDispatcherProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Base/RCTFocusChangeEvent.h b/packages/react-native/React/Base/RCTFocusChangeEvent.h new file mode 100644 index 00000000000000..73b31a2482f4cf --- /dev/null +++ b/packages/react-native/React/Base/RCTFocusChangeEvent.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +/** + * Represents a focus change event meaning that a view that can become first responder has become or resigned being first responder. + */ +@interface RCTFocusChangeEvent : RCTComponentEvent + ++ (instancetype)focusEventWithReactTag:(NSNumber *)reactTag; ++ (instancetype)blurEventWithReactTag:(NSNumber *)reactTag; + +@end diff --git a/packages/react-native/React/Base/RCTFocusChangeEvent.m b/packages/react-native/React/Base/RCTFocusChangeEvent.m new file mode 100644 index 00000000000000..6aa48d87203d1d --- /dev/null +++ b/packages/react-native/React/Base/RCTFocusChangeEvent.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTFocusChangeEvent.h" + +#import "RCTAssert.h" + +@implementation RCTFocusChangeEvent + ++ (instancetype)focusEventWithReactTag:(NSNumber *)reactTag +{ + RCTFocusChangeEvent *event = [[self alloc] initWithName:@"focus" + viewTag:reactTag + body:@{}]; + return event; +} + ++ (instancetype)blurEventWithReactTag:(NSNumber *)reactTag +{ + RCTFocusChangeEvent *event = [[self alloc] initWithName:@"blur" + viewTag:reactTag + body:@{}]; + return event; +} + +@end diff --git a/packages/react-native/React/Base/RCTFrameUpdate.h b/packages/react-native/React/Base/RCTFrameUpdate.h index 39d7b72676bea2..f10251bda70d75 100644 --- a/packages/react-native/React/Base/RCTFrameUpdate.h +++ b/packages/react-native/React/Base/RCTFrameUpdate.h @@ -6,8 +6,7 @@ */ #import - -@class CADisplayLink; +#import "RCTPlatformDisplayLink.h" // [macOS] /** * Interface containing the information about the last screen refresh. @@ -24,7 +23,7 @@ */ @property (nonatomic, readonly) NSTimeInterval deltaTime; -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithDisplayLink:(RCTPlatformDisplayLink *)displayLink NS_DESIGNATED_INITIALIZER; // [macOS] @end diff --git a/packages/react-native/React/Base/RCTFrameUpdate.m b/packages/react-native/React/Base/RCTFrameUpdate.m index 86bee10972a6c0..232fbefed7cea2 100644 --- a/packages/react-native/React/Base/RCTFrameUpdate.m +++ b/packages/react-native/React/Base/RCTFrameUpdate.m @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import "RCTPlatformDisplayLink.h" // [macOS] #import "RCTFrameUpdate.h" @@ -15,7 +15,7 @@ @implementation RCTFrameUpdate RCT_NOT_IMPLEMENTED(-(instancetype)init) -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +- (instancetype)initWithDisplayLink:(RCTPlatformDisplayLink *)displayLink // [macOS] { if ((self = [super init])) { _timestamp = displayLink.timestamp; diff --git a/packages/react-native/React/Base/RCTJSThread.h b/packages/react-native/React/Base/RCTJSThread.h index 57503e41326ffd..7c1a503572f5cd 100644 --- a/packages/react-native/React/Base/RCTJSThread.h +++ b/packages/react-native/React/Base/RCTJSThread.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Base/RCTJavaScriptLoader.h b/packages/react-native/React/Base/RCTJavaScriptLoader.h index 9893e6931660e0..9d75bf49369ad0 100755 --- a/packages/react-native/React/Base/RCTJavaScriptLoader.h +++ b/packages/react-native/React/Base/RCTJavaScriptLoader.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Base/RCTKeyCommands.h b/packages/react-native/React/Base/RCTKeyCommands.h index df94f3509d4dd6..14a22d778b0ddd 100644 --- a/packages/react-native/React/Base/RCTKeyCommands.h +++ b/packages/react-native/React/Base/RCTKeyCommands.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import @interface RCTKeyCommands : NSObject @@ -29,3 +30,4 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags; @end +#endif // [macOS] diff --git a/packages/react-native/React/Base/RCTKeyCommands.m b/packages/react-native/React/Base/RCTKeyCommands.m index 1ceaf6ec0dceb1..978e3841839544 100644 --- a/packages/react-native/React/Base/RCTKeyCommands.m +++ b/packages/react-native/React/Base/RCTKeyCommands.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTKeyCommands.h" #import @@ -265,3 +266,4 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyMod @end #endif +#endif // [macOS] diff --git a/packages/react-native/React/Base/RCTPlatformDisplayLink.h b/packages/react-native/React/Base/RCTPlatformDisplayLink.h new file mode 100644 index 00000000000000..91a51ec6b23cc7 --- /dev/null +++ b/packages/react-native/React/Base/RCTPlatformDisplayLink.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#include + +#if !TARGET_OS_OSX +#import +#define RCTPlatformDisplayLink CADisplayLink +#else + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** Class representing a timer bound to the display vsync. **/ +@interface RCTPlatformDisplayLink : NSObject + ++ (RCTPlatformDisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel; + +- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode; + +- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode; + +- (void)invalidate; + +@property (readonly, nonatomic) CFTimeInterval timestamp; +@property (readonly, nonatomic) CFTimeInterval duration; + +@property (getter=isPaused, nonatomic) BOOL paused; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/packages/react-native/React/Base/RCTRedBoxSetEnabled.m b/packages/react-native/React/Base/RCTRedBoxSetEnabled.m index 51142be37507b2..a559af055cca56 100644 --- a/packages/react-native/React/Base/RCTRedBoxSetEnabled.m +++ b/packages/react-native/React/Base/RCTRedBoxSetEnabled.m @@ -7,7 +7,7 @@ #import "RCTRedBoxSetEnabled.h" -#if RCT_DEV +#if RCT_DEV && DEBUG // [macOS] RCT_DEV is always on in the react-native-macos fork, so to not default to redboxing in release builds, trigger the initial value off of the scheme as well static BOOL redBoxEnabled = YES; #else static BOOL redBoxEnabled = NO; diff --git a/packages/react-native/React/Base/RCTReloadCommand.m b/packages/react-native/React/Base/RCTReloadCommand.m index 4d8dd0b0338049..1aa61d526379e5 100644 --- a/packages/react-native/React/Base/RCTReloadCommand.m +++ b/packages/react-native/React/Base/RCTReloadCommand.m @@ -8,7 +8,9 @@ #import "RCTReloadCommand.h" #import "RCTAssert.h" +#if !TARGET_OS_OSX // [macOS] #import "RCTKeyCommands.h" +#endif // [macOS] #import "RCTUtils.h" static NSHashTable> *listeners; @@ -28,7 +30,7 @@ void RCTRegisterReloadCommandListener(id listener) if (!listeners) { listeners = [NSHashTable weakObjectsHashTable]; } -#if RCT_DEV +#if RCT_DEV && !TARGET_OS_OSX // [macOS] RCTAssertMainQueue(); // because registerKeyCommandWithInput: must be called on the main thread static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -38,7 +40,7 @@ void RCTRegisterReloadCommandListener(id listener) RCTTriggerReloadCommandListeners(@"Command + R"); }]; }); -#endif +#endif // [macOS] [listeners addObject:listener]; [listenersLock unlock]; } diff --git a/packages/react-native/React/Base/RCTRootContentView.h b/packages/react-native/React/Base/RCTRootContentView.h index 50a5f3ff93fc70..762c7c4feffd11 100644 --- a/packages/react-native/React/Base/RCTRootContentView.h +++ b/packages/react-native/React/Base/RCTRootContentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Base/RCTRootContentView.m b/packages/react-native/React/Base/RCTRootContentView.m index 24b918cc7f1a1d..32aec8ca69eeb8 100644 --- a/packages/react-native/React/Base/RCTRootContentView.m +++ b/packages/react-native/React/Base/RCTRootContentView.m @@ -8,6 +8,7 @@ #import "RCTRootContentView.h" #import "RCTBridge.h" +#import "RCTConstants.h" // [macOS] #import "RCTPerformanceLogger.h" #import "RCTRootView.h" #import "RCTRootViewInternal.h" @@ -16,6 +17,11 @@ #import "UIView+React.h" @implementation RCTRootContentView +{ // [macOS +#if TARGET_OS_OSX + BOOL _subscribedToWindowNotifications; +#endif +} // macOS] - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge @@ -29,6 +35,10 @@ - (instancetype)initWithFrame:(CGRect)frame _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; [_touchHandler attachToView:self]; [_bridge.uiManager registerRootView:self]; +#if TARGET_OS_OSX // [macOS + self.postsFrameChangedNotifications = YES; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sendFrameChangedEvent:) name:NSViewFrameDidChangeNotification object:self]; +#endif // macOS] } return self; } @@ -36,13 +46,50 @@ - (instancetype)initWithFrame:(CGRect)frame RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (nonnull NSCoder *)aDecoder) +#if TARGET_OS_OSX // [macOS +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)viewWillMoveToWindow:(nullable NSWindow *)newWindow +{ + if (_subscribedToWindowNotifications && + self.window != nil && + self.window != newWindow) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSWindowDidChangeBackingPropertiesNotification + object:self.window]; + _subscribedToWindowNotifications = NO; + } +} + +- (void)viewDidMoveToWindow +{ + if (!_subscribedToWindowNotifications && + self.window != nil) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(sendFrameChangedEvent:) + name:NSWindowDidChangeBackingPropertiesNotification + object:self.window]; + _subscribedToWindowNotifications = YES; + } +} + +- (void)sendFrameChangedEvent:(__unused NSNotification *)notification +{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTRootViewFrameDidChangeNotification object:self]; +} + +#endif // macOS] + - (void)layoutSubviews { [super layoutSubviews]; [self updateAvailableSize]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(RCTUIView *)subview atIndex:(NSInteger)atIndex // [macOS] { [super insertReactSubview:subview atIndex:atIndex]; [_bridge.performanceLogger markStopForTag:RCTPLTTI]; @@ -81,10 +128,10 @@ - (void)updateAvailableSize [_bridge.uiManager setAvailableSize:self.availableSize forRootView:self]; } -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] { // The root content view itself should never receive touches - UIView *hitView = [super hitTest:point withEvent:event]; + RCTPlatformView *hitView = [super hitTest:point withEvent:event]; // [macOS] if (_passThroughTouches && hitView == self) { return nil; } diff --git a/packages/react-native/React/Base/RCTRootView.h b/packages/react-native/React/Base/RCTRootView.h index 2903273639d0af..2f0f05df4d1f3f 100644 --- a/packages/react-native/React/Base/RCTRootView.h +++ b/packages/react-native/React/Base/RCTRootView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -47,7 +47,7 @@ extern * like any ordinary UIView. You can have multiple RCTRootViews on screen at * once, all controlled by the same JavaScript application. */ -@interface RCTRootView : UIView +@interface RCTRootView : RCTUIView // [macOS] /** * - Designated initializer - @@ -121,21 +121,21 @@ extern @property (nonatomic, weak, nullable) UIViewController *reactViewController; /** - * The root view casted as UIView. Used by splash screen libraries. + * The root view casted as RCTUIView. Used by splash screen libraries. // [macOS] */ -@property (nonatomic, strong, readonly) UIView *view; +@property (nonatomic, strong, readonly) RCTUIView *view; // [macOS] /** * The React-managed contents view of the root view. */ -@property (nonatomic, strong, readonly) UIView *contentView; +@property (nonatomic, strong, readonly) RCTUIView *contentView; // [macOS] /** * A view to display while the JavaScript is loading, so users aren't presented * with a blank screen. By default this is nil, but you can override it with * (for example) a UIActivityIndicatorView or a placeholder image. */ -@property (nonatomic, strong, nullable) UIView *loadingView; +@property (nonatomic, strong, nullable) RCTUIView *loadingView; // [macOS] /** * When set, any touches on the RCTRootView that are not matched up to any of the child diff --git a/packages/react-native/React/Base/RCTRootView.m b/packages/react-native/React/Base/RCTRootView.m index 5acd5f03c4c951..592043f82df2e5 100644 --- a/packages/react-native/React/Base/RCTRootView.m +++ b/packages/react-native/React/Base/RCTRootView.m @@ -15,7 +15,8 @@ #import "RCTBridge+Private.h" #import "RCTBridge.h" #import "RCTConstants.h" -#import "RCTKeyCommands.h" +#import "RCTDevSettings.h" // [macOS] +// [macOS] remove #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTPerformanceLogger.h" #import "RCTProfile.h" @@ -28,6 +29,10 @@ #import "RCTView.h" #import "UIView+React.h" +#if __has_include("RCTDevMenu.h") // [macOS +#import "RCTDevMenu.h" +#endif // macOS] + NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; @interface RCTUIManager (RCTRootView) @@ -59,7 +64,9 @@ - (instancetype)initWithFrame:(CGRect)frame } if (self = [super initWithFrame:frame]) { + /* [TODO(OSS Candidate ISS#2710739): don't set the background color on mac or ios so that the view is invisible during initial render self.backgroundColor = [UIColor whiteColor]; + ]TODO(OSS Candidate ISS#2710739) */ _bridge = bridge; _moduleName = moduleName; @@ -116,7 +123,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder) -- (UIView *)view +- (RCTUIView *)view // [macOS] { return self; } @@ -156,7 +163,24 @@ - (void)layoutSubviews { [super layoutSubviews]; _contentView.frame = self.bounds; +#if !TARGET_OS_OSX // [macOS] _loadingView.center = (CGPoint){CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)}; +#else // [macOS + NSRect bounds = self.bounds; + NSSize loadingViewSize = _loadingView.frame.size; + CGFloat scale = self.window.backingScaleFactor; + if (scale == 0.0 && RCTRunningInTestEnvironment()) { + // When running in the test environment the view is not on screen. + // Use a scaleFactor of 1 so that the test results are machine independent. + scale = 1; + } + RCTAssert(scale != 0.0, @"Layout occurs before the view is in a window?"); + + _loadingView.frameOrigin = NSMakePoint( + RCTRoundPixelValue(bounds.origin.x + ((bounds.size.width - loadingViewSize.width) / 2), scale), + RCTRoundPixelValue(bounds.origin.y + ((bounds.size.height - loadingViewSize.height) / 2), scale) + ); +#endif // macOS] } - (void)setMinimumSize:(CGSize)minimumSize @@ -183,10 +207,14 @@ - (UIViewController *)reactViewController - (BOOL)canBecomeFirstResponder { +#if !TARGET_OS_OSX // [macOS] return YES; +#else // [macOS + return NO; // commit 01aba7e8: Merged PR 94656: Enable keyboard accessibility and support for focus ring drawing for button, textfields etc +#endif // macOS] } -- (void)setLoadingView:(UIView *)loadingView +- (void)setLoadingView:(RCTUIView *)loadingView // [macOS] { _loadingView = loadingView; if (!_contentView.contentHasAppeared) { @@ -210,6 +238,7 @@ - (void)hideLoadingView dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_loadingViewFadeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +#if !TARGET_OS_OSX // [macOS] [UIView transitionWithView:self duration:self->_loadingViewFadeDuration options:UIViewAnimationOptionTransitionCrossDissolve @@ -219,7 +248,16 @@ - (void)hideLoadingView completion:^(__unused BOOL finished) { [self->_loadingView removeFromSuperview]; }]; - }); +#else // [macOS + [NSAnimationContext runAnimationGroup:^(__unused NSAnimationContext *context) { + self->_loadingView.animator.alphaValue = 0.0; + } completionHandler:^{ + [self->_loadingView removeFromSuperview]; + self->_loadingView.hidden = YES; + self->_loadingView.alphaValue = 1.0; + }]; +#endif // macOS] + }); } else { _loadingView.hidden = YES; [_loadingView removeFromSuperview]; @@ -307,10 +345,10 @@ - (void)setSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility _contentView.sizeFlexibility = _sizeFlexibility; } -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] { // The root view itself should never receive touches - UIView *hitView = [super hitTest:point withEvent:event]; + RCTPlatformView *hitView = [super hitTest:point withEvent:event]; // [macOS] if (self.passThroughTouches && hitView == self) { return nil; } @@ -342,13 +380,24 @@ - (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize _intrinsicContentSize = intrinsicContentSize; + [self invalidateIntrinsicContentSize]; +#if !TARGET_OS_OSX // [macOS] + [self.superview setNeedsLayout]; +#else // [macOS + [self.superview setNeedsLayout:YES]; +#endif // macOS] + // Don't notify the delegate if the content remains invisible or its size has not changed if (bothSizesHaveAZeroDimension || sizesAreEqual) { return; } [self invalidateIntrinsicContentSize]; - [self.superview setNeedsLayout]; + #if !TARGET_OS_OSX // [macOS] + [self.superview setNeedsLayout]; + #else // [macOS + [self.superview setNeedsLayout:YES]; + #endif // macOS] [_delegate rootViewDidChangeIntrinsicSize:self]; } @@ -365,6 +414,7 @@ - (void)contentViewInvalidated [self showLoadingView]; } +#if !TARGET_OS_OSX // [macOS] - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; @@ -379,12 +429,41 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey : self.traitCollection, }]; } +#else // [macOS +- (void)viewDidChangeEffectiveAppearance +{ + [super viewDidChangeEffectiveAppearance]; + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTUserInterfaceStyleDidChangeNotification + object:self + userInfo:@{ + RCTUserInterfaceStyleDidChangeNotificationAppearanceKey : self.effectiveAppearance, + }]; +} +#endif // macOS] + + +#if TARGET_OS_OSX // [macOS +- (NSMenu *)menuForEvent:(NSEvent *)event +{ + NSMenu *menu = nil; +#if __has_include("RCTDevMenu.h") && RCT_DEV + menu = [[_bridge devMenu] menu]; +#endif + if (menu == nil) { + menu = [super menuForEvent:event]; + } + if (menu) { + [[_contentView touchHandler] willShowMenuWithEvent:event]; + } + return menu; +} +#endif // macOS] - (void)dealloc { [_contentView invalidate]; } - @end @implementation RCTRootView (Deprecated) diff --git a/packages/react-native/React/Base/RCTTouchHandler.h b/packages/react-native/React/Base/RCTTouchHandler.h index 6b1b0642dff002..93c4e8c3deb469 100644 --- a/packages/react-native/React/Base/RCTTouchHandler.h +++ b/packages/react-native/React/Base/RCTTouchHandler.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @@ -15,9 +15,17 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (void)attachToView:(UIView *)view; -- (void)detachFromView:(UIView *)view; +- (void)attachToView:(RCTUIView *)view; // [macOS] +- (void)detachFromView:(RCTUIView *)view; // [macOS] - (void)cancel; +#if TARGET_OS_OSX // [macOS ++ (instancetype)touchHandlerForEvent:(NSEvent *)event; ++ (instancetype)touchHandlerForView:(NSView *)view; + +- (void)willShowMenuWithEvent:(NSEvent *)event; +- (void)cancelTouchWithEvent:(NSEvent *)event; +#endif // macOS] + @end diff --git a/packages/react-native/React/Base/RCTTouchHandler.m b/packages/react-native/React/Base/RCTTouchHandler.m index 61fc96b86bb9fa..ee4ada17d17e66 100644 --- a/packages/react-native/React/Base/RCTTouchHandler.m +++ b/packages/react-native/React/Base/RCTTouchHandler.m @@ -7,7 +7,10 @@ #import "RCTTouchHandler.h" +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] +#import // [macOS] #import "RCTAssert.h" #import "RCTBridge.h" @@ -33,13 +36,16 @@ @implementation RCTTouchHandler { * These must be kept track of because `UIKit` destroys the touch targets * if touches are canceled, and we have no other way to recover this info. */ - NSMutableOrderedSet *_nativeTouches; + NSMutableOrderedSet *_nativeTouches; // [macOS] NSMutableArray *_reactTouches; - NSMutableArray *_touchViews; + NSMutableArray *_touchViews; // [macOS] - __weak UIView *_cachedRootView; + __weak RCTPlatformView *_cachedRootView; // [macOS] uint16_t _coalescingKey; +#if TARGET_OS_OSX// [macOS + BOOL _shouldSendMouseUpOnSystemBehalf; +#endif// macOS] } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -53,12 +59,18 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _reactTouches = [NSMutableArray new]; _touchViews = [NSMutableArray new]; +#if !TARGET_OS_OSX // [macOS] // `cancelsTouchesInView` and `delaysTouches*` are needed in order to be used as a top level // event delegated recognizer. Otherwise, lower-level components not built // using RCT, will fail to recognize gestures. self.cancelsTouchesInView = NO; self.delaysTouchesBegan = NO; // This is default value. self.delaysTouchesEnded = NO; +#else // [macOS + self.delaysPrimaryMouseButtonEvents = NO; // default is NO. + self.delaysSecondaryMouseButtonEvents = NO; // default is NO. + self.delaysOtherMouseButtonEvents = NO; // default is NO. +#endif // macOS] self.delegate = self; } @@ -67,15 +79,18 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge } RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action) +#if TARGET_OS_OSX // [macOS +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)coder) +#endif // macOS] -- (void)attachToView:(UIView *)view +- (void)attachToView:(RCTUIView *)view // [macOS] { RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view."); [view addGestureRecognizer:self]; } -- (void)detachFromView:(UIView *)view +- (void)detachFromView:(RCTUIView *)view // [macOS] { RCTAssertParam(view); RCTAssert(self.view == view, @"RCTTouchHandler attached to another view."); @@ -85,12 +100,19 @@ - (void)detachFromView:(UIView *)view #pragma mark - Bookkeeping for touch indices -- (void)_recordNewTouches:(NSSet *)touches +- (void)_recordNewTouches:(NSSet *)touches { +#if !TARGET_OS_OSX // [macOS] for (UITouch *touch in touches) { +#else // [macOS + for (NSEvent *touch in touches) { +#endif // macOS] + RCTAssert(![_nativeTouches containsObject:touch], @"Touch is already recorded. This is a critical bug."); // Find closest React-managed touchable view + +#if !TARGET_OS_OSX // [macOS] UIView *targetView = touch.view; while (targetView) { if (targetView.reactTag && targetView.userInteractionEnabled) { @@ -103,6 +125,48 @@ - (void)_recordNewTouches:(NSSet *)touches if (!reactTag || !targetView.userInteractionEnabled) { continue; } +#else // [macOS + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + // The assumption here is that a RCTUIView/RCTSurfaceView will always have a superview. + CGPoint touchLocation = [self.view.superview convertPoint:touch.locationInWindow fromView:nil]; + NSView *targetView = [self.view hitTest:touchLocation]; + // Don't record clicks on scrollbars. + if ([targetView isKindOfClass:[NSScroller class]]) { + continue; + } + // Pair the mouse down events with mouse up events so our _nativeTouches cache doesn't get stale + if ([targetView isKindOfClass:[NSControl class]]) { + _shouldSendMouseUpOnSystemBehalf = [(NSControl*)targetView isEnabled]; + } else if ([targetView isKindOfClass:[NSText class]]) { + _shouldSendMouseUpOnSystemBehalf = [(NSText*)targetView isSelectable]; + } + else if ([targetView.superview isKindOfClass:[RCTUITextField class]]) { + _shouldSendMouseUpOnSystemBehalf = [(RCTUITextField*)targetView.superview isSelectable]; + } else { + _shouldSendMouseUpOnSystemBehalf = NO; + } + touchLocation = [targetView convertPoint:touchLocation fromView:self.view.superview]; + + while (targetView) { + BOOL isUserInteractionEnabled = NO; + if ([((RCTUIView*)targetView) respondsToSelector:@selector(isUserInteractionEnabled)]) { // [macOS] + isUserInteractionEnabled = ((RCTUIView*)targetView).isUserInteractionEnabled; // [macOS] + } + if (targetView.reactTag && isUserInteractionEnabled) { + break; + } + targetView = targetView.superview; + } + + NSNumber *reactTag = [targetView reactTagAtPoint:touchLocation]; + BOOL isUserInteractionEnabled = NO; + if ([((RCTUIView*)targetView) respondsToSelector:@selector(isUserInteractionEnabled)]) { // [macOS] + isUserInteractionEnabled = ((RCTUIView*)targetView).isUserInteractionEnabled; // [macOS] + } + if (!reactTag || !isUserInteractionEnabled) { + continue; + } +#endif // macOS] // Get new, unique touch identifier for the react touch const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices @@ -130,10 +194,17 @@ - (void)_recordNewTouches:(NSSet *)touches } } -- (void)_recordRemovedTouches:(NSSet *)touches +- (void)_recordRemovedTouches:(NSSet *)touches { +#if !TARGET_OS_OSX // [macOS] for (UITouch *touch in touches) { - NSUInteger index = [_nativeTouches indexOfObject:touch]; + NSInteger index = [_nativeTouches indexOfObject:touch]; +#else // [macOS + for (NSEvent *touch in touches) { + NSInteger index = [_nativeTouches indexOfObjectPassingTest:^BOOL(NSEvent *event, __unused NSUInteger idx, __unused BOOL *stop) { + return touch.eventNumber == event.eventNumber; + }]; +#endif // macOS] if (index == NSNotFound) { continue; } @@ -146,6 +217,7 @@ - (void)_recordRemovedTouches:(NSSet *)touches - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex { +#if !TARGET_OS_OSX // [macOS] UITouch *nativeTouch = _nativeTouches[touchIndex]; CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window]; RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch"); @@ -153,6 +225,15 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex UIView *touchView = _touchViews[touchIndex]; CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView]; +#else // [macOS + NSEvent *nativeTouch = _nativeTouches[touchIndex]; + CGPoint location = nativeTouch.locationInWindow; + RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch"); + CGPoint rootViewLocation = [_cachedRootView convertPoint:location fromView:nil]; + + NSView *touchView = _touchViews[touchIndex]; + CGPoint touchViewLocation = [touchView convertPoint:location fromView:nil]; +#endif // macOS] NSMutableDictionary *reactTouch = _reactTouches[touchIndex]; reactTouch[@"pageX"] = @(RCTSanitizeNaNValue(rootViewLocation.x, @"touchEvent.pageX")); @@ -161,6 +242,7 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex reactTouch[@"locationY"] = @(RCTSanitizeNaNValue(touchViewLocation.y, @"touchEvent.locationY")); reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS +#if !TARGET_OS_OSX // [macOS] // TODO: force for a 'normal' touch is usually 1.0; // should we expose a `normalTouchForce` constant somewhere (which would // have a value of `1.0 / nativeTouch.maximumPossibleForce`)? @@ -170,6 +252,28 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force / nativeTouch.maximumPossibleForce)); reactTouch[@"altitudeAngle"] = @(RCTZeroIfNaN(nativeTouch.altitudeAngle)); } +#else // [macOS + NSEventModifierFlags modifierFlags = nativeTouch.modifierFlags; + if (modifierFlags & NSEventModifierFlagShift) { + reactTouch[@"shiftKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagControl) { + reactTouch[@"ctrlKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagOption) { + reactTouch[@"altKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagCommand) { + reactTouch[@"metaKey"] = @YES; + } + + NSEventType type = nativeTouch.type; + if (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp || type == NSEventTypeLeftMouseDragged) { + reactTouch[@"button"] = @0; + } else if (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp || type == NSEventTypeRightMouseDragged) { + reactTouch[@"button"] = @2; + } +#endif // macOS] } /** @@ -183,15 +287,31 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex * (start/end/move/cancel) and the indices that represent "changed" `Touch`es * from that array. */ +#if !TARGET_OS_OSX // [macOS] - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName +#else // [macOS +- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName +#endif // macOS] { // Update touches NSMutableArray *changedIndexes = [NSMutableArray new]; +#if !TARGET_OS_OSX // [macOS] for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; +#else // [macOS + for (NSEvent *touch in touches) { + NSInteger index = [_nativeTouches indexOfObjectPassingTest:^BOOL(NSEvent *event, __unused NSUInteger idx, __unused BOOL *stop) { + return touch.eventNumber == event.eventNumber; + }]; +#endif // macOS] + if (index == NSNotFound) { continue; } + +#if TARGET_OS_OSX // [macOS + _nativeTouches[index] = touch; +#endif // macOS] [self _updateReactTouchAtIndex:index]; [changedIndexes addObject:@(index)]; @@ -244,7 +364,7 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSStrin */ - (void)_cacheRootView { - UIView *rootView = self.view; + RCTPlatformView *rootView = self.view; // [macOS] while (rootView.superview && ![rootView isReactRootView] && ![rootView isKindOfClass:[RCTSurfaceView class]]) { rootView = rootView.superview; } @@ -253,7 +373,8 @@ - (void)_cacheRootView #pragma mark - Gesture Recognizer Delegate Callbacks -static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) +#if !TARGET_OS_OSX // [macOS] +static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) // [macOS] { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) { @@ -263,7 +384,7 @@ static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) return YES; } -static BOOL RCTAnyTouchesChanged(NSSet *touches) +static BOOL RCTAnyTouchesChanged(NSSet *touches) // [macOS] { for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) { @@ -272,19 +393,23 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) } return NO; } +#endif // [macOS] #pragma mark - `UIResponder`-ish touch-delivery methods -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +- (void)interactionsBegan:(NSSet *)touches // [macOS] { - [super touchesBegan:touches withEvent:event]; - [self _cacheRootView]; // "start" has to record new touches *before* extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; + // [macOS Filter out touches that were ignored. + touches = [touches objectsPassingTest:^(id touch, BOOL *stop) { + return [_nativeTouches containsObject:touch]; + }]; // macOS] + [self _updateAndDispatchTouches:touches eventName:@"touchStart"]; if (self.state == UIGestureRecognizerStatePossible) { @@ -294,43 +419,130 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +- (void)interactionsMoved:(NSSet *)touches // [macOS] { - [super touchesMoved:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventName:@"touchMove"]; self.state = UIGestureRecognizerStateChanged; } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +- (void)interactionsEnded:(NSSet *)touches withEvent:(UIEvent*)event // [macOS] { - [super touchesEnded:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventName:@"touchEnd"]; - +#if !TARGET_OS_OSX // [macOS] if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) { self.state = UIGestureRecognizerStateEnded; } else if (RCTAnyTouchesChanged(event.allTouches)) { self.state = UIGestureRecognizerStateChanged; } +#else // [macOS + self.state = UIGestureRecognizerStateEnded; +#endif // macOS] [self _recordRemovedTouches:touches]; } -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +- (void)interactionsCancelled:(NSSet *)touches withEvent:(UIEvent*)event // [macOS] { - [super touchesCancelled:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventName:@"touchCancel"]; - +#if !TARGET_OS_OSX // [macOS] if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) { self.state = UIGestureRecognizerStateCancelled; } else if (RCTAnyTouchesChanged(event.allTouches)) { self.state = UIGestureRecognizerStateChanged; } - +#else // [macOS + self.state = UIGestureRecognizerStateCancelled; +#endif // macOS] + [self _recordRemovedTouches:touches]; } + +#if !TARGET_OS_OSX // [macOS] +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + [self interactionsBegan:touches]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; + [self interactionsMoved:touches]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + [self interactionsEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + [self interactionsCancelled:touches withEvent:event]; +} +#else // [macOS + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + // This will only be called if the hit-tested view returns YES for acceptsFirstMouse, + // therefore asking it again would be redundant. + return YES; +} + +- (void)mouseDown:(NSEvent *)event +{ + [super mouseDown:event]; + [self interactionsBegan:[NSSet setWithObject:event]]; + // [macOS + if (_shouldSendMouseUpOnSystemBehalf) { + _shouldSendMouseUpOnSystemBehalf = NO; + + NSEvent *newEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp + location:[event locationInWindow] + modifierFlags:[event modifierFlags] + timestamp:[event timestamp] + windowNumber:[event windowNumber] + context:nil + eventNumber:[event eventNumber] + clickCount:[event clickCount] + pressure:[event pressure]]; + [self interactionsEnded:[NSSet setWithObject:newEvent] withEvent:newEvent]; + // macOS] + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + [super rightMouseDown:event]; + [self interactionsBegan:[NSSet setWithObject:event]]; +} + +- (void)mouseDragged:(NSEvent *)event +{ + [super mouseDragged:event]; + [self interactionsMoved:[NSSet setWithObject:event]]; +} + +- (void)rightMouseDragged:(NSEvent *)event +{ + [super rightMouseDragged:event]; + [self interactionsMoved:[NSSet setWithObject:event]]; +} + +- (void)mouseUp:(NSEvent *)event +{ + [super mouseUp:event]; + [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; +} + +- (void)rightMouseUp:(NSEvent *)event +{ + [super rightMouseUp:event]; + [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; +} + +#endif // macOS] - (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGestureRecognizer { @@ -341,7 +553,7 @@ - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestu { // We fail in favour of other external gesture recognizers. // iOS will ask `delegate`'s opinion about this gesture recognizer little bit later. - return ![preventingGestureRecognizer.view isDescendantOfView:self.view]; + return !RCTUIViewIsDescendantOfView(preventingGestureRecognizer.view, self.view); // macOS } - (void)reset @@ -365,6 +577,45 @@ - (void)cancel self.enabled = YES; } +#if TARGET_OS_OSX // [macOS ++ (instancetype)touchHandlerForEvent:(NSEvent *)event { + // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow]; + return [self touchHandlerForView:hitView]; +} + ++ (instancetype)touchHandlerForView:(NSView *)view { + if ([view isKindOfClass:[RCTRootView class]]) { + // The RCTTouchHandler is attached to the contentView. + view = ((RCTRootView *)view).contentView; + } + + while (view) { + for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:[self class]]) { + return (RCTTouchHandler *)gestureRecognizer; + } + } + + view = view.superview; + } + + return nil; +} + +- (void)willShowMenuWithEvent:(NSEvent *)event +{ + if (event.type == NSEventTypeRightMouseDown) { + [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; + } +} + +- (void)cancelTouchWithEvent:(NSEvent *)event +{ + [self interactionsCancelled:[NSSet setWithObject:event] withEvent:event]; +} +#endif // macOS] + #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer diff --git a/packages/react-native/React/Base/RCTUIKit.h b/packages/react-native/React/Base/RCTUIKit.h new file mode 100644 index 00000000000000..94614f55775bf0 --- /dev/null +++ b/packages/react-native/React/Base/RCTUIKit.h @@ -0,0 +1,607 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#include + +#include + +#if !TARGET_OS_OSX + +#import + +NS_ASSUME_NONNULL_BEGIN + +// +// functionally equivalent types +// + +UIKIT_STATIC_INLINE CGFloat UIImageGetScale(UIImage *image) +{ + return image.scale; +} + +UIKIT_STATIC_INLINE CGImageRef UIImageGetCGImageRef(UIImage *image) +{ + return image.CGImage; +} + +UIKIT_STATIC_INLINE UIImage *UIImageWithContentsOfFile(NSString *filePath) +{ + return [UIImage imageWithContentsOfFile:filePath]; +} + +UIKIT_STATIC_INLINE UIImage *UIImageWithData(NSData *imageData) +{ + return [UIImage imageWithData:imageData]; +} + +UIKIT_STATIC_INLINE UIBezierPath *UIBezierPathWithRoundedRect(CGRect rect, CGFloat cornerRadius) +{ + return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; +} + +UIKIT_STATIC_INLINE void UIBezierPathAppendPath(UIBezierPath *path, UIBezierPath *appendPath) +{ + [path appendPath:appendPath]; +} + +UIKIT_STATIC_INLINE CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path) +{ + return [path CGPath]; +} + +// +// substantially different types +// + +// UIView +#define RCTPlatformView UIView +#define RCTUIView UIView +#define RCTUIScrollView UIScrollView +#define RCTPlatformWindow UIWindow + +UIKIT_STATIC_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, __unused UIEvent *__nullable event) +{ + return [view hitTest:point withEvent:event]; +} + +UIKIT_STATIC_INLINE BOOL RCTUIViewSetClipsToBounds(RCTPlatformView *view) +{ + return view.clipsToBounds; +} + +UIKIT_STATIC_INLINE void RCTUIViewSetContentModeRedraw(UIView *view) +{ + view.contentMode = UIViewContentModeRedraw; +} + +UIKIT_STATIC_INLINE BOOL RCTUIViewIsDescendantOfView(RCTPlatformView *view, RCTPlatformView *parent) +{ + return [view isDescendantOfView:parent]; +} + +UIKIT_STATIC_INLINE NSValue *NSValueWithCGRect(CGRect rect) +{ + return [NSValue valueWithCGRect:rect]; +} + +UIKIT_STATIC_INLINE NSValue *NSValueWithCGSize(CGSize size) +{ + return [NSValue valueWithCGSize:size]; +} + +UIKIT_STATIC_INLINE CGRect CGRectValue(NSValue *value) +{ + return [value CGRectValue]; +} + +// +// semantically equivalent types +// + +#define RCTUIColor UIColor + +UIKIT_STATIC_INLINE UIFont *UIFontWithSize(UIFont *font, CGFloat pointSize) +{ + return [font fontWithSize:pointSize]; +} + +UIKIT_STATIC_INLINE CGFloat UIFontLineHeight(UIFont *font) +{ + return [font lineHeight]; +} + +NS_ASSUME_NONNULL_END + +#else // TARGET_OS_OSX [ + +#import + +NS_ASSUME_NONNULL_BEGIN + +// +// semantically equivalent constants +// + +// UIApplication.h/NSApplication.h +#define UIApplicationDidBecomeActiveNotification NSApplicationDidBecomeActiveNotification +#define UIApplicationDidEnterBackgroundNotification NSApplicationDidHideNotification +#define UIApplicationDidFinishLaunchingNotification NSApplicationDidFinishLaunchingNotification +#define UIApplicationWillResignActiveNotification NSApplicationWillResignActiveNotification +#define UIApplicationWillEnterForegroundNotification NSApplicationWillUnhideNotification + +// UIFontDescriptor.h/NSFontDescriptor.h +#define UIFontDescriptorFamilyAttribute NSFontFamilyAttribute; +#define UIFontDescriptorNameAttribute NSFontNameAttribute; +#define UIFontDescriptorFaceAttribute NSFontFaceAttribute; +#define UIFontDescriptorSizeAttribute NSFontSizeAttribute + +#define UIFontDescriptorTraitsAttribute NSFontTraitsAttribute +#define UIFontDescriptorFeatureSettingsAttribute NSFontFeatureSettingsAttribute + +#define UIFontSymbolicTrait NSFontSymbolicTrait +#define UIFontWeightTrait NSFontWeightTrait +#define UIFontFeatureTypeIdentifierKey NSFontFeatureTypeIdentifierKey +#define UIFontFeatureSelectorIdentifierKey NSFontFeatureSelectorIdentifierKey + +#define UIFontWeightUltraLight NSFontWeightUltraLight +#define UIFontWeightThin NSFontWeightThin +#define UIFontWeightLight NSFontWeightLight +#define UIFontWeightRegular NSFontWeightRegular +#define UIFontWeightMedium NSFontWeightMedium +#define UIFontWeightSemibold NSFontWeightSemibold +#define UIFontWeightBold NSFontWeightBold +#define UIFontWeightHeavy NSFontWeightHeavy +#define UIFontWeightBlack NSFontWeightBlack + +// RCTActivityIndicatorView.h +#define UIActivityIndicatorView NSProgressIndicator + + +// UIGeometry.h/NSGeometry.h +#define UIEdgeInsetsZero NSEdgeInsetsZero + +// UIView.h/NSLayoutConstraint.h +#define UIViewNoIntrinsicMetric -1 +// NSViewNoIntrinsicMetric is defined to -1 but is only available on macOS 10.11 and higher. On previous versions it was NSViewNoInstrinsicMetric (misspelled) and also defined to -1. + +// UIInterface.h/NSUserInterfaceLayout.h +#define UIUserInterfaceLayoutDirection NSUserInterfaceLayoutDirection + +// +// semantically equivalent enums +// + +// UIGestureRecognizer.h/NSGestureRecognizer.h +enum +{ + UIGestureRecognizerStatePossible = NSGestureRecognizerStatePossible, + UIGestureRecognizerStateBegan = NSGestureRecognizerStateBegan, + UIGestureRecognizerStateChanged = NSGestureRecognizerStateChanged, + UIGestureRecognizerStateEnded = NSGestureRecognizerStateEnded, + UIGestureRecognizerStateCancelled = NSGestureRecognizerStateCancelled, + UIGestureRecognizerStateFailed = NSGestureRecognizerStateFailed, + UIGestureRecognizerStateRecognized = NSGestureRecognizerStateRecognized, +}; + +// UIFontDescriptor.h/NSFontDescriptor.h +enum +{ + UIFontDescriptorTraitItalic = NSFontItalicTrait, + UIFontDescriptorTraitBold = NSFontBoldTrait, + UIFontDescriptorTraitCondensed = NSFontCondensedTrait, +}; + +// UIView.h/NSView.h +enum : NSUInteger +{ + UIViewAutoresizingNone = NSViewNotSizable, + UIViewAutoresizingFlexibleLeftMargin = NSViewMinXMargin, + UIViewAutoresizingFlexibleWidth = NSViewWidthSizable, + UIViewAutoresizingFlexibleRightMargin = NSViewMaxXMargin, + UIViewAutoresizingFlexibleTopMargin = NSViewMinYMargin, + UIViewAutoresizingFlexibleHeight = NSViewHeightSizable, + UIViewAutoresizingFlexibleBottomMargin = NSViewMaxYMargin, +}; + +// UIView/NSView.h +typedef NS_ENUM(NSInteger, UIViewContentMode) { + UIViewContentModeScaleAspectFill = NSViewLayerContentsPlacementScaleProportionallyToFill, + UIViewContentModeScaleAspectFit = NSViewLayerContentsPlacementScaleProportionallyToFit, + UIViewContentModeScaleToFill = NSViewLayerContentsPlacementScaleAxesIndependently, + UIViewContentModeCenter = NSViewLayerContentsPlacementCenter, +}; + +// UIInterface.h/NSUserInterfaceLayout.h +enum : NSInteger +{ + UIUserInterfaceLayoutDirectionLeftToRight = NSUserInterfaceLayoutDirectionLeftToRight, + UIUserInterfaceLayoutDirectionRightToLeft = NSUserInterfaceLayoutDirectionRightToLeft, +}; + +// RCTActivityIndicatorView.h +typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) { + UIActivityIndicatorViewStyleLarge, + UIActivityIndicatorViewStyleMedium, +}; + + +// +// semantically equivalent functions +// + +// UIGeometry.h/NSGeometry.h +NS_INLINE CGRect UIEdgeInsetsInsetRect(CGRect rect, NSEdgeInsets insets) +{ + rect.origin.x += insets.left; + rect.origin.y += insets.top; + rect.size.width -= (insets.left + insets.right); + rect.size.height -= (insets.top + insets.bottom); + return rect; +} + +NS_INLINE BOOL UIEdgeInsetsEqualToEdgeInsets(NSEdgeInsets insets1, NSEdgeInsets insets2) +{ + return NSEdgeInsetsEqual(insets1, insets2); +} + +NS_INLINE NSString *NSStringFromCGSize(CGSize size) +{ + return NSStringFromSize(NSSizeFromCGSize(size)); +} + +NS_INLINE NSString *NSStringFromCGRect(CGRect rect) +{ + return NSStringFromRect(NSRectFromCGRect(rect)); +} + +#ifdef __cplusplus +extern "C" { +#endif + +// UIGraphics.h +CGContextRef UIGraphicsGetCurrentContext(void); +void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); +NSImage *UIGraphicsGetImageFromCurrentImageContext(void); +void UIGraphicsEndImageContext(void); +CGImageRef UIImageGetCGImageRef(NSImage *image); + +#ifdef __cplusplus +} +#endif // __cpusplus + +// +// semantically equivalent types +// + +// UIAccessibility.h/NSAccessibility.h +@compatibility_alias UIAccessibilityCustomAction NSAccessibilityCustomAction; + +// UIColor.h/NSColor.h +#define RCTUIColor NSColor + +// UIFont.h/NSFont.h +// Both NSFont and UIFont are toll-free bridged to CTFontRef so we'll assume they're semantically equivalent +@compatibility_alias UIFont NSFont; + +// UIViewController.h/NSViewController.h +@compatibility_alias UIViewController NSViewController; + +NS_INLINE NSFont *UIFontWithSize(NSFont *font, CGFloat pointSize) +{ + return [NSFont fontWithDescriptor:font.fontDescriptor size:pointSize]; +} + +NS_INLINE CGFloat UIFontLineHeight(NSFont *font) +{ + return ceilf(font.ascender + ABS(font.descender) + font.leading); +} + +// UIFontDescriptor.h/NSFontDescriptor.h +// Both NSFontDescriptor and UIFontDescriptor are toll-free bridged to CTFontDescriptorRef so we'll assume they're semantically equivalent +@compatibility_alias UIFontDescriptor NSFontDescriptor; +typedef NSFontSymbolicTraits UIFontDescriptorSymbolicTraits; +typedef NSFontWeight UIFontWeight; + +// UIGeometry.h/NSGeometry.h +typedef NSEdgeInsets UIEdgeInsets; + +NS_INLINE NSEdgeInsets UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right) +{ + return NSEdgeInsetsMake(top, left, bottom, right); +} + +// +// functionally equivalent types +// + +// These types have the same purpose but may differ semantically. Use with care! + +#define UIEvent NSEvent +#define UITouchType NSTouchType +#define UIEventButtonMask NSEventButtonMask +#define UIKeyModifierFlags NSEventModifierFlags + +// UIGestureRecognizer +#define UIGestureRecognizer NSGestureRecognizer +#define UIGestureRecognizerDelegate NSGestureRecognizerDelegate + +// UIApplication +#define UIApplication NSApplication + +// UIImage +@compatibility_alias UIImage NSImage; + +typedef NS_ENUM(NSInteger, UIImageRenderingMode) { + UIImageRenderingModeAlwaysOriginal, + UIImageRenderingModeAlwaysTemplate, +}; + +#ifdef __cplusplus +extern "C" +#endif +CGFloat UIImageGetScale(NSImage *image); + +CGImageRef UIImageGetCGImageRef(NSImage *image); + +NS_INLINE UIImage *UIImageWithContentsOfFile(NSString *filePath) +{ + return [[NSImage alloc] initWithContentsOfFile:filePath]; +} + +NS_INLINE UIImage *UIImageWithData(NSData *imageData) +{ + return [[NSImage alloc] initWithData:imageData]; +} + +NSData *UIImagePNGRepresentation(NSImage *image); +NSData *UIImageJPEGRepresentation(NSImage *image, CGFloat compressionQuality); + +// UIBezierPath +@compatibility_alias UIBezierPath NSBezierPath; + +UIBezierPath *UIBezierPathWithRoundedRect(CGRect rect, CGFloat cornerRadius); + +void UIBezierPathAppendPath(UIBezierPath *path, UIBezierPath *appendPath); + +CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path); + +// +// substantially different types +// + +// UIView +#define RCTPlatformView NSView + +#define RCTPlatformWindow NSWindow + +@interface RCTUIView : RCTPlatformView + +@property (nonatomic, readonly) BOOL canBecomeFirstResponder; +- (BOOL)becomeFirstResponder; +@property(nonatomic, readonly) BOOL isFirstResponder; + +@property (nonatomic, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; + +- (NSView *)hitTest:(CGPoint)point withEvent:(UIEvent *_Nullable)event; +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; + +- (void)insertSubview:(NSView *)view atIndex:(NSInteger)index; + +- (void)didMoveToWindow; + +- (void)setNeedsLayout; +- (void)layoutIfNeeded; + +- (void)layoutSubviews; + +- (void)setNeedsDisplay; + +// FUTURE: When Xcode 14 is no longer supported (CI is building with Xcode 15), we can remove this override since it's now declared on NSView +@property BOOL clipsToBounds; +@property (nonatomic, copy) NSColor *backgroundColor; +@property (nonatomic) CGAffineTransform transform; + +/** + * Specifies whether the view should receive the mouse down event when the + * containing window is in the background. + */ +@property (nonatomic, assign) BOOL acceptsFirstMouse; + +@property (nonatomic, assign) BOOL mouseDownCanMoveWindow; + +/** + * Specifies whether the view participates in the key view loop as user tabs through different controls + * This is equivalent to acceptsFirstResponder on mac OS. + */ +@property (nonatomic, assign) BOOL focusable; +/** + * Specifies whether focus ring should be drawn when the view has the first responder status. + */ +@property (nonatomic, assign) BOOL enableFocusRing; + +@end + +// UIScrollView + +@interface RCTUIScrollView : NSScrollView + +// UIScrollView properties missing in NSScrollView +@property (nonatomic, assign) CGPoint contentOffset; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) CGSize contentSize; +@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; +@property (nonatomic, assign) BOOL showsVerticalScrollIndicator; +@property (nonatomic, assign) UIEdgeInsets scrollIndicatorInsets; +@property (nonatomic, assign) CGFloat zoomScale; +@property (nonatomic, assign) BOOL alwaysBounceHorizontal; +@property (nonatomic, assign) BOOL alwaysBounceVertical; +// macOS specific properties +@property (nonatomic, assign) BOOL enableFocusRing; +@property (nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled; + +@end + +@interface RCTClipView : NSClipView + +@property (nonatomic, assign) BOOL constrainScrolling; + +@end + + +NS_INLINE RCTPlatformView *RCTUIViewHitTestWithEvent(RCTPlatformView *view, CGPoint point, __unused UIEvent *__nullable event) +{ + return [view hitTest:point]; +} + +BOOL RCTUIViewSetClipsToBounds(RCTPlatformView *view); + +NS_INLINE void RCTUIViewSetContentModeRedraw(RCTPlatformView *view) +{ + view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; +} + +NS_INLINE BOOL RCTUIViewIsDescendantOfView(RCTPlatformView *view, RCTPlatformView *parent) +{ + return [view isDescendantOf:parent]; +} + +NS_INLINE NSValue *NSValueWithCGRect(CGRect rect) +{ + return [NSValue valueWithBytes:&rect objCType:@encode(CGRect)]; +} + +NS_INLINE NSValue *NSValueWithCGSize(CGSize size) +{ + return [NSValue valueWithBytes:&size objCType:@encode(CGSize)]; +} + +NS_INLINE CGRect CGRectValue(NSValue *value) +{ + CGRect rect = CGRectZero; + [value getValue:&rect]; + return rect; +} + +NS_ASSUME_NONNULL_END + +#endif // ] TARGET_OS_OSX + +#if !TARGET_OS_OSX +typedef UIApplication RCTUIApplication; +#else +typedef NSApplication RCTUIApplication; +#endif + +// +// fabric component types +// + +// RCTUISlider + +#if !TARGET_OS_OSX +typedef UISlider RCTUISlider; +#else +@protocol RCTUISliderDelegate; + +@interface RCTUISlider : NSSlider +NS_ASSUME_NONNULL_BEGIN +@property (nonatomic, weak) id delegate; +@property (nonatomic, readonly) BOOL pressed; +@property (nonatomic, assign) float value; +@property (nonatomic, assign) float minimumValue; +@property (nonatomic, assign) float maximumValue; +@property (nonatomic, strong) NSColor *minimumTrackTintColor; +@property (nonatomic, strong) NSColor *maximumTrackTintColor; + +- (void)setValue:(float)value animated:(BOOL)animated; +NS_ASSUME_NONNULL_END +@end +#endif + +#if TARGET_OS_OSX // [macOS +@protocol RCTUISliderDelegate +@optional +NS_ASSUME_NONNULL_BEGIN +- (void)slider:(RCTUISlider *)slider didPress:(BOOL)press; +NS_ASSUME_NONNULL_END +@end +#endif // macOS] + +// RCTUILabel + +#if !TARGET_OS_OSX +typedef UILabel RCTUILabel; +#else +@interface RCTUILabel : NSTextField +NS_ASSUME_NONNULL_BEGIN +@property(nonatomic, copy) NSString* _Nullable text; +@property(nonatomic, assign) NSInteger numberOfLines; +@property(nonatomic, assign) NSTextAlignment textAlignment; +NS_ASSUME_NONNULL_END +@end +#endif + +// RCTUISwitch + +#if !TARGET_OS_OSX +typedef UISwitch RCTUISwitch; +#else +@interface RCTUISwitch : NSSwitch +NS_ASSUME_NONNULL_BEGIN +@property (nonatomic, getter=isOn) BOOL on; + +- (void)setOn:(BOOL)on animated:(BOOL)animated; + +NS_ASSUME_NONNULL_END +@end +#endif + +// RCTUIActivityIndicatorView + +#if !TARGET_OS_OSX +typedef UIActivityIndicatorView RCTUIActivityIndicatorView; +#else +@interface RCTUIActivityIndicatorView : NSProgressIndicator +NS_ASSUME_NONNULL_BEGIN +@property (nonatomic, assign) UIActivityIndicatorViewStyle activityIndicatorViewStyle; +@property (nonatomic, assign) BOOL hidesWhenStopped; +@property (nullable, readwrite, nonatomic, strong) RCTUIColor *color; +@property (nonatomic, readonly, getter=isAnimating) BOOL animating; + +- (void)startAnimating; +- (void)stopAnimating; +NS_ASSUME_NONNULL_END +@end + +#endif + +// RCTUITouch + +#if !TARGET_OS_OSX +typedef UITouch RCTUITouch; +#else +@interface RCTUITouch : NSEvent +@end +#endif + +// RCTUIImageView + +#if !TARGET_OS_OSX +typedef UIImageView RCTUIImageView; +#else +@interface RCTUIImageView : NSImageView +NS_ASSUME_NONNULL_BEGIN +// FUTURE: When Xcode 14 is no longer supported (CI is building with Xcode 15), we can remove this override since it's now declared on NSView +@property (assign) BOOL clipsToBounds; +@property (nonatomic, strong) RCTUIColor *tintColor; +@property (nonatomic, assign) UIViewContentMode contentMode; +NS_ASSUME_NONNULL_END +@end +#endif diff --git a/packages/react-native/React/Base/RCTUtils.h b/packages/react-native/React/Base/RCTUtils.h index dc218c47f199fc..c018f27f51f5af 100644 --- a/packages/react-native/React/Base/RCTUtils.h +++ b/packages/react-native/React/Base/RCTUtils.h @@ -9,7 +9,7 @@ #import #import -#import +#import // [macOS] #import #import @@ -50,16 +50,22 @@ RCT_EXTERN CGSize RCTScreenSize(void); RCT_EXTERN CGSize RCTViewportSize(void); // Round float coordinates to nearest whole screen pixel (not point) +#if !TARGET_OS_OSX // [macOS] RCT_EXTERN CGFloat RCTRoundPixelValue(CGFloat value); RCT_EXTERN CGFloat RCTCeilPixelValue(CGFloat value); RCT_EXTERN CGFloat RCTFloorPixelValue(CGFloat value); +#else // [macOS +RCT_EXTERN CGFloat RCTRoundPixelValue(CGFloat value, CGFloat scale); +RCT_EXTERN CGFloat RCTCeilPixelValue(CGFloat value, CGFloat scale); +RCT_EXTERN CGFloat RCTFloorPixelValue(CGFloat value, CGFloat scale); +#endif // macOS] // Convert a size in points to pixels, rounded up to the nearest integral size RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale); // Method swizzling -RCT_EXTERN void RCTSwapClassMethods(Class cls, SEL original, SEL replacement); -RCT_EXTERN void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); +RCT_EXTERN IMP RCTSwapClassMethods(Class cls, SEL original, SEL replacement); // [macOS] +RCT_EXTERN IMP RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); // [macOS] RCT_EXTERN void RCTSwapInstanceMethodWithBlock(Class cls, SEL original, id replacementBlock, SEL replacementSelector); // Module subclass support @@ -81,12 +87,15 @@ RCT_EXTERN NSString *const RCTErrorUnspecified; // Returns YES if React is running in a test environment RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); +#if !TARGET_OS_OSX // [macOS] // Returns YES if React is running in an iOS App Extension RCT_EXTERN BOOL RCTRunningInAppExtension(void); +#endif // [macOS] // Returns the shared UIApplication instance, or nil if running in an App Extension RCT_EXTERN UIApplication *__nullable RCTSharedApplication(void); +#if !TARGET_OS_OSX // [macOS] // Returns the current main window, useful if you need to access the root view // or view controller RCT_EXTERN UIWindow *__nullable RCTKeyWindow(void); @@ -97,6 +106,7 @@ RCT_EXTERN UIViewController *__nullable RCTPresentedViewController(void); // Does this device support force touch (aka 3D Touch)? RCT_EXTERN BOOL RCTForceTouchAvailable(void); +#endif // [macOS] // Create an NSError in the RCTErrorDomain RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); @@ -162,8 +172,13 @@ RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[_Nonnul // Converts a CGColor to a hex string RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color); +#if !TARGET_OS_OSX // [macOS] // Get standard localized string (if it exists) RCT_EXTERN NSString *RCTUIKitLocalizedString(NSString *string); +#endif // [macOS] + +// Get a human readable type string from an NSObject. For example NSString becomes string +RCT_EXTERN NSString *RCTHumanReadableType(NSObject *obj); // Get a human readable type string from an NSObject. For example NSString becomes string RCT_EXTERN NSString *RCTHumanReadableType(NSObject *obj); diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 01bd5cde9fc3df..1fd3ceeb9151b5 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -13,7 +13,7 @@ #import #import -#import +#import // [macOS] #import @@ -21,6 +21,8 @@ #import "RCTAssert.h" #import "RCTLog.h" +static const NSUInteger RCTMaxCachableImageCount = 100; + NSString *const RCTErrorUnspecified = @"EUNSPECIFIED"; // Returns the Path of Home directory @@ -296,6 +298,7 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp } } +#if !TARGET_OS_OSX // [macOS] static dispatch_once_t onceTokenScreenScale; static CGFloat screenScale; @@ -355,7 +358,23 @@ CGSize RCTScreenSize(void) return size; } +#else // [macOS +CGFloat RCTScreenScale() +{ + return [NSScreen mainScreen].backingScaleFactor; +} + +CGFloat RCTFontSizeMultiplier(void) { + return 1.0; +} + +CGSize RCTScreenSize(void) +{ + return [NSScreen mainScreen].frame.size; +} +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] CGSize RCTViewportSize(void) { UIWindow *window = RCTKeyWindow(); @@ -379,6 +398,28 @@ CGFloat RCTFloorPixelValue(CGFloat value) CGFloat scale = RCTScreenScale(); return floor(value * scale) / scale; } +#else // [macOS +CGSize RCTViewportSize() +{ + NSScreen* screen = [NSScreen mainScreen]; + return screen ? screen.frame.size : RCTScreenSize(); +} + +CGFloat RCTRoundPixelValue(CGFloat value, CGFloat scale) +{ + return round(value * scale) / scale; +} + +CGFloat RCTCeilPixelValue(CGFloat value, CGFloat scale) +{ + return ceil(value * scale) / scale; +} + +CGFloat RCTFloorPixelValue(CGFloat value, CGFloat scale) +{ + return floor(value * scale) / scale; +} +#endif // macOS] CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) { @@ -388,7 +429,7 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) }; } -void RCTSwapClassMethods(Class cls, SEL original, SEL replacement) +IMP RCTSwapClassMethods(Class cls, SEL original, SEL replacement) // [macOS] { Method originalMethod = class_getClassMethod(cls, original); IMP originalImplementation = method_getImplementation(originalMethod); @@ -403,9 +444,11 @@ void RCTSwapClassMethods(Class cls, SEL original, SEL replacement) } else { method_exchangeImplementations(originalMethod, replacementMethod); } + + return originalImplementation; // [macOS] } -void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) +IMP RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) // [macOS] { Method originalMethod = class_getInstanceMethod(cls, original); IMP originalImplementation = method_getImplementation(originalMethod); @@ -420,6 +463,8 @@ void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) } else { method_exchangeImplementations(originalMethod, replacementMethod); } + + return originalImplementation; // [macOS] } void RCTSwapInstanceMethodWithBlock(Class cls, SEL original, id replacementBlock, SEL replacementSelector) @@ -529,21 +574,28 @@ BOOL RCTRunningInTestEnvironment(void) return isTestEnvironment; } +#if !TARGET_OS_OSX // [macOS] BOOL RCTRunningInAppExtension(void) { return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"]; } +#endif // [macOS] UIApplication *__nullable RCTSharedApplication(void) { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { return nil; } return [[UIApplication class] performSelector:@selector(sharedApplication)]; +#else // [macOS + return NSApp; +#endif // macOS] } -UIWindow *__nullable RCTKeyWindow(void) +RCTPlatformWindow *__nullable RCTKeyWindow(void) // [macOS] { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { return nil; } @@ -555,8 +607,12 @@ BOOL RCTRunningInAppExtension(void) } } return nil; +#else // [macOS + return [NSApp keyWindow]; +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] UIViewController *__nullable RCTPresentedViewController(void) { if ([RCTUtilsUIOverride hasPresentedViewController]) { @@ -585,6 +641,7 @@ BOOL RCTForceTouchAvailable(void) return forceSupported && (RCTKeyWindow() ?: [UIView new]).traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable; } +#endif // [macOS] NSError *RCTErrorWithMessage(NSString *message) { @@ -798,9 +855,30 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) return RCTImageFromLocalAssetURL(bundleImageUrl); } +#if TARGET_OS_OSX // [macOS +static NSCache *RCTLocalImageCache() +{ + static NSCache *imageCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + imageCache = [NSCache new]; + imageCache.countLimit = RCTMaxCachableImageCount; + }); + return imageCache; +} +#endif // macOS] + UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL) { NSString *imageName = RCTBundlePathForURL(imageURL); +#if TARGET_OS_OSX // [macOS + NSURL *bundleImageURL = nil; + + UIImage *cachedImage = [RCTLocalImageCache() objectForKey:imageURL]; + if (cachedImage) { + return cachedImage; + } +#endif // macOS] NSBundle *bundle = nil; NSArray *imagePathComponents = [imageName pathComponents]; @@ -809,12 +887,33 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) NSString *bundlePath = [imagePathComponents firstObject]; bundle = bundleForPath([bundlePath stringByDeletingPathExtension]); imageName = [imageName substringFromIndex:(bundlePath.length + 1)]; +#if TARGET_OS_OSX // [macOS + // Bundle structure under macOS uses Contents/Resources structure unlike iOS to store the assets. + // If the image asset is placed under a sub-directory inside of Resources folder, then first + // get the URL path to the image and then use this URL to load the image. + NSString *subDirectory = nil; + if (([imagePathComponents count] > 3) && + [imageName hasPrefix:@"Contents/Resources/"]) { + subDirectory = [[imageName stringByReplacingOccurrencesOfString:@"Contents/Resources/" withString:@""] stringByDeletingLastPathComponent]; + } + NSString *imageExtension = [imageName pathExtension]; + NSString *imageNameWithoutExt = [[imageName lastPathComponent] stringByDeletingPathExtension]; + bundleImageURL = [bundle URLForResource:imageNameWithoutExt withExtension:imageExtension subdirectory:subDirectory]; +#endif // macOS] } +#if TARGET_OS_OSX // [macOS + imageName = [imageName stringByDeletingPathExtension]; +#endif // macOS] + UIImage *image = nil; if (imageName) { if (bundle) { +#if !TARGET_OS_OSX // [macOS] image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; +#else // [macOS + image = (bundleImageURL == nil) ? [bundle imageForResource:imageName] : [[NSImage alloc] initWithContentsOfURL:bundleImageURL]; +#endif // macOS] } else { image = [UIImage imageNamed:imageName]; } @@ -828,7 +927,14 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) if (filePath.pathExtension.length == 0) { filePath = [filePath stringByAppendingPathExtension:@"png"]; } +#if !TARGET_OS_OSX // [macOS] image = [UIImage imageWithContentsOfFile:filePath]; +#else // [macOS + // macOS keeps file handles in open state for lifetime of image if "initWithContentsOfFile:" is used with path inside app bundle + // Workaround is to load file in data and then convert data to image + NSData *data = [NSData dataWithContentsOfFile:filePath]; + image = [[NSImage alloc] initWithData:data]; +#endif // macOS] } } @@ -841,13 +947,24 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) error:nil]; for (NSURL *frameworkURL in possibleFrameworks) { bundle = [NSBundle bundleWithURL:frameworkURL]; +#if !TARGET_OS_OSX // [macOS] image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; +#else // [macOS + image = [bundle imageForResource:imageName]; +#endif // macOS] if (image) { RCTLogWarn(@"Image %@ not found in mainBundle, but found in %@", imageName, bundle); break; } } } + +#if TARGET_OS_OSX // [macOS + if (image) { + [RCTLocalImageCache() setObject:image forKey:imageURL]; + } +#endif // macOS] + return image; } @@ -952,12 +1069,14 @@ RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4]) } } +#if !TARGET_OS_OSX // [macOS] // (https://github.com/0xced/XCDFormInputAccessoryView/blob/master/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m#L10-L14) NSString *RCTUIKitLocalizedString(NSString *string) { NSBundle *UIKitBundle = [NSBundle bundleForClass:[UIApplication class]]; return UIKitBundle ? [UIKitBundle localizedStringForKey:string value:string table:nil] : string; } +#endif // [macOS] NSString *RCTHumanReadableType(NSObject *obj) { @@ -1070,5 +1189,9 @@ RCT_EXTERN BOOL RCTValidateTypeOfViewCommandArgument( BOOL RCTIsAppActive(void) { +#if !TARGET_OS_OSX // [macOS] return [RCTSharedApplication() applicationState] == UIApplicationStateActive; +#else // [macOS + return [RCTSharedApplication() isActive]; +#endif // macOS] } diff --git a/packages/react-native/React/Base/RCTUtilsUIOverride.h b/packages/react-native/React/Base/RCTUtilsUIOverride.h index dc9e76c9e9529d..2e8cdc5783c5e8 100644 --- a/packages/react-native/React/Base/RCTUtilsUIOverride.h +++ b/packages/react-native/React/Base/RCTUtilsUIOverride.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] @interface RCTUtilsUIOverride : NSObject /** diff --git a/packages/react-native/React/Base/RCTUtilsUIOverride.m b/packages/react-native/React/Base/RCTUtilsUIOverride.m index c272d3eeea9f70..fc9c6545de4d62 100644 --- a/packages/react-native/React/Base/RCTUtilsUIOverride.m +++ b/packages/react-native/React/Base/RCTUtilsUIOverride.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#import // [macOS] #import "RCTUtilsUIOverride.h" @implementation RCTUtilsUIOverride diff --git a/packages/react-native/React/Base/RCTViewRegistry.m b/packages/react-native/React/Base/RCTViewRegistry.m index dc54d78c686029..66315069953554 100644 --- a/packages/react-native/React/Base/RCTViewRegistry.m +++ b/packages/react-native/React/Base/RCTViewRegistry.m @@ -26,9 +26,9 @@ - (void)setBridgelessComponentViewProvider:(RCTBridgelessComponentViewProvider)b _bridgelessComponentViewProvider = bridgelessComponentViewProvider; } -- (UIView *)viewForReactTag:(NSNumber *)reactTag +- (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag // [macOS] { - UIView *view = nil; + RCTPlatformView *view = nil; // [macOS] RCTBridge *bridge = _bridge; if (bridge) { @@ -50,7 +50,7 @@ - (void)addUIBlock:(RCTViewRegistryUIBlock)block __weak __typeof(self) weakSelf = self; if (_bridge) { - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { __typeof(self) strongSelf = weakSelf; if (strongSelf) { block(strongSelf); diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceDelegate.h b/packages/react-native/React/Base/Surface/RCTSurfaceDelegate.h index 21d8b9d73dfe70..caf2fb43d19877 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceDelegate.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceDelegate.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceProtocol.h b/packages/react-native/React/Base/Surface/RCTSurfaceProtocol.h index 0a4bec61cc0320..239ef98530e0af 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceProtocol.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceProtocol.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowViewDelegate.h b/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowViewDelegate.h index 44f29c839ad38d..0a417efc6f3d32 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowViewDelegate.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowViewDelegate.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceRootView.h b/packages/react-native/React/Base/Surface/RCTSurfaceRootView.h index b11d3f476e0ab3..82b42e1381cab5 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceRootView.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceRootView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceStage.h b/packages/react-native/React/Base/Surface/RCTSurfaceStage.h index 2397b752d085f2..216942546f37bf 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceStage.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceStage.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #import diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceView+Internal.h b/packages/react-native/React/Base/Surface/RCTSurfaceView+Internal.h index c208e5de49fcbc..9a2d8437adf6dd 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceView+Internal.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceView+Internal.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #import #import diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceView.h b/packages/react-native/React/Base/Surface/RCTSurfaceView.h index e6bfad491e1bbb..73dcfc03bb716c 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceView.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import NS_ASSUME_NONNULL_BEGIN @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN /** * UIView instance which represents the Surface */ -@interface RCTSurfaceView : UIView +@interface RCTSurfaceView : RCTUIView // [macOS] - (instancetype)initWithSurface:(RCTSurface *)surface NS_DESIGNATED_INITIALIZER; diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceView.mm b/packages/react-native/React/Base/Surface/RCTSurfaceView.mm index 0f49650d52c4da..6b49277e058cd8 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceView.mm +++ b/packages/react-native/React/Base/Surface/RCTSurfaceView.mm @@ -31,6 +31,13 @@ - (instancetype)initWithSurface:(RCTSurface *)surface return self; } +#if TARGET_OS_OSX // [macOS +- (BOOL)isFlipped +{ + return YES; +} +#endif // macOS] + #pragma mark - Internal Interface - (void)setRootView:(RCTSurfaceRootView *_Nullable)rootView diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h index 02092dd0449f44..0ce206db5758f1 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -31,9 +31,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) RCTRootViewSizeFlexibility sizeFlexibility; @property (nonatomic, weak) id delegate; @property (nonatomic, weak) UIViewController *reactViewController; -@property (nonatomic, strong, readonly) UIView *view; -@property (nonatomic, strong, readonly) UIView *contentView; -@property (nonatomic, strong) UIView *loadingView; +@property (nonatomic, strong, readonly) RCTUIView *view; // [macOS] +@property (nonatomic, strong, readonly) RCTUIView *contentView; // [macOS] +@property (nonatomic, strong) RCTUIView *loadingView; // [macOS] @property (nonatomic, assign) BOOL passThroughTouches; @property (nonatomic, assign) NSTimeInterval loadingViewFadeDelay; @property (nonatomic, assign) NSTimeInterval loadingViewFadeDuration; diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm index 670c02299601ec..e7587430460157 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm @@ -70,12 +70,12 @@ - (NSString *)moduleName return super.surface.moduleName; } -- (UIView *)view +- (RCTUIView *)view // [macOS] { - return (UIView *)super.surface.view; + return (RCTUIView *)super.surface.view; // [macOS] } -- (UIView *)contentView +- (RCTUIView *)contentView { return self; } @@ -105,14 +105,14 @@ - (void)setAppProperties:(NSDictionary *)appProperties [super.surface setProperties:appProperties]; } -- (UIView *)loadingView +- (RCTUIView *)loadingView // [macOS] { return super.activityIndicatorViewFactory ? super.activityIndicatorViewFactory() : nil; } -- (void)setLoadingView:(UIView *)loadingView +- (void)setLoadingView:(RCTUIView *)loadingView // [macOS] { - super.activityIndicatorViewFactory = ^UIView *(void) + super.activityIndicatorViewFactory = ^RCTUIView *(void) // [macOS] { return loadingView; }; diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.h b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.h index 47096221a7df62..163117ed807ccc 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.h +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -15,7 +15,7 @@ @class RCTBridge; @class RCTSurface; -typedef UIView *_Nullable (^RCTSurfaceHostingViewActivityIndicatorViewFactory)(void); +typedef RCTUIView *_Nullable (^RCTSurfaceHostingViewActivityIndicatorViewFactory)(void); // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN * This class can be used as easy-to-use general purpose integration point * of ReactNative-powered experiences in UIKit based apps. */ -@interface RCTSurfaceHostingView : UIView +@interface RCTSurfaceHostingView : RCTUIView // [macOS] /** * Designated initializer. diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm index 78f991eecfb091..8ee81daf6c4701 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm @@ -21,8 +21,8 @@ @interface RCTSurfaceHostingView () @end @implementation RCTSurfaceHostingView { - UIView *_Nullable _activityIndicatorView; - UIView *_Nullable _surfaceView; + RCTUIView *_Nullable _activityIndicatorView; // [macOS] + RCTUIView *_Nullable _surfaceView; // [macOS] RCTSurfaceStage _stage; } @@ -42,7 +42,7 @@ - (instancetype)initWithSurface:(id)surface [self _updateViews]; // For backward compatibility with RCTRootView, set a color here instead of transparent (OS default). - self.backgroundColor = [UIColor whiteColor]; + self.backgroundColor = [RCTUIColor whiteColor]; // [macOS] } return self; @@ -62,7 +62,11 @@ - (void)layoutSubviews RCTSurfaceMinimumSizeAndMaximumSizeFromSizeAndSizeMeasureMode( self.bounds.size, _sizeMeasureMode, &minimumSize, &maximumSize); +#if !TARGET_OS_OSX // [macOS] CGRect windowFrame = [self.window convertRect:self.frame fromView:self.superview]; +#else // [macOS + CGRect windowFrame = [self.window.contentView convertRect:self.frame toView:self.superview]; +#endif // macOS] [_surface setMinimumSize:minimumSize maximumSize:maximumSize viewportOffset:windowFrame.origin]; } @@ -84,7 +88,11 @@ - (CGSize)sizeThatFits:(CGSize)size { if (RCTSurfaceStageIsPreparing(_stage)) { if (_activityIndicatorView) { +#if !TARGET_OS_OSX // [macOS] return [_activityIndicatorView sizeThatFits:size]; +#else // [macOS + return [_activityIndicatorView fittingSize]; +#endif // macOS] } return CGSizeZero; @@ -182,6 +190,7 @@ - (void)setActivityIndicatorViewFactory:(RCTSurfaceHostingViewActivityIndicatorV #pragma mark - UITraitCollection updates +#if !TARGET_OS_OSX // [macOS] - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; @@ -192,13 +201,29 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey : self.traitCollection, }]; } +#else // [macOS +- (void)viewDidChangeEffectiveAppearance +{ + [super viewDidChangeEffectiveAppearance]; + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTUserInterfaceStyleDidChangeNotification + object:self + userInfo:@{ + RCTUserInterfaceStyleDidChangeNotificationAppearanceKey : self.effectiveAppearance, + }]; +} +#endif // macOS] #pragma mark - Private stuff - (void)_invalidateLayout { [self invalidateIntrinsicContentSize]; +#if !TARGET_OS_OSX // [macOS] [self.superview setNeedsLayout]; +#else // [macOS + [self.superview setNeedsLayout:YES]; +#endif // macOS] } - (void)_updateViews diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.h b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.h index edfc5a0fbc5311..891c42088a10be 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.h +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.mm index 8bcb5d1d932485..e7a2844d76eff1 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.mm +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.mm @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTSurfaceSizeMeasureMode.h" diff --git a/packages/react-native/React/Base/macOS/RCTPlatform.m b/packages/react-native/React/Base/macOS/RCTPlatform.m new file mode 100644 index 00000000000000..5b8cdac554bbec --- /dev/null +++ b/packages/react-native/React/Base/macOS/RCTPlatform.m @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] +#if TARGET_OS_OSX + +#import "RCTPlatform.h" + +#import + +#import "RCTUtils.h" +#import "RCTVersion.h" + +@implementation RCTPlatform + +RCT_EXPORT_MODULE(PlatformConstants) + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (NSDictionary *)constantsToExport +{ + NSOperatingSystemVersion osVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + return @{ + @"osVersion": [NSString stringWithFormat:@"%ld.%ld.%ld", osVersion.majorVersion, osVersion.minorVersion, osVersion.patchVersion], + @"isTesting": @(RCTRunningInTestEnvironment()), + @"reactNativeVersion": RCTGetReactNativeVersion(), + }; +} + +Class RCTPlatformCls(void) { + return RCTPlatform.class; +} + +@end +#endif \ No newline at end of file diff --git a/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m b/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m new file mode 100644 index 00000000000000..6ee09fde67a600 --- /dev/null +++ b/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] +#if TARGET_OS_OSX + +#import "RCTPlatformDisplayLink.h" + +#import +#import + +#import +#import + +#import + +@interface RCTPlatformDisplayLink () + +@property (nonatomic, strong) NSRunLoop *runLoop; + +@end + +@implementation RCTPlatformDisplayLink +{ + CVDisplayLinkRef _displayLink; + SEL _selector; + __weak id _target; + NSRunLoop *_runLoop; + NSMutableArray *_modes; + os_unfair_lock _lock; // OS_UNFAIR_LOCK_INIT == 0 +} + ++ (RCTPlatformDisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel +{ + RCTPlatformDisplayLink *displayLink = [self.class new]; + displayLink->_target = target; + displayLink->_selector = sel; + return displayLink; +} + +static CVReturn RCTPlatformDisplayLinkCallBack(__unused CVDisplayLinkRef displayLink, __unused const CVTimeStamp* now, __unused const CVTimeStamp* outputTime, __unused CVOptionFlags flagsIn, __unused CVOptionFlags* flagsOut, void* displayLinkContext) +{ + @autoreleasepool { + RCTPlatformDisplayLink *rctDisplayLink = (__bridge RCTPlatformDisplayLink*)displayLinkContext; + + // Lock and check for invalidation prior to calling out to the runloop + os_unfair_lock_lock(&rctDisplayLink->_lock); + if (rctDisplayLink->_runLoop != nil) { + CFRunLoopRef cfRunLoop = [rctDisplayLink->_runLoop getCFRunLoop]; + CFRunLoopPerformBlock(cfRunLoop, (__bridge CFArrayRef)rctDisplayLink->_modes, ^{ + @autoreleasepool { + [rctDisplayLink tick]; + } + }); + CFRunLoopWakeUp(cfRunLoop); + } + os_unfair_lock_unlock(&rctDisplayLink->_lock); + } + return kCVReturnSuccess; +} + +- (void)dealloc +{ + if (_displayLink != NULL) { + CVDisplayLinkStop(_displayLink); + CVDisplayLinkRelease(_displayLink); + _displayLink = NULL; + } +} + +- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode +{ + os_unfair_lock_lock(&_lock); + _runLoop = runloop; + + if (_displayLink != NULL) { + [_modes addObject:mode]; + os_unfair_lock_unlock(&_lock); + return; + } + + _modes = @[mode].mutableCopy; + os_unfair_lock_unlock(&_lock); + CVReturn ret = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); + if (ret != kCVReturnSuccess) { + ret = CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &_displayLink); + } + RCTAssert(ret == kCVReturnSuccess, @"Cannot create display link"); + CVDisplayLinkSetOutputCallback(_displayLink, &RCTPlatformDisplayLinkCallBack, (__bridge void *)(self)); + CVDisplayLinkStart(_displayLink); +} + +- (void)removeFromRunLoop:(__unused NSRunLoop *)runloop forMode:(NSRunLoopMode)mode +{ + [_modes removeObject:mode]; + if (_modes.count == 0) { + [self invalidate]; + } +} + +- (void)invalidate +{ + if (_runLoop != nil) { + os_unfair_lock_lock(&_lock); + _runLoop = nil; + _modes = nil; + os_unfair_lock_unlock(&_lock); + + // CVDisplayLinkStop attempts to acquire a mutex possibly held during the callback's invocation. + // Stop the display link outside of the lock to avoid deadlocking here. + if (_displayLink != NULL) { + CVDisplayLinkStop(_displayLink); + } + } +} + +- (void)setPaused:(BOOL)paused +{ + if (paused) { + CVDisplayLinkStop(_displayLink); + } else { + CVDisplayLinkStart(_displayLink); + } +} + +- (BOOL)isPaused +{ + return !CVDisplayLinkIsRunning(_displayLink); +} + +- (NSTimeInterval)timestamp +{ + CVTimeStamp now; + now.version = 0; + memset(&now, 0 , sizeof(now)); + CVDisplayLinkGetCurrentTime(_displayLink, &now); + return (NSTimeInterval)now.hostTime / (NSTimeInterval)CVGetHostClockFrequency(); +} + +- (NSTimeInterval)duration +{ + NSTimeInterval duration = 0; + const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(_displayLink); + if (!(time.flags & kCVTimeIsIndefinite)) { + duration = (NSTimeInterval)time.timeValue / (NSTimeInterval)time.timeScale; + } + return duration; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" +- (void)tick +{ + if (_selector && [_target respondsToSelector:_selector]) { + [_target performSelector:_selector withObject:self]; + } +} +#pragma clang diagnostic pop + +@end +#endif \ No newline at end of file diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m new file mode 100644 index 00000000000000..01c96db75eeeca --- /dev/null +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -0,0 +1,860 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#if TARGET_OS_OSX + +#import + +#import + +#import + +#import +#import + +static char RCTGraphicsContextSizeKey; + +// +// semantically equivalent functions +// + +// UIGraphics.h + +CGContextRef UIGraphicsGetCurrentContext(void) +{ + return [[NSGraphicsContext currentContext] CGContext]; +} + +void UIGraphicsBeginImageContextWithOptions(CGSize size, __unused BOOL opaque, CGFloat scale) +{ + if (scale == 0.0) + { + // TODO: Assert. We can't assume a display scale on macOS + scale = 1.0; + } + + size_t width = ceilf(size.width * scale); + size_t height = ceilf(size.height * scale); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8/*bitsPerComponent*/, width * 4/*bytesPerRow*/, colorSpace, kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(colorSpace); + + if (ctx != NULL) + { + // flip the context (top left at 0, 0) and scale it + CGContextTranslateCTM(ctx, 0.0, height); + CGContextScaleCTM(ctx, scale, -scale); + + NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES]; + objc_setAssociatedObject(graphicsContext, &RCTGraphicsContextSizeKey, [NSValue valueWithSize:size], OBJC_ASSOCIATION_COPY_NONATOMIC); + + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:graphicsContext]; + + CFRelease(ctx); + } +} + +NSImage *UIGraphicsGetImageFromCurrentImageContext(void) +{ + NSImage *image = nil; + NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; + + NSValue *sizeValue = objc_getAssociatedObject(graphicsContext, &RCTGraphicsContextSizeKey); + if (sizeValue != nil) { + CGImageRef cgImage = CGBitmapContextCreateImage([graphicsContext CGContext]); + + if (cgImage != NULL) { + NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; + image = [[NSImage alloc] initWithSize:[sizeValue sizeValue]]; + [image addRepresentation:imageRep]; + CFRelease(cgImage); + } + } + + return image; +} + +void UIGraphicsEndImageContext(void) +{ + RCTAssert(objc_getAssociatedObject([NSGraphicsContext currentContext], &RCTGraphicsContextSizeKey), @"The current graphics context is not a React image context!"); + [NSGraphicsContext restoreGraphicsState]; +} + +// +// functionally equivalent types +// + +// UIImage + +CGFloat UIImageGetScale(NSImage *image) +{ + if (image == nil) { + return 0.0; + } + + RCTAssert(image.representations.count == 1, @"The scale can only be derived if the image has one representation."); + + NSImageRep *imageRep = image.representations.firstObject; + if (imageRep != nil) { + NSSize imageSize = image.size; + NSSize repSize = CGSizeMake(imageRep.pixelsWide, imageRep.pixelsHigh); + + return round(fmax(repSize.width / imageSize.width, repSize.height / imageSize.height)); + } + + return 1.0; +} + +CGImageRef UIImageGetCGImageRef(NSImage *image) +{ + return [image CGImageForProposedRect:NULL context:NULL hints:NULL]; +} + +static NSData *NSImageDataForFileType(NSImage *image, NSBitmapImageFileType fileType, NSDictionary *properties) +{ + RCTAssert(image.representations.count == 1, @"Expected only a single representation since UIImage only supports one."); + + NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; + if (![imageRep isKindOfClass:[NSBitmapImageRep class]]) { + RCTAssert([imageRep isKindOfClass:[NSBitmapImageRep class]], @"We need an NSBitmapImageRep to create an image."); + return nil; + } + + return [imageRep representationUsingType:fileType properties:properties]; +} + +NSData *UIImagePNGRepresentation(NSImage *image) { + return NSImageDataForFileType(image, NSBitmapImageFileTypePNG, @{}); +} + +NSData *UIImageJPEGRepresentation(NSImage *image, CGFloat compressionQuality) { + return NSImageDataForFileType(image, + NSBitmapImageFileTypeJPEG, + @{NSImageCompressionFactor: @(compressionQuality)}); +} + +// UIBezierPath +UIBezierPath *UIBezierPathWithRoundedRect(CGRect rect, CGFloat cornerRadius) +{ + return [NSBezierPath bezierPathWithRoundedRect:rect xRadius:cornerRadius yRadius:cornerRadius]; +} + +void UIBezierPathAppendPath(UIBezierPath *path, UIBezierPath *appendPath) +{ + return [path appendBezierPath:appendPath]; +} + +CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) +{ + CGPathRef immutablePath = NULL; + + // Draw the path elements. + NSInteger numElements = [bezierPath elementCount]; + if (numElements > 0) + { + CGMutablePathRef path = CGPathCreateMutable(); + NSPoint points[3]; + BOOL didClosePath = YES; + + for (NSInteger i = 0; i < numElements; i++) + { + switch ([bezierPath elementAtIndex:i associatedPoints:points]) + { + case NSMoveToBezierPathElement: + CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); + break; + + case NSLineToBezierPathElement: + CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); + didClosePath = NO; + break; + + case NSCurveToBezierPathElement: + CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, + points[1].x, points[1].y, + points[2].x, points[2].y); + didClosePath = NO; + break; + + case NSClosePathBezierPathElement: + CGPathCloseSubpath(path); + didClosePath = YES; + break; + } + } + + // Be sure the path is closed or Quartz may not do valid hit detection. + if (!didClosePath) + CGPathCloseSubpath(path); + + immutablePath = CGPathCreateCopy(path); + CGPathRelease(path); + } + + return immutablePath; +} + +// +// substantially different types +// + +// UIView + + +@implementation RCTUIView +{ +@private + NSColor *_backgroundColor; + BOOL _clipsToBounds; + BOOL _userInteractionEnabled; + BOOL _mouseDownCanMoveWindow; +} + ++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key +{ + NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; + NSString *alternatePath = nil; + + // alpha is a wrapper for alphaValue + if ([key isEqualToString:@"alpha"]) { + alternatePath = @"alphaValue"; + // isAccessibilityElement is a wrapper for accessibilityElement + } else if ([key isEqualToString:@"isAccessibilityElement"]) { + alternatePath = @"accessibilityElement"; + } + + if (alternatePath != nil) { + keyPaths = keyPaths != nil ? [keyPaths setByAddingObject:alternatePath] : [NSSet setWithObject:alternatePath]; + } + + return keyPaths; +} + +static RCTUIView *RCTUIViewCommonInit(RCTUIView *self) +{ + if (self != nil) { + self.wantsLayer = YES; + self->_userInteractionEnabled = YES; + self->_enableFocusRing = YES; + self->_mouseDownCanMoveWindow = YES; + } + return self; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + return RCTUIViewCommonInit([super initWithFrame:frameRect]); +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + return RCTUIViewCommonInit([super initWithCoder:coder]); +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + if (self.acceptsFirstMouse || [super acceptsFirstMouse:event]) { + return YES; + } + + // If any RCTUIView view above has acceptsFirstMouse set, then return YES here. + NSView *view = self; + while ((view = view.superview)) { + if ([view isKindOfClass:[RCTUIView class]] && [(RCTUIView *)view acceptsFirstMouse]) { + return YES; + } + } + + return NO; +} + +- (BOOL)acceptsFirstResponder +{ + return [self canBecomeFirstResponder]; +} + +- (BOOL)isFirstResponder { + return [[self window] firstResponder] == self; +} + +- (void)viewDidMoveToWindow +{ + [self didMoveToWindow]; +} + +- (BOOL)mouseDownCanMoveWindow{ + return _mouseDownCanMoveWindow; +} + +- (void)setMouseDownCanMoveWindow:(BOOL)mouseDownCanMoveWindow{ + _mouseDownCanMoveWindow = mouseDownCanMoveWindow; +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (CGFloat)alpha +{ + return self.alphaValue; +} + +- (void)setAlpha:(CGFloat)alpha +{ + self.alphaValue = alpha; +} + + +- (CGAffineTransform)transform +{ + return self.layer.affineTransform; +} + +- (void)setTransform:(CGAffineTransform)transform +{ + self.layer.affineTransform = transform; +} + +- (NSView *)hitTest:(NSPoint)point +{ + return [self hitTest:NSPointToCGPoint(point) withEvent:nil]; +} + +- (BOOL)wantsUpdateLayer +{ + return [self respondsToSelector:@selector(displayLayer:)]; +} + +- (void)updateLayer +{ + CALayer *layer = [self layer]; + if (_backgroundColor) { + // updateLayer will be called when the view's current appearance changes. + // The layer's backgroundColor is a CGColor which is not appearance aware + // so it has to be reset from the view's NSColor ivar. + [layer setBackgroundColor:[_backgroundColor CGColor]]; + } + [(id)self displayLayer:layer]; +} + +- (void)drawRect:(CGRect)rect +{ + if (_backgroundColor) { + [_backgroundColor set]; + NSRectFill(rect); + } + [super drawRect:rect]; +} + +- (void)layout +{ + if (self.window != nil) { + [self layoutSubviews]; + } +} + +- (BOOL)canBecomeFirstResponder +{ + return [super acceptsFirstResponder]; +} + +- (BOOL)becomeFirstResponder +{ + return [[self window] makeFirstResponder:self]; +} + +@synthesize userInteractionEnabled = _userInteractionEnabled; + +- (NSView *)hitTest:(CGPoint)point withEvent:(__unused UIEvent *)event +{ + return self.userInteractionEnabled ? [super hitTest:NSPointFromCGPoint(point)] : nil; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(__unused UIEvent *)event +{ + return self.userInteractionEnabled ? NSPointInRect(NSPointFromCGPoint(point), self.bounds) : NO; +} + +- (void)insertSubview:(NSView *)view atIndex:(NSInteger)index +{ + NSArray<__kindof NSView *> *subviews = self.subviews; + if ((NSUInteger)index == subviews.count) { + [self addSubview:view]; + } else { + [self addSubview:view positioned:NSWindowBelow relativeTo:subviews[index]]; + } +} + +- (void)didMoveToWindow +{ + [super viewDidMoveToWindow]; +} + +- (void)setNeedsLayout +{ + self.needsLayout = YES; +} + +- (void)layoutIfNeeded +{ + if ([self needsLayout]) { + [self layout]; + } +} + +- (void)layoutSubviews +{ + [super layout]; +} + +- (void)setNeedsDisplay +{ + self.needsDisplay = YES; +} + +@synthesize clipsToBounds = _clipsToBounds; + +@synthesize backgroundColor = _backgroundColor; + +- (void)setBackgroundColor:(NSColor *)backgroundColor +{ + if (_backgroundColor != backgroundColor && ![_backgroundColor isEqual:backgroundColor]) + { + _backgroundColor = [backgroundColor copy]; + [self setNeedsDisplay:YES]; + } +} + +// We purposely don't use RCTCursor for the parameter type here because it would introduce an import cycle: +// RCTUIKit > RCTCursor > RCTConvert > RCTUIKit +- (void)setCursor:(NSInteger)cursor +{ + // This method is required to be defined due to [RCTVirtualTextViewManager view] returning a RCTUIView. +} + +@end + +// RCTUIScrollView + +@implementation RCTUIScrollView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.scrollEnabled = YES; + self.drawsBackground = NO; + } + + return self; +} + +- (void)setEnableFocusRing:(BOOL)enableFocusRing { + if (_enableFocusRing != enableFocusRing) { + _enableFocusRing = enableFocusRing; + } + + if (enableFocusRing) { + // NSTextView has no focus ring by default so let's use the standard Aqua focus ring. + [self setFocusRingType:NSFocusRingTypeExterior]; + } else { + [self setFocusRingType:NSFocusRingTypeNone]; + } +} + +// UIScrollView properties missing from NSScrollView +- (CGPoint)contentOffset +{ + return self.documentVisibleRect.origin; +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + [self.documentView scrollPoint:contentOffset]; +} + +- (UIEdgeInsets)contentInset +{ + return super.contentInsets; +} + +- (void)setContentInset:(UIEdgeInsets)insets +{ + super.contentInsets = insets; +} + +- (CGSize)contentSize +{ + return self.documentView.frame.size; +} + +- (void)setContentSize:(CGSize)contentSize +{ + CGRect frame = self.documentView.frame; + frame.size = contentSize; + self.documentView.frame = frame; +} + +- (BOOL)showsHorizontalScrollIndicator +{ + return self.hasHorizontalScroller; +} + +- (void)setShowsHorizontalScrollIndicator:(BOOL)show +{ + self.hasHorizontalScroller = show; +} + +- (BOOL)showsVerticalScrollIndicator +{ + return self.hasVerticalScroller; +} + +- (void)setShowsVerticalScrollIndicator:(BOOL)show +{ + self.hasVerticalScroller = show; +} + +- (UIEdgeInsets)scrollIndicatorInsets +{ + return self.scrollerInsets; +} + +- (void)setScrollIndicatorInsets:(UIEdgeInsets)insets +{ + self.scrollerInsets = insets; +} + +- (CGFloat)zoomScale +{ + return self.magnification; +} + +- (void)setZoomScale:(CGFloat)zoomScale +{ + self.magnification = zoomScale; +} + +- (BOOL)alwaysBounceHorizontal +{ + return self.horizontalScrollElasticity != NSScrollElasticityNone; +} + +- (void)setAlwaysBounceHorizontal:(BOOL)alwaysBounceHorizontal +{ + self.horizontalScrollElasticity = alwaysBounceHorizontal ? NSScrollElasticityAllowed : NSScrollElasticityNone; +} + +- (BOOL)alwaysBounceVertical +{ + return self.verticalScrollElasticity != NSScrollElasticityNone; +} + +- (void)setAlwaysBounceVertical:(BOOL)alwaysBounceVertical +{ + self.verticalScrollElasticity = alwaysBounceVertical ? NSScrollElasticityAllowed : NSScrollElasticityNone; +} + +@end + +BOOL RCTUIViewSetClipsToBounds(RCTPlatformView *view) +{ + // NSViews are always clipped to bounds + BOOL clipsToBounds = YES; + + // But see if UIView overrides that behavior + if ([view respondsToSelector:@selector(clipsToBounds)]) + { + clipsToBounds = [(id)view clipsToBounds]; + } + + return clipsToBounds; +} + +@implementation RCTClipView + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + self.constrainScrolling = NO; + self.drawsBackground = NO; + } + + return self; +} + +- (NSRect)constrainBoundsRect:(NSRect)proposedBounds +{ + if (self.constrainScrolling) { + return NSMakeRect(0, 0, 0, 0); + } + + return [super constrainBoundsRect:proposedBounds]; +} + +@end + +// RCTUISlider + +@implementation RCTUISlider {} + +- (void)setValue:(float)value animated:(__unused BOOL)animated +{ + self.animator.floatValue = value; +} + +@end + + +// RCTUILabel + +@implementation RCTUILabel {} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (self = [super initWithFrame:frameRect]) { + [self setBezeled:NO]; + [self setDrawsBackground:NO]; + [self setEditable:NO]; + [self setSelectable:NO]; + [self setWantsLayer:YES]; + } + + return self; +} + +@end + +@implementation RCTUISwitch + +- (BOOL)isOn +{ + return self.state == NSControlStateValueOn; +} + +- (void)setOn:(BOOL)on +{ + [self setOn:on animated:NO]; +} + +- (void)setOn:(BOOL)on animated:(BOOL)animated { + self.state = on ? NSControlStateValueOn : NSControlStateValueOff; +} + +@end + +// RCTUIActivityIndicatorView + +@interface RCTUIActivityIndicatorView () +@property (nonatomic, readwrite, getter=isAnimating) BOOL animating; +@end + +@implementation RCTUIActivityIndicatorView {} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + self.displayedWhenStopped = NO; + self.style = NSProgressIndicatorStyleSpinning; + } + return self; +} + +- (void)startAnimating +{ + // `wantsLayer` gets reset after the animation is stopped. We have to + // reset it in order for CALayer filters to take effect. + [self setWantsLayer:YES]; + [self startAnimation:self]; +} + +- (void)stopAnimating +{ + [self stopAnimation:self]; +} + +- (void)startAnimation:(id)sender +{ + [super startAnimation:sender]; + self.animating = YES; +} + +- (void)stopAnimation:(id)sender +{ + [super stopAnimation:sender]; + self.animating = NO; +} + +- (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle +{ + _activityIndicatorViewStyle = activityIndicatorViewStyle; + + switch (activityIndicatorViewStyle) { + case UIActivityIndicatorViewStyleLarge: + if (@available(macOS 11.0, *)) { + self.controlSize = NSControlSizeLarge; + } else { + self.controlSize = NSControlSizeRegular; + } + break; + case UIActivityIndicatorViewStyleMedium: + self.controlSize = NSControlSizeRegular; + break; + default: + break; + } +} + +- (void)setColor:(RCTUIColor*)color +{ + if (_color != color) { + _color = color; + [self setNeedsDisplay:YES]; + } +} + +- (void)updateLayer +{ + [super updateLayer]; + if (_color != nil) { + CGFloat r, g, b, a; + [[_color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]] getRed:&r green:&g blue:&b alpha:&a]; + + CIFilter *colorPoly = [CIFilter filterWithName:@"CIColorPolynomial"]; + [colorPoly setDefaults]; + + CIVector *redVector = [CIVector vectorWithX:r Y:0 Z:0 W:0]; + CIVector *greenVector = [CIVector vectorWithX:g Y:0 Z:0 W:0]; + CIVector *blueVector = [CIVector vectorWithX:b Y:0 Z:0 W:0]; + [colorPoly setValue:redVector forKey:@"inputRedCoefficients"]; + [colorPoly setValue:greenVector forKey:@"inputGreenCoefficients"]; + [colorPoly setValue:blueVector forKey:@"inputBlueCoefficients"]; + + [[self layer] setFilters:@[colorPoly]]; + } else { + [[self layer] setFilters:nil]; + } +} + +- (void)setHidesWhenStopped:(BOOL)hidesWhenStopped +{ + self.displayedWhenStopped = !hidesWhenStopped; +} + +- (BOOL)hidesWhenStopped +{ + return !self.displayedWhenStopped; +} + +- (void)setHidden:(BOOL)hidden +{ + if ([self hidesWhenStopped] && ![self isAnimating]) { + [super setHidden:YES]; + } else { + [super setHidden:hidden]; + } +} + +@end + +// RCTUIImageView + +@implementation RCTUIImageView { + CALayer *_tintingLayer; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setLayer:[[CALayer alloc] init]]; + [self setWantsLayer:YES]; + } + + return self; +} + +- (BOOL)clipsToBounds +{ + return [[self layer] masksToBounds]; +} + +- (void)setClipsToBounds:(BOOL)clipsToBounds +{ + [[self layer] setMasksToBounds:clipsToBounds]; +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + _contentMode = contentMode; + + CALayer *layer = [self layer]; + switch (contentMode) { + case UIViewContentModeScaleAspectFill: + [layer setContentsGravity:kCAGravityResizeAspectFill]; + break; + + case UIViewContentModeScaleAspectFit: + [layer setContentsGravity:kCAGravityResizeAspect]; + break; + + case UIViewContentModeScaleToFill: + [layer setContentsGravity:kCAGravityResize]; + break; + + case UIViewContentModeCenter: + [layer setContentsGravity:kCAGravityCenter]; + break; + + default: + break; + } +} + +- (UIImage *)image +{ + return [[self layer] contents]; +} + +- (void)setImage:(UIImage *)image +{ + CALayer *layer = [self layer]; + + if ([layer contents] != image || [layer backgroundColor] != nil) { + if (_tintColor) { + if (!_tintingLayer) { + _tintingLayer = [CALayer new]; + [_tintingLayer setFrame:self.bounds]; + [_tintingLayer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; + [_tintingLayer setZPosition:1.0]; + CIFilter *sourceInCompositingFilter = [CIFilter filterWithName:@"CISourceInCompositing"]; + [sourceInCompositingFilter setDefaults]; + [_tintingLayer setCompositingFilter:sourceInCompositingFilter]; + [layer addSublayer:_tintingLayer]; + } + [_tintingLayer setBackgroundColor:_tintColor.CGColor]; + } else { + [_tintingLayer removeFromSuperlayer]; + _tintingLayer = nil; + } + + if (image != nil && [image resizingMode] == NSImageResizingModeTile) { + [layer setContents:nil]; + [layer setBackgroundColor:[NSColor colorWithPatternImage:image].CGColor]; + } else { + [layer setContents:image]; + [layer setBackgroundColor:nil]; + } + } +} + +@end + +#endif diff --git a/packages/react-native/React/CoreModules/CoreModulesPlugins.mm b/packages/react-native/React/CoreModules/CoreModulesPlugins.mm index fc1587becada12..6f40ef9eb59c48 100644 --- a/packages/react-native/React/CoreModules/CoreModulesPlugins.mm +++ b/packages/react-native/React/CoreModules/CoreModulesPlugins.mm @@ -19,7 +19,9 @@ Class RCTCoreModulesClassProvider(const char *name) { // Intentionally leak to avoid crashing after static destructors are run. static const auto sCoreModuleClassMap = new const std::unordered_map{ +#if !TARGET_OS_OSX // [macOS] Do we need these? {"AccessibilityManager", RCTAccessibilityManagerCls}, +#endif // [macOS] {"ActionSheetManager", RCTActionSheetManagerCls}, {"AlertManager", RCTAlertManagerCls}, {"AppState", RCTAppStateCls}, @@ -34,7 +36,9 @@ Class RCTCoreModulesClassProvider(const char *name) { {"I18nManager", RCTI18nManagerCls}, {"KeyboardObserver", RCTKeyboardObserverCls}, {"LogBox", RCTLogBoxCls}, +#if !TARGET_OS_OSX // [macOS] Do we need these? {"PerfMonitor", RCTPerfMonitorCls}, +#endif // [macOS] {"PlatformConstants", RCTPlatformCls}, {"RedBox", RCTRedBoxCls}, {"SourceCode", RCTSourceCodeCls}, diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager.h b/packages/react-native/React/CoreModules/RCTAccessibilityManager.h index 01af72e81420ff..1d0fe173ae309c 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager.h +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager.h @@ -22,6 +22,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; / @property (nonatomic, assign) BOOL isBoldTextEnabled; @property (nonatomic, assign) BOOL isGrayscaleEnabled; +@property (nonatomic, assign) BOOL isHighContrastEnabled; // [macOS] maps to shouldIncreaseContrast on macOS @property (nonatomic, assign) BOOL isInvertColorsEnabled; @property (nonatomic, assign) BOOL isReduceMotionEnabled; @property (nonatomic, assign) BOOL prefersCrossFadeTransitions; diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm index 4aaabcaba30be7..5ad70adf02c134 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm @@ -17,6 +17,8 @@ #import "CoreModulesPlugins.h" +#if !TARGET_OS_OSX // [macOS] + NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAccessibilityManagerDidUpdateMultiplierNotification"; @@ -418,3 +420,4 @@ Class RCTAccessibilityManagerCls(void) { return RCTAccessibilityManager.class; } +#endif // [macOS] diff --git a/packages/react-native/React/CoreModules/RCTActionSheetManager.h b/packages/react-native/React/CoreModules/RCTActionSheetManager.h index 338310b89c0885..98a882f12932dd 100644 --- a/packages/react-native/React/CoreModules/RCTActionSheetManager.h +++ b/packages/react-native/React/CoreModules/RCTActionSheetManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/CoreModules/RCTActionSheetManager.mm b/packages/react-native/React/CoreModules/RCTActionSheetManager.mm index 38a4d165807803..69ba1a70a006cc 100644 --- a/packages/react-native/React/CoreModules/RCTActionSheetManager.mm +++ b/packages/react-native/React/CoreModules/RCTActionSheetManager.mm @@ -20,19 +20,47 @@ using namespace facebook::react; -@interface RCTActionSheetManager () - +@interface RCTActionSheetManager () < +#if !TARGET_OS_OSX // [macOS] +UIActionSheetDelegate +#else // [macOS +NSSharingServicePickerDelegate +#endif // macOS] +, NativeActionSheetManagerSpec> + +#if !TARGET_OS_OSX // [macOS] Unlike iOS, we will only ever have one NSMenu present at a time @property (nonatomic, strong) NSMutableArray *alertControllers; +#endif // [macOS] @end @implementation RCTActionSheetManager +#if TARGET_OS_OSX // [macOS +{ + /* Unlike UIAlertAction (which takes a block for it's action), NSMenuItem takes a selector. + * That selector no longer has has access to the method argument `callback`, so we must save it + * as an instance variable, that we can access in `menuItemDidTap`. We must do this as well for + * `failureCallback` and `successCallback`. + */ + NSMapTable *_callbacks; + RCTResponseSenderBlock _failureCallback; + RCTResponseSenderBlock _successCallback; + NSArray *_excludedActivities; + NSString *_sharingSubject; + +} +#endif // macOS] - (instancetype)init { self = [super init]; if (self) { +#if !TARGET_OS_OSX // [macOS] _alertControllers = [NSMutableArray new]; +#else // [macOS + _callbacks = [NSMapTable new]; +#endif // macOS] + } return self; } @@ -51,6 +79,7 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } +#if !TARGET_OS_OSX // [macOS] - (void)presentViewController:(UIViewController *)alertController onParentViewController:(UIViewController *)parentViewController anchorViewTag:(NSNumber *)anchorViewTag @@ -67,18 +96,45 @@ - (void)presentViewController:(UIViewController *)alertController alertController.popoverPresentationController.sourceRect = sourceView.bounds; [parentViewController presentViewController:alertController animated:YES completion:nil]; } +#else // [macOS +- (void)presentMenu:(NSMenu *)menu + anchorViewTag:(NSNumber *)anchorViewTag +{ + NSView *sourceView = nil; + if (anchorViewTag) { + sourceView = [self.viewRegistry_DEPRECATED viewForReactTag:anchorViewTag]; + } + + NSPoint location = CGPointZero; + if (sourceView != nil) { + // Display under the anchorview + CGRect bounds = [sourceView bounds]; + + CGFloat originX = [sourceView userInterfaceLayoutDirection] == NSUserInterfaceLayoutDirectionRightToLeft ? NSMaxX(bounds) : NSMinX(bounds); + location = NSMakePoint(originX, NSMaxY(bounds)); + } else { + // Display at mouse location if no anchorView provided + location = [NSEvent mouseLocation]; + } + [menu popUpMenuPositioningItem:menu.itemArray.firstObject atLocation:location inView:sourceView]; +} +#endif // [macOS] RCT_EXPORT_METHOD(showActionSheetWithOptions : (JS::NativeActionSheetManager::SpecShowActionSheetWithOptionsOptions &)options callback : (RCTResponseSenderBlock)callback) { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { RCTLogError(@"Unable to show action sheet from app extension"); return; } +#endif // [macOS] NSString *title = options.title(); +#if !TARGET_OS_OSX // [macOS] Unused on macOS NSString *message = options.message(); +#endif // [macOS] NSArray *buttons = RCTConvertOptionalVecToArray(options.options(), ^id(NSString *element) { return element; }); @@ -91,6 +147,7 @@ - (void)presentViewController:(UIViewController *)alertController return @(element); }); } +#if !TARGET_OS_OSX // [macOS] NSMenu doesn't have an equivalent of destructive buttons if (options.destructiveButtonIndices()) { destructiveButtonIndices = RCTConvertVecToArray(*options.destructiveButtonIndices(), ^id(double element) { return @(element); @@ -101,26 +158,31 @@ - (void)presentViewController:(UIViewController *)alertController } UIViewController *controller = RCTPresentedViewController(); +#endif // [macOS] NSNumber *anchor = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil]; +#if !TARGET_OS_OSX // [macOS] UIColor *tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil]; UIColor *cancelButtonTintColor = [RCTConvert UIColor:options.cancelButtonTintColor() ? @(*options.cancelButtonTintColor()) : nil]; if (controller == nil) { + // [macOS nil check our dict values before inserting them or we may crash RCTLogError( @"Tried to display action sheet but there is no application window. options: %@", @{ - @"title" : title, - @"message" : message, - @"options" : buttons, + @"title" : title ?: [NSNull null], + @"message" : message ?: [NSNull null], + @"options" : buttons ?: [NSNull null], @"cancelButtonIndex" : @(cancelButtonIndex), - @"destructiveButtonIndices" : destructiveButtonIndices, - @"anchor" : anchor, - @"tintColor" : tintColor, - @"cancelButtonTintColor" : cancelButtonTintColor, - @"disabledButtonIndices" : disabledButtonIndices, + @"destructiveButtonIndices" : destructiveButtonIndices ?: [NSNull null], + @"anchor" : anchor ?: [NSNull null], + @"tintColor" : tintColor ?: [NSNull null], + @"cancelButtonTintColor" : cancelButtonTintColor ?: [NSNull null], + @"disabledButtonIndices" : disabledButtonIndices ?: [NSNull null], }); + // macOS] return; } +#endif // [macOS] /* * The `anchor` option takes a view to set as the anchor for the share @@ -129,17 +191,26 @@ - (void)presentViewController:(UIViewController *)alertController */ NSNumber *anchorViewTag = anchor; +#if !TARGET_OS_OSX // [macOS] UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet]; +#else // [macOS + NSMenu *menu = [[NSMenu alloc] initWithTitle:title ?: @""]; + [menu setAutoenablesItems:NO]; + [_callbacks setObject:callback forKey:menu]; +#endif // macOS] NSInteger index = 0; +#if !TARGET_OS_OSX // [macOS] bool isCancelButtonIndex = false; // The handler for a button might get called more than once when tapping outside // the action sheet on iPad. RCTResponseSenderBlock can only be called once so // keep track of callback invocation here. __block bool callbackInvoked = false; +#endif // [macOS] for (NSString *option in buttons) { +#if !TARGET_OS_OSX // [macOS] UIAlertActionStyle style = UIAlertActionStyleDefault; if ([destructiveButtonIndices containsObject:@(index)]) { style = UIAlertActionStyleDestructive; @@ -162,6 +233,17 @@ - (void)presentViewController:(UIViewController *)alertController [actionButton setValue:cancelButtonTintColor forKey:@"titleTextColor"]; } [alertController addAction:actionButton]; +#else // [macOS + if (index == cancelButtonIndex) { + // NSMenu doesn't need a cancel button, you can just click outside the menu + continue; + } + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:option action:@selector(menuItemDidTap:) keyEquivalent:@""]; + [item setTag:index]; + [item setTarget:self]; + [menu addItem:item]; +#endif // macOS] index++; } @@ -169,7 +251,12 @@ - (void)presentViewController:(UIViewController *)alertController if (disabledButtonIndices) { for (NSNumber *disabledButtonIndex in disabledButtonIndices) { if ([disabledButtonIndex integerValue] < buttons.count) { +#if !TARGET_OS_OSX // [macOS] [alertController.actions[[disabledButtonIndex integerValue]] setEnabled:false]; +#else // [macOS + NSMenuItem *menuItem = [[menu itemArray] objectAtIndex:[disabledButtonIndex integerValue]]; + [menuItem setEnabled:NO]; +#endif // macOS] } else { RCTLogError( @"Index %@ from `disabledButtonIndices` is out of bounds. Maximum index value is %@.", @@ -180,6 +267,7 @@ - (void)presentViewController:(UIViewController *)alertController } } +#if !TARGET_OS_OSX // [macOS] alertController.view.tintColor = tintColor; NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()]; @@ -193,10 +281,14 @@ - (void)presentViewController:(UIViewController *)alertController [_alertControllers addObject:alertController]; [self presentViewController:alertController onParentViewController:controller anchorViewTag:anchorViewTag]; +#else // [macOS + [self presentMenu:menu anchorViewTag:anchorViewTag]; +#endif // macOS] } RCT_EXPORT_METHOD(dismissActionSheet) { +#if !TARGET_OS_OSX // [macOS] if (_alertControllers.count == 0) { RCTLogWarn(@"Unable to dismiss action sheet"); } @@ -204,6 +296,7 @@ - (void)presentViewController:(UIViewController *)alertController id _alertController = [_alertControllers lastObject]; [_alertController dismissViewControllerAnimated:YES completion:nil]; [_alertControllers removeLastObject]; +#endif // [macOS] } RCT_EXPORT_METHOD(showShareActionSheetWithOptions @@ -211,10 +304,12 @@ - (void)presentViewController:(UIViewController *)alertController : (RCTResponseSenderBlock)failureCallback successCallback : (RCTResponseSenderBlock)successCallback) { +#if !TARGET_OS_OSX // [macOS] if (RCTRunningInAppExtension()) { RCTLogError(@"Unable to show action sheet from app extension"); return; } +#endif // [macOS] NSMutableArray *items = [NSMutableArray array]; NSString *message = options.message(); @@ -240,6 +335,7 @@ - (void)presentViewController:(UIViewController *)alertController return; } +#if !TARGET_OS_OSX // [macOS] UIActivityViewController *shareController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; @@ -280,8 +376,81 @@ - (void)presentViewController:(UIViewController *)alertController } [self presentViewController:shareController onParentViewController:controller anchorViewTag:anchorViewTag]; +#else // [macOS + NSArray *excludedActivityTypes = RCTConvertOptionalVecToArray(options.excludedActivityTypes(), ^id(NSString *element) { return element; }); + NSMutableArray *excludedTypes = [NSMutableArray array]; + for (NSString *excludeActivityType in excludedActivityTypes) { + NSSharingService *sharingService = [NSSharingService sharingServiceNamed:excludeActivityType]; + if (sharingService) { + [excludedTypes addObject:sharingService]; + } + } + _excludedActivities = excludedTypes.copy; + _sharingSubject = options.subject(); + _failureCallback = failureCallback; + _successCallback = successCallback; + RCTPlatformView *sourceView = nil; + NSNumber *anchorViewTag = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil]; + if (anchorViewTag) { + sourceView = [self.viewRegistry_DEPRECATED viewForReactTag:anchorViewTag]; + } + NSView *contentView = sourceView ?: NSApp.keyWindow.contentView; + NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:items]; + picker.delegate = self; + [picker showRelativeToRect:contentView.bounds ofView:contentView preferredEdge:NSRectEdgeMinX]; +#endif // macOS] +} + +#if TARGET_OS_OSX // [macOS + +#pragma mark - NSSharingServicePickerDelegate methods + +- (void)menuItemDidTap:(NSMenuItem*)menuItem +{ + NSMenu *menu = menuItem.menu; + NSInteger buttonIndex = menuItem.tag; + RCTResponseSenderBlock callback = [_callbacks objectForKey:menu]; + if (callback) { + callback(@[@(buttonIndex)]); + [_callbacks removeObjectForKey:menu]; + } else { + RCTLogWarn(@"No callback registered for menu: %@", menu.title); + } +} + +- (void)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker didChooseSharingService:(NSSharingService *)service +{ + if (service){ + service.subject = _sharingSubject; + } +} + +- (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(NSArray *)items error:(NSError *)error +{ + _failureCallback(@[RCTJSErrorFromNSError(error)]); } +- (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items +{ + NSRange range = [sharingService.description rangeOfString:@"\\[com.apple.share.*\\]" options:NSRegularExpressionSearch]; + if (range.location == NSNotFound) { + _successCallback(@[@NO, (id)kCFNull]); + return; + } + range.location++; // Start after [ + range.length -= 2; // Remove both [ and ] + NSString *activityType = [sharingService.description substringWithRange:range]; + _successCallback(@[@YES, RCTNullIfNil(activityType)]); +} + +- (NSArray *)sharingServicePicker:(__unused NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(__unused NSArray *)items proposedSharingServices:(NSArray *)proposedServices +{ + return [proposedServices filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSSharingService *service, __unused NSDictionary * _Nullable bindings) { + return ![self->_excludedActivities containsObject:service]; + }]]; +} +#endif // macOS] + - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { return std::make_shared(params); diff --git a/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.h b/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.h new file mode 100644 index 00000000000000..78209940638dfc --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.h @@ -0,0 +1,12 @@ +/* + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@interface RCTAddressSanitizerCrashManager : NSObject + +@end diff --git a/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.mm b/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.mm new file mode 100644 index 00000000000000..d4adaa414f2fa7 --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTAddressSanitizerCrashManager.mm @@ -0,0 +1,18 @@ +// +// RCTAddressSanitizerCrashManager.mm +// Pods +// + +#import "RCTAddressSanitizerCrashManager.h" + +@implementation RCTAddressSanitizerCrashManager + +RCT_EXPORT_MODULE(ASANCrash) + +RCT_EXPORT_METHOD(invokeMemoryCrash) { + char *s = (char*)malloc(100); + free(s); + strcpy(s, "Hello world!"); // AddressSanitizer: heap-use-after-free +} + +@end diff --git a/packages/react-native/React/CoreModules/RCTAlertController.h b/packages/react-native/React/CoreModules/RCTAlertController.h index 62adf2f3ac9e03..f9a5d6580d0a71 100644 --- a/packages/react-native/React/CoreModules/RCTAlertController.h +++ b/packages/react-native/React/CoreModules/RCTAlertController.h @@ -5,11 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] +#if !TARGET_OS_OSX // [macOS] @interface RCTAlertController : UIAlertController +#else // [macOS +@interface RCTAlertController : NSViewController +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] - (void)show:(BOOL)animated completion:(void (^)(void))completion; - (void)hide; +#endif // [macOS] @end diff --git a/packages/react-native/React/CoreModules/RCTAlertController.mm b/packages/react-native/React/CoreModules/RCTAlertController.mm index cd01ef56d72684..698d7d1ae7856e 100644 --- a/packages/react-native/React/CoreModules/RCTAlertController.mm +++ b/packages/react-native/React/CoreModules/RCTAlertController.mm @@ -11,12 +11,15 @@ @interface RCTAlertController () +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong) UIWindow *alertWindow; +#endif // [macOS] @end @implementation RCTAlertController +#if !TARGET_OS_OSX // [macOS] - (UIWindow *)alertWindow { if (_alertWindow == nil) { @@ -76,5 +79,6 @@ - (UIWindow *)getUIWindowFromScene return nil; } +#endif // [macOS] @end diff --git a/packages/react-native/React/CoreModules/RCTAlertManager.h b/packages/react-native/React/CoreModules/RCTAlertManager.h index 2d2a7446fd5385..25f63bd6d55cd6 100644 --- a/packages/react-native/React/CoreModules/RCTAlertManager.h +++ b/packages/react-native/React/CoreModules/RCTAlertManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/CoreModules/RCTAlertManager.mm b/packages/react-native/React/CoreModules/RCTAlertManager.mm index 085166e88aa15b..ee3c6b048393c7 100644 --- a/packages/react-native/React/CoreModules/RCTAlertManager.mm +++ b/packages/react-native/React/CoreModules/RCTAlertManager.mm @@ -49,9 +49,19 @@ - (dispatch_queue_t)methodQueue - (void)invalidate { +#if !TARGET_OS_OSX // [macOS] for (UIAlertController *alertController in _alertControllers) { [alertController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; } +#else // [macOS + for (NSAlert *alert in _alertControllers) { + if (alert.window.sheetParent) { + [alert.window.sheetParent endSheet:alert.window]; + } else { + [alert.window close]; + } + } +#endif // macOS] } /** @@ -77,17 +87,23 @@ - (void)invalidate [RCTConvert NSDictionaryArray:RCTConvertOptionalVecToArray(args.buttons(), ^id(id element) { return element; })]; +#if !TARGET_OS_OSX // [macOS] NSString *defaultValue = [RCTConvert NSString:args.defaultValue()]; NSString *cancelButtonKey = [RCTConvert NSString:args.cancelButtonKey()]; NSString *destructiveButtonKey = [RCTConvert NSString:args.destructiveButtonKey()]; NSString *preferredButtonKey = [RCTConvert NSString:args.preferredButtonKey()]; UIKeyboardType keyboardType = [RCTConvert UIKeyboardType:args.keyboardType()]; +#else // [macOS + BOOL critical = args.critical().value_or(NO); + BOOL modal = args.modal().value_or(NO); + NSArray *defaultInputs = [RCTConvert NSDictionaryArray:RCTConvertOptionalVecToArray(args.defaultInputs(), ^id(id element) { return element; })]; +#endif // macOS] if (!title && !message) { RCTLogError(@"Must specify either an alert title, or message, or both"); return; } - +#if !TARGET_OS_OSX // [macOS] if (buttons.count == 0) { if (type == RCTAlertViewStyleDefault) { buttons = @[ @{@"0" : RCTUIKitLocalizedString(@"OK")} ]; @@ -198,6 +214,99 @@ - (void)invalidate dispatch_async(dispatch_get_main_queue(), ^{ [alertController show:YES completion:nil]; }); +#else // [macOS + + NSAlert *alert = [NSAlert new]; + if (title.length > 0) { + alert.messageText = title; + } + if (message.length > 0) { + alert.informativeText = message; + } + + if (critical) { + alert.alertStyle = NSAlertStyleCritical; + } + + NSView *accessoryView = nil; + + const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 200.0, 22.0); + const NSRect RCTUsernamePasswordFrame = NSMakeRect(0.0, 0.0, 200.0, 50.0); + + id (^textFieldDefaults)(NSTextField *, BOOL) = ^id(NSTextField *textField, BOOL isPassword) { + textField.cell.scrollable = YES; + textField.cell.wraps = YES; + textField.maximumNumberOfLines = 1; + textField.stringValue = (isPassword ? defaultInputs.lastObject[@"default"] : defaultInputs.firstObject[@"default"]) ?: @""; + textField.placeholderString = isPassword ? defaultInputs.lastObject[@"placeholder"] : defaultInputs.firstObject[@"placeholder"]; + return textField; + }; + + switch (type) { + case RCTAlertViewStylePlainTextInput: { + accessoryView = textFieldDefaults([[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame], NO); + accessoryView.translatesAutoresizingMaskIntoConstraints = YES; + break; + } + case RCTAlertViewStyleSecureTextInput: { + accessoryView = textFieldDefaults([[NSSecureTextField alloc] initWithFrame:RCTSingleTextFieldFrame], NO); + break; + } + case RCTAlertViewStyleLoginAndPasswordInput: { + accessoryView = [[NSView alloc] initWithFrame:RCTUsernamePasswordFrame]; + + NSSecureTextField *password = textFieldDefaults([[NSSecureTextField alloc] initWithFrame:RCTSingleTextFieldFrame], YES); + NSTextField *input = textFieldDefaults([[NSTextField alloc] initWithFrame:NSMakeRect(CGRectGetMinX(password.frame), CGRectGetMaxY(password.frame), CGRectGetWidth(password.frame), CGRectGetHeight(password.frame))], NO); + + [accessoryView addSubview:input]; + [accessoryView addSubview:password]; + + break; + } + case RCTAlertViewStyleDefault: + break; + } + alert.accessoryView = accessoryView; + + for (NSDictionary *button in buttons) { + if (button.count != 1) { + RCTLogError(@"Button definitions should have exactly one key."); + } + NSString *buttonKey = button.allKeys.firstObject; + NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]]; + [alert addButtonWithTitle:buttonTitle]; + } + + void (^callbacksHandlers)(NSModalResponse response) = ^void(NSModalResponse response) { + NSString *buttonKey = @"0"; + if (response >= NSAlertFirstButtonReturn) { + buttonKey = buttons[response - NSAlertFirstButtonReturn].allKeys.firstObject; + } + NSArray *textfields = [accessoryView isKindOfClass:NSTextField.class] ? @[accessoryView] : accessoryView.subviews; + if (textfields.count == 2) { + NSDictionary *loginCredentials = @{ + @"login": textfields.firstObject.stringValue, + @"password": textfields.lastObject.stringValue + }; + callback(@[buttonKey, loginCredentials]); + } else if (textfields.count == 1) { + callback(@[buttonKey, textfields.firstObject.stringValue]); + } else { + callback(@[buttonKey]); + } + }; + + if (!_alertControllers) { + _alertControllers = [NSHashTable weakObjectsHashTable]; + } + [_alertControllers addObject:alert]; + + if (modal) { + callbacksHandlers([alert runModal]); + } else { + [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:callbacksHandlers]; + } +#endif // macOS] } - (std::shared_ptr)getTurboModule: diff --git a/packages/react-native/React/CoreModules/RCTAppState.mm b/packages/react-native/React/CoreModules/RCTAppState.mm index 0aa63fc6b53390..94aa2e7ddba6aa 100644 --- a/packages/react-native/React/CoreModules/RCTAppState.mm +++ b/packages/react-native/React/CoreModules/RCTAppState.mm @@ -7,6 +7,7 @@ #import "RCTAppState.h" +#import // [macOS] #import #import #import @@ -17,6 +18,7 @@ static NSString *RCTCurrentAppState() { +#if !TARGET_OS_OSX // [macOS] static NSDictionary *states; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -26,12 +28,21 @@ @(UIApplicationStateInactive) : @"inactive" }; }); - if (RCTRunningInAppExtension()) { return @"extension"; } - return states[@(RCTSharedApplication().applicationState)] ?: @"unknown"; +#else // [macOS + + if (RCTSharedApplication().isActive) { + return @"active"; + } else if (RCTSharedApplication().isHidden) { + return @"background"; + } + return @"unknown"; + +#endif // macOS] + } @interface RCTAppState () @@ -92,10 +103,12 @@ - (void)startObserving object:nil]; } +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif // [macOS] } - (void)stopObserving diff --git a/packages/react-native/React/CoreModules/RCTAppearance.h b/packages/react-native/React/CoreModules/RCTAppearance.h index 9909ca9e741d7d..fa42f718f4a487 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.h +++ b/packages/react-native/React/CoreModules/RCTAppearance.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -14,7 +14,11 @@ RCT_EXTERN void RCTEnableAppearancePreference(BOOL enabled); RCT_EXTERN void RCTOverrideAppearancePreference(NSString *const); RCT_EXTERN NSString *RCTCurrentOverrideAppearancePreference(); +#if !TARGET_OS_OSX // [macOS] RCT_EXTERN NSString *RCTColorSchemePreference(UITraitCollection *traitCollection); +#else // [macOS +RCT_EXTERN NSString *RCTColorSchemePreference(NSAppearance *appearance); +#endif // macOS] @interface RCTAppearance : RCTEventEmitter - (instancetype)init; diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index 449ff0396be889..0a0c21e1c2c521 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -36,6 +36,7 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride) return sColorSchemeOverride; } +#if !TARGET_OS_OSX // [macOS] NSString *RCTColorSchemePreference(UITraitCollection *traitCollection) { static NSDictionary *appearances; @@ -59,6 +60,30 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride) return appearances[@(traitCollection.userInterfaceStyle)] ?: RCTAppearanceColorSchemeLight; } +#else // [macOS +NSString *RCTColorSchemePreference(NSAppearance *appearance) +{ + static NSDictionary *appearances; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + appearances = @{ + NSAppearanceNameAqua: RCTAppearanceColorSchemeLight, + NSAppearanceNameDarkAqua: RCTAppearanceColorSchemeDark + }; + }); + + if (!sAppearancePreferenceEnabled) { + // Return the default if the app doesn't allow different color schemes. + return RCTAppearanceColorSchemeLight; + } + + appearance = appearance ?: [NSApp effectiveAppearance]; + + NSAppearanceName appearanceName = [appearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]]; + return appearances[appearanceName] ?: RCTAppearanceColorSchemeLight; +} +#endif // macOS] @interface RCTAppearance () @end @@ -70,8 +95,13 @@ @implementation RCTAppearance { - (instancetype)init { if ((self = [super init])) { +#if !TARGET_OS_OSX // [macOS] UITraitCollection *traitCollection = RCTSharedApplication().delegate.window.traitCollection; _currentColorScheme = RCTColorSchemePreference(traitCollection); +#else // [macOS + NSAppearance *appearance = RCTSharedApplication().appearance; + _currentColorScheme = RCTColorSchemePreference(appearance); +#endif // macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appearanceChanged:) name:RCTUserInterfaceStyleDidChangeNotification @@ -99,12 +129,22 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_METHOD(setColorScheme : (NSString *)style) { +#if !TARGET_OS_OSX // [macOS] UIUserInterfaceStyle userInterfaceStyle = [RCTConvert UIUserInterfaceStyle:style]; NSArray<__kindof UIWindow *> *windows = RCTSharedApplication().windows; for (UIWindow *window in windows) { window.overrideUserInterfaceStyle = userInterfaceStyle; } +#else // [macOS + NSAppearance *appearance = nil; + if ([style isEqualToString:@"light"]) { + appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + } else if ([style isEqualToString:@"dark"]) { + appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + } + RCTSharedApplication().appearance = appearance; +#endif // macOS] } RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme) @@ -112,14 +152,23 @@ - (dispatch_queue_t)methodQueue return _currentColorScheme; } + - (void)appearanceChanged:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; +#if !TARGET_OS_OSX // [macOS] UITraitCollection *traitCollection = nil; if (userInfo) { traitCollection = userInfo[RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey]; } NSString *newColorScheme = RCTColorSchemePreference(traitCollection); +#else // [macOS + NSAppearance *appearance = nil; + if (userInfo) { + appearance = userInfo[RCTUserInterfaceStyleDidChangeNotificationAppearanceKey]; + } + NSString *newColorScheme = RCTColorSchemePreference(appearance); +#endif // macOS] if (![_currentColorScheme isEqualToString:newColorScheme]) { _currentColorScheme = newColorScheme; [self sendEventWithName:@"appearanceChanged" body:@{@"colorScheme" : newColorScheme}]; diff --git a/packages/react-native/React/CoreModules/RCTClipboard.mm b/packages/react-native/React/CoreModules/RCTClipboard.mm index 2dc098a4730273..dab2dffe39b571 100644 --- a/packages/react-native/React/CoreModules/RCTClipboard.mm +++ b/packages/react-native/React/CoreModules/RCTClipboard.mm @@ -8,7 +8,7 @@ #import "RCTClipboard.h" #import -#import +#import // [macOS] #import "CoreModulesPlugins.h" @@ -28,14 +28,25 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_METHOD(setString : (NSString *)content) { +#if !TARGET_OS_OSX // [macOS] UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; clipboard.string = (content ?: @""); +#else // [macOS + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setString:(content ? : @"") forType:NSPasteboardTypeString]; +#endif // macOS] } RCT_EXPORT_METHOD(getString : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject) { +#if !TARGET_OS_OSX // [macOS] UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; resolve((clipboard.string ?: @"")); +#else // [macOS + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + resolve(([pasteboard stringForType:NSPasteboardTypeString] ? : @"")); +#endif // macOS] } - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index ab9c945790e358..3e2dcd83436b0e 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -15,8 +15,10 @@ #import #import #import +#import // [macOS] #import #import +#import // [macOS] #import "CoreModulesPlugins.h" @@ -28,8 +30,13 @@ @interface RCTDevLoadingView () #if RCT_DEV_MENU @implementation RCTDevLoadingView { +#if !TARGET_OS_OSX // [macOS] UIWindow *_window; UILabel *_label; +#else // [macOS + NSWindow *_window; + NSTextField *_label; +#endif // macOS] NSDate *_showDate; BOOL _hiding; dispatch_block_t _initialMessageBlock; @@ -91,7 +98,7 @@ - (void)hideBannerAfter:(CGFloat)delay [self performSelector:@selector(hide) withObject:nil afterDelay:delay]; } -- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor +- (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColor:(RCTUIColor *)backgroundColor // [macOS] { if (!RCTDevLoadingViewGetEnabled() || _hiding) { return; @@ -114,6 +121,7 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( dispatch_async(dispatch_get_main_queue(), ^{ self->_showDate = [NSDate date]; if (!self->_window && !RCTRunningInTestEnvironment()) { +#if !TARGET_OS_OSX // [macOS] CGSize screenSize = [UIScreen mainScreen].bounds.size; UIWindow *window = RCTSharedApplication().keyWindow; @@ -129,8 +137,28 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular]; self->_label.textAlignment = NSTextAlignmentCenter; +#else // [macOS + NSRect screenFrame = [NSScreen mainScreen].visibleFrame; + self->_window = [[NSPanel alloc] initWithContentRect:NSMakeRect(screenFrame.origin.x + round((screenFrame.size.width - 375) / 2), screenFrame.size.height - 20, 375, 19) + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]; + self->_window.releasedWhenClosed = NO; + self->_window.backgroundColor = [NSColor clearColor]; + + NSTextField *label = [[NSTextField alloc] initWithFrame:self->_window.contentView.bounds]; + label.alignment = NSTextAlignmentCenter; + label.bezeled = NO; + label.editable = NO; + label.selectable = NO; + label.wantsLayer = YES; + label.layer.cornerRadius = label.frame.size.height / 3; + self->_label = label; + [[self->_window contentView] addSubview:label]; +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] self->_label.text = message; self->_label.textColor = color; @@ -139,6 +167,12 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( UIWindowScene *scene = (UIWindowScene *)RCTSharedApplication().connectedScenes.anyObject; self->_window.windowScene = scene; +#else // [macOS + self->_label.stringValue = message; + self->_label.textColor = color; + self->_label.backgroundColor = backgroundColor; + [self->_window orderFront:nil]; +#endif // macOS] }); [self hideBannerAfter:15.0]; @@ -166,6 +200,7 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( const NSTimeInterval MIN_PRESENTED_TIME = 0.6; NSTimeInterval presentedTime = [[NSDate date] timeIntervalSinceDate:self->_showDate]; NSTimeInterval delay = MAX(0, MIN_PRESENTED_TIME - presentedTime); +#if !TARGET_OS_OSX // [macOS] CGRect windowFrame = self->_window.frame; [UIView animateWithDuration:0.25 delay:delay @@ -179,6 +214,16 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( self->_window = nil; self->_hiding = false; }]; +#else // [macOS] + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [NSAnimationContext runAnimationGroup:^(__unused NSAnimationContext *context) { + self->_window.animator.alphaValue = 0.0; + } completionHandler:^{ + [self->_window close]; + self->_window = nil; + }]; + }); +#endif // macOS] }); } @@ -188,16 +233,20 @@ - (void)showProgressMessage:(NSString *)message // This is an optimization. Since the progress can come in quickly, // we want to do the minimum amount of work to update the UI, // which is to only update the label text. +#if !TARGET_OS_OSX // [macOS] _label.text = message; +#else // [macOS + self->_label.stringValue = message; +#endif // macOS] return; } - UIColor *color = [UIColor whiteColor]; - UIColor *backgroundColor = [UIColor colorWithHue:105 saturation:0 brightness:.25 alpha:1]; + RCTUIColor *color = [RCTUIColor whiteColor]; // [macOS] + RCTUIColor *backgroundColor = [RCTUIColor colorWithHue:105 saturation:0 brightness:.25 alpha:1]; // [macOS] if ([self isDarkModeEnabled]) { - color = [UIColor colorWithHue:208 saturation:0.03 brightness:.14 alpha:1]; - backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1]; + color = [RCTUIColor colorWithHue:208 saturation:0.03 brightness:.14 alpha:1]; // [macOS] + backgroundColor = [RCTUIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1]; // [macOS] } [self showMessage:message color:color backgroundColor:backgroundColor]; @@ -205,16 +254,21 @@ - (void)showProgressMessage:(NSString *)message - (void)showOfflineMessage { - UIColor *color = [UIColor whiteColor]; - UIColor *backgroundColor = [UIColor blackColor]; - - if ([self isDarkModeEnabled]) { - color = [UIColor blackColor]; - backgroundColor = [UIColor whiteColor]; - } + // [macOS isDarkModeEnabled should only be run on the main thread + __weak __typeof(self) weakSelf = self; + RCTExecuteOnMainQueue(^{ + RCTUIColor *color = [RCTUIColor whiteColor]; // [macOS] + RCTUIColor *backgroundColor = [RCTUIColor blackColor]; // [macOS] + + if ([weakSelf isDarkModeEnabled]) { + color = [RCTUIColor blackColor]; // [macOS] + backgroundColor = [RCTUIColor whiteColor]; // [macOS] + } - NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME]; - [self showMessage:message color:color backgroundColor:backgroundColor]; + NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME]; + [weakSelf showMessage:message color:color backgroundColor:backgroundColor]; + }); + // macOS] } - (BOOL)isDarkModeEnabled @@ -273,7 +327,7 @@ + (NSString *)moduleName + (void)setEnabled:(BOOL)enabled { } -- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor +- (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColor:(RCTUIColor *)backgroundColor // [macOS] RCTUIColor { } - (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor diff --git a/packages/react-native/React/CoreModules/RCTDevMenu.h b/packages/react-native/React/CoreModules/RCTDevMenu.h index b9b76f63e6e385..09535399fad2c2 100644 --- a/packages/react-native/React/CoreModules/RCTDevMenu.h +++ b/packages/react-native/React/CoreModules/RCTDevMenu.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -76,6 +76,13 @@ RCT_EXTERN NSString *const RCTShowDevMenuNotification; */ - (void)addItem:(RCTDevMenuItem *)item; +#if TARGET_OS_OSX // [macOS +/** + * Creates the NSMenu for macOS. + */ +- (NSMenu *)menu; +#endif // macOS] + @end typedef NSString * (^RCTDevMenuItemTitleBlock)(void); diff --git a/packages/react-native/React/CoreModules/RCTDevMenu.mm b/packages/react-native/React/CoreModules/RCTDevMenu.mm index 2154e3ebefc3e7..f0bbe1503ce830 100644 --- a/packages/react-native/React/CoreModules/RCTDevMenu.mm +++ b/packages/react-native/React/CoreModules/RCTDevMenu.mm @@ -12,7 +12,9 @@ #import #import #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] #import #import #import @@ -25,10 +27,18 @@ NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; +#if !TARGET_OS_OSX // [macOS] + +// [macOS +typedef void (*MotionEndedWithEventImpType)(id self, SEL selector, UIEventSubtype motion, UIEvent *event); +static MotionEndedWithEventImpType RCTOriginalUIWindowMotionEndedWithEventImp = nil; +// macOS] + @implementation UIWindow (RCTDevMenu) - (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event { + RCTOriginalUIWindowMotionEndedWithEventImp(self, @selector(motionEnded:withEvent:), motion, event); // [macOS] if (event.subtype == UIEventSubtypeMotionShake) { [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil]; } @@ -36,6 +46,8 @@ - (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)eve @end +#endif // [macOS] + @implementation RCTDevMenuItem { RCTDevMenuItemTitleBlock _titleBlock; dispatch_block_t _handler; @@ -83,14 +95,20 @@ - (NSString *)title @end +#if !TARGET_OS_OSX // [macOS] + typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action); +#endif // [macOS] + @interface RCTDevMenu () @end @implementation RCTDevMenu { +#if !TARGET_OS_OSX // [macOS] UIAlertController *_actionSheet; +#endif // [macOS] NSMutableArray *_extraMenuItems; } @@ -103,10 +121,10 @@ @implementation RCTDevMenu { + (void)initialize { +#if !TARGET_OS_OSX // [macOS] // We're swizzling here because it's poor form to override methods in a category, - // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's - // no need to call the original implementation. - RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); + RCTOriginalUIWindowMotionEndedWithEventImp = (MotionEndedWithEventImpType) RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); // [macOS] +#endif // [macOS] } + (BOOL)requiresMainQueueSetup @@ -191,15 +209,17 @@ - (dispatch_queue_t)methodQueue - (void)invalidate { _presentedItems = nil; +#if !TARGET_OS_OSX // [macOS] [_actionSheet dismissViewControllerAnimated:YES completion:^(void){ }]; +#endif // [macOS] } - (void)showOnShake { if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isShakeToShowDevMenuEnabled]) { - for (UIWindow *window in [RCTSharedApplication() windows]) { + for (RCTPlatformWindow *window in [RCTSharedApplication() windows]) { NSString *recursiveDescription = [window valueForKey:@"recursiveDescription"]; if ([recursiveDescription containsString:@"RCTView"]) { [self show]; @@ -209,6 +229,7 @@ - (void)showOnShake } } +#if !TARGET_OS_OSX // [macOS] - (void)toggle { if (_actionSheet) { @@ -225,6 +246,7 @@ - (BOOL)isActionSheetShown { return _actionSheet != nil; } +#endif // [macOS] - (void)addItem:(NSString *)title handler:(void (^)(void))handler { @@ -303,6 +325,7 @@ - (void)setDefaultJSBundle return @"Configure Bundler"; } handler:^{ +#if !TARGET_OS_OSX // [macOS] UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Configure Bundler" message:@"Provide a custom bundler address, port, and entrypoint." @@ -359,6 +382,14 @@ - (void)setDefaultJSBundle return; }]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL]; +#else // [macOS + NSAlert *alert = [NSAlert new]; + [alert setMessageText:@"Change packager location"]; + [alert setInformativeText:@"Input packager IP, port and entrypoint"]; + [alert addButtonWithTitle:@"Use bundled JS"]; + [alert setAlertStyle:NSWarningAlertStyle]; + [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:nil]; +#endif // macOS] }]]; [items addObjectsFromArray:_extraMenuItems]; @@ -367,6 +398,7 @@ - (void)setDefaultJSBundle RCT_EXPORT_METHOD(show) { +#if !TARGET_OS_OSX // [macOS] if (_actionSheet || RCTRunningInAppExtension()) { return; } @@ -399,9 +431,17 @@ - (void)setDefaultJSBundle _presentedItems = items; [RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil]; +#else // [macOS + NSMenu *menu = [self menu]; + NSWindow *window = [NSApp keyWindow]; + NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseUp location:CGPointMake(0, 0) modifierFlags:0 timestamp:NSTimeIntervalSince1970 windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:0 pressure:0.1]; + [NSMenu popUpContextMenu:menu withEvent:event forView:[window contentView]]; +#endif // macOS] + [_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]]; } +#if !TARGET_OS_OSX // [macOS] - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item { return ^(__unused UIAlertAction *action) { @@ -412,6 +452,53 @@ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__ self->_actionSheet = nil; }; } +#else // [macOS +- (NSMenu *)menu +{ + if ([_bridge.devSettings isSecondaryClickToShowDevMenuEnabled]) { + NSMenu *menu = nil; + if (_bridge) { + NSString *desc = _bridge.bridgeDescription; + if (desc.length == 0) { + desc = NSStringFromClass([_bridge class]); + } + NSString *title = [NSString stringWithFormat:@"React Native: Development\n(%@)", desc]; + + menu = [NSMenu new]; + + NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc]initWithString:title]; + [attributedTitle setAttributes: @{ NSFontAttributeName : [NSFont menuFontOfSize:0] } range: NSMakeRange(0, [attributedTitle length])]; + NSMenuItem *titleItem = [NSMenuItem new]; + [titleItem setAttributedTitle:attributedTitle]; + [menu addItem:titleItem]; + + [menu addItem:[NSMenuItem separatorItem]]; + + NSArray *items = [self _menuItemsToPresent]; + for (RCTDevMenuItem *item in items) { + NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item title] action:@selector(menuItemSelected:) keyEquivalent:@""]; + [menuItem setTarget:self]; + [menuItem setRepresentedObject:item]; + [menu addItem:menuItem]; + } + } + return menu; + } + return nil; +} + +-(void)menuItemSelected:(id)sender +{ + NSMenuItem *menuItem = (NSMenuItem *)sender; + RCTDevMenuItem *item = (RCTDevMenuItem *)[menuItem representedObject]; + [item callHandler]; +} + +- (void)setSecondaryClickToShow:(BOOL)secondaryClickToShow +{ + _bridge.devSettings.isSecondaryClickToShowDevMenuEnabled = secondaryClickToShow; +} +#endif // macOS] #pragma mark - deprecated methods and properties diff --git a/packages/react-native/React/CoreModules/RCTDevSettings.h b/packages/react-native/React/CoreModules/RCTDevSettings.h index 35a24b5d9191b2..38835835468adc 100644 --- a/packages/react-native/React/CoreModules/RCTDevSettings.h +++ b/packages/react-native/React/CoreModules/RCTDevSettings.h @@ -30,6 +30,13 @@ */ - (id)settingForKey:(NSString *)key; +// [macOS +/** + * Returns all keys that are overridden + */ +- (NSArray *)overridenKeys; +// macOS] + @end @protocol RCTDevSettingsInspectable @@ -62,6 +69,14 @@ */ @property (nonatomic, assign) BOOL isShakeToShowDevMenuEnabled; +// [macOS +/* + * Whether secondary click will show RCTDevMenu. The menu is enabled by default if RCT_DEV=1, but + * you may wish to disable it so that you can provide your own contextual menu. + */ +@property (nonatomic, assign) BOOL isSecondaryClickToShowDevMenuEnabled; +// macOS] + /** * Whether performance profiling is enabled. */ diff --git a/packages/react-native/React/CoreModules/RCTDevSettings.mm b/packages/react-native/React/CoreModules/RCTDevSettings.mm index 346738bccb6362..e1c2070a6e96fc 100644 --- a/packages/react-native/React/CoreModules/RCTDevSettings.mm +++ b/packages/react-native/React/CoreModules/RCTDevSettings.mm @@ -19,6 +19,7 @@ #import #import #import +#import // [macOS] #import #import "CoreModulesPlugins.h" @@ -30,6 +31,7 @@ static NSString *const kRCTDevSettingExecutorOverrideClass = @"executor-override"; static NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow"; static NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey"; +static NSString *const kRCTDevSettingSecondClickToShowDevMenu = @"secondClickToShow"; // [macOS] static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu"; @@ -101,6 +103,13 @@ - (id)settingForKey:(NSString *)key return _settings[key]; } +// [macOS +- (NSArray *)overridenKeys +{ + return [_settings allKeys]; +} +// macOS] + - (void)_reloadWithDefaults:(NSDictionary *)defaultValues { NSDictionary *existingSettings = [_userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey]; @@ -110,7 +119,11 @@ - (void)_reloadWithDefaults:(NSDictionary *)defaultValues _settings[key] = defaultValues[key]; } } - [_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey]; + + // [macOS] protect against race conditions where another thread holds a mutext trying to set this at the same time + RCTExecuteOnMainQueue(^{ + [self->_userDefaults setObject:self->_settings forKey:kRCTDevSettingsUserDefaultsKey]; + }); } @end @@ -129,7 +142,7 @@ @interface RCTDevSettings () dataSource; +@property (atomic, readwrite, strong) id dataSource; // [macOS] protect against race conditions where another thread changes the _dataSource @end @@ -146,6 +159,7 @@ - (instancetype)init NSDictionary *defaultValues = @{ kRCTDevSettingShakeToShowDevMenu : @YES, kRCTDevSettingHotLoadingEnabled : @YES, + kRCTDevSettingSecondClickToShowDevMenu: @YES, // [macOS] }; RCTDevSettingsUserDefaultsDataSource *dataSource = [[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues]; @@ -176,7 +190,7 @@ - (instancetype)initWithDataSource:(id)dataSource - (void)initialize { -#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION +#if DEBUG && RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION // [macOS] if (self.bridge) { RCTBridge *__weak weakBridge = self.bridge; _bridgeExecutorOverrideToken = [[RCTPackagerConnection sharedPackagerConnection] @@ -207,7 +221,7 @@ - (void)initialize } #endif -#if RCT_ENABLE_INSPECTOR +#if RCT_ENABLE_INSPECTOR && !TARGET_OS_UIKITFORMAC && DEBUG // [macOS] if (self.bridge) { // We need this dispatch to the main thread because the bridge is not yet // finished with its initialisation. By the time it relinquishes control of @@ -269,12 +283,12 @@ - (void)invalidate - (void)_updateSettingWithValue:(id)value forKey:(NSString *)key { - [_dataSource updateSettingWithValue:value forKey:key]; + [[self dataSource] updateSettingWithValue:value forKey:key]; // [macOS] protect against race conditions where another thread changes the _dataSource } - (id)settingForKey:(NSString *)key { - return [_dataSource settingForKey:key]; + return [[self dataSource] settingForKey:key]; // [macOS] protect against race conditions where another thread changes the _dataSource } - (BOOL)isDeviceDebuggingAvailable @@ -332,7 +346,19 @@ - (BOOL)isShakeToShowDevMenuEnabled return [[self settingForKey:kRCTDevSettingShakeToShowDevMenu] boolValue]; } -RCT_EXPORT_METHOD(setIsDebuggingRemotely : (BOOL)enabled) +// [macOS +RCT_EXPORT_METHOD(setIsSecondaryClickToShowDevMenuEnabled:(BOOL)enabled) +{ + [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingSecondClickToShowDevMenu]; +} + +- (BOOL)isSecondaryClickToShowDevMenuEnabled +{ + return [[self settingForKey:kRCTDevSettingSecondClickToShowDevMenu] boolValue]; +} +// macOS] + +RCT_EXPORT_METHOD(setIsDebuggingRemotely:(BOOL)enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingIsDebuggingRemotely]; [self _remoteDebugSettingDidChange]; @@ -552,6 +578,8 @@ @interface RCTDevSettings () @implementation RCTDevSettings +RCT_EXPORT_MODULE() // [macOS] + - (instancetype)initWithDataSource:(id)dataSource { return [super init]; diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.h b/packages/react-native/React/CoreModules/RCTDeviceInfo.h index 5b129ea8dcedc6..bfaffabf7f0271 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.h +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.h @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -#import + +#import // [macOS] #import diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm index f2d55e334a9a1f..85a544846a7be1 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -16,6 +16,7 @@ #import #import #import +#import "UIView+React.h" // [macOS] #import "CoreModulesPlugins.h" @@ -25,7 +26,9 @@ @interface RCTDeviceInfo () *dimsWindow = @{ @"width" : @(window.width), @@ -126,7 +145,12 @@ - (NSDictionary *)_exportedDimensions (RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"]; RCTAssert(accessibilityManager, @"Failed to get exported dimensions: AccessibilityManager is nil"); CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0; +#if !TARGET_OS_OSX // [macOS] return RCTExportedDimensions(fontScale); +#else // [macOS + // TODO: Saad - get root view here + return RCTExportedDimensions(nil); +#endif // macOS] } - (NSDictionary *)constantsToExport @@ -166,6 +190,8 @@ - (void)didReceiveNewContentSizeMultiplier }); } +#if !TARGET_OS_OSX // [macOS] + - (void)interfaceOrientationDidChange { __weak __typeof(self) weakSelf = self; @@ -206,6 +232,7 @@ - (void)_interfaceOrientationDidChange #pragma clang diagnostic pop } } +#endif // [macOS] - (void)interfaceFrameDidChange { diff --git a/packages/react-native/React/CoreModules/RCTEventDispatcher.h b/packages/react-native/React/CoreModules/RCTEventDispatcher.h index 3b31791ae73d96..f799968f28f8c1 100644 --- a/packages/react-native/React/CoreModules/RCTEventDispatcher.h +++ b/packages/react-native/React/CoreModules/RCTEventDispatcher.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #import #import diff --git a/packages/react-native/React/CoreModules/RCTEventDispatcher.mm b/packages/react-native/React/CoreModules/RCTEventDispatcher.mm index 3669e2e7d64b58..27250968895ab6 100644 --- a/packages/react-native/React/CoreModules/RCTEventDispatcher.mm +++ b/packages/react-native/React/CoreModules/RCTEventDispatcher.mm @@ -79,7 +79,7 @@ - (void)sendTextEventWithType:(RCTTextEventType)type key:(NSString *)key eventCount:(NSInteger)eventCount { - static NSString *events[] = {@"focus", @"blur", @"change", @"submitEditing", @"endEditing", @"keyPress"}; + static NSString *events[] = {@"focus", @"blur", @"change", @"submitEditing", @"endEditing", @"keyPress", @"keyDown", @"keyUp"}; NSMutableDictionary *body = [[NSMutableDictionary alloc] initWithDictionary:@{ @"eventCount" : @(eventCount), diff --git a/packages/react-native/React/CoreModules/RCTFPSGraph.h b/packages/react-native/React/CoreModules/RCTFPSGraph.h index 7b90481a6ebac0..73c141d53b2ba1 100644 --- a/packages/react-native/React/CoreModules/RCTFPSGraph.h +++ b/packages/react-native/React/CoreModules/RCTFPSGraph.h @@ -5,19 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #if RCT_DEV -@interface RCTFPSGraph : UIView +@interface RCTFPSGraph : RCTPlatformView // [macOS] @property (nonatomic, assign, readonly) NSUInteger FPS; @property (nonatomic, assign, readonly) NSUInteger maxFPS; @property (nonatomic, assign, readonly) NSUInteger minFPS; -- (instancetype)initWithFrame:(CGRect)frame color:(UIColor *)color NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithFrame:(CGRect)frame color:(RCTUIColor *)color NS_DESIGNATED_INITIALIZER; // [macOS] - (void)onTick:(NSTimeInterval)timestamp; diff --git a/packages/react-native/React/CoreModules/RCTFPSGraph.mm b/packages/react-native/React/CoreModules/RCTFPSGraph.mm index 1fc4bd24b9c37f..1fd14d36af8491 100644 --- a/packages/react-native/React/CoreModules/RCTFPSGraph.mm +++ b/packages/react-native/React/CoreModules/RCTFPSGraph.mm @@ -23,7 +23,7 @@ @implementation RCTFPSGraph { UILabel *_label; CGFloat *_frames; - UIColor *_color; + RCTUIColor *_color; // [macOS] NSTimeInterval _prevTime; NSUInteger _frameCount; @@ -35,7 +35,7 @@ @implementation RCTFPSGraph { CGFloat _scale; } -- (instancetype)initWithFrame:(CGRect)frame color:(UIColor *)color +- (instancetype)initWithFrame:(CGRect)frame color:(RCTUIColor *)color // [macOS] { if ((self = [super initWithFrame:frame])) { _frameCount = -1; diff --git a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm index ce483ddceb95fc..8031e9741979ce 100644 --- a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm +++ b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm @@ -23,6 +23,8 @@ @implementation RCTKeyboardObserver - (void)startObserving { +#if !TARGET_OS_OSX // [macOS] + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; #define ADD_KEYBOARD_HANDLER(NAME, SELECTOR) [nc addObserver:self selector:@selector(SELECTOR:) name:NAME object:nil] @@ -35,6 +37,7 @@ - (void)startObserving ADD_KEYBOARD_HANDLER(UIKeyboardDidChangeFrameNotification, keyboardDidChangeFrame); #undef ADD_KEYBOARD_HANDLER +#endif // [macOS] } - (NSArray *)supportedEvents @@ -90,7 +93,7 @@ -(void)EVENT : (NSNotification *)notification @"height" : @(rect.size.height), }; } - +#if !TARGET_OS_OSX // [macOS] static NSString *RCTAnimationNameForCurve(UIViewAnimationCurve curve) { switch (curve) { @@ -106,9 +109,11 @@ -(void)EVENT : (NSNotification *)notification return @"keyboard"; } } +#endif // [macOS] static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification) { +#if !TARGET_OS_OSX // [macOS] NSDictionary *userInfo = notification.userInfo; CGRect beginFrame = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; @@ -124,6 +129,9 @@ -(void)EVENT : (NSNotification *)notification @"easing" : RCTAnimationNameForCurve(curve), @"isEventFromThisApp" : isLocalUserInfoKey == 1 ? @YES : @NO, }; +#else // [macOS + return @{}; +#endif // macOS] } Class RCTKeyboardObserverCls(void) diff --git a/packages/react-native/React/CoreModules/RCTLogBox.h b/packages/react-native/React/CoreModules/RCTLogBox.h index 17f7f4d98ef861..a02cc5309899cb 100644 --- a/packages/react-native/React/CoreModules/RCTLogBox.h +++ b/packages/react-native/React/CoreModules/RCTLogBox.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTLogBoxView.h" @interface RCTLogBox : NSObject diff --git a/packages/react-native/React/CoreModules/RCTLogBox.mm b/packages/react-native/React/CoreModules/RCTLogBox.mm index 041e46e26c9d8a..8473fa5f0b21b3 100644 --- a/packages/react-native/React/CoreModules/RCTLogBox.mm +++ b/packages/react-native/React/CoreModules/RCTLogBox.mm @@ -55,15 +55,27 @@ - (void)setSurfacePresenter:(id)surfacePresenter } if (strongSelf->_bridgelessSurfacePresenter) { +#if !TARGET_OS_OSX // [macOS] strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow() surfacePresenter:strongSelf->_bridgelessSurfacePresenter]; +#else // [macOS + strongSelf->_view = [[RCTLogBoxView alloc] initWithSurfacePresenter:strongSelf->_bridgelessSurfacePresenter]; +#endif // macOS] [strongSelf->_view show]; } else if (strongSelf->_bridge && strongSelf->_bridge.valid) { if (strongSelf->_bridge.surfacePresenter) { +#if !TARGET_OS_OSX // [macOS] strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow() surfacePresenter:strongSelf->_bridge.surfacePresenter]; +#else // [macOS + strongSelf->_view = [[RCTLogBoxView alloc] initWithSurfacePresenter:strongSelf->_bridge.surfacePresenter]; +#endif // macOS] } else { +#if !TARGET_OS_OSX // [macOS] strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow() bridge:strongSelf->_bridge]; +#else // [macOS + strongSelf->_view = [[RCTLogBoxView alloc] initWithBridge:self->_bridge]; +#endif // macOS] } [strongSelf->_view show]; } diff --git a/packages/react-native/React/CoreModules/RCTLogBoxView.h b/packages/react-native/React/CoreModules/RCTLogBoxView.h index 5c790e44665133..3ec4285cff40c5 100644 --- a/packages/react-native/React/CoreModules/RCTLogBoxView.h +++ b/packages/react-native/React/CoreModules/RCTLogBoxView.h @@ -8,7 +8,9 @@ #import #import #import -#import +#import // [macOS] + +#if !TARGET_OS_OSX // [macOS] @interface RCTLogBoxView : UIWindow @@ -22,3 +24,18 @@ - (void)show; @end + +#else // [macOS + +@interface RCTLogBoxView : NSWindow + +- (instancetype)initWithSurfacePresenter:(id)surfacePresenter; +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +- (void)setHidden:(BOOL)hidden; + +- (void)show; + +@end + +#endif // macOS] diff --git a/packages/react-native/React/CoreModules/RCTLogBoxView.mm b/packages/react-native/React/CoreModules/RCTLogBoxView.mm index b2de00b58515bd..f280f44673899e 100644 --- a/packages/react-native/React/CoreModules/RCTLogBoxView.mm +++ b/packages/react-native/React/CoreModules/RCTLogBoxView.mm @@ -11,6 +11,8 @@ #import #import +#if !TARGET_OS_OSX // [macOS] + @implementation RCTLogBoxView { RCTSurface *_surface; } @@ -85,3 +87,89 @@ - (void)show } @end + +#else // [macOS + +@implementation RCTLogBoxView { + RCTSurface *_surface; +} + +- (instancetype)initWithSurfacePresenter:(id)surfacePresenter +{ + NSRect bounds = NSMakeRect(0, 0, 600, 800); + if ((self = [self initWithContentRect:bounds + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:YES])) { + id surface = [surfacePresenter createFabricSurfaceForModuleName:@"LogBox" + initialProperties:@{}]; + [surface start]; + RCTSurfaceHostingView *rootView = [[RCTSurfaceHostingView alloc] + initWithSurface:surface + sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact]; + + self.contentView = rootView; + self.contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + } + return self; +} + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + NSRect bounds = NSMakeRect(0, 0, 600, 800); + if ((self = [self initWithContentRect:bounds + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:YES])) { + _surface = [[RCTSurface alloc] initWithBridge:bridge moduleName:@"LogBox" initialProperties:@{}]; + + [_surface start]; + [_surface setSize:bounds.size]; + + if (![_surface synchronouslyWaitForStage:RCTSurfaceStageSurfaceDidInitialMounting timeout:1]) { + RCTLogInfo(@"Failed to mount LogBox within 1s"); + } + + self.contentView = (NSView *)_surface.view; + self.contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + } + return self; +} + +- (void)setHidden:(BOOL)hidden // [macOS +{ + if (hidden) { + if (NSApp.modalWindow == self) { + [NSApp stopModal]; + } + [self orderOut:nil]; + } +} // macOS] + +- (void)show +{ + if (!RCTRunningInTestEnvironment()) { + // Run the modal loop outside of the dispatch queue because it is not reentrant. + [self performSelectorOnMainThread:@selector(_showModal) withObject:nil waitUntilDone:NO]; + } + else { + [NSApp activateIgnoringOtherApps:YES]; + [self makeKeyAndOrderFront:nil]; + } +} + +- (void)_showModal +{ + NSModalSession session = [NSApp beginModalSessionForWindow:self]; + + while ([NSApp runModalSession:session] == NSModalResponseContinue) { + // Spin the runloop so that the main dispatch queue is processed. + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; + } + + [NSApp endModalSession:session]; +} + +@end + +#endif // macOS] diff --git a/packages/react-native/React/CoreModules/RCTPerfMonitor.mm b/packages/react-native/React/CoreModules/RCTPerfMonitor.mm index a089054feb6fb0..f728963f1a905a 100644 --- a/packages/react-native/React/CoreModules/RCTPerfMonitor.mm +++ b/packages/react-native/React/CoreModules/RCTPerfMonitor.mm @@ -148,7 +148,7 @@ - (RCTDevMenuItem *)devMenuItem { if (!_devMenuItem) { __weak __typeof__(self) weakSelf = self; - __weak RCTDevSettings *devSettings = [self->_moduleRegistry moduleForName:"DevSettings"]; + __weak RCTDevSettings *devSettings = [[self bridge] devSettings]; // [macOS] if (devSettings.isPerfMonitorShown) { [weakSelf show]; } diff --git a/packages/react-native/React/CoreModules/RCTPlatform.mm b/packages/react-native/React/CoreModules/RCTPlatform.mm index d6ceed27dfa320..a11fd37202ca3b 100644 --- a/packages/react-native/React/CoreModules/RCTPlatform.mm +++ b/packages/react-native/React/CoreModules/RCTPlatform.mm @@ -7,7 +7,7 @@ #import "RCTPlatform.h" -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/CoreModules/RCTRedBox.h b/packages/react-native/React/CoreModules/RCTRedBox.h index e4eed8732c40e9..06fbdf42fa8246 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.h +++ b/packages/react-native/React/CoreModules/RCTRedBox.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index dc58594a80702c..33113b5b8a09ca 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -14,7 +14,9 @@ #import #import #import +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] #import #import #import @@ -27,6 +29,7 @@ @class RCTRedBoxWindow; +#if !TARGET_OS_OSX // [macOS] @interface UIButton (RCTRedBox) @property (nonatomic) RCTRedBoxButtonPressHandler rct_handler; @@ -61,6 +64,7 @@ - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UICo } @end +#endif // [macOS] @protocol RCTRedBoxWindowActionDelegate @@ -70,6 +74,7 @@ - (void)loadExtraDataViewController; @end +#if !TARGET_OS_OSX // [macOS] @interface RCTRedBoxWindow : NSObject @property (nonatomic, strong) UIViewController *rootViewController; @property (nonatomic, weak) id actionDelegate; @@ -435,18 +440,369 @@ - (BOOL)canBecomeFirstResponder } @end +#else // [macOS + +@interface RCTRedBoxScrollView : NSScrollView +@end + +@implementation RCTRedBoxScrollView + +- (NSSize)intrinsicContentSize +{ + NSView *documentView = self.documentView; + return documentView != nil ? documentView.intrinsicContentSize : super.intrinsicContentSize; +} + +@end + +@interface RCTRedBoxWindow : NSObject + +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate; +- (void)dismiss; + +@property (nonatomic, weak) id actionDelegate; + +@end + +@implementation RCTRedBoxWindow +{ + NSWindow *_window; + NSTableView *_stackTraceTableView; + NSString *_lastErrorMessage; + NSArray *_lastStackTrace; + BOOL _visible; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _window = [[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:YES]; + _window.backgroundColor = [NSColor colorWithRed:0.8 green:0 blue:0 alpha:1]; + _window.animationBehavior = NSWindowAnimationBehaviorDocumentWindow; + + NSScrollView *scrollView = [[RCTRedBoxScrollView alloc] initWithFrame:NSZeroRect]; + scrollView.translatesAutoresizingMaskIntoConstraints = NO; + scrollView.backgroundColor = [NSColor clearColor]; + scrollView.drawsBackground = NO; + scrollView.hasVerticalScroller = YES; + + NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"info"]; + tableColumn.editable = false; + tableColumn.resizingMask = NSTableColumnAutoresizingMask; + + _stackTraceTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; + _stackTraceTableView.dataSource = self; + _stackTraceTableView.delegate = self; + _stackTraceTableView.headerView = nil; + _stackTraceTableView.allowsColumnReordering = NO; + _stackTraceTableView.allowsColumnResizing = NO; + _stackTraceTableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle; + _stackTraceTableView.backgroundColor = [NSColor clearColor]; + _stackTraceTableView.allowsTypeSelect = NO; + [_stackTraceTableView addTableColumn:tableColumn]; + scrollView.documentView = _stackTraceTableView; + + NSButton *dismissButton = [[NSButton alloc] initWithFrame:NSZeroRect]; + dismissButton.accessibilityIdentifier = @"redbox-dismiss"; + dismissButton.translatesAutoresizingMaskIntoConstraints = NO; + dismissButton.target = self; + dismissButton.action = @selector(dismiss:); + [dismissButton setButtonType:NSButtonTypeMomentaryPushIn]; + dismissButton.bezelStyle = NSBezelStyleRounded; + dismissButton.title = @"Dismiss (Esc)"; + dismissButton.keyEquivalent = @"\e"; + [dismissButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + + NSButton *reloadButton = [[NSButton alloc] initWithFrame:NSZeroRect]; + reloadButton.accessibilityIdentifier = @"redbox-reload"; + reloadButton.translatesAutoresizingMaskIntoConstraints = NO; + reloadButton.target = self; + reloadButton.action = @selector(reload:); + reloadButton.bezelStyle = NSBezelStyleRounded; + reloadButton.title = @"Reload JS (\u2318R)"; + [reloadButton setButtonType:NSButtonTypeMomentaryPushIn]; + reloadButton.keyEquivalent = @"r"; + reloadButton.keyEquivalentModifierMask = NSEventModifierFlagCommand; + [reloadButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [reloadButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + + NSButton *copyButton = [[NSButton alloc] initWithFrame:NSZeroRect]; + copyButton.accessibilityIdentifier = @"redbox-copy"; + copyButton.translatesAutoresizingMaskIntoConstraints = NO; + copyButton.target = self; + copyButton.action = @selector(copyStack:); + copyButton.title = @"Copy (\u2325\u2318C)"; + copyButton.bezelStyle = NSBezelStyleRounded; + [copyButton setButtonType:NSButtonTypeMomentaryPushIn]; + copyButton.keyEquivalent = @"c"; + copyButton.keyEquivalentModifierMask = NSEventModifierFlagOption | NSEventModifierFlagCommand; + [copyButton setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + + NSView *contentView = _window.contentView; + [contentView addSubview:scrollView]; + [contentView addSubview:dismissButton]; + [contentView addSubview:reloadButton]; + [contentView addSubview:copyButton]; + + [NSLayoutConstraint activateConstraints:@[ + // the window shouldn't be any bigger than 375x643 points + [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:375], + [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationLessThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:643], + // scroll view hugs the left, top, and right sides of the window, and the buttons at the bottom + [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeLeading multiplier:1 constant:16], + [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeTop multiplier:1 constant:16], + [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeTrailing multiplier:1 constant:-16], + [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeTop multiplier:1 constant:-8], + // buttons have equal widths + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeWidth multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:copyButton attribute:NSLayoutAttributeWidth multiplier:1 constant:0], + // buttons are centered horizontally in the window + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:contentView attribute:NSLayoutAttributeLeading multiplier:1 constant:16], + [NSLayoutConstraint constraintWithItem:copyButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationLessThanOrEqual toItem:contentView attribute:NSLayoutAttributeTrailing multiplier:1 constant:-16], + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeLeading multiplier:1 constant:-8], + [NSLayoutConstraint constraintWithItem:reloadButton attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:copyButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeTrailing multiplier:1 constant:8], + // buttons are baseline aligned + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:reloadButton attribute:NSLayoutAttributeBaseline multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:dismissButton attribute:NSLayoutAttributeBaseline relatedBy:NSLayoutRelationEqual toItem:copyButton attribute:NSLayoutAttributeBaseline multiplier:1 constant:0], + // buttons appear at the bottom of the window + [NSLayoutConstraint constraintWithItem:reloadButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:contentView attribute:NSLayoutAttributeBottom multiplier:1 constant:-16], + ]]; + } + return self; +} + +- (void)dealloc +{ + // VSO#1878643: On macOS the RedBox can be dealloc'd on the JS thread causing the Main Thread Checker to throw when the NSTableView properties below are accessed. + NSTableView *stackTraceTableView = _stackTraceTableView; + RCTUnsafeExecuteOnMainQueueSync(^{ + stackTraceTableView.dataSource = nil; + stackTraceTableView.delegate = nil; + }); +} + +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate errorCookie:(int)errorCookie +{ + // Show if this is a new message, or if we're updating the previous message + if ((!_visible && !isUpdate) || (_visible && isUpdate && [_lastErrorMessage isEqualToString:message])) { + _lastStackTrace = stack; + + // message is displayed using UILabel, which is unable to render text of + // unlimited length, so we truncate it + _lastErrorMessage = [message substringToIndex:MIN((NSUInteger)10000, message.length)]; + + [_window layoutIfNeeded]; // layout the window for the correct width + [_stackTraceTableView reloadData]; // load the new data + [_stackTraceTableView.enclosingScrollView invalidateIntrinsicContentSize]; // the height of the scroll view changed with the new data + [_window layoutIfNeeded]; // layout the window for the correct height + + if (!_visible) { + _visible = YES; + [_window center]; + if (!RCTRunningInTestEnvironment()) { + // Run the modal loop outside of the dispatch queue because it is not reentrant. + [self performSelectorOnMainThread:@selector(showModal) withObject:nil waitUntilDone:NO]; + } + else { + [NSApp activateIgnoringOtherApps:YES]; + [_window makeKeyAndOrderFront:nil]; + } + } + } +} + +- (void)showModal +{ + NSModalSession session = [NSApp beginModalSessionForWindow:_window]; + + while ([NSApp runModalSession:session] == NSModalResponseContinue) { + // Spin the runloop so that the main dispatch queue is processed. + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; + } + + [NSApp endModalSession:session]; +} + +- (void)dismiss +{ + if (_visible) { + [NSApp stopModal]; + [_window orderOut:self]; + _visible = NO; + } +} + +- (IBAction)dismiss:(__unused NSButton *)sender +{ + [self dismiss]; +} + +- (IBAction)reload:(__unused NSButton *)sender +{ + [_actionDelegate reloadFromRedBoxWindow:self]; +} + +- (IBAction)copyStack:(__unused NSButton *)sender +{ + // TODO: This is copy/paste from the iOS implementation + NSMutableString *fullStackTrace; + + if (_lastErrorMessage != nil) { + fullStackTrace = [_lastErrorMessage mutableCopy]; + [fullStackTrace appendString:@"\n\n"]; + } + else { + fullStackTrace = [NSMutableString string]; + } + + for (RCTJSStackFrame *stackFrame in _lastStackTrace) { + [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]]; + if (stackFrame.file) { + [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; + } + } + + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setString:fullStackTrace forType:NSPasteboardTypeString]; +} + +- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame +{ + // TODO: This is copy/paste from the iOS implementation + NSString *lineInfo = [NSString stringWithFormat:@"%@:%zd", + [stackFrame.file lastPathComponent], + stackFrame.lineNumber]; + + if (stackFrame.column != 0) { + lineInfo = [lineInfo stringByAppendingFormat:@":%zd", stackFrame.column]; + } + return lineInfo; +} + +#pragma mark - TableView + +- (NSInteger)numberOfRowsInTableView:(__unused NSTableView *)tableView +{ + return (_lastErrorMessage != nil) + _lastStackTrace.count; +} + +- (BOOL)tableView:(__unused NSTableView *)tableView shouldSelectRow:(__unused NSInteger)row +{ + return NO; +} + +- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSTableCellView *view = [tableView makeViewWithIdentifier:tableColumn.identifier owner:nil]; + + if (view == nil) { + view = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; + view.identifier = tableColumn.identifier; + + NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; + label.translatesAutoresizingMaskIntoConstraints = NO; + label.backgroundColor = [NSColor clearColor]; + label.drawsBackground = NO; + label.bezeled = NO; + label.editable = NO; + [label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + + [view addSubview:label]; + view.textField = label; + + [NSLayoutConstraint activateConstraints:@[ + [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeLeading multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTop multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0], + [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeBottom multiplier:1 constant:0], + ]]; + } + + view.textField.attributedStringValue = [self attributedStringForRow:row]; + + return view; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row +{ + NSAttributedString *attributedString = [self attributedStringForRow:row]; + NSRect boundingRect = [attributedString boundingRectWithSize:NSMakeSize(tableView.frame.size.width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin]; + CGFloat height = ceilf(NSMaxY(boundingRect)); + + if (row == 0) { + height += 32; + } + + return height; +} + +- (NSAttributedString *)attributedStringForRow:(NSUInteger)row +{ + if (_lastErrorMessage != nil) { + if (row == 0) { + NSDictionary *attributes = @{ + NSForegroundColorAttributeName : [NSColor whiteColor], + NSFontAttributeName : [NSFont systemFontOfSize:16], + }; + return [[NSAttributedString alloc] initWithString:_lastErrorMessage attributes:attributes]; + } + --row; + } + + RCTJSStackFrame *stackFrame = _lastStackTrace[row]; + + NSMutableParagraphStyle *titleParagraphStyle = [NSMutableParagraphStyle new]; + titleParagraphStyle.lineBreakMode = NSLineBreakByCharWrapping; + + NSDictionary *titleAttributes = @{ + NSForegroundColorAttributeName : [NSColor colorWithWhite:1 alpha:0.9], + NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:14], + NSParagraphStyleAttributeName : titleParagraphStyle, + }; + + NSString *rawTitle = stackFrame.methodName ?: @"(unnamed method)"; + NSAttributedString *title = [[NSAttributedString alloc] initWithString:rawTitle attributes:titleAttributes]; + if (stackFrame.file == nil) { + return title; + } + + NSMutableParagraphStyle *frameParagraphStyle = [NSMutableParagraphStyle new]; + frameParagraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle; + + NSDictionary *frameAttributes = @{ + NSForegroundColorAttributeName : [NSColor colorWithWhite:1 alpha:0.7], + NSFontAttributeName : [NSFont fontWithName:@"Menlo-Regular" size:11], + NSParagraphStyleAttributeName : frameParagraphStyle, + }; + + NSMutableAttributedString *frameSource = [[NSMutableAttributedString alloc] initWithString:[self formatFrameSource:stackFrame] attributes:frameAttributes]; + [frameSource replaceCharactersInRange:NSMakeRange(0, 0) withString:@"\n"]; + [frameSource insertAttributedString:title atIndex:0]; + return frameSource; +} + +@end + +#endif // macOS] @interface RCTRedBox () < RCTInvalidating, RCTRedBoxWindowActionDelegate, +#if !TARGET_OS_OSX // [macOS] RCTRedBoxExtraDataActionDelegate, +#endif // [macOS] NativeRedBoxSpec> @end @implementation RCTRedBox { RCTRedBoxWindow *_window; NSMutableArray> *_errorCustomizers; +#if !TARGET_OS_OSX // [macOS] RCTRedBoxExtraDataViewController *_extraDataViewController; +#endif // [macOS] NSMutableArray *_customButtonTitles; NSMutableArray *_customButtonHandlers; } @@ -582,10 +938,12 @@ - (void)showErrorMessage:(NSString *)message errorCookie:(int)errorCookie { dispatch_async(dispatch_get_main_queue(), ^{ +#if !TARGET_OS_OSX // [macOS] if (self->_extraDataViewController == nil) { self->_extraDataViewController = [RCTRedBoxExtraDataViewController new]; self->_extraDataViewController.actionDelegate = self; } +#endif // [macOS] #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -594,9 +952,13 @@ - (void)showErrorMessage:(NSString *)message #pragma clang diagnostic pop if (!self->_window) { +#if !TARGET_OS_OSX // [macOS] self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds - customButtonTitles:self->_customButtonTitles - customButtonHandlers:self->_customButtonHandlers]; + customButtonTitles:self->_customButtonTitles + customButtonHandlers:self->_customButtonHandlers]; +#else // [macOS + self->_window = [RCTRedBoxWindow new]; +#endif // macOS] self->_window.actionDelegate = self; } @@ -609,8 +971,8 @@ - (void)showErrorMessage:(NSString *)message }); } -- (void)loadExtraDataViewController -{ +- (void)loadExtraDataViewController { +#if !TARGET_OS_OSX // [macOS] dispatch_async(dispatch_get_main_queue(), ^{ // Make sure the CMD+E shortcut doesn't call this twice if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) { @@ -619,18 +981,25 @@ - (void)loadExtraDataViewController completion:nil]; } }); +#endif // [macOS] } -RCT_EXPORT_METHOD(setExtraData : (NSDictionary *)extraData forIdentifier : (NSString *)identifier) -{ - [_extraDataViewController addExtraData:extraData forIdentifier:identifier]; +RCT_EXPORT_METHOD(setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier) { +#if !TARGET_OS_OSX // [macOS] + [_extraDataViewController addExtraData:extraData forIdentifier:identifier]; +#endif // [macOS] } RCT_EXPORT_METHOD(dismiss) { +#if TARGET_OS_OSX // [macOS + [self->_window performSelectorOnMainThread:@selector(dismiss) withObject:nil waitUntilDone:NO]; +#else // [macOS dispatch_async(dispatch_get_main_queue(), ^{ [self->_window dismiss]; + self->_window = nil; // [macOS] release _window now to ensure its UIKit ivars are dealloc'd on the main thread as the RCTRedBox can be dealloc'd on a background thread. }); +#endif // macOS] } - (void)invalidate diff --git a/packages/react-native/React/CoreModules/RCTStatusBarManager.h b/packages/react-native/React/CoreModules/RCTStatusBarManager.h index ff1e02e0d679c9..0f20f4da8a95e3 100644 --- a/packages/react-native/React/CoreModules/RCTStatusBarManager.h +++ b/packages/react-native/React/CoreModules/RCTStatusBarManager.h @@ -5,15 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @interface RCTConvert (UIStatusBar) +#if !TARGET_OS_OSX // [macOS] + (UIStatusBarStyle)UIStatusBarStyle:(id)json; + (UIStatusBarAnimation)UIStatusBarAnimation:(id)json; +#endif // [macOS] @end diff --git a/packages/react-native/React/CoreModules/RCTStatusBarManager.mm b/packages/react-native/React/CoreModules/RCTStatusBarManager.mm index 29309135309eac..8abd78b5d2be1d 100644 --- a/packages/react-native/React/CoreModules/RCTStatusBarManager.mm +++ b/packages/react-native/React/CoreModules/RCTStatusBarManager.mm @@ -12,6 +12,7 @@ #import #import +#if !TARGET_OS_OSX // [macOS] #import @implementation RCTConvert (UIStatusBar) @@ -47,6 +48,8 @@ + (UIStatusBarStyle)UIStatusBarStyle:(id)json RCT_DYNAMIC @interface RCTStatusBarManager () @end +#endif // [macOS] + @implementation RCTStatusBarManager static BOOL RCTViewControllerBasedStatusBarAppearance() @@ -74,6 +77,8 @@ + (BOOL)requiresMainQueueSetup return @[ @"statusBarFrameDidChange", @"statusBarFrameWillChange" ]; } +#if !TARGET_OS_OSX // [macOS] + - (void)startObserving { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; @@ -186,6 +191,8 @@ - (void)applicationWillChangeStatusBarFrame:(NSNotification *)notification return std::make_shared(params); } +#endif // [macOS] + @end Class RCTStatusBarManagerCls(void) diff --git a/packages/react-native/React/CoreModules/RCTTiming.mm b/packages/react-native/React/CoreModules/RCTTiming.mm index 5158741724194b..3f8bba25c2e9c5 100644 --- a/packages/react-native/React/CoreModules/RCTTiming.mm +++ b/packages/react-native/React/CoreModules/RCTTiming.mm @@ -129,11 +129,16 @@ - (void)setup _timers = [NSMutableDictionary new]; _inBackground = NO; RCTExecuteOnMainQueue(^{ +#if !TARGET_OS_OSX // [macOS] if (!self->_inBackground && [RCTSharedApplication() applicationState] == UIApplicationStateBackground) { +#else // [macOS + if (!self->_inBackground && ![RCTSharedApplication() isHidden]) { +#endif [self appDidMoveToBackground]; } }); +#if !TARGET_OS_OSX // [macOS] for (NSString *name in @[ UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification, @@ -151,6 +156,7 @@ - (void)setup name:name object:nil]; } +#endif // [macOS] } - (void)dealloc diff --git a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm index 3fd7238911f559..5096d4268b8d84 100644 --- a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm +++ b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm @@ -68,37 +68,40 @@ - (void)invalidate : (JS::NativeWebSocketModule::SpecConnectOptions &)options socketID : (double)socketID) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; - - // We load cookies from sharedHTTPCookieStorage (shared with XHR and - // fetch). To get secure cookies for wss URLs, replace wss with https - // in the URL. - NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:true]; - if ([components.scheme.lowercaseString isEqualToString:@"wss"]) { - components.scheme = @"https"; - } + RCTAssertParam(URL); // [macOS] prevent crashes when URL is erroneously nil + if (URL != nil) { // [macOS] prevent crashes when URL is erroneously nil + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; + + // We load cookies from sharedHTTPCookieStorage (shared with XHR and + // fetch). To get secure cookies for wss URLs, replace wss with https + // in the URL. + NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:true]; + if ([components.scheme.lowercaseString isEqualToString:@"wss"]) { + components.scheme = @"https"; + } - // Load and set the cookie header. - NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL]; - request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + // Load and set the cookie header. + NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL]; + request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; - // Load supplied headers - if ([options.headers() isKindOfClass:NSDictionary.class]) { - NSDictionary *headers = (NSDictionary *)options.headers(); - [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - [request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key]; - }]; - } + // Load supplied headers + if ([options.headers() isKindOfClass:NSDictionary.class]) { + NSDictionary *headers = (NSDictionary *)options.headers(); + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + [request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key]; + }]; + } - SRWebSocket *webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols]; - [webSocket setDelegateDispatchQueue:[self methodQueue]]; - webSocket.delegate = self; - webSocket.reactTag = @(socketID); - if (!_sockets) { - _sockets = [NSMutableDictionary new]; - } - _sockets[@(socketID)] = webSocket; - [webSocket open]; + SRWebSocket *webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols]; + [webSocket setDelegateDispatchQueue:[self methodQueue]]; + webSocket.delegate = self; + webSocket.reactTag = @(socketID); + if (!_sockets) { + _sockets = [NSMutableDictionary new]; + } + _sockets[@(socketID)] = webSocket; + [webSocket open]; + } // [macOS] prevent crashes when URL is erroneously nil } RCT_EXPORT_METHOD(send : (NSString *)message forSocketID : (double)socketID) diff --git a/packages/react-native/React/CxxBridge/RCTCxxBridge.mm b/packages/react-native/React/CxxBridge/RCTCxxBridge.mm index af931fd45ca10f..56501ddb0d10bb 100644 --- a/packages/react-native/React/CxxBridge/RCTCxxBridge.mm +++ b/packages/react-native/React/CxxBridge/RCTCxxBridge.mm @@ -31,6 +31,7 @@ #import #import #import +#import // [macOS] #import #import #import @@ -255,6 +256,13 @@ - (RCTBridgeModuleDecorator *)bridgeModuleDecorator return _jsMessageThread; } +// [macOS +- (std::weak_ptr)reactInstance +{ + return _reactInstance; +} +// macOS] + - (BOOL)isInspectable { return _reactInstance ? _reactInstance->isInspectable() : NO; @@ -295,10 +303,12 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge [RCTBridge setCurrentBridge:self]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif // [macOS] RCTLogSetBridgeModuleRegistry(_objCModuleRegistry); RCTLogSetBridgeCallableJSModules(_callableJSModules); @@ -308,7 +318,9 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge - (void)dealloc { +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] removeObserver:self]; +#endif // [macOS] } + (void)runRunLoop @@ -692,7 +704,7 @@ - (void)_initializeBridgeLocked:(std::shared_ptr)executorFact { std::lock_guard guard(_moduleRegistryLock); - // This is async, but any calls into JS are blocked by the m_syncReady CV in Instance + // This is async, but any calls into JS are blocked by the m_syncReady CV in Instance _reactInstance->initializeBridge( std::make_unique(self), executorFactory, diff --git a/packages/react-native/React/DevSupport/RCTDevLoadingViewProtocol.h b/packages/react-native/React/DevSupport/RCTDevLoadingViewProtocol.h index ec9ed3a36c8187..976b7ba421f20e 100644 --- a/packages/react-native/React/DevSupport/RCTDevLoadingViewProtocol.h +++ b/packages/react-native/React/DevSupport/RCTDevLoadingViewProtocol.h @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @class RCTLoadingProgress; @protocol RCTDevLoadingViewProtocol + (void)setEnabled:(BOOL)enabled; -- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor; +- (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColor:(RCTUIColor *)backgroundColor; // [macOS] - (void)showWithURL:(NSURL *)URL; - (void)updateProgress:(RCTLoadingProgress *)progress; - (void)hide; diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.h b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.h index 5be6db1c28412e..b0e1fee81c21c9 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.h +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.h @@ -6,8 +6,7 @@ */ #import -#import - +#import // [macOS] #import #import diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index 819e14a39fd71b..680caad02c84e4 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -10,7 +10,7 @@ #if RCT_DEV || RCT_REMOTE_PROFILE #import -#import +#import // [macOS] #import #import @@ -42,8 +42,12 @@ static NSURL *getInspectorDeviceUrl(NSURL *bundleURL) { +#if !TARGET_OS_OSX // [macOS] NSString *escapedDeviceName = [[[UIDevice currentDevice] name] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; +#else // [macOS + NSString *escapedDeviceName = @""; +#endif // macOS] NSString *escapedAppName = [[[NSBundle mainBundle] bundleIdentifier] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@", @@ -102,9 +106,21 @@ + (RCTInspectorPackagerConnection *)connectWithBundleURL:(NSURL *)bundleURL } NSString *key = [inspectorURL absoluteString]; + // [macOS safety check to avoid a crash + if (key == nil) { + RCTLogError(@"failed to get inspector URL"); + return nil; + } + // macOS] RCTInspectorPackagerConnection *connection = socketConnections[key]; if (!connection || !connection.isConnected) { connection = [[RCTInspectorPackagerConnection alloc] initWithURL:inspectorURL]; + // [macOS safety check to avoid a crash + if (connection == nil) { + RCTLogError(@"failed to initialize RCTInspectorPackagerConnection"); + return nil; + } + // macOS] socketConnections[key] = connection; [connection connect]; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.h index 0551b81eb70960..ee6444ffc6f1ea 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm index 09b6b70700a27a..2167fc151b2f12 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm @@ -8,6 +8,7 @@ #import "RCTActivityIndicatorViewComponentView.h" #import +#import // [macOS] #import #import @@ -28,7 +29,7 @@ static UIActivityIndicatorViewStyle convertActivityIndicatorViewStyle(const Acti } @implementation RCTActivityIndicatorViewComponentView { - UIActivityIndicatorView *_activityIndicatorView; + RCTUIActivityIndicatorView *_activityIndicatorView; // [macOS] } #pragma mark - RCTComponentViewProtocol @@ -44,7 +45,7 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; - _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds]; + _activityIndicatorView = [[RCTUIActivityIndicatorView alloc] initWithFrame:self.bounds]; // [macOS] _activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; if (defaultProps->animating) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index e48d5470179e4f..912f2974880481 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -143,6 +143,7 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(con const auto &imageProps = static_cast(*_props); +#if !TARGET_OS_OSX // [macOS] if (imageProps.tintColor) { image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } @@ -155,6 +156,12 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(con image = [image resizableImageWithCapInsets:RCTUIEdgeInsetsFromEdgeInsets(imageProps.capInsets) resizingMode:UIImageResizingModeStretch]; } +#else + if (imageProps.resizeMode == ImageResizeMode::Repeat) { + image.capInsets = RCTUIEdgeInsetsFromEdgeInsets(imageProps.capInsets); + image.resizingMode = NSImageResizingModeTile; + } +#endif // [macOS] if (imageProps.blurRadius > __FLT_EPSILON__) { // Blur on a background thread to avoid blocking interaction. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h index c2c80a662203fb..37201c3502d19f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm index 454c3ff1ac279d..e9dabd8f5bc4fa 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm @@ -20,18 +20,18 @@ using namespace facebook::react; -static UIView *_Nullable RCTFindTextInputWithNativeId(UIView *view, NSString *nativeId) +static RCTUIView *_Nullable RCTFindTextInputWithNativeId(RCTUIView *view, NSString *nativeId) // [macOS] { if ([view respondsToSelector:@selector(inputAccessoryViewID)] && [view respondsToSelector:@selector(setInputAccessoryView:)]) { - UIView *typed = (UIView *)view; + RCTUIView *typed = (RCTUIView *)view; // [macOS] if (!nativeId || [typed.inputAccessoryViewID isEqualToString:nativeId]) { return typed; } } - for (UIView *subview in view.subviews) { - UIView *result = RCTFindTextInputWithNativeId(subview, nativeId); + for (RCTUIView *subview in view.subviews) { // [macOS] + RCTUIView *result = RCTFindTextInputWithNativeId(subview, nativeId); // [macOS] if (result) { return result; } @@ -44,7 +44,7 @@ @implementation RCTInputAccessoryComponentView { InputAccessoryShadowNode::ConcreteState::Shared _state; RCTInputAccessoryContentView *_contentView; RCTSurfaceTouchHandler *_touchHandler; - UIView __weak *_textInput; + RCTUIView __weak *_textInput; // [macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -66,8 +66,12 @@ - (void)didMoveToWindow if (self.window && !_textInput) { if (self.nativeId) { +#if !TARGET_OS_OSX // [macOS] _textInput = RCTFindTextInputWithNativeId(self.window, self.nativeId); _textInput.inputAccessoryView = _contentView; +#else // [macOS + _textInput = RCTFindTextInputWithNativeId(self.window.contentView, self.nativeId); +#endif // macOS] } else { _textInput = RCTFindTextInputWithNativeId(_contentView, nil); } @@ -83,7 +87,7 @@ - (BOOL)canBecomeFirstResponder return true; } -- (UIView *)inputAccessoryView +- (RCTUIView *)inputAccessoryView // [macOS] { return _contentView; } @@ -95,12 +99,12 @@ + (ComponentDescriptorProvider)componentDescriptorProvider return concreteComponentDescriptorProvider(); } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [_contentView insertSubview:childComponentView atIndex:index]; } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [childComponentView removeFromSuperview]; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h index 0054ce06aeae7b..f26111542fbbc6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] -@interface RCTInputAccessoryContentView : UIView +@interface RCTInputAccessoryContentView : RCTUIView // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm index 09de0401910031..9d790fb4c495c7 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm @@ -8,7 +8,7 @@ #import "RCTInputAccessoryContentView.h" @implementation RCTInputAccessoryContentView { - UIView *_safeAreaContainer; + RCTUIView *_safeAreaContainer; // [macOS] NSLayoutConstraint *_heightConstraint; } @@ -17,7 +17,7 @@ - (instancetype)init if (self = [super init]) { self.autoresizingMask = UIViewAutoresizingFlexibleHeight; - _safeAreaContainer = [UIView new]; + _safeAreaContainer = [RCTUIView new]; // [macOS] _safeAreaContainer.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_safeAreaContainer]; @@ -40,7 +40,7 @@ - (CGSize)intrinsicContentSize return CGSizeZero; } -- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index +- (void)insertSubview:(RCTUIView *)view atIndex:(NSInteger)index // [macOS] { [_safeAreaContainer insertSubview:view atIndex:index]; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h index fae91e1b10d4b9..5585dde4dd166e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm index 42fa691dfebd55..8377b7b3728716 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm @@ -22,7 +22,7 @@ @implementation RCTLegacyViewManagerInteropComponentView { NSMutableArray *_viewsToBeMounted; - NSMutableArray *_viewsToBeUnmounted; + NSMutableArray *_viewsToBeUnmounted; // [macOS] RCTLegacyViewManagerInteropCoordinatorAdapter *_adapter; LegacyViewManagerInteropShadowNode::ConcreteState::Shared _state; BOOL _hasInvokedForwardingWarning; @@ -41,9 +41,9 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] { - UIView *result = [super hitTest:point withEvent:event]; + RCTUIView *result = (RCTUIView *)[super hitTest:point withEvent:event]; // [macOS] if (result == _adapter.paperView) { return self; @@ -149,7 +149,7 @@ - (void)prepareForRecycle [super prepareForRecycle]; } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [_viewsToBeMounted addObject:@{ kRCTLegacyInteropChildIndexKey : [NSNumber numberWithInteger:index], @@ -157,7 +157,7 @@ - (void)mountChildComponentView:(UIView *)childCompone }]; } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { if (_adapter) { [_adapter.paperView removeReactSubview:childComponentView]; @@ -197,9 +197,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask for (NSDictionary *mountInstruction in _viewsToBeMounted) { NSNumber *index = mountInstruction[kRCTLegacyInteropChildIndexKey]; - UIView *childView = mountInstruction[kRCTLegacyInteropChildComponentKey]; + RCTUIView *childView = mountInstruction[kRCTLegacyInteropChildComponentKey]; // [macOS] if ([childView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) { - UIView *target = ((RCTLegacyViewManagerInteropComponentView *)childView).contentView; + RCTUIView *target = ((RCTLegacyViewManagerInteropComponentView *)childView).contentView; // [macOS] [_adapter.paperView insertReactSubview:target atIndex:index.integerValue]; } else { [_adapter.paperView insertReactSubview:childView atIndex:index.integerValue]; @@ -208,7 +208,7 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask [_viewsToBeMounted removeAllObjects]; - for (UIView *view in _viewsToBeUnmounted) { + for (RCTUIView *view in _viewsToBeUnmounted) { // [macOS] [_adapter.paperView removeReactSubview:view]; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.h index d62aaca3ff2dd7..2dbeb42ff45e4f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCoordinator:(RCTLegacyViewManagerInteropCoordinator *)coordinator reactTag:(NSInteger)tag; -@property (strong, nonatomic) UIView *paperView; +@property (strong, nonatomic) RCTPlatformView *paperView; // [macOS] @property (nonatomic, copy, nullable) void (^eventInterceptor)(std::string eventName, folly::dynamic event); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm index 0fea1bfc95afbc..f327e0deb75a24 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm @@ -29,7 +29,7 @@ - (void)dealloc [_coordinator removeObserveForTag:_tag]; } -- (UIView *)paperView +- (RCTPlatformView *)paperView // [macOS] { if (!_paperView) { _paperView = [_coordinator createPaperViewWithTag:_tag]; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h index 149a9788e2c1b2..c082d7eab7f8bb 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @protocol RCTFabricModalHostViewControllerDelegate - (void)boundsDidChange:(CGRect)newBounds; @@ -15,6 +15,8 @@ @property (nonatomic, weak) id delegate; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm index db5b92fb8f48e4..b7d2b3c5a53614 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm @@ -15,6 +15,7 @@ @implementation RCTFabricModalHostViewController { RCTSurfaceTouchHandler *_touchHandler; } +#if !TARGET_OS_OSX // [macOS] - (instancetype)init { if (!(self = [super init])) { @@ -76,5 +77,6 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations return _supportedInterfaceOrientations; } #endif // RCT_DEV +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h index 75a134f7d010b2..8dc36ee547f5b7 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h @@ -13,6 +13,7 @@ */ @interface RCTModalHostViewComponentView : RCTViewComponentView +#if !TARGET_OS_OSX // [macOS] /** * Subclasses may override this method and present the modal on different view controller. * Default implementation presents the modal on `[self reactViewController]`. @@ -28,5 +29,6 @@ - (void)dismissViewController:(UIViewController *)modalViewController animated:(BOOL)animated completion:(void (^)(void))completion; +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm index 445a5c79a5afaf..338fc7c82c94ea 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm @@ -21,6 +21,7 @@ using namespace facebook::react; +#if !TARGET_OS_OSX // [macOS] static UIInterfaceOrientationMask supportedOrientationsMask(ModalHostViewSupportedOrientationsMask mask) { UIInterfaceOrientationMask supportedOrientations = 0; @@ -93,6 +94,7 @@ static UIModalPresentationStyle presentationConfiguration(const ModalHostViewPro : ModalHostViewEventEmitter::OnOrientationChangeOrientation::Landscape; return {orientation}; } +#endif // [macOS] @interface RCTModalHostViewComponentView () @@ -104,9 +106,10 @@ @implementation RCTModalHostViewComponentView { BOOL _shouldAnimatePresentation; BOOL _shouldPresent; BOOL _isPresented; - UIView *_modalContentsSnapshot; + RCTUIView *_modalContentsSnapshot; // [macOS] } +#if !TARGET_OS_OSX // [macOS] - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { @@ -165,7 +168,7 @@ - (void)ensurePresentedOnlyIfNeeded _isPresented = NO; // To animate dismissal of view controller, snapshot of // view hierarchy needs to be added to the UIViewController. - UIView *snapshot = _modalContentsSnapshot; + RCTUIView *snapshot = _modalContentsSnapshot; // [macOS] [self.viewController.view addSubview:snapshot]; [self dismissViewController:self.viewController @@ -269,15 +272,16 @@ - (void)updateState:(const facebook::react::State::Shared &)state _state = std::static_pointer_cast(state); } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [self.viewController.view insertSubview:childComponentView atIndex:index]; } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [childComponentView removeFromSuperview]; } +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Root/RCTRootComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Root/RCTRootComponentView.h index cce48d6ffcb922..1bf3b1f3cd5da8 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Root/RCTRootComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Root/RCTRootComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h index 542b88cf14fae7..260d6e74fd77e2 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm index 4c9060f1a622fd..4f77afbc931c5e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm @@ -29,12 +29,23 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (UIEdgeInsets)_safeAreaInsets +{ + if (@available(iOS 11.0, *)) { + return self.safeAreaInsets; + } + + return UIEdgeInsetsZero; +} + +#if !TARGET_OS_OSX // [macOS] - (void)safeAreaInsetsDidChange { [super safeAreaInsetsDidChange]; [self _updateStateIfNecessary]; } +#endif // [macOS] - (void)_updateStateIfNecessary { @@ -42,15 +53,23 @@ - (void)_updateStateIfNecessary return; } - UIEdgeInsets insets = self.safeAreaInsets; + UIEdgeInsets insets = [self _safeAreaInsets]; + CGFloat scale = _layoutMetrics.pointScaleFactor; // [macOS] +#if !TARGET_OS_OSX // [macOS] insets.left = RCTRoundPixelValue(insets.left); insets.top = RCTRoundPixelValue(insets.top); insets.right = RCTRoundPixelValue(insets.right); insets.bottom = RCTRoundPixelValue(insets.bottom); +#else // [macOS + insets.left = RCTRoundPixelValue(insets.left, scale); + insets.top = RCTRoundPixelValue(insets.top, scale); + insets.right = RCTRoundPixelValue(insets.right, scale); + insets.bottom = RCTRoundPixelValue(insets.bottom, scale); +#endif // macOS] auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets); - auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold. - + auto threshold = 1.0 / scale + 0.01; // Size of a pixel plus some small threshold. [macOS] + _state->updateState( [=](const SafeAreaViewShadowNode::ConcreteState::Data &oldData) -> SafeAreaViewShadowNode::ConcreteState::SharedData { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTCustomPullToRefreshViewProtocol.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTCustomPullToRefreshViewProtocol.h index 2b97cd1a372560..da8b9a80dce1fa 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTCustomPullToRefreshViewProtocol.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTCustomPullToRefreshViewProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] /** * Denotes a view which implements custom pull to refresh functionality. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h index 6402bc6c86c155..4eeb05208b1d79 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN */ @protocol RCTEnhancedScrollViewOverridingDelegate -- (BOOL)touchesShouldCancelInContentView:(UIView *)view; +- (BOOL)touchesShouldCancelInContentView:(RCTUIView *)view; // [macOS] @end @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN * `UIScrollView` subclass which has some improvements and tweaks * which are not directly related to React Native. */ -@interface RCTEnhancedScrollView : UIScrollView +@interface RCTEnhancedScrollView : RCTUIScrollView // [macOS] /* * Returns a delegate splitter that can be used to create as many `UIScrollView` delegates as needed. @@ -40,7 +40,9 @@ NS_ASSUME_NONNULL_BEGIN * resilient to other code as possible: even if something else nil the delegate, other delegates that were subscribed * via the splitter will continue working. */ +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong, readonly) RCTGenericDelegateSplitter> *delegateSplitter; +#endif // [macOS] @property (nonatomic, weak) id overridingDelegate; @property (nonatomic, assign) BOOL pinchGestureEnabled; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm index 93af874f451c58..765f9871a0826b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -7,12 +7,24 @@ #import "RCTEnhancedScrollView.h" #import - -@interface RCTEnhancedScrollView () +#import +#import + +@interface RCTEnhancedScrollView () < +#if !TARGET_OS_OSX // [macOS] + UIScrollViewDelegate +#else // [macOS + RCTScrollableProtocol, RCTAutoInsetsProtocol +#endif // macOS] +> @end @implementation RCTEnhancedScrollView { +#if !TARGET_OS_OSX // [macOS] __weak id _publicDelegate; +#else// [macOS + __weak id _publicDelegate; +#endif // macOS] BOOL _isSetContentOffsetDisabled; } @@ -30,6 +42,7 @@ + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { +#if !TARGET_OS_OSX // [macOS] // We set the default behavior to "never" so that iOS // doesn't do weird things to UIScrollView insets automatically // and keeps it as an opt-in behavior. @@ -45,6 +58,7 @@ - (instancetype)initWithFrame:(CGRect)frame [weakSelf setPrivateDelegate:delegate]; }]; [_delegateSplitter addDelegate:self]; +#endif // [macOS] } return self; @@ -88,7 +102,8 @@ - (void)setContentOffset:(CGPoint)contentOffset RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); } -- (BOOL)touchesShouldCancelInContentView:(UIView *)view +#if !TARGET_OS_OSX // [macOS] +- (BOOL)touchesShouldCancelInContentView:(RCTUIView *)view // [macOS] { if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) { return [_overridingDelegate touchesShouldCancelInContentView:view]; @@ -128,11 +143,13 @@ - (void)setDelegate:(id)delegate } } +#endif // [macOS] + #pragma mark - UIScrollViewDelegate -- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView +- (void)scrollViewWillEndDragging:(RCTUIScrollView *)scrollView withVelocity:(CGPoint)velocity - targetContentOffset:(inout CGPoint *)targetContentOffset + targetContentOffset:(inout CGPoint *)targetContentOffset // [macOS] { if (self.snapToOffsets && self.snapToOffsets.count > 0) { // An alternative to enablePaging and snapToInterval which allows setting custom @@ -259,7 +276,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView #pragma mark - -- (BOOL)isHorizontal:(UIScrollView *)scrollView +- (BOOL)isHorizontal:(RCTUIScrollView *)scrollView // [macOS] { return scrollView.contentSize.width > self.frame.size.width; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h index 914a2494a57923..81cf1e5f051f6f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm index 231777cd48950a..6623b4b6bb498f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm @@ -24,7 +24,9 @@ @interface RCTPullToRefreshViewComponentView () (); _props = defaultProps; +#if !TARGET_OS_OSX // [macOS] _refreshControl = [UIRefreshControl new]; [_refreshControl addTarget:self action:@selector(handleUIControlEventValueChanged) forControlEvents:UIControlEventValueChanged]; +#endif // [macOS] } return self; @@ -61,11 +65,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & const auto &newConcreteProps = static_cast(*props); if (newConcreteProps.refreshing != oldConcreteProps.refreshing) { +#if !TARGET_OS_OSX // [macOS] if (newConcreteProps.refreshing) { [_refreshControl beginRefreshing]; } else { [_refreshControl endRefreshing]; } +#endif // [macOS] } BOOL needsUpdateTitle = NO; @@ -97,7 +103,9 @@ - (void)_updateTitle const auto &concreteProps = static_cast(*_props); if (concreteProps.title.empty()) { +#if !TARGET_OS_OSX // [macOS] _refreshControl.attributedTitle = nil; +#endif // [macOS] return; } @@ -106,8 +114,10 @@ - (void)_updateTitle attributes[NSForegroundColorAttributeName] = RCTUIColorFromSharedColor(concreteProps.titleColor); } +#if !TARGET_OS_OSX // [macOS] _refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:RCTNSStringFromString(concreteProps.title) attributes:attributes]; +#endif // [macOS] } #pragma mark - Attaching & Detaching @@ -132,9 +142,11 @@ - (void)_attach return; } +#if !TARGET_OS_OSX // [macOS] if (@available(macCatalyst 13.1, *)) { _scrollViewComponentView.scrollView.refreshControl = _refreshControl; } +#endif // [macOS] } - (void)_detach @@ -144,11 +156,13 @@ - (void)_detach } // iOS requires to end refreshing before unmounting. +#if !TARGET_OS_OSX // [macOS] [_refreshControl endRefreshing]; if (@available(macCatalyst 13.1, *)) { _scrollViewComponentView.scrollView.refreshControl = nil; } +#endif // [macOS] _scrollViewComponentView = nil; } @@ -161,11 +175,13 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)setNativeRefreshing:(BOOL)refreshing { +#if !TARGET_OS_OSX // [macOS] if (refreshing) { [_refreshControl beginRefreshing]; } else { [_refreshControl endRefreshing]; } +#endif // [macOS] } #pragma mark - RCTRefreshableProtocol diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h index 8edb46868b19e8..a339ef9d65d49a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -28,25 +28,27 @@ NS_ASSUME_NONNULL_BEGIN /* * Finds and returns the closet RCTScrollViewComponentView component to the given view */ -+ (nullable RCTScrollViewComponentView *)findScrollViewComponentViewForView:(UIView *)view; ++ (nullable RCTScrollViewComponentView *)findScrollViewComponentViewForView:(RCTUIView *)view; // [macOS] /* * Returns an actual UIScrollView that this component uses under the hood. */ -@property (nonatomic, strong, readonly) UIScrollView *scrollView; +@property (nonatomic, strong, readonly) RCTUIScrollView *scrollView; // [macOS] /* * Returns the subview of the scroll view that the component uses to mount all subcomponents into. That's useful to * separate component views from auxiliary views to be able to reliably implement pull-to-refresh- and RTL-related * functionality. */ -@property (nonatomic, strong, readonly) UIView *containerView; +@property (nonatomic, strong, readonly) RCTUIView *containerView; // [macOS] /* * Returns a delegate splitter that can be used to subscribe for UIScrollView delegate. */ +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong, readonly) RCTGenericDelegateSplitter> *scrollViewDelegateSplitter; +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 504b0ef147ae0b..a5c85a84c48d6b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -28,6 +28,7 @@ static const CGFloat kClippingLeeway = 44.0; +#if !TARGET_OS_OSX // [macOS] static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(const ScrollViewProps &props) { switch (props.keyboardDismissMode) { @@ -56,7 +57,7 @@ static UIScrollViewIndicatorStyle RCTUIScrollViewIndicatorStyleFromProps(const S // This is just a workaround to allow animations based on onScroll event. // This is only used to animate sticky headers in ScrollViews, and only the contentOffset and tag is used. // TODO: T116850910 [Fabric][iOS] Make Fabric not use legacy RCTEventDispatcher for native-driven AnimatedEvents -static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInteger tag) +static void RCTSendScrollEventForNativeAnimations_DEPRECATED(RCTUIScrollView *scrollView, NSInteger tag) // [macOS] { static uint16_t coalescingKey = 0; RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:@"onScroll" @@ -78,9 +79,12 @@ static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrol userInfo:userInfo]; } } +#endif // [macOS] @interface RCTScrollViewComponentView () < +#if !TARGET_OS_OSX // [macOS] UIScrollViewDelegate, +#endif // [macOS] RCTScrollViewProtocol, RCTScrollableProtocol, RCTEnhancedScrollViewOverridingDelegate> @@ -101,16 +105,16 @@ @implementation RCTScrollViewComponentView { CGPoint _contentOffsetWhenClipped; - __weak UIView *_contentView; + __weak RCTUIView *_contentView; // [macOS] CGRect _prevFirstVisibleFrame; - __weak UIView *_firstVisibleView; + __weak RCTUIView *_firstVisibleView; // [macOS] } -+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view ++ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(RCTUIView *)view // [macOS] { do { - view = view.superview; + view = (RCTUIView *)view.superview; // [macOS] } while (view != nil && ![view isKindOfClass:[RCTScrollViewComponentView class]]); return (RCTScrollViewComponentView *)view; } @@ -123,16 +127,25 @@ - (instancetype)initWithFrame:(CGRect)frame _scrollView = [[RCTEnhancedScrollView alloc] initWithFrame:self.bounds]; _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#if !TARGET_OS_OSX // [macOS] _scrollView.delaysContentTouches = NO; +#endif // [macOS] ((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self; _isUserTriggeredScrolling = NO; _shouldUpdateContentInsetAdjustmentBehavior = YES; [self addSubview:_scrollView]; - _containerView = [[UIView alloc] initWithFrame:CGRectZero]; + _containerView = [[RCTUIView alloc] initWithFrame:CGRectZero]; // [macOS] +#if !TARGET_OS_OSX // [macOS] [_scrollView addSubview:_containerView]; - +#else // [macOS + _containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [_scrollView setDocumentView:_containerView]; +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] [self.scrollViewDelegateSplitter addDelegate:self]; +#endif // [macOS] if (CoreFeatures::disableScrollEventThrottleRequirement) { _scrollEventThrottle = 0; @@ -148,13 +161,17 @@ - (void)dealloc { // Removing all delegates from the splitter nils the actual delegate which prevents a crash on UIScrollView // deallocation. +#if !TARGET_OS_OSX // [macOS] [self.scrollViewDelegateSplitter removeAllDelegates]; +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] - (RCTGenericDelegateSplitter> *)scrollViewDelegateSplitter { return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter; } +#endif // [macOS] #pragma mark - RCTMountingTransactionObserving @@ -188,7 +205,9 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics : CGAffineTransformMakeScale(-1, 1); _containerView.transform = transform; +#if !TARGET_OS_OSX // [macOS] _scrollView.transform = transform; +#endif // [macOS] } } @@ -211,19 +230,25 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // FIXME: Commented props are not supported yet. MAP_SCROLL_VIEW_PROP(alwaysBounceHorizontal); MAP_SCROLL_VIEW_PROP(alwaysBounceVertical); +#if !TARGET_OS_OSX // [macOS] MAP_SCROLL_VIEW_PROP(bounces); MAP_SCROLL_VIEW_PROP(bouncesZoom); MAP_SCROLL_VIEW_PROP(canCancelContentTouches); +#endif // [macOS] MAP_SCROLL_VIEW_PROP(centerContent); // MAP_SCROLL_VIEW_PROP(automaticallyAdjustContentInsets); +#if !TARGET_OS_OSX // [macOS] MAP_SCROLL_VIEW_PROP(decelerationRate); MAP_SCROLL_VIEW_PROP(directionalLockEnabled); MAP_SCROLL_VIEW_PROP(maximumZoomScale); MAP_SCROLL_VIEW_PROP(minimumZoomScale); +#endif // [macOS] MAP_SCROLL_VIEW_PROP(scrollEnabled); +#if !TARGET_OS_OSX // [macOS] MAP_SCROLL_VIEW_PROP(pagingEnabled); MAP_SCROLL_VIEW_PROP(pinchGestureEnabled); MAP_SCROLL_VIEW_PROP(scrollsToTop); +#endif // [macOS] MAP_SCROLL_VIEW_PROP(showsHorizontalScrollIndicator); MAP_SCROLL_VIEW_PROP(showsVerticalScrollIndicator); @@ -232,7 +257,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } if (oldScrollViewProps.indicatorStyle != newScrollViewProps.indicatorStyle) { +#if !TARGET_OS_OSX // [macOS] _scrollView.indicatorStyle = RCTUIScrollViewIndicatorStyleFromProps(newScrollViewProps); +#endif // [macOS] } if (oldScrollViewProps.scrollEventThrottle != newScrollViewProps.scrollEventThrottle) { @@ -276,6 +303,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & scrollView.snapToOffsets = snapToOffsets; } +#if !TARGET_OS_OSX // [macOS] if (oldScrollViewProps.automaticallyAdjustsScrollIndicatorInsets != newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets) { scrollView.automaticallyAdjustsScrollIndicatorInsets = newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets; @@ -295,12 +323,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } _shouldUpdateContentInsetAdjustmentBehavior = NO; } - +#endif // [macOS] + MAP_SCROLL_VIEW_PROP(disableIntervalMomentum); MAP_SCROLL_VIEW_PROP(snapToInterval); if (oldScrollViewProps.keyboardDismissMode != newScrollViewProps.keyboardDismissMode) { +#if !TARGET_OS_OSX // [macOS] scrollView.keyboardDismissMode = RCTUIKeyboardDismissModeFromProps(newScrollViewProps); +#endif // [macOS] } [super updateProps:props oldProps:oldProps]; @@ -347,7 +378,7 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block [((RCTEnhancedScrollView *)_scrollView) preserveContentOffsetWithBlock:block]; } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [_containerView insertSubview:childComponentView atIndex:index]; if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) { @@ -355,7 +386,7 @@ - (void)mountChildComponentView:(UIView *)childCompone } } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { [childComponentView removeFromSuperview]; if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)] && @@ -370,17 +401,17 @@ - (void)unmountChildComponentView:(UIView *)childCompo */ - (BOOL)_shouldDisableScrollInteraction { - UIView *ancestorView = self.superview; + RCTUIView *ancestorView = (RCTUIView *)self.superview; // [macOS] while (ancestorView) { if ([ancestorView respondsToSelector:@selector(isJSResponder)]) { - BOOL isJSResponder = ((UIView *)ancestorView).isJSResponder; + BOOL isJSResponder = ((RCTUIView *)ancestorView).isJSResponder; // [macOS] if (isJSResponder) { return YES; } } - ancestorView = ancestorView.superview; + ancestorView = (RCTUIView *)ancestorView.superview; // [macOS] } return NO; @@ -422,7 +453,9 @@ - (void)prepareForRecycle // We set the default behavior to "never" so that iOS // doesn't do weird things to UIScrollView insets automatically // and keeps it as an opt-in behavior. +#if !TARGET_OS_OSX // [macOS] _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; +#endif // [macOS] _shouldUpdateContentInsetAdjustmentBehavior = YES; _state.reset(); _isUserTriggeredScrolling = NO; @@ -437,14 +470,14 @@ - (void)prepareForRecycle #pragma mark - UIScrollViewDelegate -- (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view +- (BOOL)touchesShouldCancelInContentView:(__unused RCTUIView *)view // [macOS] { // Historically, `UIScrollView`s in React Native do not cancel touches // started on `UIControl`-based views (as normal iOS `UIScrollView`s do). return ![self _shouldDisableScrollInteraction]; } -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) { [self _updateStateWithContentOffset]; @@ -456,31 +489,32 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView if (_eventEmitter) { static_cast(*_eventEmitter).onScroll([self _scrollViewMetrics]); } - +#if !TARGET_OS_OSX // [macOS] RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag); +#endif // [macOS] } [self _remountChildrenIfNeeded]; } -- (void)scrollViewDidZoom:(UIScrollView *)scrollView +- (void)scrollViewDidZoom:(RCTUIScrollView *)scrollView // [macOS] { [self scrollViewDidScroll:scrollView]; } -- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView +- (BOOL)scrollViewShouldScrollToTop:(RCTUIScrollView *)scrollView // [macOS] { _isUserTriggeredScrolling = YES; return YES; } -- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView +- (void)scrollViewDidScrollToTop:(RCTUIScrollView *)scrollView // [macOS] { _isUserTriggeredScrolling = NO; [self _updateStateWithContentOffset]; } -- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +- (void)scrollViewWillBeginDragging:(RCTUIScrollView *)scrollView // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -492,7 +526,7 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView _isUserTriggeredScrolling = YES; } -- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +- (void)scrollViewDidEndDragging:(RCTUIScrollView *)scrollView willDecelerate:(BOOL)decelerate // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -511,7 +545,7 @@ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL } } -- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView +- (void)scrollViewWillBeginDecelerating:(RCTUIScrollView *)scrollView // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -522,7 +556,7 @@ - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView static_cast(*_eventEmitter).onMomentumScrollBegin([self _scrollViewMetrics]); } -- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView +- (void)scrollViewDidEndDecelerating:(RCTUIScrollView *)scrollView // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -535,12 +569,12 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView _isUserTriggeredScrolling = NO; } -- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView +- (void)scrollViewDidEndScrollingAnimation:(RCTUIScrollView *)scrollView // [macOS] { [self _handleFinishedScrolling:scrollView]; } -- (void)_handleFinishedScrolling:(UIScrollView *)scrollView +- (void)_handleFinishedScrolling:(RCTUIScrollView *)scrollView // [macOS] { [self _forceDispatchNextScrollEvent]; [self scrollViewDidScroll:scrollView]; @@ -553,7 +587,7 @@ - (void)_handleFinishedScrolling:(UIScrollView *)scrollView [self _updateStateWithContentOffset]; } -- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view +- (void)scrollViewWillBeginZooming:(RCTUIScrollView *)scrollView withView:(nullable RCTUIView *)view // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -564,7 +598,7 @@ - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable static_cast(*_eventEmitter).onScrollBeginDrag([self _scrollViewMetrics]); } -- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale +- (void)scrollViewDidEndZooming:(RCTUIScrollView *)scrollView withView:(nullable RCTUIView *)view atScale:(CGFloat)scale // [macOS] { [self _forceDispatchNextScrollEvent]; @@ -576,7 +610,7 @@ - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UI [self _updateStateWithContentOffset]; } -- (UIView *)viewForZoomingInScrollView:(__unused UIScrollView *)scrollView +- (RCTUIView *)viewForZoomingInScrollView:(__unused RCTUIScrollView *)scrollView // [macOS] { return _containerView; } @@ -597,7 +631,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)flashScrollIndicators { +#if !TARGET_OS_OSX // [macOS] [_scrollView flashScrollIndicators]; +#endif // [macOS] } - (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated @@ -644,7 +680,7 @@ - (void)scrollToEnd:(BOOL)animated #pragma mark - Child views mounting -- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTUIView *)clipView // [macOS] { // Do nothing. ScrollView manages its subview clipping individually in `_remountChildren`. } @@ -665,8 +701,10 @@ - (void)_remountChildrenIfNeeded - (void)_remountChildren { +#if !TARGET_OS_OSX // [macOS] [_scrollView updateClippedSubviewsWithClipRect:CGRectInset(_scrollView.bounds, -kClippingLeeway, -kClippingLeeway) relativeToView:_scrollView]; +#endif // [macOS] } #pragma mark - RCTScrollableProtocol @@ -694,7 +732,9 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated [self _forceDispatchNextScrollEvent]; +#if !TARGET_OS_OSX // [macOS] [_scrollView setContentOffset:offset animated:animated]; +#endif // [macOS] if (!animated) { // When not animated, the expected workflow in ``scrollViewDidEndScrollingAnimation`` after scrolling is not going @@ -705,9 +745,12 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { +#if !TARGET_OS_OSX // [macOS] [_scrollView zoomToRect:rect animated:animated]; +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] - (void)addScrollListener:(NSObject *)scrollListener { [self.scrollViewDelegateSplitter addDelegate:scrollListener]; @@ -717,6 +760,7 @@ - (void)removeScrollListener:(NSObject *)scrollListener { [self.scrollViewDelegateSplitter removeDelegate:scrollListener]; } +#endif // [macOS] #pragma mark - Maintain visible content position @@ -731,7 +775,7 @@ - (void)_prepareForMaintainVisibleScrollPosition int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible; for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) { // Find the first entirely visible view. - UIView *subview = _contentView.subviews[ii]; + RCTUIView *subview = _contentView.subviews[ii]; // [macOS] BOOL hasNewView = NO; if (horizontal) { hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.h index 09fb855be5bd4b..4317aa4e7b1310 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm index d4ed3860e474c7..a405256e9300fc 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm @@ -22,7 +22,7 @@ @interface RCTSwitchComponentView () @end @implementation RCTSwitchComponentView { - UISwitch *_switchView; + RCTUISwitch *_switchView; // [macOS] BOOL _isInitialValueSet; } @@ -32,9 +32,14 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; - _switchView = [[UISwitch alloc] initWithFrame:self.bounds]; + _switchView = [[RCTUISwitch alloc] initWithFrame:self.bounds]; // [macOS] +#if !TARGET_OS_OSX // [macOS] [_switchView addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; +#else // [macOS + [_switchView setTarget:self]; + [_switchView setAction:@selector(onChange:)]; +#endif // macOS] self.contentView = _switchView; } @@ -72,25 +77,27 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _switchView.enabled = !newSwitchProps.disabled; } +#if !TARGET_OS_OSX // [macOS] // `tintColor` if (oldSwitchProps.tintColor != newSwitchProps.tintColor) { - _switchView.tintColor = RCTUIColorFromSharedColor(newSwitchProps.tintColor); + _switchView.tintColor = RCTUIColorFromSharedColor(newSwitchProps.tintColor); // [macOS] } // `onTintColor if (oldSwitchProps.onTintColor != newSwitchProps.onTintColor) { - _switchView.onTintColor = RCTUIColorFromSharedColor(newSwitchProps.onTintColor); + _switchView.onTintColor = RCTUIColorFromSharedColor(newSwitchProps.onTintColor); // [macOS] } // `thumbTintColor` if (oldSwitchProps.thumbTintColor != newSwitchProps.thumbTintColor) { - _switchView.thumbTintColor = RCTUIColorFromSharedColor(newSwitchProps.thumbTintColor); + _switchView.thumbTintColor = RCTUIColorFromSharedColor(newSwitchProps.thumbTintColor); // [macOS] } +#endif // [macOS] [super updateProps:props oldProps:oldProps]; } -- (void)onChange:(UISwitch *)sender +- (void)onChange:(RCTUISwitch *)sender // [macOS] { const auto &props = static_cast(*_props); if (props.value == sender.on) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h index 7d06fb61ba8297..3fcaf0bbcf20fd 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h @@ -5,11 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN +#if !TARGET_OS_OSX // [macOS] @interface RCTAccessibilityElement : UIAccessibilityElement +#else // [macOS +@interface RCTAccessibilityElement : NSAccessibilityElement +#endif // macOS] /* * Frame of the accessibility element in parent coordinate system. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm index 7ce44a3f1741cf..dbe9c2beac3ce7 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm @@ -9,14 +9,16 @@ @implementation RCTAccessibilityElement +#if !TARGET_OS_OSX // [macOS] - (CGRect)accessibilityFrame { - UIView *container = (UIView *)self.accessibilityContainer; + RCTUIView *container = (RCTUIView *)self.accessibilityContainer; // [macOS] if (CGRectEqualToRect(_frame, CGRectZero)) { return UIAccessibilityConvertFrameToScreenCoordinates(container.bounds, container); } else { return UIAccessibilityConvertFrameToScreenCoordinates(_frame, container); } } +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h index 3fb0da7c2a5f08..a088fb514d8ff1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -19,12 +19,14 @@ layoutManager:(RCTTextLayoutManager *)layoutManager paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes frame:(CGRect)frame - view:(UIView *)view; + view:(RCTUIView *)view; // [macOS] +#if !TARGET_OS_OSX // [macOS] /* * Returns an array of `UIAccessibilityElement`s to be used for `UIAccessibilityContainer` implementation. */ - (NSArray *)accessibilityElements; +#endif // [macOS] /** @abstract To make sure the provider is up to date. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm index b3676564857049..da58af276b5d65 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm @@ -21,19 +21,21 @@ using namespace facebook::react; @implementation RCTParagraphComponentAccessibilityProvider { +#if !TARGET_OS_OSX // [macOS] NSMutableArray *_accessibilityElements; +#endif // [macOS] AttributedString _attributedString; RCTTextLayoutManager *_layoutManager; ParagraphAttributes _paragraphAttributes; CGRect _frame; - __weak UIView *_view; + __weak RCTUIView *_view; // [macOS] } - (instancetype)initWithString:(facebook::react::AttributedString)attributedString layoutManager:(RCTTextLayoutManager *)layoutManager paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame - view:(UIView *)view + view:(RCTUIView *)view // [macOS] { if (self = [super init]) { _attributedString = attributedString; @@ -45,6 +47,7 @@ - (instancetype)initWithString:(facebook::react::AttributedString)attributedStri return self; } +#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { if (_accessibilityElements) { @@ -172,6 +175,7 @@ - (instancetype)initWithString:(facebook::react::AttributedString)attributedStri _accessibilityElements = elements; return _accessibilityElements; } +#endif // [macOS] - (BOOL)isUpToDate:(facebook::react::AttributedString)currentAttributedString { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h index 0744e4a530c86b..b02c6893772b9b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 27f52dd15150dc..e2db81977263a3 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -8,7 +8,10 @@ #import "RCTParagraphComponentView.h" #import "RCTParagraphComponentAccessibilityProvider.h" +#if !TARGET_OS_OSX // [macOS] #import +#endif // [macOS] + #import #import #import @@ -28,7 +31,9 @@ @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; +#if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; +#endif // [macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -37,8 +42,10 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; - self.opaque = NO; +#if !TARGET_OS_OSX // [macOS] self.contentMode = UIViewContentModeRedraw; + self.opaque = NO; +#endif // [macOS] } return self; @@ -87,11 +94,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _paragraphAttributes = newParagraphProps.paragraphAttributes; if (newParagraphProps.isSelectable != oldParagraphProps.isSelectable) { +#if !TARGET_OS_OSX // [macOS] if (newParagraphProps.isSelectable) { [self enableContextMenu]; } else { [self disableContextMenu]; } +#endif // [macOS] } [super updateProps:props oldProps:oldProps]; @@ -145,6 +154,7 @@ - (BOOL)isAccessibilityElement return NO; } +#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -177,6 +187,7 @@ - (UIAccessibilityTraits)accessibilityTraits { return [super accessibilityTraits] | UIAccessibilityTraitStaticText; } +#endif // [macOS] #pragma mark - RCTTouchableComponentViewProtocol @@ -207,6 +218,7 @@ - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point #pragma mark - Context Menu +#if !TARGET_OS_OSX // [macOS] - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self @@ -238,6 +250,7 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture [menuController setMenuVisible:YES animated:YES]; #endif } +#endif // [macOS] - (BOOL)canBecomeFirstResponder { @@ -253,7 +266,11 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return YES; } +#if !TARGET_OS_OSX // [macOS] return [self.nextResponder canPerformAction:action withSender:sender]; +#else // [macOS + return NO; +#endif // macOS] } - (void)copy:(id)sender @@ -272,8 +289,14 @@ - (void)copy:(id)sender [item setObject:attributedText.string forKey:(id)kUTTypeUTF8PlainText]; +#if !TARGET_OS_OSX // [macOS] UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[ item ]; +#else // [macOS + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setData:rtf forType:NSPasteboardTypeRTFD]; +#endif // macOS] } @end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.h index 927fbc4e3d33d3..36495da8236a79 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index e7b69bff2f9e68..fda7f88c6ebd5d 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -29,7 +29,11 @@ @interface RCTTextInputComponentView () *_backedTextInputView; +#if !TARGET_OS_OSX // [macOS] + RCTUIView *_backedTextInputView; +#else // [macOS + RCTPlatformView *_backedTextInputView; +#endif // macOS] NSUInteger _mostRecentEventCount; NSAttributedString *_lastStringStateWasUpdatedWith; @@ -84,7 +88,9 @@ - (void)didMoveToWindow if (self.window && !_didMoveToWindow) { const auto &props = static_cast(*_props); if (props.autoFocus) { +#if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; +#endif // [macOS] } _didMoveToWindow = YES; } @@ -115,6 +121,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & [self _setMultiline:newTextInputProps.traits.multiline]; } + +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) { _backedTextInputView.autocapitalizationType = RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType); @@ -124,6 +132,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.autocorrectionType = RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect); } +#endif // [macOS] if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) { _backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden; @@ -138,6 +147,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically; } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) { _backedTextInputView.keyboardAppearance = RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance); @@ -147,20 +157,24 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.spellCheckingType = RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck); } +#endif // [macOS] if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) { _backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden; } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) { _backedTextInputView.clearButtonMode = RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode); } +#endif // [macOS] if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) { _backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled; } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) { _backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry; } @@ -185,6 +199,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.smartInsertDeleteType = RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete); } +#endif // [macOS] // Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentionally here // because they are being checked on-demand. @@ -203,9 +218,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier())); } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) { _backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor); } +#endif // [macOS] if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); @@ -243,6 +260,9 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics { [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; +#if TARGET_OS_OSX // [macOS + _backedTextInputView.pointScaleFactor = layoutMetrics.pointScaleFactor; +#endif // macOS] _backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth)); _backedTextInputView.textContainerInset = @@ -417,9 +437,57 @@ - (void)textInputDidChangeSelection } } +#if TARGET_OS_OSX // [macOS +- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled {} + + +- (void)continuousSpellCheckingDidChange:(BOOL)enabled {} + + +- (void)grammarCheckingDidChange:(BOOL)enabled {} + + +- (BOOL)hasValidKeyDownOrValidKeyUp:(nonnull NSString *)key { + return YES; +} + +- (void)submitOnKeyDownIfNeeded:(nonnull NSEvent *)event {} + +- (void)textInputDidCancel {} + +- (NSDragOperation)textInputDraggingEntered:(nonnull id)draggingInfo { + return NSDragOperationNone; +} + +- (void)textInputDraggingExited:(nonnull id)draggingInfo { + return; +} + +- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id)sender { + return YES; +} + +- (BOOL)textInputShouldHandleDeleteForward:(nonnull id)sender { + return YES; +} + +- (BOOL)textInputShouldHandleDragOperation:(nonnull id)draggingInfo { + return YES; +} + +- (BOOL)textInputShouldHandleKeyEvent:(nonnull NSEvent *)event { + return YES; +} + +- (BOOL)textInputShouldHandlePaste:(nonnull id)sender { + return YES; +} + +#endif // macOS] + #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if (_eventEmitter) { static_cast(*_eventEmitter).onScroll([self _textInputMetrics]); @@ -459,6 +527,7 @@ - (void)setTextAndSelection:(NSInteger)eventCount [self _updateState]; } +#if !TARGET_OS_OSX // [macOS] UITextPosition *startPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:start]; UITextPosition *endPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument @@ -468,6 +537,7 @@ - (void)setTextAndSelection:(NSInteger)eventCount UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; [_backedTextInputView setSelectedTextRange:range notifyDelegate:NO]; } +#endif // [macOS] _comingFromJS = NO; } @@ -475,6 +545,7 @@ - (void)setTextAndSelection:(NSInteger)eventCount - (void)setDefaultInputAccessoryView { +#if !TARGET_OS_OSX // [macOS] // InputAccessoryView component sets the inputAccessoryView when inputAccessoryViewID exists if (_backedTextInputView.inputAccessoryViewID) { if (_backedTextInputView.isFirstResponder) { @@ -500,27 +571,30 @@ - (void)setDefaultInputAccessoryView UIToolbar *toolbarView = [UIToolbar new]; [toolbarView sizeToFit]; UIBarButtonItem *flexibleSpace = - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem *doneButton = - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone - target:self - action:@selector(handleInputAccessoryDoneButton)]; + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(handleInputAccessoryDoneButton)]; toolbarView.items = @[ flexibleSpace, doneButton ]; _backedTextInputView.inputAccessoryView = toolbarView; } else { _backedTextInputView.inputAccessoryView = nil; } - + if (_backedTextInputView.isFirstResponder) { [_backedTextInputView reloadInputViews]; } +#endif // [macOS] } - (void)handleInputAccessoryDoneButton { +#if !TARGET_OS_OSX // [macOS] if ([self textInputShouldReturn]) { [_backedTextInputView endEditing:YES]; } +#endif // [macOS] } #pragma mark - Other @@ -532,11 +606,13 @@ - (TextInputMetrics)_textInputMetrics metrics.selectionRange = [self _selectionRange]; metrics.eventCount = _mostRecentEventCount; +#if !TARGET_OS_OSX // [macOS] CGPoint contentOffset = _backedTextInputView.contentOffset; metrics.contentOffset = {contentOffset.x, contentOffset.y}; UIEdgeInsets contentInset = _backedTextInputView.contentInset; metrics.contentInset = {contentInset.left, contentInset.top, contentInset.right, contentInset.bottom}; +#endif // [macOS] CGSize contentSize = _backedTextInputView.contentSize; metrics.contentSize = {contentSize.width, contentSize.height}; @@ -544,8 +620,10 @@ - (TextInputMetrics)_textInputMetrics CGSize layoutMeasurement = _backedTextInputView.bounds.size; metrics.layoutMeasurement = {layoutMeasurement.width, layoutMeasurement.height}; +#if !TARGET_OS_OSX // [macOS] CGFloat zoomScale = _backedTextInputView.zoomScale; metrics.zoomScale = zoomScale; +#endif // [macOS] return metrics; } @@ -566,12 +644,17 @@ - (void)_updateState - (AttributedString::Range)_selectionRange { +#if !TARGET_OS_OSX // [macOS] UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange; NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start]; NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]; return AttributedString::Range{(int)start, (int)(end - start)}; +#else // [macOS + // [Fabric] Placeholder till we implement selection in Fabric + return AttributedString::Range({0, 1}); +#endif // macOS] } - (void)_restoreTextSelection @@ -580,11 +663,13 @@ - (void)_restoreTextSelection if (!selection.has_value()) { return; } +#if !TARGET_OS_OSX // [macOS] auto start = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:selection->start]; auto end = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:selection->end]; auto range = [_backedTextInputView textRangeFromPosition:start toPosition:end]; [_backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; +#endif // [macOS] } - (void)_setAttributedString:(NSAttributedString *)attributedString @@ -592,6 +677,7 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) { return; } +#if !TARGET_OS_OSX // [macOS] UITextRange *selectedRange = _backedTextInputView.selectedTextRange; NSInteger oldTextLength = _backedTextInputView.attributedText.string.length; _backedTextInputView.attributedText = attributedString; @@ -608,12 +694,17 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString } [self _restoreTextSelection]; _lastStringStateWasUpdatedWith = attributedString; +#endif // [macOS] } - (void)_setMultiline:(BOOL)multiline { [_backedTextInputView removeFromSuperview]; - UIView *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; +#if !TARGET_OS_OSX // [macOS] + RCTUIView *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; +#else // [macOS + RCTUITextView *backedTextInputView = [RCTUITextView new]; +#endif // macOS] backedTextInputView.frame = _backedTextInputView.frame; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); _backedTextInputView = backedTextInputView; @@ -643,9 +734,18 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe }]; BOOL shouldFallbackToBareTextComparison = - [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] || - [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] || - _backedTextInputView.markedTextRange || _backedTextInputView.isSecureTextEntry || fontHasBeenUpdatedBySystem; +#if !TARGET_OS_OSX // [macOS] + [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] || + [_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] || + _backedTextInputView.markedTextRange || + _backedTextInputView.isSecureTextEntry || +#else // [macOS + // There are multiple Korean input sources (2-Set, 3-Set, etc). Check substring instead instead + [[[_backedTextInputView inputContext] selectedKeyboardInputSource] containsString:@"com.apple.inputmethod.Korean"] || + [_backedTextInputView hasMarkedText] || + [_backedTextInputView isKindOfClass:[NSSecureTextField class]] || +#endif // macOS] + fontHasBeenUpdatedBySystem; if (shouldFallbackToBareTextComparison) { return ([newText.string isEqualToString:oldText.string]); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h index 02a1570df344ad..fd4bb5fd03f2c3 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h @@ -4,8 +4,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +#import -#import +#import // [macOS] +#import // [macOS] +#import // [macOS] #import @@ -15,9 +18,16 @@ NS_ASSUME_NONNULL_BEGIN void RCTCopyBackedTextInput( - UIView *fromTextInput, - UIView *toTextInput); - +#if !TARGET_OS_OSX // [macOS] + RCTUIView *fromTextInput, + RCTUIView *toTextInput +#else // [macOS + RCTUITextView *fromTextInput, + RCTUITextView *toTextInput +#endif // macOS] +); + +#if !TARGET_OS_OSX // [macOS] UITextAutocorrectionType RCTUITextAutocorrectionTypeFromOptionalBool(std::optional autoCorrect); UITextAutocapitalizationType RCTUITextAutocapitalizationTypeFromAutocapitalizationType( @@ -40,5 +50,6 @@ UITextContentType RCTUITextContentTypeFromString(const std::string &contentType) UITextInputPasswordRules *RCTUITextInputPasswordRulesFromString(const std::string &passwordRules); UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional smartInsertDelete); +#endif // [macOS] NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index d4b91c921b1114..bac8eeb7685704 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -19,27 +19,42 @@ } void RCTCopyBackedTextInput( - UIView *fromTextInput, - UIView *toTextInput) +#if !TARGET_OS_OSX // [macOS] + RCTUIView *fromTextInput, + RCTUIView *toTextInput +#else // [macOS + RCTUITextView *fromTextInput, + RCTUITextView *toTextInput +#endif // macOS] +) { toTextInput.attributedText = RCTSanitizeAttributedString(fromTextInput.attributedText); toTextInput.placeholder = fromTextInput.placeholder; toTextInput.placeholderColor = fromTextInput.placeholderColor; toTextInput.textContainerInset = fromTextInput.textContainerInset; +#if !TARGET_OS_OSX // [macOS] toTextInput.inputAccessoryView = fromTextInput.inputAccessoryView; +#endif // [macOS] toTextInput.textInputDelegate = fromTextInput.textInputDelegate; toTextInput.placeholderColor = fromTextInput.placeholderColor; toTextInput.defaultTextAttributes = fromTextInput.defaultTextAttributes; +#if !TARGET_OS_OSX // [macOS] toTextInput.autocapitalizationType = fromTextInput.autocapitalizationType; toTextInput.autocorrectionType = fromTextInput.autocorrectionType; +#endif // [macOS] toTextInput.contextMenuHidden = fromTextInput.contextMenuHidden; toTextInput.editable = fromTextInput.editable; +#if !TARGET_OS_OSX // [macOS] toTextInput.enablesReturnKeyAutomatically = fromTextInput.enablesReturnKeyAutomatically; toTextInput.keyboardAppearance = fromTextInput.keyboardAppearance; toTextInput.spellCheckingType = fromTextInput.spellCheckingType; +#endif // [macOS] toTextInput.caretHidden = fromTextInput.caretHidden; +#if !TARGET_OS_OSX // [macOS] toTextInput.clearButtonMode = fromTextInput.clearButtonMode; +#endif // [macOS] toTextInput.scrollEnabled = fromTextInput.scrollEnabled; +#if !TARGET_OS_OSX // [macOS] toTextInput.secureTextEntry = fromTextInput.secureTextEntry; toTextInput.keyboardType = fromTextInput.keyboardType; toTextInput.textContentType = fromTextInput.textContentType; @@ -47,8 +62,10 @@ void RCTCopyBackedTextInput( toTextInput.passwordRules = fromTextInput.passwordRules; [toTextInput setSelectedTextRange:fromTextInput.selectedTextRange notifyDelegate:NO]; +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] UITextAutocorrectionType RCTUITextAutocorrectionTypeFromOptionalBool(std::optional autoCorrect) { return autoCorrect.has_value() ? (*autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo) @@ -251,3 +268,4 @@ UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std:: ? (*smartInsertDelete ? UITextSmartInsertDeleteTypeYes : UITextSmartInsertDeleteTypeNo) : UITextSmartInsertDeleteTypeDefault; } +#endif // [macOS] diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.h index fcd2ebe7b7cc5a..be5a2c0c908e9a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.h @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] +#import "React/RCTUITextView.h" #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.mm index fc39e2592de4d0..620580dbf8d342 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedComponent/RCTUnimplementedNativeComponentView.mm @@ -14,7 +14,7 @@ using namespace facebook::react; @implementation RCTUnimplementedNativeComponentView { - UILabel *_label; + RCTUILabel *_label; // [macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -24,15 +24,21 @@ - (instancetype)initWithFrame:(CGRect)frame _props = defaultProps; CGRect bounds = self.bounds; - _label = [[UILabel alloc] initWithFrame:bounds]; - _label.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3]; + _label = [[RCTUILabel alloc] initWithFrame:bounds]; // [macOS] + _label.backgroundColor = [RCTUIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3]; +#if !TARGET_OS_OSX // [macOS] _label.layoutMargins = UIEdgeInsetsMake(12, 12, 12, 12); +#endif // [macOS] _label.lineBreakMode = NSLineBreakByWordWrapping; _label.numberOfLines = 0; _label.textAlignment = NSTextAlignmentCenter; - _label.textColor = [UIColor whiteColor]; + _label.textColor = [RCTUIColor whiteColor]; // [macOS] +#if !TARGET_OS_OSX // [macOS] self.contentView = _label; +#else // [macOS + [self.contentView addSubview:_label]; +#endif // macOS] } return self; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.h index 8c2c82a3f0c5c8..8c7bc564ab69cb 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.mm index 6a9efe577fb045..bb77576771ee9c 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/UnimplementedView/RCTUnimplementedViewComponentView.mm @@ -21,7 +21,7 @@ using namespace facebook::react; @implementation RCTUnimplementedViewComponentView { - UILabel *_label; + RCTUILabel *_label; // [macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -30,16 +30,22 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; - _label = [[UILabel alloc] initWithFrame:self.bounds]; - _label.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3]; + _label = [[RCTUILabel alloc] initWithFrame:self.bounds]; + _label.backgroundColor = [RCTUIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3]; _label.lineBreakMode = NSLineBreakByCharWrapping; _label.numberOfLines = 0; _label.textAlignment = NSTextAlignmentCenter; - _label.textColor = [UIColor whiteColor]; + _label.textColor = [RCTUIColor whiteColor]; +#if !TARGET_OS_OSX // [macOS] _label.allowsDefaultTighteningForTruncation = YES; _label.adjustsFontSizeToFitWidth = YES; +#endif // [macOS] +#if !TARGET_OS_OSX // [macOS] self.contentView = _label; +#else // [macOS + [self.contentView addSubview:_label]; +#endif // macOS] } return self; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 36092fea549a3e..7a903401112fa8 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -22,12 +22,12 @@ NS_ASSUME_NONNULL_BEGIN /** * UIView class for component. */ -@interface RCTViewComponentView : UIView { +@interface RCTViewComponentView : RCTUIView { @protected facebook::react::LayoutMetrics _layoutMetrics; facebook::react::SharedViewProps _props; facebook::react::SharedViewEventEmitter _eventEmitter; -} +} // [macOS] /** * Represents the `UIView` instance that is being automatically attached to @@ -37,7 +37,7 @@ NS_ASSUME_NONNULL_BEGIN * to embed/bridge pure native views as component views. * Defaults to `nil`. Assign `nil` to remove view as subview. */ -@property (nonatomic, strong, nullable) UIView *contentView; +@property (nonatomic, strong, nullable) RCTPlatformView *contentView; // [macOS] /** * Provides access to `nativeId` prop of the component. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index d06b0aa8d1c2bf..a382fe0da93e1b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -14,6 +14,7 @@ #import #import #import +#import // [macOS] #import #import #import @@ -27,12 +28,12 @@ using namespace facebook::react; @implementation RCTViewComponentView { - UIColor *_backgroundColor; + RCTUIColor *_backgroundColor; // [macOS] CALayer *_borderLayer; BOOL _needsInvalidateLayer; BOOL _isJSResponder; BOOL _removeClippedSubviews; - NSMutableArray *_reactSubviews; + NSMutableArray *_reactSubviews; // [macOS] NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; } @@ -49,7 +50,9 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; _reactSubviews = [NSMutableArray new]; +#if !TARGET_OS_OSX // [macOS] self.multipleTouchEnabled = YES; +#endif // [macOS] } return self; } @@ -59,7 +62,11 @@ - (instancetype)initWithFrame:(CGRect)frame return _props; } -- (void)setContentView:(UIView *)contentView +#if !TARGET_OS_OSX // [macOS] +- (void)setContentView:(RCTUIView *)contentView // [macOS] +#else // [macOS +- (void)setContentView:(RCTPlatformView *)contentView // [macOS] +#endif // macOS] { if (_contentView) { [_contentView removeFromSuperview]; @@ -68,8 +75,11 @@ - (void)setContentView:(UIView *)contentView _contentView = contentView; if (_contentView) { - [self addSubview:_contentView]; _contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); +#if TARGET_OS_OSX // [macOS + _contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#endif // macOS] + [self addSubview:_contentView]; } } @@ -82,12 +92,12 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event return CGRectContainsPoint(hitFrame, point); } -- (UIColor *)backgroundColor +- (RCTUIColor *)backgroundColor // [macOS] { return _backgroundColor; } -- (void)setBackgroundColor:(UIColor *)backgroundColor +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS] { _backgroundColor = backgroundColor; } @@ -103,7 +113,7 @@ + (ComponentDescriptorProvider)componentDescriptorProvider return concreteComponentDescriptorProvider(); } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { RCTAssert( childComponentView.superview == nil, @@ -120,7 +130,7 @@ - (void)mountChildComponentView:(UIView *)childCompone } } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { if (_removeClippedSubviews) { [_reactSubviews removeObjectAtIndex:index]; @@ -144,7 +154,7 @@ - (void)unmountChildComponentView:(UIView *)childCompo [childComponentView removeFromSuperview]; } -- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTUIView *)clipView // [macOS] { if (!_removeClippedSubviews) { // Use default behavior if unmounting is disabled @@ -165,7 +175,7 @@ - (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIVie clipRect = [clipView convertRect:clipRect toView:self]; // Mount / unmount views - for (UIView *view in _reactSubviews) { + for (RCTUIView *view in _reactSubviews) { // [macOS] if (CGRectIntersectsRect(clipRect, view.frame)) { // View is at least partially visible, so remount it if unmounted [self addSubview:view]; @@ -214,7 +224,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `backgroundColor` if (oldViewProps.backgroundColor != newViewProps.backgroundColor) { - self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor); + self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor); // [macOS] needsInvalidateLayer = YES; } @@ -252,7 +262,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `shouldRasterize` if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) { self.layer.shouldRasterize = newViewProps.shouldRasterize; +#if !TARGET_OS_OSX // [macOS] self.layer.rasterizationScale = newViewProps.shouldRasterize ? [UIScreen mainScreen].scale : 1.0; +#else // [macOS + self.layer.rasterizationScale = 1.0; +#endif // macOS] } // `pointerEvents` @@ -295,6 +309,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId); } +#if !TARGET_OS_OSX // [macOS] // `accessible` if (oldViewProps.accessible != newViewProps.accessible) { self.accessibilityElement.isAccessibilityElement = newViewProps.accessible; @@ -365,6 +380,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.accessibilityElement.accessibilityValue = nil; } } +#endif // [macOS] // `testId` if (oldViewProps.testId != newViewProps.testId) { @@ -462,7 +478,7 @@ - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSetpointerEvents) { case PointerEventsMode::Auto: @@ -504,7 +524,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event case PointerEventsMode::BoxOnly: return [self pointInside:point withEvent:event] ? self : nil; case PointerEventsMode::BoxNone: - UIView *view = [self betterHitTest:point withEvent:event]; + RCTUIView *view = [self betterHitTest:point withEvent:event]; // [macOS] return view != self ? view : nil; } } @@ -631,6 +651,12 @@ - (void)invalidateLayer RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors); +#if TARGET_OS_OSX // [macOS + CGFloat scaleFactor = _layoutMetrics.pointScaleFactor; +#else + // On iOS setting the scaleFactor to 0.0 will default to the device's native scale factor. + CGFloat scaleFactor = 0.0; +#endif // macOS] UIImage *image = RCTGetBorderImage( RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left), layer.bounds.size, @@ -638,7 +664,8 @@ - (void)invalidateLayer RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), borderColors, _backgroundColor.CGColor, - self.clipsToBounds); + self.clipsToBounds, + scaleFactor); // [macOS] RCTReleaseRCTBorderColors(borderColors); @@ -651,8 +678,13 @@ - (void)invalidateLayer CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height}, CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}}; - _borderLayer.contents = (id)image.CGImage; - _borderLayer.contentsScale = image.scale; +#if !TARGET_OS_OSX // [macOS] + _borderLayer.contents = (id)image.CGImage; + _borderLayer.contentsScale = image.scale; +#else // [macOS + _borderLayer.contents = [image layerContentsForContentsScale:scaleFactor]; + _borderLayer.contentsScale = scaleFactor; +#endif // macOS] BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); if (isResizable) { @@ -694,10 +726,10 @@ - (NSObject *)accessibilityElement return self; } -static NSString *RCTRecursiveAccessibilityLabel(UIView *view) +static NSString *RCTRecursiveAccessibilityLabel(RCTUIView *view) // [macOS] { NSMutableString *result = [NSMutableString stringWithString:@""]; - for (UIView *subview in view.subviews) { + for (RCTUIView *subview in view.subviews) { // [macOS] NSString *label = subview.accessibilityLabel; if (!label) { label = RCTRecursiveAccessibilityLabel(subview); @@ -727,6 +759,7 @@ - (NSString *)accessibilityValue const auto &props = static_cast(*_props); // Handle Switch. +#if !TARGET_OS_OSX // [macOS] if ((self.accessibilityTraits & AccessibilityTraitSwitch) == AccessibilityTraitSwitch) { if (props.accessibilityState.checked == AccessibilityState::Checked) { return @"1"; @@ -734,6 +767,7 @@ - (NSString *)accessibilityValue return @"0"; } } +#endif // [macOS] NSMutableArray *valueComponents = [NSMutableArray new]; NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()] diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewClassDescriptor.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewClassDescriptor.h index b03a4642a98123..4797e137aec88b 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewClassDescriptor.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewClassDescriptor.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewDescriptor.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewDescriptor.h index 3ca65d18fcb1f1..1f4533d1a10296 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewDescriptor.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewDescriptor.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @@ -21,8 +21,7 @@ class RCTComponentViewDescriptor final { /* * Associated (and owned) native view instance. */ - __strong UIView *view = nil; - + __strong RCTUIView *view = nil; // [macOS] /* * Indicates a requirement to call on the view methods from * `RCTMountingTransactionObserving` protocol. diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.h index 9c6a6ff2674041..6df14fd9ba163c 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewFactory.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h index 1b802b5cfde373..69213be2dbc1eb 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -54,14 +54,14 @@ typedef NS_OPTIONS(NSInteger, RNComponentViewUpdateMask) { * component view. * Receiver must add `childComponentView` as a subview. */ -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index; +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index; // [macOS] /* * Called for unmounting (detaching) a child component view from `self` * component view. * Receiver must remove `childComponentView` as a subview. */ -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index; +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index; // [macOS] /* * Called for updating component's props. @@ -119,6 +119,9 @@ typedef NS_OPTIONS(NSInteger, RNComponentViewUpdateMask) { - (BOOL)isJSResponder; - (void)setIsJSResponder:(BOOL)isJSResponder; +- (NSNumber *)reactTag; // [macOS] +- (void)setReactTag:(NSNumber *)reactTag; // [macOS] + /* * This is broken. Do not use. */ diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h index 1e85eec961c50c..ff077271fff100 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -48,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN * Finds a native component view by given `tag`. * Returns `nil` if there is no registered component with the `tag`. */ -- (nullable UIView *)findComponentViewWithTag:(facebook::react::Tag)tag; +- (nullable RCTUIView *)findComponentViewWithTag:(facebook::react::Tag)tag; // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm index e78e8ae53342fd..0c6fd9dd38675f 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm @@ -31,10 +31,12 @@ - (instancetype)init if (self = [super init]) { _componentViewFactory = [RCTComponentViewFactory currentComponentViewFactory]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleApplicationDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif // [macOS] } return self; @@ -50,7 +52,11 @@ - (instancetype)init @"RCTComponentViewRegistry: Attempt to dequeue already registered component."); auto componentViewDescriptor = [self _dequeueComponentViewWithComponentHandle:componentHandle]; +#if !TARGET_OS_OSX // [macOS] componentViewDescriptor.view.tag = tag; +#else // [macOS + componentViewDescriptor.view.reactTag = @(tag); +#endif // macOS] auto it = _registry.insert({tag, componentViewDescriptor}); return it.first->second; } @@ -65,7 +71,11 @@ - (void)enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandle _registry.find(tag) != _registry.end(), @"RCTComponentViewRegistry: Attempt to enqueue unregistered component."); _registry.erase(tag); +#if !TARGET_OS_OSX // [macOS] componentViewDescriptor.view.tag = 0; +#else // [macOS + componentViewDescriptor.view.reactTag = @0; +#endif // macOS] [self _enqueueComponentViewWithComponentHandle:componentHandle componentViewDescriptor:componentViewDescriptor]; } @@ -77,7 +87,7 @@ - (void)enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandle return iterator->second; } -- (nullable UIView *)findComponentViewWithTag:(Tag)tag +- (nullable RCTUIView *)findComponentViewWithTag:(Tag)tag // [macOS] { RCTAssertMainQueue(); auto iterator = _registry.find(tag); diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h index 127b06fd474a67..22c0585a9cf832 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -37,12 +37,12 @@ NS_ASSUME_NONNULL_BEGIN * influence the intrinsic size of the view and cannot be measured using UIView/UIKit layout API. * Must be called on the main thead. */ -- (void)attachSurfaceToView:(UIView *)view surfaceId:(facebook::react::SurfaceId)surfaceId; +- (void)attachSurfaceToView:(RCTUIView *)view surfaceId:(facebook::react::SurfaceId)surfaceId; // [macOS] /** * Stops designating the view as a rendering viewport of a React Native surface. */ -- (void)detachSurfaceFromView:(UIView *)view surfaceId:(facebook::react::SurfaceId)surfaceId; +- (void)detachSurfaceFromView:(RCTUIView *)view surfaceId:(facebook::react::SurfaceId)surfaceId; // [macOS] /** * Schedule a mounting transaction to be performed on the main thread. diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index b4cfb3d0ede108..dd60be42d1e1f5 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -29,7 +29,7 @@ using namespace facebook::react; -static SurfaceId RCTSurfaceIdForView(UIView *view) +static SurfaceId RCTSurfaceIdForView(RCTUIView *view) // [macOS] { do { if (RCTIsReactRootView(@(view.tag))) { @@ -79,7 +79,7 @@ static void RCTPerformMountInstructions( auto &newChildViewDescriptor = [registry componentViewDescriptorWithTag:newChildShadowView.tag]; auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:parentShadowView.tag]; - UIView *newChildComponentView = newChildViewDescriptor.view; + RCTUIView *newChildComponentView = newChildViewDescriptor.view; // [macOS] RCTAssert(newChildShadowView.props, @"`newChildShadowView.props` must not be null."); @@ -112,7 +112,7 @@ static void RCTPerformMountInstructions( auto &oldChildShadowView = mutation.oldChildShadowView; auto &newChildShadowView = mutation.newChildShadowView; auto &newChildViewDescriptor = [registry componentViewDescriptorWithTag:newChildShadowView.tag]; - UIView *newChildComponentView = newChildViewDescriptor.view; + RCTUIView *newChildComponentView = newChildViewDescriptor.view; // [macOS] auto mask = RNComponentViewUpdateMask{}; @@ -170,7 +170,7 @@ - (void)setContextContainer:(ContextContainer::Shared)contextContainer _contextContainer = contextContainer; } -- (void)attachSurfaceToView:(UIView *)view surfaceId:(SurfaceId)surfaceId +- (void)attachSurfaceToView:(RCTUIView *)view surfaceId:(SurfaceId)surfaceId // [macOS] { RCTAssertMainQueue(); @@ -181,7 +181,7 @@ - (void)attachSurfaceToView:(UIView *)view surfaceId:(SurfaceId)surfaceId [view addSubview:rootViewDescriptor.view]; } -- (void)detachSurfaceFromView:(UIView *)view surfaceId:(SurfaceId)surfaceId +- (void)detachSurfaceFromView:(RCTUIView *)view surfaceId:(SurfaceId)surfaceId // [macOS] { RCTAssertMainQueue(); RCTComponentViewDescriptor rootViewDescriptor = [_componentViewRegistry componentViewDescriptorWithTag:surfaceId]; @@ -286,7 +286,7 @@ - (void)setIsJSResponder:(BOOL)isJSResponder { ReactTag reactTag = shadowView.tag; RCTExecuteOnMainQueue(^{ - UIView *componentView = [self->_componentViewRegistry findComponentViewWithTag:reactTag]; + RCTUIView *componentView = [self->_componentViewRegistry findComponentViewWithTag:reactTag]; // [macOS] [componentView setIsJSResponder:isJSResponder]; }); } @@ -296,7 +296,7 @@ - (void)synchronouslyUpdateViewOnUIThread:(ReactTag)reactTag componentDescriptor:(const ComponentDescriptor &)componentDescriptor { RCTAssertMainQueue(); - UIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; + RCTUIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; // [macOS] SurfaceId surfaceId = RCTSurfaceIdForView(componentView); Props::Shared oldProps = [componentView props]; Props::Shared newProps = componentDescriptor.cloneProps( @@ -331,15 +331,17 @@ - (void)synchronouslyDispatchCommandOnUIThread:(ReactTag)reactTag args:(NSArray *)args { RCTAssertMainQueue(); - UIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; + RCTUIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; // [macOS] [componentView handleCommand:commandName args:args]; } - (void)synchronouslyDispatchAccessbilityEventOnUIThread:(ReactTag)reactTag eventType:(NSString *)eventType { if ([@"focus" isEqualToString:eventType]) { - UIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; + RCTUIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; // [macOS] +#if !TARGET_OS_OSX // [macOS] UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, componentView); +#endif // [macOS] } } diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManagerDelegate.h b/packages/react-native/React/Fabric/Mounting/RCTMountingManagerDelegate.h index 21653fe033616f..3d9bc3d8837d94 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManagerDelegate.h +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManagerDelegate.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingTransactionObserving.h b/packages/react-native/React/Fabric/Mounting/RCTMountingTransactionObserving.h index bdc44b3316067a..f5e4f17d96509c 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingTransactionObserving.h +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingTransactionObserving.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #include diff --git a/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.h b/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.h index e385fb33225c5e..f71734404bea6a 100644 --- a/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.h +++ b/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @@ -14,13 +14,13 @@ NS_ASSUME_NONNULL_BEGIN /** * Default implementation of RCTComponentViewProtocol. */ -@interface UIView (ComponentViewProtocol) +@interface RCTUIView (ComponentViewProtocol) // [macOS] + (std::vector)supplementalComponentDescriptorProviders; -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index; +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index; // [macOS] -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index; +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index; // [macOS] - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps; @@ -41,10 +41,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)setIsJSResponder:(BOOL)isJSResponder; +- (NSNumber *)reactTag; // [macOS] +- (void)setReactTag:(NSNumber *)reactTag; // [macOS] + - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(nullable NSSet *)props; - (nullable NSSet *)propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; -- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; +- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTUIView *)clipView; // [macOS] @end diff --git a/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm b/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm index 9f50554560e5aa..e6fbb24916bb7a 100644 --- a/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm +++ b/packages/react-native/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm @@ -7,6 +7,8 @@ #import "UIView+ComponentViewProtocol.h" +#import // [macOS] + #import #import #import @@ -15,7 +17,7 @@ using namespace facebook::react; -@implementation UIView (ComponentViewProtocol) +@implementation RCTUIView (ComponentViewProtocol) // [macOS] + (ComponentDescriptorProvider)componentDescriptorProvider { @@ -28,7 +30,7 @@ + (ComponentDescriptorProvider)componentDescriptorProvider return {}; } -- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)mountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { RCTAssert( childComponentView.superview == nil, @@ -40,7 +42,7 @@ - (void)mountChildComponentView:(UIView *)childCompone [self insertSubview:childComponentView atIndex:index]; } -- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +- (void)unmountChildComponentView:(RCTUIView *)childComponentView index:(NSInteger)index // [macOS] { RCTAssert( childComponentView.superview == self, @@ -103,16 +105,22 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics } else { // Note: Changing `frame` when `layer.transform` is not the `identity transform` is undefined behavior. // Therefore, we must use `center` and `bounds`. +#if !TARGET_OS_OSX // [macOS] self.center = CGPoint{CGRectGetMidX(frame), CGRectGetMidY(frame)}; +#else // [macOS + self.frame = frame; +#endif // macOS] self.bounds = CGRect{CGPointZero, frame.size}; } } +#if !TARGET_OS_OSX // [macOS] if (forceUpdate || (layoutMetrics.layoutDirection != oldLayoutMetrics.layoutDirection)) { self.semanticContentAttribute = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft ? UISemanticContentAttributeForceRightToLeft : UISemanticContentAttributeForceLeftToRight; } +#endif // [macOS] if (forceUpdate || (layoutMetrics.displayType != oldLayoutMetrics.displayType)) { self.hidden = layoutMetrics.displayType == DisplayType::None; @@ -146,6 +154,16 @@ - (void)setIsJSResponder:(BOOL)isJSResponder // Default implementation does nothing. } +- (NSNumber *)reactTag +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setReactTag:(NSNumber *)reactTag +{ + objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC); +} + - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(nullable NSSet *)propKeys { // Default implementation does nothing. @@ -156,14 +174,14 @@ - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(nullable NSSet +#import // [macOS] #import #import @@ -34,26 +34,26 @@ inline std::string RCTStringFromNSString(NSString *string) return std::string{string.UTF8String ?: ""}; } -inline UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) +inline RCTUIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) // [macOS] { if (!sharedColor) { return nil; } if (*facebook::react::clearColor() == *sharedColor) { - return [UIColor clearColor]; + return [RCTUIColor clearColor]; // [macOS] } if (*facebook::react::blackColor() == *sharedColor) { - return [UIColor blackColor]; + return [RCTUIColor blackColor]; // [macOS] } if (*facebook::react::whiteColor() == *sharedColor) { - return [UIColor whiteColor]; + return [RCTUIColor whiteColor]; // [macOS] } auto components = facebook::react::colorComponentsFromColor(sharedColor); - return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; + return [RCTUIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; // [macOS] } inline CF_RETURNS_RETAINED CGColorRef _Nullable RCTCreateCGColorRefFromSharedColor( @@ -82,6 +82,7 @@ inline UIEdgeInsets RCTUIEdgeInsetsFromEdgeInsets(const facebook::react::EdgeIns return {edgeInsets.top, edgeInsets.left, edgeInsets.bottom, edgeInsets.right}; } +#if !TARGET_OS_OSX // [macOS] const UIAccessibilityTraits AccessibilityTraitSwitch = 0x20000000000001; inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( @@ -145,6 +146,7 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( } return result; }; +#endif // [macOS] inline CATransform3D RCTCATransform3DFromTransformMatrix(const facebook::react::Transform &transformMatrix) { diff --git a/packages/react-native/React/Fabric/RCTImageResponseDelegate.h b/packages/react-native/React/Fabric/RCTImageResponseDelegate.h index fde447e0e3bdef..1f80cb137cc18a 100644 --- a/packages/react-native/React/Fabric/RCTImageResponseDelegate.h +++ b/packages/react-native/React/Fabric/RCTImageResponseDelegate.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Fabric/RCTLocalizationProvider.h b/packages/react-native/React/Fabric/RCTLocalizationProvider.h index 19f87184716490..503ea2e33b0318 100644 --- a/packages/react-native/React/Fabric/RCTLocalizationProvider.h +++ b/packages/react-native/React/Fabric/RCTLocalizationProvider.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] @protocol RCTLocalizationProtocol diff --git a/packages/react-native/React/Fabric/RCTScheduler.h b/packages/react-native/React/Fabric/RCTScheduler.h index 888770cfd9fea0..ac2ba726d7f280 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.h +++ b/packages/react-native/React/Fabric/RCTScheduler.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.h b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.h index da899287499ade..dd852b2a471168 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.h +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -15,8 +15,8 @@ NS_ASSUME_NONNULL_BEGIN * Attaches (and detaches) a view to the touch handler. * The receiver does not retain the provided view. */ -- (void)attachToView:(UIView *)view; -- (void)detachFromView:(UIView *)view; +- (void)attachToView:(RCTUIView *)view; // [macOS] +- (void)detachFromView:(RCTUIView *)view; // [macOS] /* * Offset of the attached view relative to the root component in points. diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm index 67a89beac124b2..c54c46e3a88573 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm @@ -24,6 +24,7 @@ typedef NS_ENUM(NSInteger, RCTPointerEventType) { RCTPointerEventTypeCancel, }; +#if !TARGET_OS_OSX // [macOS] static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) { for (UITouch *touch in touches) { @@ -43,6 +44,7 @@ static BOOL AnyTouchesChanged(NSSet *touches) } return NO; } +#endif // [macOS] struct ActivePointer { /* @@ -53,12 +55,12 @@ static BOOL AnyTouchesChanged(NSSet *touches) /* * The component view on which the touch started. */ - UIView *initialComponentView = nil; + RCTPlatformView *initialComponentView = nil; // [macOS] /* * The current target component view of the pointer */ - UIView *componentView = nil; + RCTPlatformView *componentView = nil; // [macOS] /* * The location of the pointer relative to the root component view @@ -155,12 +157,12 @@ bool operator()(const ActivePointer &lhs, const ActivePointer &rhs) const // do not conflict static NSInteger constexpr kTouchIdentifierPoolOffset = 2; -static SharedTouchEventEmitter GetTouchEmitterFromView(UIView *componentView, CGPoint point) +static SharedTouchEventEmitter GetTouchEmitterFromView(RCTUIView *componentView, CGPoint point) // [macOS] { return [(id)componentView touchEventEmitterAtPoint:point]; } -static NSOrderedSet *GetTouchableViewsInPathToRoot(UIView *componentView) +static NSOrderedSet *GetTouchableViewsInPathToRoot(RCTPlatformView *componentView) // [macOS] { NSMutableOrderedSet *results = [NSMutableOrderedSet orderedSet]; do { @@ -172,7 +174,7 @@ static SharedTouchEventEmitter GetTouchEmitterFromView(UIView *componentView, CG return results; } -static UIView *FindClosestFabricManagedTouchableView(UIView *componentView) +static RCTPlatformView *FindClosestFabricManagedTouchableView(RCTPlatformView *componentView) // [macOS] { while (componentView) { if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { @@ -185,6 +187,7 @@ static SharedTouchEventEmitter GetTouchEmitterFromView(UIView *componentView, CG static NSInteger ButtonMaskDiffToButton(UIEventButtonMask prevButtonMask, UIEventButtonMask curButtonMask) { +#if !TARGET_OS_OSX // [macOS] if ((prevButtonMask & UIEventButtonMaskPrimary) != (curButtonMask & UIEventButtonMaskPrimary)) { return 0; } @@ -194,6 +197,7 @@ static NSInteger ButtonMaskDiffToButton(UIEventButtonMask prevButtonMask, UIEven if ((prevButtonMask & UIEventButtonMaskSecondary) != (curButtonMask & UIEventButtonMaskSecondary)) { return 2; } +#endif // [macOS] return -1; } @@ -237,7 +241,7 @@ static CGFloat RadsToDegrees(CGFloat rads) static NSInteger ButtonMaskToButtons(UIEventButtonMask buttonMask) { NSInteger buttonsMaskResult = 0; - +#if !TARGET_OS_OSX // [macOS] if ((buttonMask & UIEventButtonMaskPrimary) != 0) { buttonsMaskResult |= 1; } @@ -248,10 +252,11 @@ static NSInteger ButtonMaskToButtons(UIEventButtonMask buttonMask) if ((buttonMask & 0x4) != 0) { buttonsMaskResult |= 4; } - +#endif // [macOS] return buttonsMaskResult; } +#if !TARGET_OS_OSX // [macOS] static const char *PointerTypeCStringFromUITouchType(UITouchType type) { switch (type) { @@ -362,24 +367,37 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData( return event; } +#endif // [macOS] static void UpdateActivePointerWithUITouch( ActivePointer &activePointer, - UITouch *uiTouch, + RCTUITouch *uiTouch, // [macOS] UIEvent *uiEvent, - UIView *rootComponentView) + RCTUIView *rootComponentView) // [macOS] { +#if !TARGET_OS_OSX // [macOS] CGPoint location = [uiTouch locationInView:rootComponentView]; UIView *hitTestedView = [rootComponentView hitTest:location withEvent:nil]; activePointer.componentView = FindClosestFabricManagedTouchableView(hitTestedView); +#else // [macOS + CGPoint touchLocation = [rootComponentView.superview convertPoint:uiTouch.locationInWindow fromView:nil]; + activePointer.componentView = (RCTUIView *) [rootComponentView hitTest:touchLocation]; +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] activePointer.clientPoint = [uiTouch locationInView:rootComponentView]; activePointer.screenPoint = [rootComponentView convertPoint:activePointer.clientPoint toCoordinateSpace:rootComponentView.window.screen.coordinateSpace]; activePointer.offsetPoint = [uiTouch locationInView:activePointer.componentView]; +#else // [macOS + activePointer.offsetPoint = [activePointer.componentView convertPoint:uiTouch.locationInWindow fromView:nil]; + activePointer.screenPoint = uiTouch.locationInWindow; + activePointer.clientPoint = CGPointMake(activePointer.screenPoint.x, CGRectGetHeight(rootComponentView.window.frame) - activePointer.screenPoint.y); +#endif // macOS] activePointer.timestamp = uiTouch.timestamp; +#if !TARGET_OS_OSX // [macOS] activePointer.force = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce); activePointer.touchType = uiTouch.type; @@ -394,6 +412,16 @@ static void UpdateActivePointerWithUITouch( activePointer.button = ButtonMaskDiffToButton(activePointer.buttonMask, nextButtonMask); activePointer.buttonMask = nextButtonMask; activePointer.modifierFlags = uiEvent.modifierFlags; +#else // [macOS + NSEventType type = uiTouch.type; + if (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp || type == NSEventTypeLeftMouseDragged) { + activePointer.button = 0; + } else if (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp || type == NSEventTypeRightMouseDragged) { + activePointer.button = 2; + } + activePointer.modifierFlags = uiEvent.modifierFlags; + +#endif // macOS] } /** @@ -430,17 +458,23 @@ @interface RCTSurfacePointerHandler () @end @implementation RCTSurfacePointerHandler { - std::unordered_map<__unsafe_unretained UITouch *, ActivePointer, PointerHasher<__unsafe_unretained UITouch *>> +#if !TARGET_OS_OSX // [macOS] + std::unordered_map<__unsafe_unretained RCTUITouch *, ActivePointer, PointerHasher<__unsafe_unretained RCTUITouch *>> _activePointers; +#else // [macOS + std::unordered_map _activePointers; +#endif // macOS] /* * We hold the view weakly to prevent a retain cycle. */ - __weak UIView *_rootComponentView; + __weak RCTUIView *_rootComponentView; // [macOS] RCTIdentifierPool<11> _identifierPool; +#if !TARGET_OS_OSX // [macOS] UIHoverGestureRecognizer *_mouseHoverRecognizer API_AVAILABLE(ios(13.0)); UIHoverGestureRecognizer *_penHoverRecognizer API_AVAILABLE(ios(13.0)); +#endif // [macOS] NSMutableDictionary *> *_currentlyHoveredViewsPerPointer; @@ -454,14 +488,18 @@ - (instancetype)init // to be used as a top level event delegated recognizer. // Otherwise, lower-level components not built using React Native, // will fail to recognize gestures. +#if !TARGET_OS_OSX // [macOS] self.cancelsTouchesInView = NO; self.delaysTouchesBegan = NO; // This is default value. self.delaysTouchesEnded = NO; +#endif // [macOS] self.delegate = self; +#if !TARGET_OS_OSX // [macOS] _mouseHoverRecognizer = nil; _penHoverRecognizer = nil; +#endif // [macOS] _currentlyHoveredViewsPerPointer = [[NSMutableDictionary alloc] init]; _primaryTouchPointerId = -1; } @@ -471,13 +509,14 @@ - (instancetype)init RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action) -- (void)attachToView:(UIView *)view +- (void)attachToView:(RCTUIView *)view // [macOS] { RCTAssert(self.view == nil, @"RCTSurfacePointerHandler already has an attached view."); [view addGestureRecognizer:self]; _rootComponentView = view; +#if !TARGET_OS_OSX // [macOS] _mouseHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(mouseHovering:)]; _mouseHoverRecognizer.allowedTouchTypes = @[ @(UITouchTypeIndirectPointer) ]; [view addGestureRecognizer:_mouseHoverRecognizer]; @@ -485,9 +524,10 @@ - (void)attachToView:(UIView *)view _penHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(penHovering:)]; _penHoverRecognizer.allowedTouchTypes = @[ @(UITouchTypePencil) ]; [view addGestureRecognizer:_penHoverRecognizer]; +#endif // [macOS] } -- (void)detachFromView:(UIView *)view +- (void)detachFromView:(RCTUIView *)view // [macOS] { RCTAssertParam(view); RCTAssert(self.view == view, @"RCTSufracePointerHandler attached to another view."); @@ -495,6 +535,7 @@ - (void)detachFromView:(UIView *)view [view removeGestureRecognizer:self]; _rootComponentView = nil; +#if !TARGET_OS_OSX // [macOS] if (_mouseHoverRecognizer != nil) { [view removeGestureRecognizer:_mouseHoverRecognizer]; _mouseHoverRecognizer = nil; @@ -504,15 +545,17 @@ - (void)detachFromView:(UIView *)view [view removeGestureRecognizer:_penHoverRecognizer]; _penHoverRecognizer = nil; } +#endif } #pragma mark - UITouch to ActivePointer management -- (void)_registerTouches:(NSSet *)touches withEvent:(UIEvent *)event +- (void)_registerTouches:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] ActivePointer activePointer = {}; +#if !TARGET_OS_OSX // [macOS] // Determine the identifier of the Pointer and if it is the primary pointer switch (touch.type) { @@ -534,7 +577,7 @@ - (void)_registerTouches:(NSSet *)touches withEvent:(UIEvent *)event } break; } - +#endif // [macOS] // If the pointer has not been marked as hovering over views before the touch started, we register // that the activeTouch should not maintain its hovered state once the pointer has been lifted. auto currentlyHoveredViews = [_currentlyHoveredViewsPerPointer objectForKey:@(activePointer.identifier)]; @@ -542,18 +585,32 @@ - (void)_registerTouches:(NSSet *)touches withEvent:(UIEvent *)event activePointer.shouldLeaveWhenReleased = YES; } +#if !TARGET_OS_OSX // [macOS] activePointer.initialComponentView = FindClosestFabricManagedTouchableView(touch.view); +#else // [macOS + CGPoint touchLocation = [_rootComponentView.superview convertPoint:touch.locationInWindow fromView:nil]; + RCTPlatformView *componentView = (RCTPlatformView *) [_rootComponentView hitTest:touchLocation]; + activePointer.initialComponentView = FindClosestFabricManagedTouchableView(componentView); +#endif // macOS] UpdateActivePointerWithUITouch(activePointer, touch, event, _rootComponentView); +#if !TARGET_OS_OSX // [macOS] _activePointers.emplace(touch, activePointer); +#else // [macOS + _activePointers.emplace(touch.eventNumber, activePointer); +#endif // macOS] } } -- (void)_updateTouches:(NSSet *)touches withEvent:(UIEvent *)event +- (void)_updateTouches:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] +#if !TARGET_OS_OSX // [macOS] auto iterator = _activePointers.find(touch); +#else // [macOS + auto iterator = _activePointers.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activePointers.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activePointers.end()) { continue; @@ -562,10 +619,14 @@ - (void)_updateTouches:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)_unregisterTouches:(NSSet *)touches +- (void)_unregisterTouches:(NSSet *)touches // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] +#if !TARGET_OS_OSX // [macOS] auto iterator = _activePointers.find(touch); +#else // [macOS + auto iterator = _activePointers.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activePointers.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activePointers.end()) { continue; @@ -576,6 +637,7 @@ - (void)_unregisterTouches:(NSSet *)touches _primaryTouchPointerId = -1; } +#if !TARGET_OS_OSX // [macOS] // only need to enqueue if the touch type isn't one with a reserved identifier switch (touch.type) { case UITouchTypeIndirectPointer: @@ -588,16 +650,23 @@ - (void)_unregisterTouches:(NSSet *)touches } _activePointers.erase(touch); +#else // [macOS + _activePointers.erase(touch.eventNumber); +#endif // macOS] } } -- (std::vector)_activePointersFromTouches:(NSSet *)touches +- (std::vector)_activePointersFromTouches:(NSSet *)touches // [macOS] { std::vector activePointers; activePointers.reserve(touches.count); - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] +#if !TARGET_OS_OSX // [macOS] auto iterator = _activePointers.find(touch); +#else // [macOS + auto iterator = _activePointers.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activePointers.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activePointers.end()) { continue; @@ -610,6 +679,7 @@ - (void)_unregisterTouches:(NSSet *)touches - (void)_dispatchActivePointers:(std::vector)activePointers eventType:(RCTPointerEventType)eventType { +#if !TARGET_OS_OSX // [macOS] for (const auto &activePointer : activePointers) { PointerEvent pointerEvent = CreatePointerEventFromActivePointer(activePointer, eventType, _rootComponentView); [self handleIncomingPointerEvent:pointerEvent onView:activePointer.componentView]; @@ -648,11 +718,13 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event } } } +#endif // [macOS] } #pragma mark - `UIResponder`-ish touch-delivery methods -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +#if !TARGET_OS_OSX // [macOS] +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { [super touchesBegan:touches withEvent:event]; @@ -666,7 +738,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { [super touchesMoved:touches withEvent:event]; @@ -676,7 +748,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event self.state = UIGestureRecognizerStateChanged; } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { [super touchesEnded:touches withEvent:event]; @@ -691,7 +763,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { [super touchesCancelled:touches withEvent:event]; @@ -705,6 +777,94 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event self.state = UIGestureRecognizerStateChanged; } } +#else // [macOS +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + // This will only be called if the hit-tested view returns YES for acceptsFirstMouse, + // therefore asking it again would be redundant. + return YES; +} + +- (void)mouseDown:(NSEvent *)event +{ + [super mouseDown:event]; + + { + NSSet* touches = [NSSet setWithObject:event]; + [self _registerTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeStart]; + + if (self.state == NSGestureRecognizerStatePossible) { + self.state = NSGestureRecognizerStateBegan; + } else if (self.state == NSGestureRecognizerStateBegan) { + self.state = NSGestureRecognizerStateChanged; + } + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + [super rightMouseDown:event]; + + { + NSSet* touches = [NSSet setWithObject:event]; + [self _registerTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeStart]; + + if (self.state == NSGestureRecognizerStatePossible) { + self.state = NSGestureRecognizerStateBegan; + } else if (self.state == NSGestureRecognizerStateBegan) { + self.state = NSGestureRecognizerStateChanged; + } + } +} + +- (void)mouseDragged:(NSEvent *)event +{ + [super mouseDragged:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeMove]; + + self.state = NSGestureRecognizerStateChanged; +} + +- (void)rightMouseDragged:(NSEvent *)event +{ + [super rightMouseDragged:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeMove]; + + self.state = NSGestureRecognizerStateChanged; +} + +- (void)mouseUp:(NSEvent *)event +{ + [super mouseUp:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeEnd]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateEnded; +} + +- (void)rightMouseUp:(NSEvent *)event +{ + [super rightMouseUp:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches withEvent:event]; // [macOS] + [self _dispatchActivePointers:[self _activePointersFromTouches:touches] eventType:RCTPointerEventTypeEnd]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateEnded; +} +#endif // macOS] - (void)reset { @@ -733,6 +893,7 @@ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecog #pragma mark - Hover callbacks +#if !TARGET_OS_OSX // [macOS] - (void)penHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) { [self hovering:recognizer pointerId:kPencilPointerId pointerType:"pen"]; @@ -770,9 +931,11 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer eventEmitter->onPointerMove(event); } } +#endif // [macOS] #pragma mark - Shared pointer handlers +#if !TARGET_OS_OSX // [macOS] /** * Private method which is used for tracking the location of pointer events to manage the entering/leaving events. * The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down, @@ -868,5 +1031,6 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer return eventPathViews; } +#endif // [macOS] @end diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.h b/packages/react-native/React/Fabric/RCTSurfacePresenter.h index 7c28b3773624ca..741eceecbb9be4 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.h +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -79,7 +79,7 @@ NS_ASSUME_NONNULL_BEGIN /* * Please do not use this, this will be deleted soon. */ -- (nullable UIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; +- (nullable RCTUIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; // [macOS] @end diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 3557485f5fdb60..dde791eb424a3b 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -105,10 +105,12 @@ - (instancetype)initWithContextContainer:(ContextContainer::Shared)contextContai _scheduler = [self _createScheduler]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; +#endif // [macOS] } return self; @@ -170,10 +172,10 @@ - (RCTFabricSurface *)surfaceForRootTag:(ReactTag)rootTag initialProperties:initialProperties]; } -- (UIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag +- (RCTUIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag // [macOS] { - UIView *componentView = - [_mountingManager.componentViewRegistry findComponentViewWithTag:tag]; + RCTUIView *componentView = + [_mountingManager.componentViewRegistry findComponentViewWithTag:tag]; // [macOS] return componentView; } @@ -185,8 +187,8 @@ - (BOOL)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag props:(NSDictiona } ReactTag tag = [reactTag integerValue]; - UIView *componentView = - [_mountingManager.componentViewRegistry findComponentViewWithTag:tag]; + RCTUIView *componentView = + [_mountingManager.componentViewRegistry findComponentViewWithTag:tag]; // [macOS] if (componentView == nil) { return NO; // This view probably isn't managed by Fabric } @@ -264,6 +266,10 @@ - (RCTScheduler *)_createScheduler CoreFeatures::enablePropIteratorSetter = true; } + if (reactNativeConfig && reactNativeConfig->getBool("rn_convergence:dispatch_pointer_events")) { + RCTSetDispatchW3CPointerEvents(YES); + } + if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:use_native_state")) { CoreFeatures::useNativeState = true; } diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenterBridgeAdapter.h b/packages/react-native/React/Fabric/RCTSurfacePresenterBridgeAdapter.h index 188d19992c6760..1a701779c4ec4e 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenterBridgeAdapter.h +++ b/packages/react-native/React/Fabric/RCTSurfacePresenterBridgeAdapter.h @@ -7,7 +7,7 @@ #import #import -#import +#import // [macOS] #import NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Fabric/RCTSurfaceRegistry.h b/packages/react-native/React/Fabric/RCTSurfaceRegistry.h index 4b369cc64b4143..79ee5a7e10537a 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceRegistry.h +++ b/packages/react-native/React/Fabric/RCTSurfaceRegistry.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h index 6515e5f1750c3c..f3f802d256592d 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -15,8 +15,8 @@ NS_ASSUME_NONNULL_BEGIN * Attaches (and detaches) a view to the touch handler. * The receiver does not retain the provided view. */ -- (void)attachToView:(UIView *)view; -- (void)detachFromView:(UIView *)view; +- (void)attachToView:(RCTUIView *)view; // [macOS] +- (void)detachFromView:(RCTUIView *)view; // [macOS] /* * Offset of the attached view relative to the root component in points. diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm index a793251fc622c3..c58cd5f237646b 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import "RCTConversions.h" #import "RCTSurfacePointerHandler.h" @@ -31,7 +32,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { /* * A component view on which the touch was begun. */ - __strong UIView *componentView = nil; + __strong RCTUIView *componentView = nil; // [macOS] struct Hasher { size_t operator()(const ActiveTouch &activeTouch) const @@ -50,15 +51,21 @@ bool operator()(const ActiveTouch &lhs, const ActiveTouch &rhs) const static void UpdateActiveTouchWithUITouch( ActiveTouch &activeTouch, - UITouch *uiTouch, - UIView *rootComponentView, - CGPoint rootViewOriginOffset) + RCTUITouch *uiTouch, // [macOS] + RCTUIView *rootComponentView, // [macOS] + CGPoint rootViewOriginOffset) // [macOS] { +#if !TARGET_OS_OSX // [macOS] CGPoint offsetPoint = [uiTouch locationInView:activeTouch.componentView]; CGPoint pagePoint = [uiTouch locationInView:rootComponentView]; CGPoint screenPoint = [rootComponentView convertPoint:pagePoint toCoordinateSpace:rootComponentView.window.screen.coordinateSpace]; pagePoint = CGPointMake(pagePoint.x + rootViewOriginOffset.x, pagePoint.y + rootViewOriginOffset.y); +#else // [macOS + CGPoint offsetPoint = [activeTouch.componentView convertPoint:uiTouch.locationInWindow fromView:nil]; + CGPoint screenPoint = uiTouch.locationInWindow; + CGPoint pagePoint = CGPointMake(screenPoint.x, CGRectGetHeight(rootComponentView.window.frame) - screenPoint.y); +#endif // macOS] activeTouch.touch.offsetPoint = RCTPointFromCGPoint(offsetPoint); activeTouch.touch.screenPoint = RCTPointFromCGPoint(screenPoint); @@ -66,22 +73,59 @@ static void UpdateActiveTouchWithUITouch( activeTouch.touch.timestamp = uiTouch.timestamp; +#if !TARGET_OS_OSX // [macOS] if (RCTForceTouchAvailable()) { activeTouch.touch.force = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce); } +#else // [macOS + NSEventType type = uiTouch.type; + if (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp || type == NSEventTypeLeftMouseDragged) { + activeTouch.touch.button = 0; + } else if (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp || type == NSEventTypeRightMouseDragged) { + activeTouch.touch.button = 2; + } + + NSEventModifierFlags modifierFlags = uiTouch.modifierFlags; + if (modifierFlags & NSEventModifierFlagOption) { + activeTouch.touch.altKey = true; + } + if (modifierFlags & NSEventModifierFlagControl) { + activeTouch.touch.ctrlKey = true; + } + if (modifierFlags & NSEventModifierFlagShift) { + activeTouch.touch.shiftKey = true; + } + if (modifierFlags & NSEventModifierFlagCommand) { + activeTouch.touch.metaKey = true; + } +#endif // macOS] } -static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponentView, CGPoint rootViewOriginOffset) +static ActiveTouch CreateTouchWithUITouch(RCTUITouch *uiTouch, RCTUIView *rootComponentView, CGPoint rootViewOriginOffset) // [macOS] { ActiveTouch activeTouch = {}; // Find closest Fabric-managed touchable view - UIView *componentView = uiTouch.view; +#if !TARGET_OS_OSX // [macOS] + RCTUIView *componentView = uiTouch.view; // [macOS] +#else // [macOS + CGPoint touchLocation = [rootComponentView.superview convertPoint:uiTouch.locationInWindow fromView:nil]; + RCTUIView *componentView = (RCTUIView *) [rootComponentView hitTest:touchLocation]; // [macOS] +#endif // macOS] while (componentView) { +#if !TARGET_OS_OSX // [macOS] + CGPoint offsetPoint = [uiTouch locationInView:componentView]; +#else // [macOS + CGPoint offsetPoint = [componentView convertPoint:uiTouch.locationInWindow fromView:nil]; +#endif // macOS] if ([componentView respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { activeTouch.eventEmitter = [(id)componentView - touchEventEmitterAtPoint:[uiTouch locationInView:componentView]]; + touchEventEmitterAtPoint:offsetPoint]; +#if !TARGET_OS_OSX // [macOS] activeTouch.touch.target = (Tag)componentView.tag; +#else // [macOS + activeTouch.touch.target = (Tag)(componentView.reactTag.intValue); +#endif // macOS] activeTouch.componentView = componentView; break; } @@ -92,9 +136,10 @@ static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponen return activeTouch; } +#if !TARGET_OS_OSX // [macOS] static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) { return NO; } @@ -102,15 +147,16 @@ static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) return YES; } -static BOOL AnyTouchesChanged(NSSet *touches) +static BOOL AnyTouchesChanged(NSSet *touches) // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) { return YES; } } return NO; } +#endif // [macOS] /** * Surprisingly, `__unsafe_unretained id` pointers are not regular pointers @@ -130,13 +176,17 @@ @interface RCTSurfaceTouchHandler () @end @implementation RCTSurfaceTouchHandler { - std::unordered_map<__unsafe_unretained UITouch *, ActiveTouch, PointerHasher<__unsafe_unretained UITouch *>> +#if !TARGET_OS_OSX // [macOS] + std::unordered_map<__unsafe_unretained RCTUITouch *, ActiveTouch, PointerHasher<__unsafe_unretained RCTUITouch *>> _activeTouches; +#else // [macOS + std::unordered_map _activeTouches; +#endif // macOS] /* * We hold the view weakly to prevent a retain cycle. */ - __weak UIView *_rootComponentView; + __weak RCTUIView *_rootComponentView; // [macOS] RCTIdentifierPool<11> _identifierPool; RCTSurfacePointerHandler *_pointerHandler; @@ -149,9 +199,11 @@ - (instancetype)init // to be used as a top level event delegated recognizer. // Otherwise, lower-level components not built using React Native, // will fail to recognize gestures. +#if !TARGET_OS_OSX // [macOS] self.cancelsTouchesInView = NO; self.delaysTouchesBegan = NO; // This is default value. self.delaysTouchesEnded = NO; +#endif // [macOS] self.delegate = self; @@ -165,7 +217,7 @@ - (instancetype)init RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action) -- (void)attachToView:(UIView *)view +- (void)attachToView:(RCTUIView *)view // [macOS] { RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view."); @@ -177,7 +229,7 @@ - (void)attachToView:(UIView *)view } } -- (void)detachFromView:(UIView *)view +- (void)detachFromView:(RCTUIView *)view // [macOS] { RCTAssertParam(view); RCTAssert(self.view == view, @"RCTTouchHandler attached to another view."); @@ -190,19 +242,27 @@ - (void)detachFromView:(UIView *)view } } -- (void)_registerTouches:(NSSet *)touches +- (void)_registerTouches:(NSSet *)touches // [macOS] { - for (UITouch *touch in touches) { - auto activeTouch = CreateTouchWithUITouch(touch, _rootComponentView, _viewOriginOffset); + for (RCTUITouch *touch in touches) { // [macOS] + auto activeTouch = CreateTouchWithUITouch(touch, _rootComponentView, _viewOriginOffset); activeTouch.touch.identifier = _identifierPool.dequeue(); +#if !TARGET_OS_OSX // [macOS] _activeTouches.emplace(touch, activeTouch); +#else // [macOS + _activeTouches.emplace(touch.eventNumber, activeTouch); +#endif // macOS] } } -- (void)_updateTouches:(NSSet *)touches +- (void)_updateTouches:(NSSet *)touches // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] +#if !TARGET_OS_OSX // [macOS] auto iterator = _activeTouches.find(touch); +#else // [macOS + auto iterator = _activeTouches.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activeTouches.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activeTouches.end()) { continue; @@ -212,27 +272,39 @@ - (void)_updateTouches:(NSSet *)touches } } -- (void)_unregisterTouches:(NSSet *)touches +- (void)_unregisterTouches:(NSSet *)touches // [macOS] { - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { // [macOS] +#if !TARGET_OS_OSX // [macOS] auto iterator = _activeTouches.find(touch); +#else // [macOS + auto iterator = _activeTouches.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activeTouches.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activeTouches.end()) { continue; } auto &activeTouch = iterator->second; _identifierPool.enqueue(activeTouch.touch.identifier); +#if !TARGET_OS_OSX // [macOS] _activeTouches.erase(touch); +#else // [macOS + _activeTouches.erase(touch.eventNumber); +#endif // macOS] } } -- (std::vector)_activeTouchesFromTouches:(NSSet *)touches +- (std::vector)_activeTouchesFromTouches:(NSSet *)touches // [macOS] { std::vector activeTouches; activeTouches.reserve(touches.count); - for (UITouch *touch in touches) { + for (RCTUITouch *touch in touches) { +#if !TARGET_OS_OSX // [macOS] auto iterator = _activeTouches.find(touch); +#else // [macOS + auto iterator = _activeTouches.find(touch.eventNumber); +#endif // macOS] RCTAssert(iterator != _activeTouches.end(), @"Inconsistency between local and UIKit touch registries"); if (iterator == _activeTouches.end()) { continue; @@ -300,7 +372,9 @@ - (void)_dispatchActiveTouches:(std::vector)activeTouches eventType #pragma mark - `UIResponder`-ish touch-delivery methods -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +#if !TARGET_OS_OSX // [macOS] + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // [macOS] { [super touchesBegan:touches withEvent:event]; @@ -314,7 +388,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; @@ -324,7 +398,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event self.state = UIGestureRecognizerStateChanged; } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; @@ -339,7 +413,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event } } -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; @@ -354,6 +428,97 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event } } +#else // [macOS + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + // This will only be called if the hit-tested view returns YES for acceptsFirstMouse, + // therefore asking it again would be redundant. + return YES; +} + +- (void)mouseDown:(NSEvent *)event +{ + [super mouseDown:event]; + + { + NSSet* touches = [NSSet setWithObject:event]; + [self _registerTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchStart]; + + if (self.state == NSGestureRecognizerStatePossible) { + self.state = NSGestureRecognizerStateBegan; + } else if (self.state == NSGestureRecognizerStateBegan) { + self.state = NSGestureRecognizerStateChanged; + } + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + [super rightMouseDown:event]; + + { + NSSet* touches = [NSSet setWithObject:event]; + [self _registerTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchStart]; + + if (self.state == NSGestureRecognizerStatePossible) { + self.state = NSGestureRecognizerStateBegan; + } else if (self.state == NSGestureRecognizerStateBegan) { + self.state = NSGestureRecognizerStateChanged; + } + } +} + +- (void)mouseDragged:(NSEvent *)event +{ + [super mouseDragged:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchMove]; + + self.state = NSGestureRecognizerStateChanged; +} + +- (void)rightMouseDragged:(NSEvent *)event +{ + [super rightMouseDragged:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchMove]; + + self.state = NSGestureRecognizerStateChanged; +} + +- (void)mouseUp:(NSEvent *)event +{ + [super mouseUp:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchEnd]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateEnded; +} + +- (void)rightMouseUp:(NSEvent *)event +{ + [super rightMouseUp:event]; + + NSSet* touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; // [macOS] + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchEnd]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateEnded; +} + +#endif // macOS] + - (void)reset { [super reset]; @@ -381,9 +546,13 @@ - (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGes - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer { +#if !TARGET_OS_OSX // [macOS] // We fail in favour of other external gesture recognizers. // iOS will ask `delegate`'s opinion about this gesture recognizer little bit later. return ![preventingGestureRecognizer.view isDescendantOfView:self.view]; +#else // [macOS + return NO; +#endif // macOS] } #pragma mark - UIGestureRecognizerDelegate diff --git a/packages/react-native/React/Fabric/RCTTouchableComponentViewProtocol.h b/packages/react-native/React/Fabric/RCTTouchableComponentViewProtocol.h index 162aa866cb3ef3..b209453956e077 100644 --- a/packages/react-native/React/Fabric/RCTTouchableComponentViewProtocol.h +++ b/packages/react-native/React/Fabric/RCTTouchableComponentViewProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @protocol RCTTouchableComponentViewProtocol diff --git a/packages/react-native/React/Fabric/Surface/RCTFabricSurface.mm b/packages/react-native/React/Fabric/Surface/RCTFabricSurface.mm index 0567a52fd8915c..8b3bf0de7234af 100644 --- a/packages/react-native/React/Fabric/Surface/RCTFabricSurface.mm +++ b/packages/react-native/React/Fabric/Surface/RCTFabricSurface.mm @@ -66,10 +66,12 @@ - (instancetype)initWithSurfacePresenter:(RCTSurfacePresenter *)surfacePresenter [self _updateLayoutContext]; +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContentSizeCategoryDidChangeNotification:) name:UIContentSizeCategoryDidChangeNotification object:nil]; +#endif // [macOS] } return self; diff --git a/packages/react-native/React/Fabric/Utils/RCTGenericDelegateSplitter.h b/packages/react-native/React/Fabric/Utils/RCTGenericDelegateSplitter.h index 77bbac20d7ddc2..fdd6d5643ee656 100644 --- a/packages/react-native/React/Fabric/Utils/RCTGenericDelegateSplitter.h +++ b/packages/react-native/React/Fabric/Utils/RCTGenericDelegateSplitter.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.h b/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.h index 78d4714d386a8b..db5c37443f3823 100644 --- a/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.h +++ b/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -16,14 +16,14 @@ NS_ASSUME_NONNULL_BEGIN * changed from the one provided at initialization time (i.e. recycled). */ @interface RCTReactTaggedView : NSObject { - UIView *_view; + RCTPlatformView *_view; // [macOS] NSInteger _tag; } -+ (RCTReactTaggedView *)wrap:(UIView *)view; ++ (RCTReactTaggedView *)wrap:(RCTPlatformView *)view; // [macOS] -- (instancetype)initWithView:(UIView *)view; -- (nullable UIView *)view; +- (instancetype)initWithView:(RCTPlatformView *)view; // [macOS] +- (nullable RCTPlatformView *)view; // [macOS] - (NSInteger)tag; - (BOOL)isEqual:(id)other; diff --git a/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.mm b/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.mm index 4a4a60ff830be4..171b22644a83f7 100644 --- a/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.mm +++ b/packages/react-native/React/Fabric/Utils/RCTReactTaggedView.mm @@ -7,14 +7,16 @@ #import "RCTReactTaggedView.h" +#import + @implementation RCTReactTaggedView -+ (RCTReactTaggedView *)wrap:(UIView *)view ++ (RCTReactTaggedView *)wrap:(RCTUIView *)view // [macOS] { return [[RCTReactTaggedView alloc] initWithView:view]; } -- (instancetype)initWithView:(UIView *)view +- (instancetype)initWithView:(RCTUIView *)view // [macOS] { if (self = [super init]) { _view = view; @@ -23,7 +25,7 @@ - (instancetype)initWithView:(UIView *)view return self; } -- (nullable UIView *)view +- (nullable RCTUIView *)view // [macOS] { if (_view.tag == _tag) { return _view; diff --git a/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m b/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m new file mode 100644 index 00000000000000..f685305cd901fa --- /dev/null +++ b/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m @@ -0,0 +1,166 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + // [macOS] + #if TARGET_OS_OSX + +#import "RCTAccessibilityManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTUIManager.h" + +NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = + @"RCTAccessibilityManagerDidUpdateMultiplierNotification"; + +@implementation RCTAccessibilityManager +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +static void *AccessibilityVoiceOverChangeContext = &AccessibilityVoiceOverChangeContext; + ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (instancetype)init +{ + if (self = [super init]) { + [[NSWorkspace sharedWorkspace] addObserver:self + forKeyPath:@"voiceOverEnabled" + options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) + context:AccessibilityVoiceOverChangeContext]; + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self + selector:@selector(accessibilityDisplayOptionsChange:) + name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification + object:nil]; + _isHighContrastEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast]; + _isInvertColorsEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors]; + _isReduceMotionEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]; + _isReduceTransparencyEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]; + _isVoiceOverEnabled = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + } + return self; +} + +- (void)dealloc +{ + [[NSWorkspace sharedWorkspace] removeObserver:self + forKeyPath:@"voiceOverEnabled" + context:AccessibilityVoiceOverChangeContext]; +} + +RCT_EXPORT_METHOD(announceForAccessibility:(NSString *)announcement) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSAccessibilityPostNotificationWithUserInfo( + NSApp, + NSAccessibilityAnnouncementRequestedNotification, + @{NSAccessibilityAnnouncementKey : announcement, + NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh) + } + ); + }); +} + +RCT_EXPORT_METHOD(getCurrentHighContrastState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + callback(@[@(_isHighContrastEnabled)]); +} + +RCT_EXPORT_METHOD(getCurrentInvertColorsState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + callback(@[@(_isInvertColorsEnabled)]); +} + +RCT_EXPORT_METHOD(getCurrentReduceMotionState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + callback(@[@(_isReduceMotionEnabled)]); +} + +RCT_EXPORT_METHOD(getCurrentReduceTransparencyState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + callback(@[@(_isReduceTransparencyEnabled)]); +} + +RCT_EXPORT_METHOD(getCurrentVoiceOverState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + BOOL isVoiceOverEnabled = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + callback(@[ @(isVoiceOverEnabled) ]); +} + +RCT_EXPORT_METHOD(setAccessibilityFocus:(nonnull NSNumber *)reactTag) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSView *view = [self.bridge.uiManager viewForReactTag:reactTag]; + [[view window] makeFirstResponder:view]; + NSAccessibilityPostNotification(view, NSAccessibilityLayoutChangedNotification); + }); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (context == AccessibilityVoiceOverChangeContext) { + BOOL newIsVoiceOverEnabled = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + if (_isVoiceOverEnabled != newIsVoiceOverEnabled) { + _isVoiceOverEnabled = newIsVoiceOverEnabled; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"screenReaderChanged" + body:@(_isVoiceOverEnabled)]; +#pragma clang diagnostic pop + } + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} + +- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification +{ + BOOL newHighContrastEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast]; + BOOL newInvertColorsEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors]; + BOOL newReduceMotionEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]; + BOOL newReduceTransparencyEnabled = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]; + + if (_isHighContrastEnabled != newHighContrastEnabled) { + _isHighContrastEnabled = newHighContrastEnabled; + [_bridge.eventDispatcher sendDeviceEventWithName:@"highContrastChanged" + body:@(_isHighContrastEnabled)]; + } + if (_isInvertColorsEnabled != newInvertColorsEnabled) { + _isInvertColorsEnabled = newInvertColorsEnabled; + [_bridge.eventDispatcher sendDeviceEventWithName:@"invertColorsChanged" + body:@(_isInvertColorsEnabled)]; + } + if (_isReduceMotionEnabled != newReduceMotionEnabled) { + _isReduceMotionEnabled = newReduceMotionEnabled; + [_bridge.eventDispatcher sendDeviceEventWithName:@"reduceMotionChanged" + body:@(_isReduceMotionEnabled)]; + } + if (_isReduceTransparencyEnabled != newReduceTransparencyEnabled) { + _isReduceTransparencyEnabled = newReduceTransparencyEnabled; + [_bridge.eventDispatcher sendDeviceEventWithName:@"reduceTransparencyChanged" + body:@(_isReduceTransparencyEnabled)]; + } +} + +@end + #endif diff --git a/packages/react-native/React/Modules/RCTEventEmitter.h b/packages/react-native/React/Modules/RCTEventEmitter.h index 6ad213393ad440..b2b203a0c1a1b6 100644 --- a/packages/react-native/React/Modules/RCTEventEmitter.h +++ b/packages/react-native/React/Modules/RCTEventEmitter.h @@ -13,24 +13,24 @@ */ @interface RCTEventEmitter : NSObject -@property (nonatomic, weak) RCTBridge *bridge; -@property (nonatomic, weak) RCTModuleRegistry *moduleRegistry; -@property (nonatomic, weak) RCTViewRegistry *viewRegistry_DEPRECATED; +@property (nonatomic, weak) RCTBridge * _Nullable bridge; // [macOS] +@property (nonatomic, weak) RCTModuleRegistry * _Nullable moduleRegistry; // [macOS] +@property (nonatomic, weak) RCTViewRegistry * _Nullable viewRegistry_DEPRECATED; // [macOS] -- (instancetype)initWithDisabledObservation; +- (instancetype _Nullable)initWithDisabledObservation; // [macOS] /** * Override this method to return an array of supported event names. Attempting * to observe or send an event that isn't included in this list will result in * an error. */ -- (NSArray *)supportedEvents; +- (NSArray *_Nullable)supportedEvents; // [macOS] /** * Send an event that does not relate to a specific view, e.g. a navigation * or data update notification. */ -- (void)sendEventWithName:(NSString *)name body:(id)body; +- (void)sendEventWithName:(NSString *_Nullable)name body:(id _Nullable )body; // [macOS] - (BOOL)canSendEvents_DEPRECATED; @@ -44,7 +44,7 @@ - (void)invalidate NS_REQUIRES_SUPER; -- (void)addListener:(NSString *)eventName; +- (void)addListener:(NSString *_Nullable)eventName; // [macOS] - (void)removeListeners:(double)count; @end diff --git a/packages/react-native/React/Modules/RCTI18nUtil.m b/packages/react-native/React/Modules/RCTI18nUtil.m index f77cb0c103bf5f..575c9e16069b6a 100644 --- a/packages/react-native/React/Modules/RCTI18nUtil.m +++ b/packages/react-native/React/Modules/RCTI18nUtil.m @@ -7,7 +7,7 @@ #import "RCTI18nUtil.h" -#import +#import // [macOS] @implementation RCTI18nUtil diff --git a/packages/react-native/React/Modules/RCTLayoutAnimation.h b/packages/react-native/React/Modules/RCTLayoutAnimation.h index 4248dca850a3b3..f95d6a8cd675dc 100644 --- a/packages/react-native/React/Modules/RCTLayoutAnimation.h +++ b/packages/react-native/React/Modules/RCTLayoutAnimation.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Modules/RCTLayoutAnimation.m b/packages/react-native/React/Modules/RCTLayoutAnimation.m index d88fa0fba699ae..64e697cf578cd5 100644 --- a/packages/react-native/React/Modules/RCTLayoutAnimation.m +++ b/packages/react-native/React/Modules/RCTLayoutAnimation.m @@ -11,8 +11,11 @@ @implementation RCTLayoutAnimation +#if !TARGET_OS_OSX // [macOS] static UIViewAnimationCurve _currentKeyboardAnimationCurve; +#endif // [macOS] +#if !TARGET_OS_OSX // [macOS] static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnimationType type) { switch (type) { @@ -32,7 +35,26 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim return UIViewAnimationOptionCurveEaseInOut; } } +#else // [macOS +static NSString *CAMediaTimingFunctionNameFromRCTAnimationType(RCTAnimationType type) +{ + switch (type) { + case RCTAnimationTypeLinear: + return kCAMediaTimingFunctionLinear; + case RCTAnimationTypeEaseIn: + return kCAMediaTimingFunctionEaseIn; + case RCTAnimationTypeEaseOut: + return kCAMediaTimingFunctionEaseOut; + case RCTAnimationTypeEaseInEaseOut: + return kCAMediaTimingFunctionEaseInEaseOut; + default: + RCTLogError(@"Unsupported animation type %zd", type); + return kCAMediaTimingFunctionDefault; + } +} +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] // Use a custom initialization function rather than implementing `+initialize` so that we can control // when the initialization code runs. `+initialize` runs immediately before the first message is sent // to the class which may be too late for us. By this time, we may have missed some @@ -53,6 +75,7 @@ + (void)keyboardWillChangeFrame:(NSNotification *)notification NSDictionary *userInfo = notification.userInfo; _currentKeyboardAnimationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; } +#endif // [macOS] - (instancetype)initWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay @@ -95,10 +118,12 @@ - (instancetype)initWithDuration:(NSTimeInterval)duration config:(NSDictionary * } _animationType = [RCTConvert RCTAnimationType:config[@"type"]]; +#if !TARGET_OS_OSX // [macOS] if (_animationType == RCTAnimationTypeSpring) { _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; } +#endif // [macOS] } return self; @@ -106,6 +131,7 @@ - (instancetype)initWithDuration:(NSTimeInterval)duration config:(NSDictionary * - (void)performAnimations:(void (^)(void))animations withCompletionBlock:(void (^)(BOOL completed))completionBlock { +#if !TARGET_OS_OSX // [macOS] if (_animationType == RCTAnimationTypeSpring) { [UIView animateWithDuration:_duration delay:_delay @@ -124,6 +150,36 @@ - (void)performAnimations:(void (^)(void))animations withCompletionBlock:(void ( animations:animations completion:completionBlock]; } +#else // [macOS + NSString *timingFunctionName = CAMediaTimingFunctionNameFromRCTAnimationType(_animationType); + + CAMediaTimingFunction *timingFunction = [CAMediaTimingFunction functionWithName:timingFunctionName]; + NSTimeInterval duration = _duration; + + dispatch_block_t runAnimationGroup = ^{ + [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { + context.duration = duration; + context.timingFunction = timingFunction; + context.allowsImplicitAnimation = YES; + + if (animations != nil) { + animations(); + } + } + completionHandler:^{ + if (completionBlock != nil) { + completionBlock(YES); + } + }]; + }; + + if (_delay == 0.0) { + runAnimationGroup(); + } else { + dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_delay * NSEC_PER_SEC)); + dispatch_after(time, dispatch_get_main_queue(), runAnimationGroup); + } +#endif // macOS] } - (BOOL)isEqual:(RCTLayoutAnimation *)animation diff --git a/packages/react-native/React/Modules/RCTLayoutAnimationGroup.h b/packages/react-native/React/Modules/RCTLayoutAnimationGroup.h index 02ae76278f15d9..02b01c325181cc 100644 --- a/packages/react-native/React/Modules/RCTLayoutAnimationGroup.h +++ b/packages/react-native/React/Modules/RCTLayoutAnimationGroup.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.h b/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.h index 1308cdea2f4904..7fa65162613921 100644 --- a/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.h +++ b/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import @protocol RCTRedBoxExtraDataActionDelegate @@ -18,3 +19,5 @@ - (void)addExtraData:(NSDictionary *)data forIdentifier:(NSString *)identifier; @end +#endif // [macOS] + diff --git a/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.m b/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.m index fb2b2e338477b2..702bad5a873e03 100644 --- a/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.m +++ b/packages/react-native/React/Modules/RCTRedBoxExtraDataViewController.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTRedBoxExtraDataViewController.h" @interface RCTRedBoxExtraDataCell : UITableViewCell @@ -245,3 +246,4 @@ - (void)reload } @end +#endif // [macOS] diff --git a/packages/react-native/React/Modules/RCTSurfacePresenterStub.h b/packages/react-native/React/Modules/RCTSurfacePresenterStub.h index 9cf334854744ca..2b56d4b850ac23 100644 --- a/packages/react-native/React/Modules/RCTSurfacePresenterStub.h +++ b/packages/react-native/React/Modules/RCTSurfacePresenterStub.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN - (id)createFabricSurfaceForModuleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties; -- (nullable UIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; +- (nullable RCTPlatformView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; // [macOS] - (BOOL)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag props:(NSDictionary *)props; - (void)addObserver:(id)observer; - (void)removeObserver:(id)observer; diff --git a/packages/react-native/React/Modules/RCTUIManager.h b/packages/react-native/React/Modules/RCTUIManager.h index 0979c965f8fdca..ae70d203c401ff 100644 --- a/packages/react-native/React/Modules/RCTUIManager.h +++ b/packages/react-native/React/Modules/RCTUIManager.h @@ -5,13 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#import - #import #import #import #import #import +#import // [macOS] #import /** @@ -20,6 +19,8 @@ */ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification; +void RCTTraverseViewNodes(id view, void (^block)(id)); // [macOS] + @class RCTLayoutAnimationGroup; @class RCTUIManagerObserverCoordinator; @@ -37,7 +38,7 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier /** * Register a root view with the RCTUIManager. */ -- (void)registerRootView:(UIView *)rootView; +- (void)registerRootView:(RCTUIView *)rootView; // [macOS] /** * Gets the view name associated with a reactTag. @@ -47,7 +48,7 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier /** * Gets the view associated with a reactTag. */ -- (UIView *)viewForReactTag:(NSNumber *)reactTag; +- (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag; // [macOS] /** * Gets the shadow view associated with a reactTag. @@ -61,7 +62,7 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier * this value does not affect root node size style properties. * Can be considered as something similar to `setSize:forView:` but applicable only for root view. */ -- (void)setAvailableSize:(CGSize)availableSize forRootView:(UIView *)rootView; +- (void)setAvailableSize:(CGSize)availableSize forRootView:(RCTUIView *)rootView; // [macOS] /** * Sets local data for a shadow view corresponded with given view. @@ -72,20 +73,20 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier * the shadow view. * Please respect one-directional data flow of React. */ -- (void)setLocalData:(NSObject *)localData forView:(UIView *)view; +- (void)setLocalData:(NSObject *)localData forView:(RCTUIView *)view; // [macOS] /** * Set the size of a view. This might be in response to a screen rotation * or some other layout event outside of the React-managed view hierarchy. */ -- (void)setSize:(CGSize)size forView:(UIView *)view; +- (void)setSize:(CGSize)size forView:(RCTPlatformView *)view; // [macOS] /** * Set the natural size of a view, which is used when no explicit size is set. * Use `UIViewNoIntrinsicMetric` to ignore a dimension. * The `size` must NOT include padding and border. */ -- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(UIView *)view; +- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(RCTPlatformView *)view; // [macOS] /** * Sets up layout animation which will perform on next layout pass. @@ -124,7 +125,7 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier * @param completion the completion block that will hand over the rootView, if any. * */ -- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView *view))completion; +- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(RCTPlatformView *view))completion; // [macOS] /** * Finds a view that is tagged with nativeID as its nativeID prop @@ -134,7 +135,7 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier * @param nativeID the id reference to native component relative to root view. * @param rootTag the react tag of root view hierarchy from which to find the view. */ -- (UIView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag; +- (RCTPlatformView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag; // [macOS] /** * Register a view that is tagged with nativeID as its nativeID prop @@ -142,12 +143,12 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier * @param nativeID the id reference to native component relative to root view. * @param view the view that is tagged with nativeID as its nativeID prop. */ -- (void)setNativeID:(NSString *)nativeID forView:(UIView *)view; +- (void)setNativeID:(NSString *)nativeID forView:(RCTUIView *)view; // [macOS] /** * The view that is currently first responder, according to the JS context. */ -+ (UIView *)JSResponder; ++ (RCTPlatformView *)JSResponder; // [macOS] /** * In some cases we might want to trigger layout from native side. diff --git a/packages/react-native/React/Modules/RCTUIManager.m b/packages/react-native/React/Modules/RCTUIManager.m index 9fe11430c8b7f0..b36041d3a8d201 100644 --- a/packages/react-native/React/Modules/RCTUIManager.m +++ b/packages/react-native/React/Modules/RCTUIManager.m @@ -10,6 +10,8 @@ #import #import +#import // [macOS] + #import "RCTAssert.h" #import "RCTBridge+Private.h" #import "RCTBridge.h" @@ -17,6 +19,7 @@ #import "RCTComponentData.h" #import "RCTConvert.h" #import "RCTDefines.h" +#import "RCTDevSettings.h" // [macOS] #import "RCTEventDispatcherProtocol.h" #import "RCTLayoutAnimation.h" #import "RCTLayoutAnimationGroup.h" @@ -27,7 +30,9 @@ #import "RCTRootContentView.h" #import "RCTRootShadowView.h" #import "RCTRootViewInternal.h" +#if !TARGET_OS_OSX // [macOS] #import "RCTScrollableProtocol.h" +#endif // [macOS] #import "RCTShadowView+Internal.h" #import "RCTShadowView.h" #import "RCTSurfaceRootShadowView.h" @@ -38,8 +43,12 @@ #import "RCTView.h" #import "RCTViewManager.h" #import "UIView+React.h" +#import "RCTUIKit.h" // [macOS] +#import "RCTDeviceInfo.h" // [macOS] + +#import -static void RCTTraverseViewNodes(id view, void (^block)(id)) +void RCTTraverseViewNodes(id view, void (^block)(id)) // [macOS] { if (view.reactTag) { block(view); @@ -70,8 +79,8 @@ @implementation RCTUIManager { RCTLayoutAnimationGroup *_layoutAnimationGroup; // Main thread only NSMutableDictionary *_shadowViewRegistry; // RCT thread only - NSMutableDictionary *_viewRegistry; // Main thread only - NSMapTable *_nativeIDRegistry; + NSMutableDictionary *_viewRegistry; // Main thread only // [macOS] + NSMapTable *_nativeIDRegistry; // [macOS] NSMapTable *> *_shadowViewsWithUpdatedProps; // UIManager queue only. NSHashTable *_shadowViewsWithUpdatedChildren; // UIManager queue only. @@ -102,7 +111,7 @@ - (void)invalidate RCTExecuteOnMainQueue(^{ RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"UIManager invalidate", nil); for (NSNumber *rootViewTag in self->_rootViewTags) { - UIView *rootView = self->_viewRegistry[rootViewTag]; + RCTPlatformView *rootView = self->_viewRegistry[rootViewTag]; // [macOS] if ([rootView conformsToProtocol:@protocol(RCTInvalidating)]) { [(id)rootView invalidate]; } @@ -128,7 +137,7 @@ - (void)invalidate return _shadowViewRegistry; } -- (NSMutableDictionary *)viewRegistry +- (NSMutableDictionary *)viewRegistry // [macOS] { // NOTE: this method only exists so that it can be accessed by unit tests if (!_viewRegistry) { @@ -177,6 +186,7 @@ - (void)setBridge:(RCTBridge *)bridge } } +#if !TARGET_OS_OSX // [macOS] // This dispatch_async avoids a deadlock while configuring native modules dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] addObserver:self @@ -190,10 +200,12 @@ - (void)setBridge:(RCTBridge *)bridge name:UIDeviceOrientationDidChangeNotification object:nil]; [RCTLayoutAnimation initializeStatics]; +#endif // [macOS] } #pragma mark - Event emitting +#if !TARGET_OS_OSX // [macOS] - (void)didReceiveNewContentSizeMultiplier { // Report the event across the bridge. @@ -214,7 +226,9 @@ - (void)didReceiveNewContentSizeMultiplier [self setNeedsLayout]; }); } +#endif // [macOS] +#if !TARGET_OS_OSX // [macOS] // Names and coordinate system from html5 spec: // https://developer.mozilla.org/en-US/docs/Web/API/Screen.orientation // https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation @@ -267,6 +281,7 @@ - (void)namedOrientationDidChange body:orientationEvent]; #pragma clang diagnostic pop } +#endif // macOS] - (dispatch_queue_t)methodQueue { @@ -306,7 +321,7 @@ - (void)registerRootView:(RCTRootContentView *)rootView NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @"View %@ with tag #%@ is not a root view", rootView, reactTag); - UIView *existingView = _viewRegistry[reactTag]; + RCTPlatformView *existingView = _viewRegistry[reactTag]; // [macOS] RCTAssert( existingView == nil || existingView == rootView, @"Expect all root views to have unique tag. Added %@ twice", @@ -318,12 +333,13 @@ - (void)registerRootView:(RCTRootContentView *)rootView _viewRegistry[reactTag] = rootView; // Register shadow view + RCTRootShadowView *shadowView = [RCTRootShadowView new]; // [macOS] do this early to prevent RCTI18nUtil deadlock + RCTExecuteOnUIManagerQueue(^{ if (!self->_viewRegistry) { return; } - RCTRootShadowView *shadowView = [RCTRootShadowView new]; shadowView.availableSize = availableSize; shadowView.reactTag = reactTag; shadowView.viewName = NSStringFromClass([rootView class]); @@ -340,7 +356,9 @@ - (NSString *)viewNameForReactTag:(NSNumber *)reactTag return name; } - __block UIView *view; +// Re-enable componentViewName_DO_NOT_USE_THIS_IS_BROKEN once macOS uses Fabric +#if !TARGET_OS_OSX // [macOS] + __block RCTPlatformView *view; // [macOS] RCTUnsafeExecuteOnMainQueueSync(^{ view = self->_viewRegistry[reactTag]; }); @@ -353,13 +371,14 @@ - (NSString *)viewNameForReactTag:(NSNumber *)reactTag } #pragma clang diagnostic pop +#endif // [macOS] return nil; } -- (UIView *)viewForReactTag:(NSNumber *)reactTag +- (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag // [macOS] { RCTAssertMainQueue(); - UIView *view = [_bridge.surfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue]; + RCTPlatformView *view = [_bridge.surfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue]; // [macOS] if (!view) { view = _viewRegistry[reactTag]; } @@ -368,7 +387,9 @@ - (UIView *)viewForReactTag:(NSNumber *)reactTag - (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag { +#if !TARGET_OS_OSX // [macOS] RCTAssertUIManagerQueue(); +#endif // [macOS] return _shadowViewRegistry[reactTag]; } @@ -390,27 +411,40 @@ - (void)_executeBlockWithShadowView:(void (^)(RCTShadowView *shadowView))block f }); } -- (void)setAvailableSize:(CGSize)availableSize forRootView:(UIView *)rootView +- (void)setAvailableSize:(CGSize)availableSize forRootView:(RCTUIView *)rootView // [macOS] { RCTAssertMainQueue(); - [self - _executeBlockWithShadowView:^(RCTShadowView *shadowView) { - RCTAssert( - [shadowView isKindOfClass:[RCTRootShadowView class]], @"Located shadow view is actually not root view."); - RCTRootShadowView *rootShadowView = (RCTRootShadowView *)shadowView; + void (^block)(RCTShadowView *) = ^(RCTShadowView *shadowView) { + RCTAssert( + [shadowView isKindOfClass:[RCTRootShadowView class]], @"Located shadow view is actually not root view."); - if (CGSizeEqualToSize(availableSize, rootShadowView.availableSize)) { - return; - } + RCTRootShadowView *rootShadowView = (RCTRootShadowView *)shadowView; - rootShadowView.availableSize = availableSize; - [self setNeedsLayout]; - } - forTag:rootView.reactTag]; + if (CGSizeEqualToSize(availableSize, rootShadowView.availableSize)) { + return; + } + + rootShadowView.availableSize = availableSize; + [self setNeedsLayout]; + }; + +#if TARGET_OS_OSX // [macOS + if (rootView.inLiveResize) { + NSNumber* tag = rootView.reactTag; + // Synchronously relayout to prevent "tearing" when resizing windows. + // Still run block asynchronously below so it "wins" after any in-flight layout. + RCTUnsafeExecuteOnUIManagerQueueSync(^{ + RCTShadowView *shadowView = self->_shadowViewRegistry[tag]; + block(shadowView); + }); + } +#endif // macOS] + + [self _executeBlockWithShadowView:block forTag:rootView.reactTag]; } -- (void)setLocalData:(NSObject *)localData forView:(UIView *)view +- (void)setLocalData:(NSObject *)localData forView:(RCTUIView *)view // [macOS] { RCTAssertMainQueue(); [self @@ -421,19 +455,19 @@ - (void)setLocalData:(NSObject *)localData forView:(UIView *)view forTag:view.reactTag]; } -- (UIView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag +- (RCTPlatformView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag { if (!nativeID || !rootTag) { return nil; } - UIView *view; + RCTPlatformView *view; // [macOS] @synchronized(self) { view = [_nativeIDRegistry objectForKey:RCTNativeIDRegistryKey(nativeID, rootTag)]; } return view; } -- (void)setNativeID:(NSString *)nativeID forView:(UIView *)view +- (void)setNativeID:(NSString *)nativeID forView:(RCTUIView *)view // [macOS] { if (!nativeID || !view) { return; @@ -447,7 +481,7 @@ - (void)setNativeID:(NSString *)nativeID forView:(UIView *)view }); } -- (void)setSize:(CGSize)size forView:(UIView *)view +- (void)setSize:(CGSize)size forView:(RCTUIView *)view // [macOS] { RCTAssertMainQueue(); [self @@ -462,7 +496,7 @@ - (void)setSize:(CGSize)size forView:(UIView *)view forTag:view.reactTag]; } -- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(UIView *)view +- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(RCTUIView *)view // [macOS] { RCTAssertMainQueue(); [self @@ -591,7 +625,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * CGSize contentSize = shadowView.layoutMetrics.frame.size; RCTExecuteOnMainQueue(^{ - UIView *view = self->_viewRegistry[reactTag]; + RCTPlatformView *view = self->_viewRegistry[reactTag]; // [macOS] RCTAssert(view != nil, @"view (for ID %@) not found", reactTag); RCTRootView *rootView = (RCTRootView *)[view superview]; @@ -603,7 +637,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * } // Perform layout (possibly animated) - return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; RCTLayoutAnimationGroup *layoutAnimationGroup = uiManager->_layoutAnimationGroup; @@ -613,7 +647,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * for (NSNumber *reactTag in reactTags) { RCTFrameData frameData = frameDataArray[index++]; - UIView *view = viewRegistry[reactTag]; + RCTPlatformView *view = viewRegistry[reactTag]; // [macOS] CGRect frame = frameData.frame; UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection; @@ -761,15 +795,15 @@ - (void)_removeChildren:(NSArray> *)children fromContainer:(id< /** * Remove subviews from their parent with an animation. */ -- (void)_removeChildren:(NSArray *)children - fromContainer:(UIView *)container +- (void)_removeChildren:(NSArray *)children // [macOS] + fromContainer:(RCTPlatformView *)container // [macOS] withAnimation:(RCTLayoutAnimationGroup *)animation { RCTAssertMainQueue(); RCTLayoutAnimation *deletingLayoutAnimation = animation.deletingLayoutAnimation; __block NSUInteger completionsCalled = 0; - for (UIView *removedChild in children) { + for (RCTPlatformView *removedChild in children) { // [macOS] void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; @@ -791,14 +825,26 @@ - (void)_removeChildren:(NSArray *)children // Here the problem: the default implementation of `-[UIView removeReactSubview:]` also removes the view from // UIKit's hierarchy. So, let's temporary restore the view back after removing. To do so, we have to memorize // original `superview` (which can differ from `container`) and an index of removed view. - UIView *originalSuperview = removedChild.superview; + RCTPlatformView *originalSuperview = removedChild.superview; // [macOS] NSUInteger originalIndex = [originalSuperview.subviews indexOfObjectIdenticalTo:removedChild]; +#if TARGET_OS_OSX // [macOS + NSView *nextLowerView = nil; + if (originalIndex > 0) { + nextLowerView = [originalSuperview.subviews objectAtIndex:originalIndex - 1]; + } +#endif // macOS] [container removeReactSubview:removedChild]; // Disable user interaction while the view is animating // since the view is (conceptually) deleted and not supposed to be interactive. - removedChild.userInteractionEnabled = NO; + if ([removedChild respondsToSelector:@selector(setUserInteractionEnabled:)]) { // [macOS + ((RCTUIView *)removedChild).userInteractionEnabled = NO; // [macOS] + } +#if !TARGET_OS_OSX // [macOS] [originalSuperview insertSubview:removedChild atIndex:originalIndex]; - +#else // [macOS + [originalSuperview addSubview:removedChild positioned:nextLowerView == nil ? NSWindowBelow : NSWindowAbove relativeTo:nextLowerView]; +#endif // macOS] + NSString *property = deletingLayoutAnimation.property; [deletingLayoutAnimation performAnimations:^{ @@ -827,9 +873,9 @@ - (void)_removeChildren:(NSArray *)children [_shadowViewRegistry removeObjectForKey:rootReactTag]; [_rootViewTags removeObject:rootReactTag]; - [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTAssertMainQueue(); - UIView *rootView = viewRegistry[rootReactTag]; + RCTPlatformView *rootView = viewRegistry[rootReactTag]; // [macOS] [uiManager _purgeChildren:(NSArray> *)rootView.reactSubviews fromRegistry:(NSMutableDictionary> *)viewRegistry]; [(NSMutableDictionary *)viewRegistry removeObjectForKey:rootReactTag]; @@ -865,7 +911,7 @@ - (void)_removeChildren:(NSArray *)children { RCTSetChildren(containerTag, reactTags, (NSDictionary> *)_shadowViewRegistry); - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTSetChildren(containerTag, reactTags, (NSDictionary> *)viewRegistry); }]; @@ -903,7 +949,7 @@ static void RCTSetChildren( removeAtIndices:removeAtIndices registry:(NSMutableDictionary> *)_shadowViewRegistry]; - [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] [uiManager _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices @@ -940,8 +986,8 @@ - (void)_manageChildren:(NSNumber *)containerTag BOOL isUIViewRegistry = ((id)registry == (id)_viewRegistry); if (isUIViewRegistry && _layoutAnimationGroup.deletingLayoutAnimation) { - [self _removeChildren:(NSArray *)permanentlyRemovedChildren - fromContainer:(UIView *)container + [self _removeChildren:(NSArray *)permanentlyRemovedChildren // [macOS] + fromContainer:(RCTPlatformView *)container // [macOS] withAnimation:_layoutAnimationGroup]; } else { [self _removeChildren:permanentlyRemovedChildren fromContainer:container]; @@ -988,7 +1034,9 @@ - (void)_manageChildren:(NSNumber *)containerTag _shadowViewRegistry[reactTag] = shadowView; RCTShadowView *rootView = _shadowViewRegistry[rootTag]; RCTAssert( - [rootView isKindOfClass:[RCTRootShadowView class]] || [rootView isKindOfClass:[RCTSurfaceRootShadowView class]], + [rootView isKindOfClass:[RCTRootShadowView class]] + || [rootView isKindOfClass:[RCTSurfaceRootShadowView class]] + , @"Given `rootTag` (%@) does not correspond to a valid root shadow view instance.", rootTag); shadowView.rootView = (RCTRootShadowView *)rootView; @@ -996,7 +1044,7 @@ - (void)_manageChildren:(NSNumber *)containerTag // Dispatch view creation directly to the main thread instead of adding to // UIBlocks array. This way, it doesn't get deferred until after layout. - __block UIView *preliminaryCreatedView = nil; + __block RCTPlatformView *preliminaryCreatedView = nil; // [macOS] void (^createViewBlock)(void) = ^{ // Do nothing on the second run. @@ -1020,7 +1068,7 @@ - (void)_manageChildren:(NSNumber *)containerTag RCTExecuteOnMainQueue(createViewBlock); - [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { // [macOS] createViewBlock(); if (preliminaryCreatedView) { @@ -1041,8 +1089,8 @@ - (void)_manageChildren:(NSNumber *)containerTag RCTComponentData *componentData = _componentDataByName[shadowView.viewName ?: viewName]; [componentData setProps:props forShadowView:shadowView]; - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTPlatformView *view = viewRegistry[reactTag]; // [macOS] [componentData setProps:props forView:view]; }]; @@ -1053,22 +1101,22 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin { RCTAssertMainQueue(); RCTComponentData *componentData = _componentDataByName[viewName]; - UIView *view = _viewRegistry[reactTag]; + RCTPlatformView *view = _viewRegistry[reactTag]; // [macOS] [componentData setProps:props forView:view]; } RCT_EXPORT_METHOD(focus : (nonnull NSNumber *)reactTag) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *newResponder = viewRegistry[reactTag]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *newResponder = viewRegistry[reactTag]; // [macOS] [newResponder reactFocus]; }]; } RCT_EXPORT_METHOD(blur : (nonnull NSNumber *)reactTag) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *currentResponder = viewRegistry[reactTag]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *currentResponder = viewRegistry[reactTag]; // [macOS] [currentResponder reactBlur]; }]; } @@ -1078,9 +1126,9 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin : (CGPoint)point callback : (RCTResponseSenderBlock)callback) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - UIView *target = [view hitTest:point withEvent:nil]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTPlatformView *view = viewRegistry[reactTag]; // [macOS] + RCTPlatformView *target = RCTUIViewHitTestWithEvent(view, point, nil); // [macOS] CGRect frame = [target convertRect:target.bounds toView:view]; while (target.reactTag == nil && target.superview != nil) { @@ -1105,6 +1153,8 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTComponentData *componentData = _componentDataByName[shadowView.viewName]; +// Re-enable componentViewName_DO_NOT_USE_THIS_IS_BROKEN once macOS uses Fabric +#if !TARGET_OS_OSX // [macOS] // Achtung! Achtung! // This is a remarkably hacky and ugly workaround. // We need this only temporary for some testing. We need this hack until Fabric fully implements command-execution @@ -1112,7 +1162,7 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" if (!componentData) { - __block UIView *view; + __block RCTPlatformView *view; // [macOS] RCTUnsafeExecuteOnMainQueueSync(^{ view = self->_viewRegistry[reactTag]; }); @@ -1122,6 +1172,7 @@ - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSStrin } } #pragma clang diagnostic pop +#endif // [macOS] Class managerClass = componentData.managerClass; RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)]; @@ -1269,9 +1320,9 @@ - (void)_dispatchChildrenDidChangeEvents [tags addObject:shadowView.reactTag]; } - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] for (NSNumber *tag in tags) { - UIView *view = viewRegistry[tag]; + RCTUIView *view = viewRegistry[tag]; // [macOS] [view didUpdateReactSubviews]; } }]; @@ -1294,9 +1345,9 @@ - (void)_dispatchPropsDidChangeEvents [tags setObject:props forKey:shadowView.reactTag]; } - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] for (NSNumber *tag in tags) { - UIView *view = viewRegistry[tag]; + RCTUIView *view = viewRegistry[tag]; // [macOS] [view didSetProps:[tags objectForKey:tag]]; } }]; @@ -1304,8 +1355,8 @@ - (void)_dispatchPropsDidChangeEvents RCT_EXPORT_METHOD(measure : (nonnull NSNumber *)reactTag callback : (RCTResponseSenderBlock)callback) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTPlatformView *view = viewRegistry[reactTag]; // [macOS] if (!view) { // this view was probably collapsed out RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); @@ -1314,7 +1365,7 @@ - (void)_dispatchPropsDidChangeEvents } // If in a , rootView will be the root of the modal container. - UIView *rootView = view; + RCTPlatformView *rootView = view; // [macOS] while (rootView.superview && ![rootView isReactRootView]) { rootView = rootView.superview; } @@ -1337,8 +1388,8 @@ - (void)_dispatchPropsDidChangeEvents RCT_EXPORT_METHOD(measureInWindow : (nonnull NSNumber *)reactTag callback : (RCTResponseSenderBlock)callback) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTPlatformView *view = viewRegistry[reactTag]; // [macOS] if (!view) { // this view was probably collapsed out RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); @@ -1347,7 +1398,12 @@ - (void)_dispatchPropsDidChangeEvents } // Return frame coordinates in window - CGRect windowFrame = [view.window convertRect:view.frame fromView:view.superview]; + CGRect windowFrame = [view convertRect:view.bounds toView:nil]; +#if TARGET_OS_OSX // [macOS + //The macOS default coordinate system has its origin at the lower left of the drawing area, so we need to flip the y-axis coordinate. + windowFrame.origin.y = view.window.contentView.frame.size.height - windowFrame.origin.y - windowFrame.size.height; +#endif // macOS] + callback(@[ @(windowFrame.origin.x), @(windowFrame.origin.y), @@ -1447,7 +1503,7 @@ static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTRe : (nonnull NSNumber *)reactTag blockNativeResponder : (__unused BOOL)blockNativeResponder) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] _jsResponder = viewRegistry[reactTag]; // Fabric view's are not stored in viewRegistry. We avoid logging a warning in that case. if (!_jsResponder && !RCTUIManagerTypeForTagIsFabric(reactTag)) { @@ -1458,7 +1514,7 @@ static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTRe RCT_EXPORT_METHOD(clearJSResponder) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { + [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { // [macOS] _jsResponder = nil; }]; } @@ -1625,8 +1681,8 @@ static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTRe bridge:self.bridge eventDispatcher:self.bridge.eventDispatcher]; _componentDataByName[componentData.name] = componentData; - NSMutableDictionary *directEvents = [NSMutableDictionary new]; - NSMutableDictionary *bubblingEvents = [NSMutableDictionary new]; + NSMutableDictionary *directEvents = [NSMutableDictionary new]; + NSMutableDictionary *bubblingEvents = [NSMutableDictionary new]; NSMutableDictionary *moduleConstants = moduleConstantsForComponentData(directEvents, bubblingEvents, componentData); return @{ @@ -1642,12 +1698,12 @@ static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTRe RCTLayoutAnimationGroup *layoutAnimationGroup = [[RCTLayoutAnimationGroup alloc] initWithConfig:config callback:callback]; - [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { // [macOS] [uiManager setNextLayoutAnimationGroup:layoutAnimationGroup]; }]; } -- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView *view))completion +- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(RCTPlatformView *view))completion // [macOS] { RCTAssertMainQueue(); RCTAssert(completion != nil, @"Attempted to resolve rootView for tag %@ without a completion block", reactTag); @@ -1660,7 +1716,7 @@ - (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView RCTExecuteOnUIManagerQueue(^{ NSNumber *rootTag = [self shadowViewForReactTag:reactTag].rootView.reactTag; RCTExecuteOnMainQueue(^{ - UIView *rootView = nil; + RCTPlatformView *rootView = nil; // [macOS] if (rootTag != nil) { rootView = [self viewForReactTag:rootTag]; } @@ -1669,9 +1725,10 @@ - (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView }); } -static UIView *_jsResponder; -+ (UIView *)JSResponder +static RCTPlatformView *_jsResponder; // [macOS] + ++ (RCTPlatformView *)JSResponder // [macOS] { RCTErrorNewArchitectureValidation( RCTNotAllowedInFabricWithoutLegacy, @"RCTUIManager", @"Please migrate this legacy surface to Fabric."); diff --git a/packages/react-native/React/Modules/RCTUIManagerObserverCoordinator.h b/packages/react-native/React/Modules/RCTUIManagerObserverCoordinator.h index eeb569c41ee2f0..a2447f8025d4ea 100644 --- a/packages/react-native/React/Modules/RCTUIManagerObserverCoordinator.h +++ b/packages/react-native/React/Modules/RCTUIManagerObserverCoordinator.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Modules/RCTUIManagerUtils.h b/packages/react-native/React/Modules/RCTUIManagerUtils.h index 80228dbd71a436..e5dc9b8d1b120c 100644 --- a/packages/react-native/React/Modules/RCTUIManagerUtils.h +++ b/packages/react-native/React/Modules/RCTUIManagerUtils.h @@ -6,6 +6,7 @@ */ #import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Profiler/RCTProfile.m b/packages/react-native/React/Profiler/RCTProfile.m index 75200bed749a67..eefe0b01fe1cfa 100644 --- a/packages/react-native/React/Profiler/RCTProfile.m +++ b/packages/react-native/React/Profiler/RCTProfile.m @@ -13,7 +13,7 @@ #import #import -#import +#import // [macOS] #import "RCTAssert.h" #import "RCTBridge+Private.h" @@ -48,9 +48,11 @@ static NSMutableDictionary *RCTProfileOngoingEvents; static NSTimeInterval RCTProfileStartTime; static NSUInteger RCTProfileEventID = 0; -static CADisplayLink *RCTProfileDisplayLink; static __weak RCTBridge *_RCTProfilingBridge; +#if !TARGET_OS_OSX // [macOS] +static CADisplayLink *RCTProfileDisplayLink; // [macOS] static UIWindow *RCTProfileControlsWindow; +#endif // [macOS] #pragma mark - Macros @@ -202,10 +204,10 @@ void RCTProfileTrampolineEnd(void) RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"objc_call,modules,auto"); } -static UIView *(*originalCreateView)(RCTComponentData *, SEL, NSNumber *, NSNumber *); -static UIView *RCTProfileCreateView(RCTComponentData *self, SEL _cmd, NSNumber *tag, NSNumber *rootTag) +static RCTUIView *(*originalCreateView)(RCTComponentData *, SEL, NSNumber *, NSNumber *); // [macOS] +static RCTUIView *RCTProfileCreateView(RCTComponentData *self, SEL _cmd, NSNumber *tag, NSNumber *rootTag) // [macOS] { - UIView *view = originalCreateView(self, _cmd, tag, rootTag); + RCTUIView *view = originalCreateView(self, _cmd, tag, rootTag); // [macOS] RCTProfileHookInstance(view); return view; } @@ -367,6 +369,7 @@ void RCTProfileUnhookModules(RCTBridge *bridge) #pragma mark - Private ObjC class only used for the vSYNC CADisplayLink target +#if !TARGET_OS_OSX // [macOS] @interface RCTProfile : NSObject @end @@ -424,6 +427,7 @@ + (void)drag:(UIPanGestureRecognizer *)gestureRecognizer } @end +#endif // [macOS] #pragma mark - Public Functions @@ -483,8 +487,10 @@ void RCTProfileInit(RCTBridge *bridge) RCTProfileHookModules(bridge); +#if !TARGET_OS_OSX // [macOS] RCTProfileDisplayLink = [CADisplayLink displayLinkWithTarget:[RCTProfile class] selector:@selector(vsync:)]; [RCTProfileDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +#endif // [macOS] [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidStartProfiling object:bridge]; } @@ -499,8 +505,10 @@ void RCTProfileEnd(RCTBridge *bridge, void (^callback)(NSString *)) [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidEndProfiling object:bridge]; +#if !TARGET_OS_OSX // [macOS] [RCTProfileDisplayLink invalidate]; RCTProfileDisplayLink = nil; +#endif // [macOS] RCTProfileUnhookModules(bridge); @@ -746,6 +754,7 @@ void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) NSString *message = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; if (message.length) { +#if !TARGET_OS_OSX // [macOS] dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Profile" @@ -756,6 +765,13 @@ void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) handler:nil]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:nil]; }); +#else // [macOS + NSAlert *alert = [NSAlert new]; + alert.messageText = @"Profile"; + alert.informativeText = message; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; +#endif // macOS] } } }]; @@ -763,6 +779,7 @@ void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) [task resume]; } +#if !TARGET_OS_OSX // [macOS] void RCTProfileShowControls(void) { static const CGFloat height = 30; @@ -801,5 +818,6 @@ void RCTProfileHideControls(void) RCTProfileControlsWindow.hidden = YES; RCTProfileControlsWindow = nil; } +#endif // [macOS] #endif diff --git a/packages/react-native/React/UIUtils/RCTUIUtils.h b/packages/react-native/React/UIUtils/RCTUIUtils.h index 78d647af9a9aec..7371e017e8fe79 100644 --- a/packages/react-native/React/UIUtils/RCTUIUtils.h +++ b/packages/react-native/React/UIUtils/RCTUIUtils.h @@ -7,7 +7,7 @@ #import #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -21,7 +21,12 @@ typedef struct { CGFloat width, height, scale, fontScale; } window, screen; } RCTDimensions; -extern __attribute__((visibility("default"))) RCTDimensions RCTGetDimensions(CGFloat fontScale); +extern __attribute__((visibility("default"))) +#if !TARGET_OS_OSX // [macOS] +RCTDimensions RCTGetDimensions(CGFloat fontScale); +#else // [macOS +RCTDimensions RCTGetDimensions(RCTPlatformView *rootView); +#endif // macOS] #ifdef __cplusplus } diff --git a/packages/react-native/React/UIUtils/RCTUIUtils.m b/packages/react-native/React/UIUtils/RCTUIUtils.m index ed2aa676e27a9d..bfefcb3a40a82e 100644 --- a/packages/react-native/React/UIUtils/RCTUIUtils.m +++ b/packages/react-native/React/UIUtils/RCTUIUtils.m @@ -9,8 +9,13 @@ #import "RCTUtils.h" +#if !TARGET_OS_OSX // [macOS] RCTDimensions RCTGetDimensions(CGFloat fontScale) +#else // [macOS +RCTDimensions RCTGetDimensions(RCTPlatformView *rootView) +#endif // macOS] { +#if !TARGET_OS_OSX // [macOS] UIScreen *mainScreen = UIScreen.mainScreen; CGSize screenSize = mainScreen.bounds.size; @@ -18,12 +23,39 @@ RCTDimensions RCTGetDimensions(CGFloat fontScale) mainWindow = RCTKeyWindow(); // We fallback to screen size if a key window is not found. CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize; +#else // [macOS + NSWindow *window = nil; + NSSize windowSize; + NSSize screenSize; + if (rootView != nil) { + window = [rootView window]; + windowSize = [window frame].size; + } else { + // We don't have a root view so fall back to the app's key window + window = [NSApp keyWindow]; + windowSize = [window frame].size; + } + screenSize = [[window screen] frame].size; +#endif RCTDimensions result; +#if !TARGET_OS_OSX // [macOS] typeof(result.screen) dimsScreen = { .width = screenSize.width, .height = screenSize.height, .scale = mainScreen.scale, .fontScale = fontScale}; typeof(result.window) dimsWindow = { .width = windowSize.width, .height = windowSize.height, .scale = mainScreen.scale, .fontScale = fontScale}; +#else // [macOS + typeof(result.screen) dimsScreen = { + .width = screenSize.width, + .height = screenSize.height, + .scale = [[window screen] backingScaleFactor], + .fontScale = 1}; + typeof(result.window) dimsWindow = { + .width = windowSize.width, + .height = windowSize.height, + .scale = [[window screen] backingScaleFactor], + .fontScale = 1}; +#endif // macOS] result.screen = dimsScreen; result.window = dimsWindow; diff --git a/packages/react-native/React/Views/RCTActivityIndicatorView.h b/packages/react-native/React/Views/RCTActivityIndicatorView.h index 1d5015501e59a4..81a3c5e02c200b 100644 --- a/packages/react-native/React/Views/RCTActivityIndicatorView.h +++ b/packages/react-native/React/Views/RCTActivityIndicatorView.h @@ -5,7 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @interface RCTActivityIndicatorView : UIActivityIndicatorView + +#if TARGET_OS_OSX // [macOS +@property (nonatomic, assign) UIActivityIndicatorViewStyle activityIndicatorViewStyle; +@property (nonatomic, assign) BOOL hidesWhenStopped; +@property (nullable, readwrite, nonatomic, strong) RCTUIColor *color; // [macOS] +@property (nonatomic, readonly, getter=isAnimating) BOOL animating; +- (void)startAnimating; +- (void)stopAnimating; +#endif // macOS] + @end diff --git a/packages/react-native/React/Views/RCTActivityIndicatorView.m b/packages/react-native/React/Views/RCTActivityIndicatorView.m index 14c9fb17f6ad26..4ad99224c72829 100644 --- a/packages/react-native/React/Views/RCTActivityIndicatorView.m +++ b/packages/react-native/React/Views/RCTActivityIndicatorView.m @@ -7,9 +7,116 @@ #import "RCTActivityIndicatorView.h" +#if TARGET_OS_OSX // [macOS +#import +#import + +@interface RCTActivityIndicatorView () +@property (nonatomic, readwrite, getter=isAnimating) BOOL animating; +@end +#endif // macOS] + @implementation RCTActivityIndicatorView { } +#if TARGET_OS_OSX // [macOS +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + self.displayedWhenStopped = NO; + self.style = NSProgressIndicatorStyleSpinning; + } + return self; +} + +- (void)startAnimating +{ + // `wantsLayer` gets reset after the animation is stopped. We have to + // reset it in order for CALayer filters to take effect. + [self setWantsLayer:YES]; + [self startAnimation:self]; +} + +- (void)stopAnimating +{ + [self stopAnimation:self]; +} + +- (void)startAnimation:(id)sender +{ + [super startAnimation:sender]; + self.animating = YES; +} + +- (void)stopAnimation:(id)sender +{ + [super stopAnimation:sender]; + self.animating = NO; +} + +- (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle +{ + _activityIndicatorViewStyle = activityIndicatorViewStyle; + + switch (activityIndicatorViewStyle) { + case UIActivityIndicatorViewStyleLarge: + if (@available(macOS 11.0, *)) { + self.controlSize = NSControlSizeLarge; + } else { + self.controlSize = NSControlSizeRegular; + } + break; + case UIActivityIndicatorViewStyleMedium: + self.controlSize = NSControlSizeRegular; + break; + default: + break; + } +} + +- (void)setColor:(RCTUIColor*)color +{ + if (_color != color) { + _color = color; + [self setNeedsDisplay:YES]; + } +} + +- (void)updateLayer +{ + [super updateLayer]; + if (_color != nil) { + CGFloat r, g, b, a; + [[_color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]] getRed:&r green:&g blue:&b alpha:&a]; + + CIFilter *colorPoly = [CIFilter filterWithName:@"CIColorPolynomial"]; + [colorPoly setDefaults]; + + CIVector *redVector = [CIVector vectorWithX:r Y:0 Z:0 W:0]; + CIVector *greenVector = [CIVector vectorWithX:g Y:0 Z:0 W:0]; + CIVector *blueVector = [CIVector vectorWithX:b Y:0 Z:0 W:0]; + [colorPoly setValue:redVector forKey:@"inputRedCoefficients"]; + [colorPoly setValue:greenVector forKey:@"inputGreenCoefficients"]; + [colorPoly setValue:blueVector forKey:@"inputBlueCoefficients"]; + + [[self layer] setFilters:@[colorPoly]]; + } else { + [[self layer] setFilters:nil]; + } +} + +- (void)setHidesWhenStopped:(BOOL)hidesWhenStopped +{ + self.displayedWhenStopped = !hidesWhenStopped; +} + +- (BOOL)hidesWhenStopped +{ + return !self.displayedWhenStopped; +} + +#endif // macOS] + - (void)setHidden:(BOOL)hidden { if ([self hidesWhenStopped] && ![self isAnimating]) { @@ -19,4 +126,7 @@ - (void)setHidden:(BOOL)hidden } } + + + @end diff --git a/packages/react-native/React/Views/RCTActivityIndicatorViewManager.m b/packages/react-native/React/Views/RCTActivityIndicatorViewManager.m index 74c71c453b719a..b683a4e53ea37f 100644 --- a/packages/react-native/React/Views/RCTActivityIndicatorViewManager.m +++ b/packages/react-native/React/Views/RCTActivityIndicatorViewManager.m @@ -30,26 +30,26 @@ @implementation RCTActivityIndicatorViewManager RCT_EXPORT_MODULE() -- (UIView *)view +- (RCTPlatformView *)view // [macOS] { return [RCTActivityIndicatorView new]; } RCT_EXPORT_VIEW_PROPERTY(color, UIColor) RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL) -RCT_CUSTOM_VIEW_PROPERTY(size, UIActivityIndicatorViewStyle, UIActivityIndicatorView) +RCT_CUSTOM_VIEW_PROPERTY(size, UIActivityIndicatorViewStyle, RCTActivityIndicatorView) // [macOS] { /* Setting activityIndicatorViewStyle overrides the color, so restore the original color after setting the indicator style. */ - UIColor *oldColor = view.color; + RCTUIColor *oldColor = view.color; // [macOS] view.activityIndicatorViewStyle = json ? [RCTConvert UIActivityIndicatorViewStyle:json] : defaultView.activityIndicatorViewStyle; view.color = oldColor; } -RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView) +RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, RCTActivityIndicatorView) // [macOS] { BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating]; if (animating != [view isAnimating]) { diff --git a/packages/react-native/React/Views/RCTAnimationType.h b/packages/react-native/React/Views/RCTAnimationType.h index 005b6dc09ae048..2f74280ccd2e96 100644 --- a/packages/react-native/React/Views/RCTAnimationType.h +++ b/packages/react-native/React/Views/RCTAnimationType.h @@ -8,10 +8,14 @@ #import typedef NS_ENUM(NSInteger, RCTAnimationType) { +#if !TARGET_OS_OSX // [macOS] RCTAnimationTypeSpring = 0, +#endif // [macOS] RCTAnimationTypeLinear, RCTAnimationTypeEaseIn, RCTAnimationTypeEaseOut, RCTAnimationTypeEaseInEaseOut, +#if !TARGET_OS_OSX // [macOS] RCTAnimationTypeKeyboard, +#endif // [macOS] }; diff --git a/packages/react-native/React/Views/RCTAutoInsetsProtocol.h b/packages/react-native/React/Views/RCTAutoInsetsProtocol.h index 0d12aba5e76015..df9ab00d13300e 100644 --- a/packages/react-native/React/Views/RCTAutoInsetsProtocol.h +++ b/packages/react-native/React/Views/RCTAutoInsetsProtocol.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] /** * Defines a View that wants to support auto insets adjustment diff --git a/packages/react-native/React/Views/RCTBorderDrawing.h b/packages/react-native/React/Views/RCTBorderDrawing.h index 4c92868f7c607e..dec9bdc0e70f8c 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.h +++ b/packages/react-native/React/Views/RCTBorderDrawing.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -56,6 +56,7 @@ RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInsets, const * by inspecting the image's `capInsets`. * * `borderInsets` defines the border widths for each edge. + * `scaleFactor` defines the backing scale factor of the device for supporting high-resolution drawing. */ RCT_EXTERN UIImage *RCTGetBorderImage( RCTBorderStyle borderStyle, @@ -64,4 +65,5 @@ RCT_EXTERN UIImage *RCTGetBorderImage( UIEdgeInsets borderInsets, RCTBorderColors borderColors, CGColorRef backgroundColor, - BOOL drawToEdge); + BOOL drawToEdge, + CGFloat scaleFactor); // [macOS] diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index df3d9e1cd9813d..7cea45807d9ac9 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -52,14 +52,22 @@ RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdgeInsets edge }}; } -static UIEdgeInsets RCTRoundInsetsToPixel(UIEdgeInsets edgeInsets) -{ +static UIEdgeInsets RCTRoundInsetsToPixel( + UIEdgeInsets edgeInsets, + CGFloat scaleFactor // [macOS] +) { +#if !TARGET_OS_OSX // [macOS] edgeInsets.top = RCTRoundPixelValue(edgeInsets.top); edgeInsets.bottom = RCTRoundPixelValue(edgeInsets.bottom); edgeInsets.left = RCTRoundPixelValue(edgeInsets.left); edgeInsets.right = RCTRoundPixelValue(edgeInsets.right); - - return edgeInsets; +#else // [macOS + edgeInsets.top = RCTRoundPixelValue(edgeInsets.top, scaleFactor); + edgeInsets.bottom = RCTRoundPixelValue(edgeInsets.bottom, scaleFactor); + edgeInsets.left = RCTRoundPixelValue(edgeInsets.left, scaleFactor); + edgeInsets.right = RCTRoundPixelValue(edgeInsets.right, scaleFactor); +#endif // macOS] + return edgeInsets; } static void RCTPathAddEllipticArc( @@ -172,6 +180,7 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn return RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); } +#if !TARGET_OS_OSX // [macOS] static UIGraphicsImageRenderer * RCTUIGraphicsImageRenderer(CGSize size, CGColorRef backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge) { @@ -182,6 +191,16 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:rendererFormat]; return renderer; } +#else // [macOS +static CGContextRef +RCTUIGraphicsBeginImageContext(CGSize size, CGColorRef backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge, CGFloat scaleFactor) +{ + const CGFloat alpha = CGColorGetAlpha(backgroundColor); + const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0; + UIGraphicsBeginImageContextWithOptions(size, opaque, scaleFactor); + return UIGraphicsGetCurrentContext(); +} +#endif // macOS] static UIImage *RCTGetSolidBorderImage( RCTCornerRadii cornerRadii, @@ -189,15 +208,16 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn UIEdgeInsets borderInsets, RCTBorderColors borderColors, CGColorRef backgroundColor, - BOOL drawToEdge) -{ + BOOL drawToEdge, + CGFloat scaleFactor // [macOS] +) { const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii); const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets); // Incorrect render for borders that are not proportional to device pixel: borders get stretched and become // significantly bigger than expected. // Rdar: http://www.openradar.me/15959788 - borderInsets = RCTRoundInsetsToPixel(borderInsets); + borderInsets = RCTRoundInsetsToPixel(borderInsets, scaleFactor); // [macOS] const BOOL makeStretchable = (borderInsets.left + cornerInsets.topLeft.width + borderInsets.right + cornerInsets.bottomRight.width <= @@ -231,10 +251,21 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn edgeInsets.top + 1 + edgeInsets.bottom } : viewSize; + // [macOS size must nonzero + if (size.width <= 0 || size.height <= 0) { + return nil; + } // macOS] + +#if !TARGET_OS_OSX // [macOS] UIGraphicsImageRenderer *const imageRenderer = RCTUIGraphicsImageRenderer(size, backgroundColor, hasCornerRadii, drawToEdge); UIImage *image = [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { const CGContextRef context = rendererContext.CGContext; +#else // [macOS + CGContextRef context = RCTUIGraphicsBeginImageContext(size, backgroundColor, hasCornerRadii, drawToEdge, scaleFactor); + // Add extra braces for scope to match the indentation level of the iOS block + { +#endif // macOS] const CGRect rect = {.size = size}; CGPathRef path = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii); @@ -390,10 +421,20 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn } CGPathRelease(insetPath); +#if !TARGET_OS_OSX // [macOS] }]; +#else // [macOS + } + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); +#endif // macOS] if (makeStretchable) { +#if !TARGET_OS_OSX // [macOS] image = [image resizableImageWithCapInsets:edgeInsets]; +#else // [macOS + image.capInsets = edgeInsets; +#endif // macOS] } return image; @@ -466,8 +507,9 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn UIEdgeInsets borderInsets, RCTBorderColors borderColors, CGColorRef backgroundColor, - BOOL drawToEdge) -{ + BOOL drawToEdge, + CGFloat scaleFactor // [macOS] +) { NSCParameterAssert(borderStyle == RCTBorderStyleDashed || borderStyle == RCTBorderStyleDotted); if (!RCTBorderColorsAreEqual(borderColors) || !RCTBorderInsetsAreEqual(borderInsets)) { @@ -480,11 +522,22 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn return nil; } + // [macOS viewSize must nonzero + if (viewSize.width <= 0 || viewSize.height <= 0) { + return nil; + } // macOS] + const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii); +#if !TARGET_OS_OSX // [macOS] UIGraphicsImageRenderer *const imageRenderer = RCTUIGraphicsImageRenderer(viewSize, backgroundColor, hasCornerRadii, drawToEdge); return [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { const CGContextRef context = rendererContext.CGContext; +#else // [macOS + CGContextRef context = RCTUIGraphicsBeginImageContext(viewSize, backgroundColor, hasCornerRadii, drawToEdge, scaleFactor); + // Add extra braces for scope to match the indentation level of the iOS block + { +#endif // macOS] const CGRect rect = {.size = viewSize}; if (backgroundColor) { @@ -508,14 +561,21 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn CGContextSetLineWidth(context, lineWidth); CGContextSetLineDash(context, 0, dashLengths, sizeof(dashLengths) / sizeof(*dashLengths)); - CGContextSetStrokeColorWithColor(context, [UIColor yellowColor].CGColor); + CGContextSetStrokeColorWithColor(context, [RCTUIColor yellowColor].CGColor); // [macOS] CGContextAddPath(context, path); CGContextSetStrokeColorWithColor(context, borderColors.top); CGContextStrokePath(context); CGPathRelease(path); +#if !TARGET_OS_OSX // [macOS] }]; +#else // [macOS + } + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +#endif // macOS] } UIImage *RCTGetBorderImage( @@ -525,15 +585,16 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn UIEdgeInsets borderInsets, RCTBorderColors borderColors, CGColorRef backgroundColor, - BOOL drawToEdge) -{ + BOOL drawToEdge, + CGFloat scaleFactor // [macOS] +) { switch (borderStyle) { case RCTBorderStyleSolid: - return RCTGetSolidBorderImage(cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge); + return RCTGetSolidBorderImage(cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge, scaleFactor); // [macOS] case RCTBorderStyleDashed: case RCTBorderStyleDotted: return RCTGetDashedOrDottedBorderImage( - borderStyle, cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge); + borderStyle, cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge, scaleFactor); // [macOS] case RCTBorderStyleUnset: break; } diff --git a/packages/react-native/React/Views/RCTComponentData.h b/packages/react-native/React/Views/RCTComponentData.h index 1dd11977835026..b611d94c91d4c8 100644 --- a/packages/react-native/React/Views/RCTComponentData.h +++ b/packages/react-native/React/Views/RCTComponentData.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -13,7 +13,7 @@ @class RCTBridge; @class RCTShadowView; -@class UIView; +@class RCTUIView; // [macOS] @class RCTEventDispatcherProtocol; NS_ASSUME_NONNULL_BEGIN @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN bridge:(RCTBridge *)bridge eventDispatcher:(id)eventDispatcher NS_DESIGNATED_INITIALIZER; -- (UIView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag; +- (RCTPlatformView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag; // [macOS] - (RCTShadowView *)createShadowViewWithTag:(NSNumber *)tag; - (void)setProps:(NSDictionary *)props forView:(id)view; - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowView *)shadowView; diff --git a/packages/react-native/React/Views/RCTComponentData.m b/packages/react-native/React/Views/RCTComponentData.m index 52efad6e159d21..5a017935b723fc 100644 --- a/packages/react-native/React/Views/RCTComponentData.m +++ b/packages/react-native/React/Views/RCTComponentData.m @@ -76,16 +76,18 @@ - (RCTViewManager *)manager RCT_NOT_IMPLEMENTED(-(instancetype)init) -- (UIView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag +- (RCTPlatformView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag // [macOS] { RCTAssertMainQueue(); - UIView *view = [self.manager view]; + RCTPlatformView *view = [self.manager view]; // [macOS] view.reactTag = tag; view.rootTag = rootTag; +#if !TARGET_OS_OSX // [macOS] view.multipleTouchEnabled = YES; view.userInteractionEnabled = YES; // required for touch handling view.layer.allowsGroupOpacity = YES; // required for touch handling +#endif // [macOS] return view; } diff --git a/packages/react-native/React/Views/RCTCursor.h b/packages/react-native/React/Views/RCTCursor.h new file mode 100644 index 00000000000000..a893cd40b66fa5 --- /dev/null +++ b/packages/react-native/React/Views/RCTCursor.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +typedef NS_ENUM(NSInteger, RCTCursor) { + RCTCursorAuto, + RCTCursorArrow, + RCTCursorIBeam, + RCTCursorCrosshair, + RCTCursorClosedHand, + RCTCursorOpenHand, + RCTCursorPointingHand, + RCTCursorResizeLeft, + RCTCursorResizeRight, + RCTCursorResizeLeftRight, + RCTCursorResizeUp, + RCTCursorResizeDown, + RCTCursorResizeUpDown, + RCTCursorDisappearingItem, + RCTCursorIBeamCursorForVerticalLayout, + RCTCursorOperationNotAllowed, + RCTCursorDragLink, + RCTCursorDragCopy, + RCTCursorContextualMenu, +}; + +@interface RCTConvert (RCTCursor) + ++ (RCTCursor)RCTCursor:(id)json; +#if TARGET_OS_OSX // [macOS ++ (NSCursor *)NSCursor:(RCTCursor)rctCursor; +#endif // macOS] + +@end diff --git a/packages/react-native/React/Views/RCTCursor.m b/packages/react-native/React/Views/RCTCursor.m new file mode 100644 index 00000000000000..a0f3b21146d907 --- /dev/null +++ b/packages/react-native/React/Views/RCTCursor.m @@ -0,0 +1,105 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@implementation RCTConvert (RCTCursor) + +RCT_ENUM_CONVERTER( + RCTCursor, + (@{ + @"alias" : @(RCTCursorDragLink), + @"auto" : @(RCTCursorAuto), + @"col-resize" : @(RCTCursorResizeLeftRight), + @"context-menu" : @(RCTCursorContextualMenu), + @"copy" : @(RCTCursorDragCopy), + @"crosshair" : @(RCTCursorCrosshair), + @"default" : @(RCTCursorArrow), + @"disappearing-item" : @(RCTCursorDisappearingItem), + @"e-resize" : @(RCTCursorResizeRight), + @"grab" : @(RCTCursorOpenHand), + @"grabbing" : @(RCTCursorClosedHand), + @"n-resize" : @(RCTCursorResizeUp), + @"no-drop" : @(RCTCursorOperationNotAllowed), + @"not-allowed" : @(RCTCursorOperationNotAllowed), + @"pointer" : @(RCTCursorPointingHand), + @"row-resize" : @(RCTCursorResizeUpDown), + @"s-resize" : @(RCTCursorResizeDown), + @"text" : @(RCTCursorIBeam), + @"vertical-text" : @(RCTCursorIBeamCursorForVerticalLayout), + @"w-resize" : @(RCTCursorResizeLeft), + }), + RCTCursorAuto, + integerValue) + +#if TARGET_OS_OSX // [macOS ++ (NSCursor *)NSCursor:(RCTCursor)rctCursor +{ + NSCursor *cursor; + + switch (rctCursor) { + case RCTCursorArrow: + cursor = [NSCursor arrowCursor]; + break; + case RCTCursorClosedHand: + cursor = [NSCursor closedHandCursor]; + break; + case RCTCursorContextualMenu: + cursor = [NSCursor contextualMenuCursor]; + break; + case RCTCursorCrosshair: + cursor = [NSCursor crosshairCursor]; + break; + case RCTCursorDisappearingItem: + cursor = [NSCursor disappearingItemCursor]; + break; + case RCTCursorDragCopy: + cursor = [NSCursor dragCopyCursor]; + break; + case RCTCursorDragLink: + cursor = [NSCursor dragLinkCursor]; + break; + case RCTCursorIBeam: + cursor = [NSCursor IBeamCursor]; + break; + case RCTCursorIBeamCursorForVerticalLayout: + cursor = [NSCursor IBeamCursorForVerticalLayout]; + break; + case RCTCursorOpenHand: + cursor = [NSCursor openHandCursor]; + break; + case RCTCursorOperationNotAllowed: + cursor = [NSCursor operationNotAllowedCursor]; + break; + case RCTCursorPointingHand: + cursor = [NSCursor pointingHandCursor]; + break; + case RCTCursorResizeDown: + cursor = [NSCursor resizeDownCursor]; + break; + case RCTCursorResizeLeft: + cursor = [NSCursor resizeLeftCursor]; + break; + case RCTCursorResizeLeftRight: + cursor = [NSCursor resizeLeftRightCursor]; + break; + case RCTCursorResizeRight: + cursor = [NSCursor resizeRightCursor]; + break; + case RCTCursorResizeUp: + cursor = [NSCursor resizeUpCursor]; + break; + case RCTCursorResizeUpDown: + cursor = [NSCursor resizeUpDownCursor]; + break; + } + + return cursor; +} +#endif // macOS] + +@end diff --git a/packages/react-native/React/Views/RCTFont.h b/packages/react-native/React/Views/RCTFont.h index 5ddc34da35f145..5c2189ac26fd13 100644 --- a/packages/react-native/React/Views/RCTFont.h +++ b/packages/react-native/React/Views/RCTFont.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Views/RCTFont.mm b/packages/react-native/React/Views/RCTFont.mm index bfc273bdf43d79..87484a212db96e 100644 --- a/packages/react-native/React/Views/RCTFont.mm +++ b/packages/react-native/React/Views/RCTFont.mm @@ -144,6 +144,12 @@ struct __attribute__((__packed__)) CacheKey { if (defaultFontHandler) { NSString *fontWeightDescription = FontWeightDescriptionFromUIFontWeight(weight); font = defaultFontHandler(size, fontWeightDescription); +#pragma clang diagnostic push // [macOS] +#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] + } else if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { + // Only supported on iOS8.2/macOS10.11 and above + font = [UIFont systemFontOfSize:size weight:weight]; +#pragma clang diagnostic pop // [macOS] } else { font = [UIFont systemFontOfSize:size weight:weight]; } @@ -172,7 +178,15 @@ struct __attribute__((__packed__)) CacheKey { auto names = [cache objectForKey:familyName]; if (!names) { +#if !TARGET_OS_OSX // [macOS] names = [UIFont fontNamesForFamilyName:familyName] ?: [NSArray new]; +#else // [macOS + NSMutableArray *fontNames = [NSMutableArray array]; + for (NSArray *fontSettings in [[NSFontManager sharedFontManager] availableMembersOfFontFamily:familyName]) { + [fontNames addObject:fontSettings[0]]; + } + names = fontNames; +#endif // macOS] [cache setObject:names forKey:familyName]; } return names; @@ -457,8 +471,11 @@ + (UIFont *)updateFont:(UIFont *)font } else { // Not a valid font or family RCTLogInfo(@"Unrecognized font family '%@'", familyName); +#pragma clang diagnostic push // [macOS] +#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { font = [UIFont systemFontOfSize:fontSize weight:fontWeight]; +#pragma clang diagnostic pop // [macOS] } else if (fontWeight > UIFontWeightRegular) { font = [UIFont boldSystemFontOfSize:fontSize]; } else { diff --git a/packages/react-native/React/Views/RCTHandledKey.h b/packages/react-native/React/Views/RCTHandledKey.h new file mode 100644 index 00000000000000..f0d2562d46c9df --- /dev/null +++ b/packages/react-native/React/Views/RCTHandledKey.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#if TARGET_OS_OSX +#import + +// This class is used for specifying key filtering e.g. for -[RCTView validKeysDown] and -[RCTView validKeysUp] +// Also see RCTViewKeyboardEvent, which is a React representation of an actual NSEvent that is dispatched to JS. +@interface RCTHandledKey : NSObject + ++ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray *)filter; ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)filter; + +- (instancetype)initWithKey:(NSString *)key; +- (BOOL)matchesEvent:(NSEvent *)event; + +@property (nonatomic, copy) NSString *key; + +// For the following modifiers, nil means we don't care about the presence of the modifier when filtering the key +// They are still expected to be only boolean when not nil. +@property (nonatomic, assign) NSNumber *altKey; +@property (nonatomic, assign) NSNumber *ctrlKey; +@property (nonatomic, assign) NSNumber *metaKey; +@property (nonatomic, assign) NSNumber *shiftKey; + +@end + +@interface RCTConvert (RCTHandledKey) + ++ (RCTHandledKey *)RCTHandledKey:(id)json; + +@end + +#endif diff --git a/packages/react-native/React/Views/RCTHandledKey.m b/packages/react-native/React/Views/RCTHandledKey.m new file mode 100644 index 00000000000000..aa685c3b999044 --- /dev/null +++ b/packages/react-native/React/Views/RCTHandledKey.m @@ -0,0 +1,145 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#import "objc/runtime.h" +#import +#import +#import +#import +#import + +#if TARGET_OS_OSX + +@implementation RCTHandledKey + ++ (NSArray *)validModifiers { + // keep in sync with actual properties and RCTViewKeyboardEvent + return @[@"altKey", @"ctrlKey", @"metaKey", @"shiftKey"]; +} + ++ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray *)filter { + for (RCTHandledKey *key in filter) { + if ([key matchesEvent:event]) { + return YES; + } + } + + return NO; +} + ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)filter { + for (RCTHandledKey *aKey in filter) { + if ([[aKey key] isEqualToString:key]) { + return YES; + } + } + + return NO; +} + +- (instancetype)initWithKey:(NSString *)key { + if ((self = [super init])) { + self.key = key; + } + return self; +} + +- (BOOL)matchesEvent:(NSEvent *)event +{ + NSEventType type = [event type]; + if (type != NSEventTypeKeyDown && type != NSEventTypeKeyUp) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Wrong event type (%d) sent to -[RCTHandledKey matchesEvent:]", (int)type])); + return NO; + } + + NSDictionary *body = [RCTViewKeyboardEvent bodyFromEvent:event]; + NSString *key = body[@"key"]; + if (key == nil) { + RCTFatal(RCTErrorWithMessage(@"Event body has missing value for 'key'")); + return NO; + } + + if (![key isEqualToString:self.key]) { + return NO; + } + + NSArray *modifiers = [RCTHandledKey validModifiers]; + for (NSString *modifier in modifiers) { + NSNumber *myValue = [self valueForKey:modifier]; + + if (myValue == nil) { + continue; + } + + NSNumber *eventValue = (NSNumber *)body[modifier]; + if (eventValue == nil) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has missing value for '%@'", modifier])); + return NO; + } + + if (![eventValue isKindOfClass:[NSNumber class]]) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has unexpected value of class '%@' for '%@'", + NSStringFromClass(object_getClass(eventValue)), modifier])); + return NO; + } + + if (![myValue isEqualToNumber:body[modifier]]) { + return NO; + } + } + + return YES; // keys matched; all present modifiers matched +} + +@end + +@implementation RCTConvert (RCTHandledKey) + ++ (RCTHandledKey *)RCTHandledKey:(id)json +{ + // legacy way of specifying validKeysDown and validKeysUp -- here we ignore the modifiers when comparing to the NSEvent + if ([json isKindOfClass:[NSString class]]) { + return [[RCTHandledKey alloc] initWithKey:(NSString *)json]; + } + + // modern way of specifying validKeys and validKeysUp -- here we assume missing modifiers to mean false\NO + if ([json isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)json; + NSString *key = dict[@"key"]; + if (key == nil) { + RCTLogConvertError(dict, @"a RCTHandledKey -- must include \"key\""); + return nil; + } + + RCTHandledKey *handledKey = [[RCTHandledKey alloc] initWithKey:key]; + NSArray *modifiers = RCTHandledKey.validModifiers; + for (NSString *key in modifiers) { + id value = dict[key]; + if (value == nil) { + value = @NO; // assume NO -- instead of nil i.e. "don't care" unlike the string case above. + } + + if (![value isKindOfClass:[NSNumber class]]) { + RCTLogConvertError(value, @"a boolean"); + return nil; + } + + [handledKey setValue:@([(NSNumber *)value boolValue]) forKey:key]; + } + + return handledKey; + } + + RCTLogConvertError(json, @"a RCTHandledKey -- allowed types are string and object"); + return nil; +} + +@end + +#endif diff --git a/packages/react-native/React/Views/RCTLayout.h b/packages/react-native/React/Views/RCTLayout.h index 8016463e145cb6..ac084eeb2b3529 100644 --- a/packages/react-native/React/Views/RCTLayout.h +++ b/packages/react-native/React/Views/RCTLayout.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Views/RCTModalHostView.h b/packages/react-native/React/Views/RCTModalHostView.h index 2fcdcaea83f5b2..f450293597f4a9 100644 --- a/packages/react-native/React/Views/RCTModalHostView.h +++ b/packages/react-native/React/Views/RCTModalHostView.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import #import @@ -16,7 +17,7 @@ @protocol RCTModalHostViewInteractor; -@interface RCTModalHostView : UIView +@interface RCTModalHostView : RCTUIView // [macOS] @property (nonatomic, copy) NSString *animationType; @property (nonatomic, assign) UIModalPresentationStyle presentationStyle; @@ -54,3 +55,4 @@ animated:(BOOL)animated; @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalHostView.m b/packages/react-native/React/Views/RCTModalHostView.m index dfde4ae47ab137..639889f6638536 100644 --- a/packages/react-native/React/Views/RCTModalHostView.m +++ b/packages/react-native/React/Views/RCTModalHostView.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTModalHostView.h" #import @@ -233,3 +234,4 @@ - (UIInterfaceOrientationMask)supportedOrientationsMask } @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalHostViewController.h b/packages/react-native/React/Views/RCTModalHostViewController.h index b12b0f7fc15129..49774b5219cb6a 100644 --- a/packages/react-native/React/Views/RCTModalHostViewController.h +++ b/packages/react-native/React/Views/RCTModalHostViewController.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import @interface RCTModalHostViewController : UIViewController @@ -14,3 +15,4 @@ @property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalHostViewController.m b/packages/react-native/React/Views/RCTModalHostViewController.m index 059b64157f9836..e030d8f0ae235e 100644 --- a/packages/react-native/React/Views/RCTModalHostViewController.m +++ b/packages/react-native/React/Views/RCTModalHostViewController.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTModalHostViewController.h" #import "RCTLog.h" @@ -70,3 +71,4 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations #endif // RCT_DEV @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalHostViewManager.h b/packages/react-native/React/Views/RCTModalHostViewManager.h index 11b52984b30834..b28d98dce496ef 100644 --- a/packages/react-native/React/Views/RCTModalHostViewManager.h +++ b/packages/react-native/React/Views/RCTModalHostViewManager.h @@ -9,6 +9,7 @@ #import #import +#if !TARGET_OS_OSX // [macOS] @interface RCTConvert (RCTModalHostView) + (UIModalPresentationStyle)UIModalPresentationStyle:(id)json; @@ -32,3 +33,4 @@ typedef void (^RCTModalViewInteractionBlock)( @property (nonatomic, strong) RCTModalViewInteractionBlock dismissalBlock; @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalHostViewManager.m b/packages/react-native/React/Views/RCTModalHostViewManager.m index 4b9f9ad7267c8f..bd2554ba84a8ed 100644 --- a/packages/react-native/React/Views/RCTModalHostViewManager.m +++ b/packages/react-native/React/Views/RCTModalHostViewManager.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTModalHostViewManager.h" #import "RCTBridge.h" @@ -134,3 +135,4 @@ - (void)invalidate RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock) @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RCTModalManager.h b/packages/react-native/React/Views/RCTModalManager.h index 237037fd8db6b2..cdc254518ac65b 100644 --- a/packages/react-native/React/Views/RCTModalManager.h +++ b/packages/react-native/React/Views/RCTModalManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/React/Views/RCTSegmentedControl.h b/packages/react-native/React/Views/RCTSegmentedControl.h index 7cb9ffd03dc8dd..b6727da119612a 100644 --- a/packages/react-native/React/Views/RCTSegmentedControl.h +++ b/packages/react-native/React/Views/RCTSegmentedControl.h @@ -5,11 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import +#if !TARGET_OS_OSX // [macOS] @interface RCTSegmentedControl : UISegmentedControl +#else // [macOS +@interface RCTSegmentedControl : NSSegmentedControl +#endif // macOS] + +#if TARGET_OS_OSX // [macOS] +@property (nonatomic, assign, getter = isMomentary) BOOL momentary; +#endif // [macOS] @property (nonatomic, copy) NSArray *values; @property (nonatomic, assign) NSInteger selectedIndex; diff --git a/packages/react-native/React/Views/RCTSegmentedControl.m b/packages/react-native/React/Views/RCTSegmentedControl.m index a3e399f8fda427..9bb4e4d1cb523a 100644 --- a/packages/react-native/React/Views/RCTSegmentedControl.m +++ b/packages/react-native/React/Views/RCTSegmentedControl.m @@ -9,6 +9,7 @@ #import "RCTConvert.h" #import "UIView+React.h" +#import "RCTUIKit.h" // [macOS] @implementation RCTSegmentedControl @@ -16,7 +17,13 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _selectedIndex = self.selectedSegmentIndex; +#if !TARGET_OS_OSX // [macOS] [self addTarget:self action:@selector(didChange) forControlEvents:UIControlEventValueChanged]; +#else // [macOS + self.segmentStyle = NSSegmentStyleRounded; + self.target = self; + self.action = @selector(didChange); +#endif // macOS] } return self; } @@ -24,30 +31,38 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)setValues:(NSArray *)values { _values = [values copy]; +#if !TARGET_OS_OSX // [macOS] [self removeAllSegments]; for (NSString *value in values) { [self insertSegmentWithTitle:value atIndex:self.numberOfSegments animated:NO]; } - super.selectedSegmentIndex = _selectedIndex; +#else // [macOS + self.segmentCount = values.count; + for (NSUInteger i = 0; i < values.count; i++) { + [self setLabel:values[i] forSegment:i]; + } +#endif // macOS] + self.selectedSegmentIndex = _selectedIndex; // [macOS] } - (void)setSelectedIndex:(NSInteger)selectedIndex { _selectedIndex = selectedIndex; - super.selectedSegmentIndex = selectedIndex; + self.selectedSegmentIndex = selectedIndex; // [macOS] } -- (void)setBackgroundColor:(UIColor *)backgroundColor +#if !TARGET_OS_OSX // [macOS] +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS] { [super setBackgroundColor:backgroundColor]; } -- (void)setTextColor:(UIColor *)textColor +- (void)setTextColor:(RCTUIColor *)textColor // [macOS] { [self setTitleTextAttributes:@{NSForegroundColorAttributeName : textColor} forState:UIControlStateNormal]; } -- (void)setTintColor:(UIColor *)tintColor +- (void)setTintColor:(UIColor *)tintColor // [macOS] { [super setTintColor:tintColor]; @@ -56,6 +71,7 @@ - (void)setTintColor:(UIColor *)tintColor forState:UIControlStateSelected]; [self setTitleTextAttributes:@{NSForegroundColorAttributeName : tintColor} forState:UIControlStateNormal]; } +#endif // [macOS] - (void)didChange { @@ -65,4 +81,48 @@ - (void)didChange } } +#if TARGET_OS_OSX // [macOS + +- (BOOL)isFlipped +{ + return YES; +} + +- (void)setMomentary:(BOOL)momentary +{ + self.trackingMode = momentary ? NSSegmentSwitchTrackingMomentary : NSSegmentSwitchTrackingSelectOne; +} + +- (BOOL)isMomentary +{ + return self.trackingMode == NSSegmentSwitchTrackingMomentary; +} + +- (void)setSelectedSegmentIndex:(NSInteger)selectedSegmentIndex +{ + self.selectedSegment = selectedSegmentIndex; +} + +- (NSInteger)selectedSegmentIndex +{ + return self.selectedSegment; +} + +- (NSString *)titleForSegmentAtIndex:(NSUInteger)segment +{ + return [self labelForSegment:segment]; +} + +- (void)setNumberOfSegments:(NSInteger)numberOfSegments +{ + self.segmentCount = numberOfSegments; +} + +- (NSInteger)numberOfSegments +{ + return self.segmentCount; +} + +#endif // macOS] + @end diff --git a/packages/react-native/React/Views/RCTSegmentedControlManager.m b/packages/react-native/React/Views/RCTSegmentedControlManager.m index c756374141ab3a..2e2c67627f1856 100644 --- a/packages/react-native/React/Views/RCTSegmentedControlManager.m +++ b/packages/react-native/React/Views/RCTSegmentedControlManager.m @@ -15,7 +15,7 @@ @implementation RCTSegmentedControlManager RCT_EXPORT_MODULE() -- (UIView *)view +- (RCTPlatformView *)view // [macOS] { RCTNewArchitectureValidationPlaceholder( RCTNotAllowedInFabricWithoutLegacy, @@ -26,9 +26,11 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(values, NSArray) RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(textColor, UIColor) +#endif // [macOS] RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL) RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) diff --git a/packages/react-native/React/Views/RCTShadowView+Internal.h b/packages/react-native/React/Views/RCTShadowView+Internal.h index 0455863f726dcb..bf0a277fc29054 100644 --- a/packages/react-native/React/Views/RCTShadowView+Internal.h +++ b/packages/react-native/React/Views/RCTShadowView+Internal.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Views/RCTShadowView+Layout.h b/packages/react-native/React/Views/RCTShadowView+Layout.h index e3209520881d61..e56829cf058475 100644 --- a/packages/react-native/React/Views/RCTShadowView+Layout.h +++ b/packages/react-native/React/Views/RCTShadowView+Layout.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Views/RCTShadowView.h b/packages/react-native/React/Views/RCTShadowView.h index 4cf8323be55701..8c7567076bb309 100644 --- a/packages/react-native/React/Views/RCTShadowView.h +++ b/packages/react-native/React/Views/RCTShadowView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import // Keeps RCTConvert.h here before yoga for clang module to generate correct header imports. @@ -17,7 +17,7 @@ @class RCTRootShadowView; @class RCTSparseArray; -typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry); +typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry); // [macOS] /** * ShadowView tree mirrors RCT view tree. Every node is highly stateful. @@ -51,6 +51,9 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry @property (nonatomic, assign, readonly) YGNodeRef yogaNode; @property (nonatomic, copy) NSString *viewName; @property (nonatomic, copy) RCTDirectEventBlock onLayout; +#if TARGET_OS_OSX // [macOS +@property (nonatomic) CGFloat scale; +#endif // macOS] /** * Computed layout of the view. diff --git a/packages/react-native/React/Views/RCTShadowView.m b/packages/react-native/React/Views/RCTShadowView.m index 2ef25b1a200aef..802d6b6da8d149 100644 --- a/packages/react-native/React/Views/RCTShadowView.m +++ b/packages/react-native/React/Views/RCTShadowView.m @@ -49,7 +49,12 @@ + (YGConfigRef)yogaConfig static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ yogaConfig = YGConfigNew(); - YGConfigSetPointScaleFactor(yogaConfig, RCTScreenScale()); +#if !TARGET_OS_OSX // [macOS] + float pixelsInPoint = RCTScreenScale(); +#else // [macOS + float pixelsInPoint = 1; // Use 1x alignment for macOS until we can use backing resolution +#endif // macOS] + YGConfigSetPointScaleFactor(yogaConfig, pixelsInPoint); YGConfigSetErrata(yogaConfig, YGErrataAll); }); return yogaConfig; @@ -204,6 +209,11 @@ - (instancetype)init _yogaNode = YGNodeNewWithConfig([[self class] yogaConfig]); YGNodeSetContext(_yogaNode, (__bridge void *)self); YGNodeSetPrintFunc(_yogaNode, RCTPrint); + +#if TARGET_OS_OSX // [macOS + // RCTUIManager will fix the scale if we're on a Retina display + _scale = 1.0; +#endif // macOS] } return self; } diff --git a/packages/react-native/React/Views/RCTSwitch.h b/packages/react-native/React/Views/RCTSwitch.h index dfbc7a0b32ea57..5f4775a8ab874f 100644 --- a/packages/react-native/React/Views/RCTSwitch.h +++ b/packages/react-native/React/Views/RCTSwitch.h @@ -5,11 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import -@interface RCTSwitch : UISwitch +@interface RCTSwitch : RCTUISwitch @property (nonatomic, assign) BOOL wasOn; @property (nonatomic, copy) RCTBubblingEventBlock onChange; diff --git a/packages/react-native/React/Views/RCTSwitchManager.m b/packages/react-native/React/Views/RCTSwitchManager.m index b04b51f844e11a..cb623ddd175e96 100644 --- a/packages/react-native/React/Views/RCTSwitchManager.m +++ b/packages/react-native/React/Views/RCTSwitchManager.m @@ -16,10 +16,15 @@ @implementation RCTSwitchManager RCT_EXPORT_MODULE() -- (UIView *)view +- (RCTPlatformView *)view // [macOS] { RCTSwitch *switcher = [RCTSwitch new]; +#if !TARGET_OS_OSX // [macOS] [switcher addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; +#else // [macOS + [switcher setTarget:self]; + [switcher setAction:@selector(onChange:)]; +#endif // macOS] return switcher; } @@ -35,20 +40,22 @@ - (void)onChange:(RCTSwitch *)sender RCT_EXPORT_METHOD(setValue : (nonnull NSNumber *)viewTag toValue : (BOOL)value) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[viewTag]; + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[viewTag]; // [macOS] - if ([view isKindOfClass:[UISwitch class]]) { - [(UISwitch *)view setOn:value animated:NO]; + if ([view isKindOfClass:[RCTSwitch class]]) { + [(RCTSwitch *)view setOn:value animated:NO]; } else { - RCTLogError(@"view type must be UISwitch"); + RCTLogError(@"view type must be RCTUISwitch"); // [macOS] } }]; } +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_VIEW_PROPERTY(onTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor); +#endif // [macOS] RCT_REMAP_VIEW_PROPERTY(value, on, BOOL); RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock); RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RCTSwitch) diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index 200d8b451bf59e..f556254869ef13 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -5,25 +5,48 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import #import +#import // [macOS] #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] extern const UIAccessibilityTraits SwitchAccessibilityTrait; +#endif // [macOS] @protocol RCTAutoInsetsProtocol; -@interface RCTView : UIView +@class RCTHandledKey; // [macOS] + +@interface RCTView : RCTUIView // [macOS] + +// [macOS +- (instancetype)initWithEventDispatcher:(id)eventDispatcher; + +- (BOOL)becomeFirstResponder; +- (BOOL)resignFirstResponder; + +#if TARGET_OS_OSX +- (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard; +- (BOOL)handleKeyboardEvent:(NSEvent *)event; +#endif +// macOS] /** * Accessibility event handlers */ @property (nonatomic, copy) RCTDirectEventBlock onAccessibilityAction; @property (nonatomic, copy) RCTDirectEventBlock onAccessibilityTap; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, copy) RCTDirectEventBlock onMagicTap; +#endif // [macOS] @property (nonatomic, copy) RCTDirectEventBlock onAccessibilityEscape; /** @@ -31,8 +54,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; */ @property (nonatomic, assign) RCTPointerEvents pointerEvents; -+ (void)autoAdjustInsetsForView:(UIView *)parentView - withScrollView:(UIScrollView *)scrollView ++ (void)autoAdjustInsetsForView:(RCTUIView *)parentView // [macOS] + withScrollView:(RCTUIScrollView *)scrollView // [macOS] updateOffset:(BOOL)updateOffset; /** @@ -79,16 +102,16 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; /** * Border colors (actually retained). */ -@property (nonatomic, strong) UIColor *borderTopColor; -@property (nonatomic, strong) UIColor *borderRightColor; -@property (nonatomic, strong) UIColor *borderBottomColor; -@property (nonatomic, strong) UIColor *borderLeftColor; -@property (nonatomic, strong) UIColor *borderStartColor; -@property (nonatomic, strong) UIColor *borderEndColor; -@property (nonatomic, strong) UIColor *borderColor; -@property (nonatomic, strong) UIColor *borderBlockColor; -@property (nonatomic, strong) UIColor *borderBlockEndColor; -@property (nonatomic, strong) UIColor *borderBlockStartColor; +@property (nonatomic, strong) RCTUIColor *borderTopColor; +@property (nonatomic, strong) RCTUIColor *borderRightColor; +@property (nonatomic, strong) RCTUIColor *borderBottomColor; +@property (nonatomic, strong) RCTUIColor *borderLeftColor; +@property (nonatomic, strong) RCTUIColor *borderStartColor; +@property (nonatomic, strong) RCTUIColor *borderEndColor; +@property (nonatomic, strong) RCTUIColor *borderColor; +@property (nonatomic, strong) RCTUIColor *borderBlockColor; +@property (nonatomic, strong) RCTUIColor *borderBlockEndColor; +@property (nonatomic, strong) RCTUIColor *borderBlockStartColor; /** * Border widths. @@ -135,4 +158,45 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, assign) RCTBubblingEventBlock onGotPointerCapture; @property (nonatomic, assign) RCTBubblingEventBlock onLostPointerCapture; +#if TARGET_OS_OSX // [macOS +/** + * macOS Properties + */ +@property (nonatomic, assign) RCTCursor cursor; + +@property (nonatomic, assign) CATransform3D transform3D; + +// `allowsVibrancy` is readonly on NSView, so let's create a new property to make it assignable +// that we can set through JS and the getter for `allowsVibrancy` can read in RCTView. +@property (nonatomic, assign) BOOL allowsVibrancyInternal; + +@property (nonatomic, copy) RCTDirectEventBlock onMouseEnter; +@property (nonatomic, copy) RCTDirectEventBlock onMouseLeave; +@property (nonatomic, copy) RCTDirectEventBlock onDragEnter; +@property (nonatomic, copy) RCTDirectEventBlock onDragLeave; +@property (nonatomic, copy) RCTDirectEventBlock onDrop; + +// Keyboarding events +// NOTE does not properly work with single line text inputs (most key downs). This is because those are +// presumably handled by the window's field editor. To make it work, we'd need to look into providing +// a custom field editor for NSTextField controls. +@property (nonatomic, assign) BOOL passthroughAllKeyEvents; +@property (nonatomic, copy) RCTDirectEventBlock onKeyDown; +@property (nonatomic, copy) RCTDirectEventBlock onKeyUp; +@property (nonatomic, copy) NSArray *validKeysDown; +@property (nonatomic, copy) NSArray *validKeysUp; + +// Shadow Properties +@property (nonatomic, strong) NSColor *shadowColor; +@property (nonatomic, assign) CGFloat shadowOpacity; +@property (nonatomic, assign) CGFloat shadowRadius; +@property (nonatomic, assign) CGSize shadowOffset; +#endif // macOS] + +/** + * Common Focus Properties + */ +@property (nonatomic, copy) RCTBubblingEventBlock onFocus; +@property (nonatomic, copy) RCTBubblingEventBlock onBlur; + @end diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index bb8336615a0208..f8fc24487bc33f 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -5,6 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +// [macOS +#import "objc/runtime.h" +#import "RCTHandledKey.h" +// macOS] #import "RCTView.h" #import @@ -13,18 +17,26 @@ #import "RCTAutoInsetsProtocol.h" #import "RCTBorderCurve.h" #import "RCTBorderDrawing.h" +#import "RCTFocusChangeEvent.h" // [macOS] #import "RCTI18nUtil.h" #import "RCTLocalizedString.h" #import "RCTLog.h" +#import "RCTRootContentView.h" // [macOS] #import "RCTViewUtils.h" #import "UIView+React.h" +#import "RCTViewKeyboardEvent.h" +#if TARGET_OS_OSX // [macOS +#import "RCTTextView.h" +#endif // macOS] RCT_MOCK_DEF(RCTView, RCTContentInsets); #define RCTContentInsets RCT_MOCK_USE(RCTView, RCTContentInsets) +#if !TARGET_OS_OSX // [macOS] const UIAccessibilityTraits SwitchAccessibilityTrait = 0x20000000000001; +#endif // [macOS] -@implementation UIView (RCTViewUnmounting) +@implementation RCTPlatformView (RCTViewUnmounting) // [macOS] - (void)react_remountAllSubviews { @@ -32,18 +44,18 @@ - (void)react_remountAllSubviews // this does is forward message to our subviews, // in case any of those do support it - for (UIView *subview in self.subviews) { + for (RCTUIView *subview in self.subviews) { // [macOS] [subview react_remountAllSubviews]; } } -- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTPlatformView *)clipView // [macOS] { // Even though we don't support subview unmounting // we do support clipsToBounds, so if that's enabled // we'll update the clipping - if (self.clipsToBounds && self.subviews.count > 0) { + if (RCTUIViewSetClipsToBounds(self) && self.subviews.count > 0) { // [macOS] clipRect = [clipView convertRect:clipRect toView:self]; clipRect = CGRectIntersection(clipRect, self.bounds); clipView = self; @@ -53,19 +65,19 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: // this does is forward message to our subviews, // in case any of those do support it - for (UIView *subview in self.subviews) { + for (RCTUIView *subview in self.subviews) { // [macOS] [subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } } -- (UIView *)react_findClipView +- (RCTPlatformView *)react_findClipView // [macOS] { - UIView *testView = self; - UIView *clipView = nil; + RCTPlatformView *testView = self; // [macOS] + RCTPlatformView *clipView = nil; // [macOS] CGRect clipRect = self.bounds; // We will only look for a clipping view up the view hierarchy until we hit the root view. while (testView) { - if (testView.clipsToBounds) { + if (RCTUIViewSetClipsToBounds(testView)) { // [macOS] if (clipView) { CGRect testRect = [clipView convertRect:clipRect toView:testView]; if (!CGRectContainsRect(testView.bounds, testRect)) { @@ -82,16 +94,31 @@ - (UIView *)react_findClipView } testView = testView.superview; } +#if !TARGET_OS_OSX // [macOS] return clipView ?: self.window; +#else // [macOS + return clipView ?: self.window.contentView; +#endif // macOS] } @end -static NSString *RCTRecursiveAccessibilityLabel(UIView *view) +static NSString *RCTRecursiveAccessibilityLabel(RCTUIView *view) // [macOS] { NSMutableString *str = [NSMutableString stringWithString:@""]; - for (UIView *subview in view.subviews) { + for (RCTUIView *subview in view.subviews) { // [macOS] +#if !TARGET_OS_OSX // [macOS] NSString *label = subview.accessibilityLabel; +#else // [macOS + NSString *label; + if ([subview isKindOfClass:[RCTTextView class]]) { + // on macOS VoiceOver a text element will always have its accessibilityValue read, but will only read it's accessibilityLabel if it's value is set. + // the macOS RCTTextView accessibilityValue will return its accessibilityLabel if set otherwise return its text. + label = subview.accessibilityValue; + } else { + label = subview.accessibilityLabel; + } +#endif // macOS] if (!label) { label = RCTRecursiveAccessibilityLabel(subview); } @@ -106,11 +133,27 @@ - (UIView *)react_findClipView } @implementation RCTView { - UIColor *_backgroundColor; + RCTUIColor *_backgroundColor; // [macOS] + id _eventDispatcher; // [macOS] +#if TARGET_OS_OSX // [macOS + NSTrackingArea *_trackingArea; + BOOL _hasMouseOver; + BOOL _mouseDownCanMoveWindow; +#endif // macOS] NSMutableDictionary *accessibilityActionsNameMap; NSMutableDictionary *accessibilityActionsLabelMap; } +// [macOS +- (instancetype)initWithEventDispatcher:(id)eventDispatcher +{ + if ((self = [self initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + } + return self; +} +// macOS] + - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { @@ -136,6 +179,11 @@ - (instancetype)initWithFrame:(CGRect)frame _borderCurve = RCTBorderCurveCircular; _borderStyle = RCTBorderStyleSolid; _hitTestEdgeInsets = UIEdgeInsetsZero; +#if TARGET_OS_OSX // [macOS + _transform3D = CATransform3DIdentity; + _shadowColor = nil; + _mouseDownCanMoveWindow = YES; +#endif // macOS] _backgroundColor = super.backgroundColor; } @@ -152,11 +200,21 @@ - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection [self.layer setNeedsDisplay]; } +#if !TARGET_OS_OSX // [macOS] if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) { +#pragma clang diagnostic push // [macOS] +#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] self.semanticContentAttribute = layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ? UISemanticContentAttributeForceLeftToRight : UISemanticContentAttributeForceRightToLeft; +#pragma clang diagnostic pop // [macOS] } +#else // [macOS + self.userInterfaceLayoutDirection = + layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ? + NSUserInterfaceLayoutDirectionLeftToRight : + NSUserInterfaceLayoutDirectionRightToLeft; +#endif // macOS] } #pragma mark - Hit Testing @@ -165,12 +223,14 @@ - (void)setPointerEvents:(RCTPointerEvents)pointerEvents { _pointerEvents = pointerEvents; self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); +#if !TARGET_OS_OSX // [macOS] if (pointerEvents == RCTPointerEventsBoxNone) { self.accessibilityViewIsModal = NO; } +#endif // [macOS] } -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] { BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]); if (!canReceiveTouchEvents) { @@ -179,12 +239,12 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // `hitSubview` is the topmost subview which was hit. The hit point can // be outside the bounds of `view` (e.g., if -clipsToBounds is NO). - UIView *hitSubview = nil; + RCTPlatformView *hitSubview = nil; // [macOS] BOOL isPointInside = [self pointInside:point withEvent:event]; BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly); if (needsHitSubview && (![self clipsToBounds] || isPointInside)) { // Take z-index into account when calculating the touch target. - NSArray *sortedSubviews = [self reactZIndexSortedSubviews]; + NSArray *sortedSubviews = [self reactZIndexSortedSubviews]; // [macOS] // The default behaviour of UIKit is that if a view does not contain a point, // then no subviews will be returned from hit testing, even if they contain @@ -192,16 +252,25 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // the strict containment policy (i.e., UIKit guarantees that every ancestor // of the hit view will return YES from -pointInside:withEvent:). See: // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html - for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) { - CGPoint convertedPoint = [subview convertPoint:point fromView:self]; - hitSubview = [subview hitTest:convertedPoint withEvent:event]; + for (RCTUIView *subview in [sortedSubviews reverseObjectEnumerator]) { // [macOS] + CGPoint pointForHitTest = CGPointZero; // [macOS +#if !TARGET_OS_OSX // [macOS] + pointForHitTest = [subview convertPoint:point fromView:self]; +#else // [macOS + if ([subview isKindOfClass:[RCTView class]]) { + pointForHitTest = [subview convertPoint:point fromView:self]; + } else { + pointForHitTest = point; + } +#endif // macOS] + hitSubview = RCTUIViewHitTestWithEvent(subview, pointForHitTest, event); // macOS] if (hitSubview != nil) { break; } } } - UIView *hitView = (isPointInside ? self : nil); + RCTPlatformView *hitView = (isPointInside ? self : nil); // [macOS] switch (_pointerEvents) { case RCTPointerEventsNone: @@ -235,6 +304,11 @@ - (NSString *)accessibilityLabel if (label) { return label; } +#if TARGET_OS_OSX // [macOS + // calling super.accessibilityLabel above on macOS causes the return value of this accessor to be ignored by VoiceOver. + // Calling the super's setAccessibilityLabel with nil ensures that the return value of this accessor is used by VoiceOver. + [super setAccessibilityLabel:nil]; +#endif // macOS] return RCTRecursiveAccessibilityLabel(self); } @@ -277,6 +351,7 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti return YES; } +#if !TARGET_OS_OSX // [macOS] - (NSString *)accessibilityValue { static dispatch_once_t onceToken; @@ -339,7 +414,11 @@ - (NSString *)accessibilityValue // TODO: This logic makes VoiceOver describe some AccessibilityRole which do not have a backing UIAccessibilityTrait. // It does not run on Fabric. +#if !TARGET_OS_OSX // [macOS] NSString *role = self.role ?: self.accessibilityRole; +#else // [macOS renamed prop so it doesn't conflict with -[NSAccessibility accessibilityRole]. + NSString *role = self.role ?: self.accessibilityRoleInternal; +#endif NSString *roleDescription = role ? rolesAndStatesDescription[role] : nil; if (roleDescription) { [valueComponents addObject:roleDescription]; @@ -382,14 +461,172 @@ - (NSString *)accessibilityValue [valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]]; } } - if (valueComponents.count > 0) { return [valueComponents componentsJoinedByString:@", "]; } return nil; } +#else // [macOS +- (id)accessibilityValue { + id accessibilityValue = nil; + NSAccessibilityRole role = [self accessibilityRole]; + if (role == NSAccessibilityCheckBoxRole || + role == NSAccessibilityRadioButtonRole || + role == NSAccessibilityDisclosureTriangleRole) { + for (NSString *state in [self accessibilityState]) { + id val = [self accessibilityState][state]; + if (val != nil) { + if ([state isEqualToString:@"checked"] || [state isEqualToString:@"selected"]) { + if ([val isKindOfClass:[NSNumber class]]) { + accessibilityValue = @([val boolValue]); + } else if ([val isKindOfClass:[NSString class]] && [val isEqualToString:@"mixed"]) { + accessibilityValue = @(2); // undocumented by Apple: @(2) is the accessibilityValue an NSButton has when its state is NSMixedState (-1) and causes VoiceOver to announced "mixed". + } + } + } + } + } else if ([self accessibilityRole] == NSAccessibilityStaticTextRole) { + // On macOS if the role is static text, VoiceOver will only read the text returned by accessibilityValue. + // So return accessibilityLabel which has the logic to return either either the ivar or a computed value of all the children's text. + // If the accessibilityValueInternal "text" is present, it will override this value below. + accessibilityValue = [self accessibilityLabel]; + } + + // handle accessibilityValue + + id accessibilityValueInternal = [self accessibilityValueInternal]; + if (accessibilityValueInternal != nil) { + id now = accessibilityValueInternal[@"now"]; + id text = accessibilityValueInternal[@"text"]; + if (text != nil && [text isKindOfClass:[NSString class]]) { + accessibilityValue = text; + } else if (now != nil && [now isKindOfClass:[NSNumber class]]) { + accessibilityValue = now; + } + } + + return accessibilityValue; +} -- (UIView *)reactAccessibilityElement +- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { + BOOL isAllowed = NO; + if (selector == @selector(isAccessibilityEnabled)) { + if (self.accessibilityState != nil) { + id disabled = self.accessibilityState[@"disabled"]; + if ([disabled isKindOfClass:[NSNumber class]]) { + isAllowed = YES; + } + } + } else if (selector == @selector(isAccessibilitySelected)) { + if (self.accessibilityState != nil) { + id selected = self.accessibilityState[@"selected"]; + if ([selected isKindOfClass:[NSNumber class]]) { + isAllowed = YES; + } + } + } else if (selector == @selector(isAccessibilityExpanded)) { + if (self.accessibilityState != nil) { + id expanded = self.accessibilityState[@"expanded"]; + if ([expanded isKindOfClass:[NSNumber class]]) { + isAllowed = YES; + } + } + } else if (selector == @selector(accessibilityPerformPress)) { + if (_onAccessibilityTap != nil || + (_onAccessibilityAction != nil && accessibilityActionsNameMap[@"activate"])) { + isAllowed = YES; + } + } else if (selector == @selector(accessibilityPerformIncrement)) { + if (_onAccessibilityAction != nil && accessibilityActionsNameMap[@"increment"]) { + isAllowed = YES; + } + } else if (selector == @selector(accessibilityPerformDecrement)) { + if (_onAccessibilityAction != nil && accessibilityActionsNameMap[@"decrement"]) { + isAllowed = YES; + } + } else if (selector == @selector(accessibilityPerformShowMenu)) { + if (_onAccessibilityAction != nil && accessibilityActionsNameMap[@"showMenu"]) { + isAllowed = YES; + } + } else { + isAllowed = YES; + } + return isAllowed; +} + +// This override currently serves as a workaround to avoid the generic "action 1" +// description for show menu +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (NSString *)accessibilityActionDescription:(NSString *)action { + NSString *actionDescription = nil; + if ([action isEqualToString:NSAccessibilityPressAction] || [action isEqualToString:NSAccessibilityShowMenuAction]) { + actionDescription = NSAccessibilityActionDescription(action); + } else { + actionDescription = [super accessibilityActionDescription:action]; + } + return actionDescription; +} +#pragma clang dianostic pop + +- (BOOL)isAccessibilityEnabled { + BOOL isAccessibilityEnabled = YES; + if (self.accessibilityState != nil) { + id disabled = self.accessibilityState[@"disabled"]; + if ([disabled isKindOfClass:[NSNumber class]]) { + isAccessibilityEnabled = [disabled boolValue] ? NO : YES; + } + } + return isAccessibilityEnabled; +} + +- (BOOL)isAccessibilitySelected { + BOOL isAccessibilitySelected = NO; + if (self.accessibilityState != nil) { + id selected = self.accessibilityState[@"selected"]; + if ([selected isKindOfClass:[NSNumber class]]) { + isAccessibilitySelected = [selected boolValue]; + } + } + return isAccessibilitySelected; +} + +- (BOOL)isAccessibilityExpanded { + BOOL isAccessibilityExpanded = NO; + if (self.accessibilityState != nil) { + id expanded = self.accessibilityState[@"expanded"]; + if ([expanded isKindOfClass:[NSNumber class]]) { + isAccessibilityExpanded = [expanded boolValue]; + } + } + return isAccessibilityExpanded; +} + +- (id)accessibilityMinValue { + id accessibilityMinValue = nil; + if (self.accessibilityValueInternal != nil) { + id min = self.accessibilityValueInternal[@"min"]; + if ([min isKindOfClass:[NSNumber class]]) { + accessibilityMinValue = min; + } + } + return accessibilityMinValue; +} + +- (id)accessibilityMaxValue { + id accessibilityMaxValue = nil; + if (self.accessibilityValueInternal != nil) { + id max = self.accessibilityValueInternal[@"max"]; + if ([max isKindOfClass:[NSNumber class]]) { + accessibilityMaxValue = max; + } + } + return accessibilityMaxValue; +} + +#endif // macOS] + +- (RCTPlatformView *)reactAccessibilityElement // [macOS] { return self; } @@ -412,8 +649,17 @@ - (BOOL)performAccessibilityAction:(NSString *)name return NO; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)accessibilityActivate +#else // [macOS +- (BOOL)accessibilityPerformPress +#endif // macOS] { +#if TARGET_OS_OSX // [macOS + if ([self isAccessibilityEnabled] == NO) { + return NO; + } +#endif // macOS] if ([self performAccessibilityAction:@"activate"]) { return YES; } else if (_onAccessibilityTap) { @@ -424,6 +670,7 @@ - (BOOL)accessibilityActivate } } +#if !TARGET_OS_OSX // [macOS] - (BOOL)accessibilityPerformMagicTap { if ([self performAccessibilityAction:@"magicTap"]) { @@ -435,6 +682,7 @@ - (BOOL)accessibilityPerformMagicTap return NO; } } +#endif // [macOS] - (BOOL)accessibilityPerformEscape { @@ -448,15 +696,36 @@ - (BOOL)accessibilityPerformEscape } } +#if !TARGET_OS_OSX // [macOS] - (void)accessibilityIncrement { [self performAccessibilityAction:@"increment"]; } +#else // [macOS +- (BOOL)accessibilityPerformIncrement +{ + return [self performAccessibilityAction:@"increment"]; +} +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] - (void)accessibilityDecrement { [self performAccessibilityAction:@"decrement"]; } +#else // [macOS +- (BOOL)accessibilityPerformDecrement +{ + return [self performAccessibilityAction:@"decrement"]; +} +#endif // macOS] + +#if TARGET_OS_OSX // [macOS +- (BOOL)accessibilityPerformShowMenu +{ + return [self performAccessibilityAction:@"showMenu"]; +} +#endif // macOS] - (NSString *)description { @@ -466,16 +735,126 @@ - (NSString *)description self.layer]; } +#if TARGET_OS_OSX // [macOS +- (void)setShadowColor:(NSColor *)shadowColor +{ + if (_shadowColor != shadowColor) + { + _shadowColor = shadowColor; + [self didUpdateShadow]; + } +} + +- (void)setShadowOffset:(CGSize)shadowOffset +{ + if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) + { + _shadowOffset = shadowOffset; + [self didUpdateShadow]; + } +} + +- (void)setShadowOpacity:(CGFloat)shadowOpacity +{ + if (_shadowOpacity != shadowOpacity) + { + _shadowOpacity = shadowOpacity; + [self didUpdateShadow]; + } +} + +- (void)setShadowRadius:(CGFloat)shadowRadius +{ + if (_shadowRadius != shadowRadius) + { + _shadowRadius = shadowRadius; + [self didUpdateShadow]; + } +} + +-(void)didUpdateShadow +{ + NSShadow *shadow = [NSShadow new]; + shadow.shadowColor = [[self shadowColor] colorWithAlphaComponent:[self shadowOpacity]]; + shadow.shadowOffset = [self shadowOffset]; + shadow.shadowBlurRadius = [self shadowRadius]; + [self setShadow:shadow]; +} + +- (void)viewDidMoveToWindow +{ + // Subscribe to view bounds changed notification so that the view can be notified when a + // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event + // both of which would not cause the mouseExited to be invoked. + + if ([self window] == nil) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSViewBoundsDidChangeNotification + object:nil]; + } + else if ([[self enclosingScrollView] contentView] != nil) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(viewBoundsChanged:) + name:NSViewBoundsDidChangeNotification + object:[[self enclosingScrollView] contentView]]; + } + + [self reactViewDidMoveToWindow]; // [macOS] Github#1412 + + [super viewDidMoveToWindow]; +} + +- (void)viewBoundsChanged:(NSNotification*)__unused inNotif +{ + // When an enclosing scrollview is scrolled using the scrollWheel or trackpad, + // the mouseExited: event does not get called on the view where mouseEntered: was previously called. + // This creates an unnatural pairing of mouse enter and exit events and can cause problems. + // We therefore explicitly check for this here and handle them by calling the appropriate callbacks. + + if (!_hasMouseOver && self.onMouseEnter) + { + NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + if (NSPointInRect(locationInView, [self bounds])) + { + _hasMouseOver = YES; + + [self sendMouseEventWithBlock:self.onMouseEnter + locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] + modifierFlags:0 + additionalData:nil]; + } + } + else if (_hasMouseOver && self.onMouseLeave) + { + NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + if (!NSPointInRect(locationInView, [self bounds])) + { + _hasMouseOver = NO; + + [self sendMouseEventWithBlock:self.onMouseLeave + locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] + modifierFlags:0 + additionalData:nil]; + } + } +} +#endif // macOS] + #pragma mark - Statics for dealing with layoutGuides -+ (void)autoAdjustInsetsForView:(UIView *)parentView - withScrollView:(UIScrollView *)scrollView ++ (void)autoAdjustInsetsForView:(RCTUIView *)parentView // [macOS] + withScrollView:(RCTUIScrollView *)scrollView // [macOS] updateOffset:(BOOL)updateOffset { UIEdgeInsets baseInset = parentView.contentInset; CGFloat previousInsetTop = scrollView.contentInset.top; CGPoint contentOffset = scrollView.contentOffset; +#if !TARGET_OS_OSX // [macOS] if (parentView.automaticallyAdjustContentInsets) { UIEdgeInsets autoInset = RCTContentInsets(parentView); baseInset.top += autoInset.top; @@ -483,6 +862,7 @@ + (void)autoAdjustInsetsForView:(UIView *)parentView baseInset.left += autoInset.left; baseInset.right += autoInset.right; } +#endif // [macOS] scrollView.contentInset = baseInset; scrollView.scrollIndicatorInsets = baseInset; @@ -504,7 +884,7 @@ + (void)autoAdjustInsetsForView:(UIView *)parentView - (void)react_remountAllSubviews { if (_removeClippedSubviews) { - for (UIView *view in self.reactSubviews) { + for (RCTUIView *view in self.reactSubviews) { // [macOS] if (view.superview != self) { [self addSubview:view]; [view react_remountAllSubviews]; @@ -516,7 +896,7 @@ - (void)react_remountAllSubviews } } -- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTPlatformView *)clipView // [macOS] { // TODO (#5906496): for scrollviews (the primary use-case) we could // optimize this by only doing a range check along the scroll axis, @@ -543,7 +923,7 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: clipView = self; // Mount / unmount views - for (UIView *view in self.reactSubviews) { + for (RCTUIView *view in self.reactSubviews) { // [macOS] if (!CGSizeEqualToSize(CGRectIntersection(clipRect, view.frame).size, CGSizeZero)) { // View is at least partially visible, so remount it if unmounted [self addSubview:view]; @@ -584,7 +964,7 @@ - (void)didUpdateReactSubviews - (void)updateClippedSubviews { // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; + RCTPlatformView *clipView = [self react_findClipView]; // [macOS] if (clipView) { [self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView]; } @@ -604,6 +984,32 @@ - (void)layoutSubviews } } +// [macOS +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + // If we've gained focus, notify listeners + [_eventDispatcher sendEvent:[RCTFocusChangeEvent focusEventWithReactTag:self.reactTag]]; + + return YES; +} + +- (BOOL)resignFirstResponder +{ + if (![super resignFirstResponder]) { + return NO; + } + + // If we've lost focus, notify listeners + [_eventDispatcher sendEvent:[RCTFocusChangeEvent blurEventWithReactTag:self.reactTag]]; + + return YES; +} + +#if !TARGET_OS_OSX // [macOS] - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; @@ -612,15 +1018,16 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection [self.layer setNeedsDisplay]; } } +#endif // [macOS] #pragma mark - Borders -- (UIColor *)backgroundColor +- (RCTUIColor *)backgroundColor // [macOS] RCTUIColor { return _backgroundColor; } -- (void)setBackgroundColor:(UIColor *)backgroundColor +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor // macOS RCTUIColor { if ([_backgroundColor isEqual:backgroundColor]) { return; @@ -728,16 +1135,20 @@ - (RCTCornerRadii)cornerRadii }; } +#if !TARGET_OS_OSX // [macOS] - (RCTBorderColors)borderColorsWithTraitCollection:(UITraitCollection *)traitCollection +#else // [macOS +- (RCTBorderColors)borderColors +#endif // macOS] { const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; - UIColor *directionAwareBorderLeftColor = nil; - UIColor *directionAwareBorderRightColor = nil; + RCTUIColor *directionAwareBorderLeftColor = nil; + RCTUIColor *directionAwareBorderRightColor = nil; if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) { - UIColor *borderStartColor = _borderStartColor ?: _borderLeftColor; - UIColor *borderEndColor = _borderEndColor ?: _borderRightColor; + RCTUIColor *borderStartColor = _borderStartColor ?: _borderLeftColor; // macOS RCTUIColor + RCTUIColor *borderEndColor = _borderEndColor ?: _borderRightColor; // macOS RCTUIColor directionAwareBorderLeftColor = isRTL ? borderEndColor : borderStartColor; directionAwareBorderRightColor = isRTL ? borderStartColor : borderEndColor; @@ -746,9 +1157,9 @@ - (RCTBorderColors)borderColorsWithTraitCollection:(UITraitCollection *)traitCol directionAwareBorderRightColor = (isRTL ? _borderStartColor : _borderEndColor) ?: _borderRightColor; } - UIColor *borderColor = _borderColor; - UIColor *borderTopColor = _borderTopColor; - UIColor *borderBottomColor = _borderBottomColor; + RCTUIColor *borderColor = _borderColor; + RCTUIColor *borderTopColor = _borderTopColor; + RCTUIColor *borderBottomColor = _borderBottomColor; if (_borderBlockColor) { borderTopColor = _borderBlockColor; @@ -761,12 +1172,14 @@ - (RCTBorderColors)borderColorsWithTraitCollection:(UITraitCollection *)traitCol borderTopColor = _borderBlockStartColor; } +#if !TARGET_OS_OSX // [macOS] borderColor = [borderColor resolvedColorWithTraitCollection:self.traitCollection]; borderTopColor = [borderTopColor resolvedColorWithTraitCollection:self.traitCollection]; directionAwareBorderLeftColor = [directionAwareBorderLeftColor resolvedColorWithTraitCollection:self.traitCollection]; borderBottomColor = [borderBottomColor resolvedColorWithTraitCollection:self.traitCollection]; directionAwareBorderRightColor = [directionAwareBorderRightColor resolvedColorWithTraitCollection:self.traitCollection]; +#endif // [macOS] return (RCTBorderColors){ (borderTopColor ?: borderColor).CGColor, @@ -795,11 +1208,19 @@ - (void)displayLayer:(CALayer *)layer } RCTUpdateShadowPathForView(self); + +#if TARGET_OS_OSX // [macOS + // clipsToBounds is stubbed out on macOS because it's not part of NSView + layer.masksToBounds = self.clipsToBounds; +#endif // macOS] const RCTCornerRadii cornerRadii = [self cornerRadii]; const UIEdgeInsets borderInsets = [self bordersAsInsets]; +#if !TARGET_OS_OSX // [macOS] const RCTBorderColors borderColors = [self borderColorsWithTraitCollection:self.traitCollection]; - +#else // [macOS + const RCTBorderColors borderColors = [self borderColors]; +#endif // macOS] BOOL useIOSBorderRendering = RCTCornerRadiiAreEqual(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) && RCTBorderColorsAreEqual(borderColors) && _borderStyle == RCTBorderStyleSolid && @@ -815,8 +1236,25 @@ - (void)displayLayer:(CALayer *)layer CGColorRef backgroundColor; +#if !TARGET_OS_OSX // [macOS] backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor; - +#else // [macOS + backgroundColor = [_backgroundColor CGColor]; +#endif // macOS] + +#if TARGET_OS_OSX // [macOS + CATransform3D transform = [self transform3D]; + CGPoint anchorPoint = [layer anchorPoint]; + if (CGPointEqualToPoint(anchorPoint, CGPointZero) && !CATransform3DEqualToTransform(transform, CATransform3DIdentity)) { + // https://developer.apple.com/documentation/quartzcore/calayer/1410817-anchorpoint + // This compensates for the fact that layer.anchorPoint is {0, 0} instead of {0.5, 0.5} on macOS for some reason. + CATransform3D originAdjust = CATransform3DTranslate(CATransform3DIdentity, self.frame.size.width / 2, self.frame.size.height / 2, 0); + transform = CATransform3DConcat(CATransform3DConcat(CATransform3DInvert(originAdjust), transform), originAdjust); + // Enable edge antialiasing in perspective transforms + [layer setAllowsEdgeAntialiasing:!(transform.m34 == 0.0f)]; + } + [layer setTransform:transform]; +#endif // macOS] if (useIOSBorderRendering) { layer.cornerRadius = cornerRadii.topLeft; layer.borderColor = borderColors.left; @@ -828,8 +1266,21 @@ - (void)displayLayer:(CALayer *)layer return; } +#if TARGET_OS_OSX // [macOS + CGFloat scaleFactor = self.window.backingScaleFactor; + if (scaleFactor == 0.0 && RCTRunningInTestEnvironment()) { + // When running in the test environment the view is not on screen. + // Use a scaleFactor of 1 so that the test results are machine independent. + scaleFactor = 1; + } + RCTAssert(scaleFactor != 0.0, @"displayLayer occurs before the view is in a window?"); +#else + // On iOS setting the scaleFactor to 0.0 will default to the device's native scale factor. + CGFloat scaleFactor = 0.0; +#endif // macOS] + UIImage *image = RCTGetBorderImage( - _borderStyle, layer.bounds.size, cornerRadii, borderInsets, borderColors, backgroundColor, self.clipsToBounds); + _borderStyle, layer.bounds.size, cornerRadii, borderInsets, borderColors, backgroundColor, self.clipsToBounds, scaleFactor); // [macOS] layer.backgroundColor = NULL; @@ -846,8 +1297,13 @@ - (void)displayLayer:(CALayer *)layer insets.left / size.width, insets.top / size.height, (CGFloat)1.0 / size.width, (CGFloat)1.0 / size.height); }); +#if !TARGET_OS_OSX // [macOS] layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; +#else // [macOS + layer.contents = [image layerContentsForContentsScale:scaleFactor]; + layer.contentsScale = scaleFactor; +#endif // macOS] layer.needsDisplayOnBoundsChange = YES; layer.magnificationFilter = kCAFilterNearest; @@ -917,8 +1373,9 @@ - (void)updateClippingForLayer:(CALayer *)layer #pragma mark Border Color +// macOS RCTUIColor #define setBorderColor(side) \ - -(void)setBorder##side##Color : (UIColor *)color \ + -(void)setBorder##side##Color : (RCTUIColor *)color \ { \ if ([_border##side##Color isEqual:color]) { \ return; \ @@ -990,4 +1447,346 @@ -(void)setBorder##side##Style : (RCTBorderStyle)style \ setBorderStyle() - @end +#if TARGET_OS_OSX // [macOS + +#pragma mark Focus ring + +- (CGRect)focusRingMaskBounds +{ + return self.bounds; +} + +- (void)drawFocusRingMask +{ + if ([self enableFocusRing]) { + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + RCTCornerInsets cornerInsets = RCTGetCornerInsets(self.cornerRadii, NSEdgeInsetsZero); + CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL); + + CGContextAddPath(context, path); + CGContextFillPath(context); + CGPathRelease(path); + } +} + +#pragma mark - macOS Event Handler + +- (void)resetCursorRects +{ + [self discardCursorRects]; + NSCursor *cursor = [RCTConvert NSCursor:self.cursor]; + if (cursor) { + [self addCursorRect:self.bounds cursor:cursor]; + } +} + +- (BOOL)needsPanelToBecomeKey { + // We need to override this so that mouse clicks don't move keyboard focus on focusable views by default. + return false; +} + +- (BOOL)acceptsFirstResponder +{ + return [self focusable] || [super acceptsFirstResponder]; +} + +- (void)updateTrackingAreas +{ + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + if (self.onMouseEnter || self.onMouseLeave) { + _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; + } + + [super updateTrackingAreas]; +} + +- (void)mouseEntered:(NSEvent *)event +{ + _hasMouseOver = YES; + [self sendMouseEventWithBlock:self.onMouseEnter + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; +} + +- (void)mouseExited:(NSEvent *)event +{ + _hasMouseOver = NO; + [self sendMouseEventWithBlock:self.onMouseLeave + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; +} + +- (BOOL)mouseDownCanMoveWindow{ + return _mouseDownCanMoveWindow; +} + +- (void)setMouseDownCanMoveWindow:(BOOL)mouseDownCanMoveWindow{ + _mouseDownCanMoveWindow = mouseDownCanMoveWindow; +} + +- (BOOL)allowsVibrancy { + return _allowsVibrancyInternal; +} + +- (NSDictionary*)locationInfoFromEvent:(NSEvent*)event +{ + NSPoint locationInWindow = event.locationInWindow; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + return @{@"screenX": @(locationInWindow.x), + @"screenY": @(locationInWindow.y), + @"clientX": @(locationInView.x), + @"clientY": @(locationInView.y) + }; +} + +- (void)sendMouseEventWithBlock:(RCTDirectEventBlock)block + locationInfo:(NSDictionary*)locationInfo + modifierFlags:(NSEventModifierFlags)modifierFlags + additionalData:(NSDictionary*)additionalData +{ + if (block == nil) { + return; + } + + NSMutableDictionary *body = [NSMutableDictionary new]; + + if (modifierFlags & NSEventModifierFlagShift) { + body[@"shiftKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagControl) { + body[@"ctrlKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagOption) { + body[@"altKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagCommand) { + body[@"metaKey"] = @YES; + } + + if (locationInfo) { + [body addEntriesFromDictionary:locationInfo]; + } + + if (additionalData) { + [body addEntriesFromDictionary:additionalData]; + } + + block(body); +} + +- (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard +{ + NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[]; + NSMutableArray *files = [[NSMutableArray alloc] initWithCapacity:fileNames.count]; + NSMutableArray *items = [[NSMutableArray alloc] initWithCapacity:fileNames.count]; + NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:fileNames.count]; + for (NSString *file in fileNames) { + NSURL *fileURL = [NSURL fileURLWithPath:file]; + BOOL isDir = NO; + BOOL isValid = (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDir] || isDir) ? NO : YES; + if (isValid) { + + NSString *MIMETypeString = nil; + if (fileURL.pathExtension) { + CFStringRef fileExtension = (__bridge CFStringRef)fileURL.pathExtension; + CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL); + if (UTI != NULL) { + CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); + CFRelease(UTI); + MIMETypeString = (__bridge_transfer NSString *)MIMEType; + } + } + + NSNumber *fileSizeValue = nil; + NSError *fileSizeError = nil; + BOOL success = [fileURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:&fileSizeError]; + + NSNumber *width = nil; + NSNumber *height = nil; + if ([MIMETypeString hasPrefix:@"image/"]) { + NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; + width = @(image.size.width); + height = @(image.size.height); + } + + [files addObject:@{@"name": RCTNullIfNil(fileURL.lastPathComponent), + @"type": RCTNullIfNil(MIMETypeString), + @"uri": RCTNullIfNil(fileURL.path), + @"size": success ? fileSizeValue : (id)kCFNull, + @"width": RCTNullIfNil(width), + @"height": RCTNullIfNil(height) + }]; + + [items addObject:@{@"kind": @"file", + @"type": RCTNullIfNil(MIMETypeString), + }]; + + [types addObject:RCTNullIfNil(MIMETypeString)]; + } + } + + NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + if (imageType && fileNames.count == 0) { + NSString *MIMETypeString = imageType == NSPasteboardTypePNG ? @"image/png" : @"image/tiff"; + NSData *imageData = [pasteboard dataForType:imageType]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + [files addObject:@{@"type": RCTNullIfNil(MIMETypeString), + @"uri": RCTDataURL(MIMETypeString, imageData).absoluteString, + @"size": @(imageData.length), + @"width": @(image.size.width), + @"height": @(image.size.height), + }]; + + [items addObject:@{@"kind": @"image", + @"type": RCTNullIfNil(MIMETypeString), + }]; + + [types addObject:RCTNullIfNil(MIMETypeString)]; + } + + return @{@"dataTransfer": @{@"files": files, + @"items": items, + @"types": types}}; +} + +- (NSDictionary*)locationInfoFromDraggingLocation:(NSPoint)locationInWindow +{ + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + return @{@"screenX": @(locationInWindow.x), + @"screenY": @(locationInWindow.y), + @"clientX": @(locationInView.x), + @"clientY": @(locationInView.y) + }; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + NSPasteboard *pboard = sender.draggingPasteboard; + NSDragOperation sourceDragMask = sender.draggingSourceOperationMask; + + [self sendMouseEventWithBlock:self.onDragEnter + locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation] + modifierFlags:0 + additionalData:[self dataTransferInfoFromPasteboard:pboard]]; + + if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) { + if (sourceDragMask & NSDragOperationLink) { + return NSDragOperationLink; + } else if (sourceDragMask & NSDragOperationCopy) { + return NSDragOperationCopy; + } + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id)sender +{ + [self sendMouseEventWithBlock:self.onDragLeave + locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation] + modifierFlags:0 + additionalData:[self dataTransferInfoFromPasteboard:sender.draggingPasteboard]]; +} + +- (BOOL)performDragOperation:(id )sender +{ + [self sendMouseEventWithBlock:self.onDrop + locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation] + modifierFlags:0 + additionalData:[self dataTransferInfoFromPasteboard:sender.draggingPasteboard]]; + return YES; +} + +#pragma mark - Keyboard Events + +// This dictionary is attached to the NSEvent being handled so we can ensure we only dispatch it +// once per RCTView\nativeTag. The reason we need to track this state is that certain React native +// views such as RCTUITextView inherit from views (such as NSTextView) which may or may not +// decide to bubble the event to the next responder, and we don't want to dispatch the same +// event more than once (e.g. first from RCTUITextView, and then from it's parent RCTView). +NSMutableDictionary *GetEventDispatchStateDictionary(NSEvent *event) { + static const char *key = "RCTEventDispatchStateDictionary"; + NSMutableDictionary *dict = objc_getAssociatedObject(event, key); + if (dict == nil) { + dict = [NSMutableDictionary new]; + objc_setAssociatedObject(event, key, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return dict; +} + +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event shouldBlock:(BOOL *)shouldBlock { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; + + // If the view is focusable and the component didn't explicity set the validKeysDown or validKeysUp, + // allow enter/return and spacebar key events to mimic the behavior of native controls. + if (self.focusable && validKeys == nil) { + validKeys = @[ + [[RCTHandledKey alloc] initWithKey:@"Enter"], + [[RCTHandledKey alloc] initWithKey:@" "] + ]; + } + + // If a view specifies a key, it will always be removed from the responder chain (i.e. "handled") + *shouldBlock = [RCTHandledKey event:event matchesFilter:validKeys]; + + // If an event isn't being removed from the queue, but was requested to "passthrough" by a view, + // we want to be sure we dispatch it only once for that view. See note for GetEventDispatchStateDictionary. + if ([self passthroughAllKeyEvents] && !*shouldBlock) { + NSNumber *tag = [self reactTag]; + NSMutableDictionary *dict = GetEventDispatchStateDictionary(event); + + if ([dict[tag] boolValue]) { + return nil; + } + + dict[tag] = @YES; + } + + // Don't pass events we don't care about + if (![self passthroughAllKeyEvents] && !*shouldBlock) { + return nil; + } + + return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; +} + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { + BOOL shouldBlock = YES; + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock]; + if (keyboardEvent) { + [_eventDispatcher sendEvent:keyboardEvent]; + return shouldBlock; + } + } + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + } +} + +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } +} +#endif // macOS] + +@end diff --git a/packages/react-native/React/Views/RCTViewKeyboardEvent.h b/packages/react-native/React/Views/RCTViewKeyboardEvent.h new file mode 100644 index 00000000000000..e4b60b62b2479e --- /dev/null +++ b/packages/react-native/React/Views/RCTViewKeyboardEvent.h @@ -0,0 +1,17 @@ +/* + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +@interface RCTViewKeyboardEvent : RCTComponentEvent + +#if TARGET_OS_OSX // [macOS ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event; ++ (NSString *)keyFromEvent:(NSEvent *)event; ++ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag; +#endif // macOS] + +@end diff --git a/packages/react-native/React/Views/RCTViewKeyboardEvent.m b/packages/react-native/React/Views/RCTViewKeyboardEvent.m new file mode 100644 index 00000000000000..125586ee047b74 --- /dev/null +++ b/packages/react-native/React/Views/RCTViewKeyboardEvent.m @@ -0,0 +1,85 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + // [macOS] + +#import "RCTViewKeyboardEvent.h" +#import + +@implementation RCTViewKeyboardEvent + +#if TARGET_OS_OSX ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event +{ + NSString *key = [self keyFromEvent:event]; + NSEventModifierFlags modifierFlags = event.modifierFlags; + + // when making changes here, also consider what should happen to RCTHandledKey. [macOS] + return @{ + @"key" : key, + @"capsLockKey" : (modifierFlags & NSEventModifierFlagCapsLock) ? @YES : @NO, + @"shiftKey" : (modifierFlags & NSEventModifierFlagShift) ? @YES : @NO, + @"ctrlKey" : (modifierFlags & NSEventModifierFlagControl) ? @YES : @NO, + @"altKey" : (modifierFlags & NSEventModifierFlagOption) ? @YES : @NO, + @"metaKey" : (modifierFlags & NSEventModifierFlagCommand) ? @YES : @NO, + @"numericPadKey" : (modifierFlags & NSEventModifierFlagNumericPad) ? @YES : @NO, + @"helpKey" : (modifierFlags & NSEventModifierFlagHelp) ? @YES : @NO, + @"functionKey" : (modifierFlags & NSEventModifierFlagFunction) ? @YES : @NO, + }; +} + ++ (NSString *)keyFromEvent:(NSEvent *)event +{ + NSString *key = event.charactersIgnoringModifiers; + unichar const code = key.length > 0 ? [key characterAtIndex:0] : 0; + + if (event.keyCode == 48) { + return @"Tab"; + } else if (event.keyCode == 53) { + return @"Escape"; + } else if (code == NSEnterCharacter || code == NSNewlineCharacter || code == NSCarriageReturnCharacter) { + return @"Enter"; + } else if (code == NSLeftArrowFunctionKey) { + return @"ArrowLeft"; + } else if (code == NSRightArrowFunctionKey) { + return @"ArrowRight"; + } else if (code == NSUpArrowFunctionKey) { + return @"ArrowUp"; + } else if (code == NSDownArrowFunctionKey) { + return @"ArrowDown"; + } else if (code == NSBackspaceCharacter || code == NSDeleteCharacter) { + return @"Backspace"; + } else if (code == NSDeleteFunctionKey) { + return @"Delete"; + } else if (code == NSHomeFunctionKey) { + return @"Home"; + } else if (code == NSEndFunctionKey) { + return @"End"; + } else if (code == NSPageUpFunctionKey) { + return @"PageUp"; + } else if (code == NSPageDownFunctionKey) { + return @"PageDown"; + } + + return key; +} + +// Keyboard mappings are aligned cross-platform as much as possible as per this doc https://github.com/microsoft/react-native-windows/blob/master/vnext/proposals/active/keyboard-reconcile-desktop.md ++ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag +{ + // Ignore "dead keys" (key press that waits for another key to make a character) + if (!event.charactersIgnoringModifiers.length) { + return nil; + } + + return [[self alloc] initWithName:(event.type == NSEventTypeKeyDown ? @"keyDown" : @"keyUp") + viewTag:reactTag + body:[self bodyFromEvent:event]]; +} +#endif + +@end diff --git a/packages/react-native/React/Views/RCTViewManager.h b/packages/react-native/React/Views/RCTViewManager.h index 1676e69f2aca86..f606ae32d11f3d 100644 --- a/packages/react-native/React/Views/RCTViewManager.h +++ b/packages/react-native/React/Views/RCTViewManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -19,7 +19,7 @@ @class RCTSparseArray; @class RCTUIManager; -typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, NSDictionary *viewRegistry); +typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, NSDictionary *viewRegistry); // [macOS] @interface RCTViewManager : NSObject @@ -37,7 +37,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTUIView *view = viewRegistry[viewTag]; + [view reactFocus]; + }]; +} + +RCT_EXPORT_METHOD(blur : (nonnull NSNumber *)viewTag) +{ + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTUIView *view = viewRegistry[viewTag]; + [view reactBlur]; + }]; +} +#endif // macOS] + #pragma mark - View properties // Accessibility related properties +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.isAccessibilityElement, BOOL) +#else // [macOS +RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.accessibilityElement, BOOL) +#endif // macOS] RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray) RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString) +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHint, NSString) +#else // [macOS +RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHelp, NSString) +#endif // macOS] RCT_REMAP_VIEW_PROPERTY(accessibilityLanguage, reactAccessibilityElement.accessibilityLanguage, NSString) RCT_REMAP_VIEW_PROPERTY(accessibilityValue, reactAccessibilityElement.accessibilityValueInternal, NSDictionary) +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL) RCT_REMAP_VIEW_PROPERTY(accessibilityElementsHidden, reactAccessibilityElement.accessibilityElementsHidden, BOOL) RCT_REMAP_VIEW_PROPERTY( accessibilityIgnoresInvertColors, reactAccessibilityElement.shouldAccessibilityIgnoresInvertColors, BOOL) +#endif // [macOS] RCT_REMAP_VIEW_PROPERTY(onAccessibilityAction, reactAccessibilityElement.onAccessibilityAction, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(onAccessibilityTap, reactAccessibilityElement.onAccessibilityTap, RCTDirectEventBlock) +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(onMagicTap, reactAccessibilityElement.onMagicTap, RCTDirectEventBlock) +#else // [macOS accessibilityTraits is gone in react-native and deprecated in react-native-macos, use accessibilityRole instead +RCT_CUSTOM_VIEW_PROPERTY(accessibilityTraits, NSString, RCTView) +{ + if (json) { + view.accessibilityRole = [RCTConvert accessibilityRoleFromTraits:json]; + } else { + view.accessibilityRole = defaultView.accessibilityRole; + } +} +#endif // macOS] RCT_REMAP_VIEW_PROPERTY(onAccessibilityEscape, reactAccessibilityElement.onAccessibilityEscape, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) +#else // [macOS +RCT_REMAP_VIEW_PROPERTY(opacity, alphaValue, CGFloat) +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor) RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize) RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) +#else // [macOS +RCT_EXPORT_VIEW_PROPERTY(shadowColor, NSColor) +RCT_EXPORT_VIEW_PROPERTY(shadowOffset, CGSize) +RCT_EXPORT_VIEW_PROPERTY(shadowOpacity, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(shadowRadius, CGFloat) +#endif // macOS] + RCT_REMAP_VIEW_PROPERTY(needsOffscreenAlphaCompositing, layer.allowsGroupOpacity, BOOL) RCT_CUSTOM_VIEW_PROPERTY(overflow, YGOverflow, RCTView) { @@ -209,16 +272,19 @@ - (RCTShadowView *)shadowView view.clipsToBounds = defaultView.clipsToBounds; } } +#if !TARGET_OS_OSX // [macOS] RCT_CUSTOM_VIEW_PROPERTY(shouldRasterizeIOS, BOOL, RCTView) { view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize; view.layer.rasterizationScale = view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale; } +#endif // [macOS] RCT_REMAP_VIEW_PROPERTY(transform, reactTransform, CATransform3D) RCT_REMAP_VIEW_PROPERTY(transformOrigin, reactTransformOrigin, RCTTransformOrigin) +#if !TARGET_OS_OSX // [macOS] RCT_CUSTOM_VIEW_PROPERTY(accessibilityRole, UIAccessibilityTraits, RCTView) { UIAccessibilityTraits accessibilityRoleTraits = @@ -254,8 +320,9 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie view.reactAccessibilityElement.accessibilityTraits |= view.reactAccessibilityElement.role ? view.reactAccessibilityElement.roleTraits : view.reactAccessibilityElement.accessibilityRole ? view.reactAccessibilityElement.accessibilityRoleTraits - : (defaultView.accessibilityTraits & AccessibilityRolesMask); + : (defaultView.accessibilityTraits & AccessibilityRolesMask); } +#endif // [macOS] RCT_CUSTOM_VIEW_PROPERTY(accessibilityState, NSDictionary, RCTView) { @@ -266,6 +333,7 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie return; } +#if !TARGET_OS_OSX // [macOS] const UIAccessibilityTraits AccessibilityStatesMask = UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected; view.reactAccessibilityElement.accessibilityTraits = view.reactAccessibilityElement.accessibilityTraits & ~AccessibilityStatesMask; @@ -283,11 +351,17 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie newState[s] = val; } } +#else // [macOS + for (NSString *s in state) { + id val = [state objectForKey:s]; + if (val == nil) { + continue; + } + newState[s] = val; + } +#endif // macOS] if (newState.count > 0) { view.reactAccessibilityElement.accessibilityState = newState; - // Post a layout change notification to make sure VoiceOver get notified for the state - // changes that don't happen upon users' click. - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } else { view.reactAccessibilityElement.accessibilityState = nil; } @@ -386,6 +460,47 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie } } +#if TARGET_OS_OSX // [macOS +// macOS properties +RCT_CUSTOM_VIEW_PROPERTY(acceptsFirstMouse, BOOL, RCTView) +{ + if ([view respondsToSelector:@selector(setAcceptsFirstMouse:)]) { + view.acceptsFirstMouse = json ? [RCTConvert BOOL:json] : defaultView.acceptsFirstMouse; + } +} + +RCT_EXPORT_VIEW_PROPERTY(mouseDownCanMoveWindow, BOOL) + +RCT_REMAP_VIEW_PROPERTY(allowsVibrancy, allowsVibrancyInternal, BOOL) + +RCT_CUSTOM_VIEW_PROPERTY(focusable, BOOL, RCTView) +{ + if ([view respondsToSelector:@selector(setFocusable:)]) { + view.focusable = json ? [RCTConvert BOOL:json] : defaultView.focusable; + } +} + +RCT_CUSTOM_VIEW_PROPERTY(enableFocusRing, BOOL, RCTView) +{ + if ([view respondsToSelector:@selector(setEnableFocusRing:)]) { + view.enableFocusRing = json ? [RCTConvert BOOL:json] : defaultView.enableFocusRing; + } +} + +RCT_REMAP_VIEW_PROPERTY(tooltip, toolTip, NSString) + +RCT_CUSTOM_VIEW_PROPERTY(draggedTypes, NSArray*, RCTView) +{ + NSArray *currentTypes = view.registeredDraggedTypes; + NSArray *types = json ? [RCTConvert NSPasteboardTypeArray:json] : defaultView.registeredDraggedTypes; + if (![currentTypes isEqualToArray:types]) { + [view unregisterDraggedTypes]; + } + [view registerForDraggedTypes:types]; +} + +#endif // macOS] + RCT_CUSTOM_VIEW_PROPERTY(collapsable, BOOL, RCTView) { // Property is only to be used in the new renderer. @@ -448,6 +563,29 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie RCT_REMAP_VIEW_PROPERTY(display, reactDisplay, YGDisplay) RCT_REMAP_VIEW_PROPERTY(zIndex, reactZIndex, NSInteger) +// [macOS +RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock) +// macOS] + + +#if TARGET_OS_OSX // [macOS +#pragma mark - macOS properties + +RCT_EXPORT_VIEW_PROPERTY(cursor, RCTCursor) +RCT_EXPORT_VIEW_PROPERTY(onMouseEnter, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMouseLeave, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onDragLeave, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(passthroughAllKeyEvents, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock) // macOS keyboard events +RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock) // macOS keyboard events +RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) + +#endif // macOS] + #pragma mark - ShadowView properties RCT_EXPORT_SHADOW_PROPERTY(top, YGValue) diff --git a/packages/react-native/React/Views/RCTViewUtils.h b/packages/react-native/React/Views/RCTViewUtils.h index ef2a335c0166a1..fea8846e919bbf 100644 --- a/packages/react-native/React/Views/RCTViewUtils.h +++ b/packages/react-native/React/Views/RCTViewUtils.h @@ -6,13 +6,13 @@ */ #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN RCT_EXTERN_C_BEGIN -UIEdgeInsets RCTContentInsets(UIView *view); +UIEdgeInsets RCTContentInsets(RCTPlatformView *view); // [macOS] RCT_EXTERN_C_END diff --git a/packages/react-native/React/Views/RCTViewUtils.m b/packages/react-native/React/Views/RCTViewUtils.m index d852325e6f5f36..4872e00fa5c73f 100644 --- a/packages/react-native/React/Views/RCTViewUtils.m +++ b/packages/react-native/React/Views/RCTViewUtils.m @@ -9,8 +9,9 @@ #import "UIView+React.h" -UIEdgeInsets RCTContentInsets(UIView *view) +UIEdgeInsets RCTContentInsets(RCTPlatformView *view) // [macOS] { +#if !TARGET_OS_OSX // [macOS] while (view) { UIViewController *controller = view.reactViewController; if (controller) { @@ -18,5 +19,6 @@ UIEdgeInsets RCTContentInsets(UIView *view) } view = view.superview; } +#endif // [macOS] return UIEdgeInsetsZero; } diff --git a/packages/react-native/React/Views/RCTWrapperViewController.h b/packages/react-native/React/Views/RCTWrapperViewController.h index b8277587684bd7..a0c075613c413e 100644 --- a/packages/react-native/React/Views/RCTWrapperViewController.h +++ b/packages/react-native/React/Views/RCTWrapperViewController.h @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @class RCTWrapperViewController; @interface RCTWrapperViewController : UIViewController -- (instancetype)initWithContentView:(UIView *)contentView NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContentView:(RCTUIView *)contentView NS_DESIGNATED_INITIALIZER; // [macOS] @end diff --git a/packages/react-native/React/Views/RCTWrapperViewController.m b/packages/react-native/React/Views/RCTWrapperViewController.m index eb041ad79a646e..ec84293ebd03b6 100644 --- a/packages/react-native/React/Views/RCTWrapperViewController.m +++ b/packages/react-native/React/Views/RCTWrapperViewController.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTWrapperViewController.h" #import @@ -80,3 +81,4 @@ - (void)loadView } @end +#endif diff --git a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.h index e9b330fa7c29c4..9cd5166885da63 100644 --- a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.h +++ b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import #import @@ -14,6 +15,7 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; -@property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, weak) RCTUIScrollView *scrollView; // [macOS] @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m index b09e6530c24841..7c24d50e516c1f 100644 --- a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTRefreshControl.h" #import "RCTRefreshableProtocol.h" @@ -204,3 +205,4 @@ - (void)refreshControlValueChanged } @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/packages/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m index 40aaf9c51ebda9..1ba8e955089202 100644 --- a/packages/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +++ b/packages/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import #import "RCTRefreshControl.h" @@ -41,3 +42,4 @@ - (UIView *)view } @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/RefreshControl/RCTRefreshableProtocol.h b/packages/react-native/React/Views/RefreshControl/RCTRefreshableProtocol.h index b0b06f2e4db736..122767b8617702 100644 --- a/packages/react-native/React/Views/RefreshControl/RCTRefreshableProtocol.h +++ b/packages/react-native/React/Views/RefreshControl/RCTRefreshableProtocol.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] /** * Protocol used to dispatch commands in `RCTRefreshControlManager.h`. diff --git a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.h b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.h index 4817b68d5272d3..2b4ddafa879d32 100644 --- a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.h +++ b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.m b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.m index 41dc74b06b66be..dfd4b68f8d6f74 100644 --- a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.m +++ b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaView.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTSafeAreaView.h" #import @@ -29,6 +30,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)decoder) RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) +#if DEBUG // [macOS] description is a debug-only feature - (NSString *)description { NSString *superDescription = [super description]; @@ -43,6 +45,7 @@ - (NSString *)description NSStringFromUIEdgeInsets(self.safeAreaInsets), NSStringFromUIEdgeInsets(_currentSafeAreaInsets)]; } +#endif // [macOS] static BOOL UIEdgeInsetsEqualToEdgeInsetsWithThreshold(UIEdgeInsets insets1, UIEdgeInsets insets2, CGFloat threshold) { @@ -68,3 +71,4 @@ - (void)setSafeAreaInsets:(UIEdgeInsets)safeAreaInsets } @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewLocalData.h b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewLocalData.h index b78edb76c35ece..a9482d01e687ec 100644 --- a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewLocalData.h +++ b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewLocalData.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewManager.m b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewManager.m index 284500455da21a..2e752799a9b37a 100644 --- a/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewManager.m +++ b/packages/react-native/React/Views/SafeAreaView/RCTSafeAreaViewManager.m @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#if !TARGET_OS_OSX // [macOS] #import "RCTSafeAreaViewManager.h" #import "RCTSafeAreaShadowView.h" @@ -26,3 +27,4 @@ - (RCTSafeAreaShadowView *)shadowView } @end +#endif // [macOS] diff --git a/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.h b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.h new file mode 100644 index 00000000000000..3230502b5701f7 --- /dev/null +++ b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#if TARGET_OS_OSX +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTScrollContentLocalData : NSObject + +@property (nonatomic, assign) CGFloat horizontalScrollerHeight; +@property (nonatomic, assign) CGFloat verticalScrollerWidth; + +- (instancetype)initWithVerticalScrollerWidth:(CGFloat)verticalScrollerWidth + horizontalScrollerHeight:(CGFloat)horizontalScrollerHeight; + +@end + +NS_ASSUME_NONNULL_END + +#endif // [macOS] diff --git a/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m new file mode 100644 index 00000000000000..d2da7e0a607192 --- /dev/null +++ b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// [macOS] + +#if TARGET_OS_OSX +#import "RCTScrollContentLocalData.h" + +@implementation RCTScrollContentLocalData + +- (instancetype)initWithVerticalScrollerWidth:(CGFloat)verticalScrollerWidth + horizontalScrollerHeight:(CGFloat)horizontalScrollerHeight; +{ + if (self = [super init]) { + _verticalScrollerWidth = verticalScrollerWidth; + _horizontalScrollerHeight = horizontalScrollerHeight; + } + return self; +} + +@end +#endif \ No newline at end of file diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.h b/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.h index 126a57586b6daf..d7c570055f275d 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.h +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.m b/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.m index c892556c6aa104..e62bd7287ab5f3 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentShadowView.m @@ -9,10 +9,28 @@ #import +#if TARGET_OS_OSX // [macOS +#import "RCTScrollContentLocalData.h" +#endif // macOS] + #import "RCTUtils.h" @implementation RCTScrollContentShadowView +#if TARGET_OS_OSX // [macOS +- (void)setLocalData:(RCTScrollContentLocalData *)localData +{ + RCTAssert( + [localData isKindOfClass:[RCTScrollContentLocalData class]], + @"Local data object for `RCTScrollContentView` must be `RCTScrollContentLocalData` instance."); + + super.marginEnd = (YGValue){localData.verticalScrollerWidth, YGUnitPoint}; + super.marginBottom = (YGValue){localData.horizontalScrollerHeight, YGUnitPoint}; + + [self didSetProps:@[@"marginEnd", @"marginBottom"]]; +} +#endif // macOS] + - (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayoutContext)layoutContext { if (layoutMetrics.layoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.h b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.h index 853a76b647cea2..012a2c2456dd12 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.h +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.h @@ -5,10 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import @interface RCTScrollContentView : RCTView +#if TARGET_OS_OSX // [macOS +@property (nonatomic, assign, getter=isInverted) BOOL inverted; +#endif // macOS] + @end diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m index 08fcd52c499b02..134d7217f47450 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m @@ -10,15 +10,31 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#import "RCTScrollContentLocalData.h" +#endif // macOS] + #import "RCTScrollView.h" @implementation RCTScrollContentView +#if TARGET_OS_OSX // [macOS +- (BOOL)isFlipped +{ + return !self.inverted; +} +#endif // macOS] - (void)reactSetFrame:(CGRect)frame { [super reactSetFrame:frame]; +#if !TARGET_OS_OSX // [macOS] RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview; +#else // [macOS + // macOS also has a NSClipView in its hierarchy + RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview.superview; +#endif // macOS] if (!scrollView) { return; @@ -27,6 +43,35 @@ - (void)reactSetFrame:(CGRect)frame RCTAssert([scrollView isKindOfClass:[RCTScrollView class]], @"Unexpected view hierarchy of RCTScrollView component."); [scrollView updateContentSizeIfNeeded]; +#if TARGET_OS_OSX // [macOS + // On macOS scroll indicators may float over the content view like they do in iOS + // or depending on system preferences they may be outside of the content view + // which means the clip view will be smaller than the scroll view itself. + // In such cases the content view layout must shrink accordingly otherwise + // the contents will overflow causing the scroll indicators to appear unnecessarily. + NSScrollView *platformScrollView = [scrollView scrollView]; + if ([platformScrollView scrollerStyle] == NSScrollerStyleLegacy) { + BOOL contentHasHeight = platformScrollView.contentSize.height > 0; + CGFloat horizontalScrollerHeight = ([platformScrollView hasHorizontalScroller] && contentHasHeight) ? NSHeight([[platformScrollView horizontalScroller] frame]) : 0; + CGFloat verticalScrollerWidth = [platformScrollView hasVerticalScroller] ? NSWidth([[platformScrollView verticalScroller] frame]) : 0; + + RCTScrollContentLocalData *localData = [[RCTScrollContentLocalData alloc] initWithVerticalScrollerWidth:verticalScrollerWidth horizontalScrollerHeight:horizontalScrollerHeight]; + + [[[scrollView bridge] uiManager] setLocalData:localData forView:self]; + } + + if ([platformScrollView accessibilityRole] == NSAccessibilityTableRole) { + NSMutableArray *subViews = [[NSMutableArray alloc] initWithCapacity:[[self subviews] count]]; + for (NSView *view in [self subviews]) { + if ([view isKindOfClass:[RCTView class]]) { + [subViews addObject:view]; + } + } + + [platformScrollView setAccessibilityRows:subViews]; + } + +#endif // macOS] } @end diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentViewManager.m b/packages/react-native/React/Views/ScrollView/RCTScrollContentViewManager.m index 5ebbddf30cced2..677003968dabb9 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentViewManager.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentViewManager.m @@ -14,6 +14,8 @@ @implementation RCTScrollContentViewManager RCT_EXPORT_MODULE() +RCT_EXPORT_OSX_VIEW_PROPERTY(inverted, BOOL) // [macOS] + - (RCTScrollContentView *)view { return [RCTScrollContentView new]; diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollEvent.m b/packages/react-native/React/Views/ScrollView/RCTScrollEvent.m index 944c64cdb408e1..b5139e0f930b3e 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollEvent.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollEvent.m @@ -16,6 +16,7 @@ @implementation RCTScrollEvent { CGFloat _scrollViewZoomScale; NSDictionary *_userData; uint16_t _coalescingKey; + NSDictionary *_body; // [macOS] } @synthesize viewTag = _viewTag; @@ -43,6 +44,7 @@ - (instancetype)initWithEventName:(NSString *)eventName _scrollViewZoomScale = scrollViewZoomScale; _userData = userData; _coalescingKey = coalescingKey; + _body = [self body]; // [macOS] } return self; } @@ -103,7 +105,11 @@ + (NSString *)moduleDotMethod - (NSArray *)arguments { - return @[ self.viewTag, RCTNormalizeInputEventName(self.eventName), [self body] ]; + // TODO: Revisit when FB issues their Main Thread Checker fix. + // Previously this called [self body], which in turn calls view-related methods. + // Because the arguments method is called from a background thread, we now return + // the cached metrics from _body to avoid calling main-thread-specific methods. + return @[ self.viewTag, RCTNormalizeInputEventName(self.eventName), _body ]; // [macOS] } @end diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.h b/packages/react-native/React/Views/ScrollView/RCTScrollView.h index d57793b65d9fe7..1f28474fb836ec 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.h +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -15,10 +15,17 @@ @protocol UIScrollViewDelegate; -@interface RCTScrollView : RCTView +@interface RCTScrollView : RCTView < +#if TARGET_OS_IPHONE // [macOS + UIScrollViewDelegate, +#endif + RCTScrollableProtocol, RCTAutoInsetsProtocol +> // macOS] - (instancetype)initWithEventDispatcher:(id)eventDispatcher NS_DESIGNATED_INITIALIZER; +@property (nonatomic, readonly) RCTBridge *bridge; + /** * The `RCTScrollView` may have at most one single subview. This will ensure * that the scroll view's `contentSize` will be efficiently set to the size of @@ -26,12 +33,12 @@ * efficiently since it will have already been computed by the off-main-thread * layout system. */ -@property (nonatomic, readonly) UIView *contentView; +@property (nonatomic, readonly) RCTUIView *contentView; // [macOS] /** * The underlying scrollView (TODO: can we remove this?) */ -@property (nonatomic, readonly) UIScrollView *scrollView; +@property (nonatomic, readonly) RCTUIScrollView *scrollView; // [macOS] @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @@ -50,6 +57,8 @@ @property (nonatomic, assign) BOOL inverted; /** Focus area of newly-activated text input relative to the window to compare against UIKeyboardFrameBegin/End */ @property (nonatomic, assign) CGRect firstResponderFocus; +@property (nonatomic, assign) BOOL hasOverlayStyleIndicator; // [macOS] + // NOTE: currently these event props are only declared so we can export the // event names to JS - we don't call the blocks directly because scroll events @@ -60,14 +69,21 @@ @property (nonatomic, copy) RCTDirectEventBlock onScrollEndDrag; @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollBegin; @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollEnd; +@property (nonatomic, copy) RCTDirectEventBlock onPreferredScrollerStyleDidChange; // [macOS] + +@property (nonatomic, copy) RCTDirectEventBlock onInvertedDidChange; // [macOS] + +- (void)flashScrollIndicators; // [macOS] @end -@interface UIView (RCTScrollView) +#if !TARGET_OS_OSX // [macOS] +@interface UIView (RCTScrollView) // [macOS] - (void)reactUpdateResponderOffsetForScrollView:(RCTScrollView *)scrollView; @end +#endif // [macOS] @interface RCTScrollView (Internal) diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.m b/packages/react-native/React/Views/ScrollView/RCTScrollView.m index 9e8b02708de90c..ffe7919c141626 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -7,11 +7,11 @@ #import "RCTScrollView.h" -#import +#import // [macOS] #import "RCTConvert.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTLog.h" -#import "RCTRefreshControl.h" #import "RCTScrollEvent.h" #import "RCTUIManager.h" #import "RCTUIManagerObserverCoordinator.h" @@ -21,17 +21,36 @@ #import "UIView+Private.h" #import "UIView+React.h" + +#if !TARGET_OS_OSX // [macOS] +#import "RCTRefreshControl.h" +#else // [macOS +#import "RCTI18nUtil.h" +#import "RCTViewKeyboardEvent.h" +#endif // macOS] + /** * Include a custom scroll view subclass because we want to limit certain * default UIKit behaviors such as textFields automatically scrolling * scroll views that contain them. */ -@interface RCTCustomScrollView : UIScrollView +@interface RCTCustomScrollView : +#if !TARGET_OS_OSX // [macOS] + UIScrollView +#else // [macOS + RCTUIScrollView +#endif // macOS] @property (nonatomic, assign) BOOL centerContent; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, strong) UIView *customRefreshControl; @property (nonatomic, assign) BOOL pinchGestureEnabled; - +#else // [macOS ++ (BOOL)isCompatibleWithResponsiveScrolling; +@property (nonatomic, assign, getter=isInverted) BOOL inverted; +@property (nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, strong) NSPanGestureRecognizer *panGestureRecognizer; +#endif // macOS] @end @implementation RCTCustomScrollView @@ -39,6 +58,7 @@ @implementation RCTCustomScrollView - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { +#if !TARGET_OS_OSX // [macOS] [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)]; if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) { @@ -47,16 +67,27 @@ - (instancetype)initWithFrame:(CGRect)frame // scrollbar flip because we also flip it with whole `UIScrollView` flip. self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; } - +#else // [macOS + self.scrollEnabled = YES; + self.hasHorizontalScroller = YES; + self.hasVerticalScroller = YES; + self.autohidesScrollers = YES; + self.panGestureRecognizer = [[NSPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleCustomPan:)]; +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] _pinchGestureEnabled = YES; +#endif // [macOS] } return self; } +#if !TARGET_OS_OSX // [macOS] NSScrollView's contentView is an NSClipView. Use documentView to access NSScrollView's content. - (UIView *)contentView { return ((RCTScrollView *)self.superview).contentView; } +#endif // [macOS] /** * @return Whether or not the scroll view interaction should be blocked because @@ -66,15 +97,36 @@ - (BOOL)_shouldDisableScrollInteraction { // Since this may be called on every pan, we need to make sure to only climb // the hierarchy on rare occasions. - UIView *JSResponder = [RCTUIManager JSResponder]; + RCTPlatformView *JSResponder = [RCTUIManager JSResponder]; // [macOS] if (JSResponder && JSResponder != self.superview) { - BOOL superviewHasResponder = [self isDescendantOfView:JSResponder]; + BOOL superviewHasResponder = RCTUIViewIsDescendantOfView(self, JSResponder); // [macOS] return superviewHasResponder; } return NO; } -- (void)handleCustomPan:(__unused UIPanGestureRecognizer *)sender +#if TARGET_OS_OSX // [macOS ++ (BOOL)isCompatibleWithResponsiveScrolling +{ + return YES; +} + +- (BOOL)isFlipped +{ + return !self.inverted; +} + +- (void)scrollWheel:(NSEvent *)theEvent +{ + if (!self.isScrollEnabled) { + [[self nextResponder] scrollWheel:theEvent]; + return; + } + [super scrollWheel:theEvent]; +} +#endif // macOS] + +- (void)handleCustomPan:(__unused UIGestureRecognizer *)sender // [macOS] { if ([self _shouldDisableScrollInteraction] && ![[RCTUIManager JSResponder] isKindOfClass:[RCTScrollView class]]) { self.panGestureRecognizer.enabled = NO; @@ -102,8 +154,11 @@ - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated if (CGRectIsNull(rect)) { return; } - +#if !TARGET_OS_OSX // [macOS] [super scrollRectToVisible:rect animated:animated]; +#else // [macOS + [super scrollRectToVisible:rect]; +#endif // macOS] } /** @@ -148,13 +203,15 @@ - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated * In order to have this called, you must have delaysContentTouches set to NO * (which is the not the `UIKit` default). */ -- (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view +- (BOOL)touchesShouldCancelInContentView:(__unused RCTUIView *)view // [macOS] { BOOL shouldDisableScrollInteraction = [self _shouldDisableScrollInteraction]; +#if !TARGET_OS_OSX // [macOS] if (shouldDisableScrollInteraction == NO) { [super touchesShouldCancelInContentView:view]; } +#endif // [macOS] return !shouldDisableScrollInteraction; } @@ -167,10 +224,19 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view */ - (void)setContentOffset:(CGPoint)contentOffset { - UIView *contentView = [self contentView]; + RCTUIView *contentView = nil; // [macOS] +#if !TARGET_OS_OSX // [macOS] + contentView = [self contentView]; +#else // [macOS + contentView = (RCTUIView *) self.documentView; // [macOS] NSScrollView's documentView must be of type UIView/RCTView +#endif // macOS] if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) { CGSize subviewSize = contentView.frame.size; +#if !TARGET_OS_OSX // [macOS] CGSize scrollViewSize = self.bounds.size; +#else // [macOS + CGSize scrollViewSize = self.contentView.bounds.size; +#endif // macOS] if (subviewSize.width <= scrollViewSize.width) { contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0; } @@ -178,12 +244,39 @@ - (void)setContentOffset:(CGPoint)contentOffset contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0; } } - +#if !TARGET_OS_OSX // [macOS] super.contentOffset = CGPointMake( RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"), RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); +#else // [macOS + if (!NSEqualPoints(contentOffset, self.documentVisibleRect.origin)) + { + [self.contentView scrollToPoint:contentOffset]; + [self reflectScrolledClipView:self.contentView]; + } +#endif // macOS] } +#if TARGET_OS_OSX // [macOS +- (void)setContentOffset:(CGPoint)contentOffset + animated:(BOOL)animated +{ + if (animated) { + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.3]; + [[self.contentView animator] setBoundsOrigin:contentOffset]; + // Handling a weird bug where setBoundsOrigin doesn't actually update view bounds + if ([[RCTI18nUtil sharedInstance] isRTL] && contentOffset.y < 1) { + [self.contentView scrollToPoint:contentOffset]; + [self reflectScrolledClipView:self.contentView]; + } + [NSAnimationContext endGrouping]; + } else { + self.contentOffset = contentOffset; + } +} +#endif // macOS] + - (void)setFrame:(CGRect)frame { // Preserving and revalidating `contentOffset`. @@ -198,9 +291,11 @@ - (void)setFrame:(CGRect)frame if (CGSizeEqualToSize(contentSize, CGSizeZero)) { self.contentOffset = originalOffset; } else { +#if !TARGET_OS_OSX // [macOS] if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, self.adjustedContentInset)) { contentInset = self.adjustedContentInset; } +#endif // [macOS] CGSize boundsSize = self.bounds.size; CGFloat xMaxOffset = contentSize.width - boundsSize.width + contentInset.right; CGFloat yMaxOffset = contentSize.height - boundsSize.height + contentInset.bottom; @@ -215,6 +310,7 @@ - (void)setFrame:(CGRect)frame } } +#if !TARGET_OS_OSX // [macOS] - (void)setCustomRefreshControl:(UIView *)refreshControl { if (_customRefreshControl) { @@ -247,6 +343,20 @@ - (void)didMoveToWindow // in the setter gets overridden when the view loads. self.pinchGestureRecognizer.enabled = _pinchGestureEnabled; } +#endif // [macOS] + +#if TARGET_OS_OSX // [macOS +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + [super setAccessibilityLabel:accessibilityLabel]; + [[self documentView] setAccessibilityLabel:accessibilityLabel]; +} +- (void)setDocumentView:(__kindof NSView *)documentView +{ + [super setDocumentView:documentView]; + [documentView setAccessibilityLabel:[self accessibilityLabel]]; +} +#endif // macOS] - (BOOL)shouldGroupAccessibilityChildren { @@ -262,18 +372,25 @@ @interface RCTScrollView () @implementation RCTScrollView { id _eventDispatcher; CGRect _prevFirstVisibleFrame; - __weak UIView *_firstVisibleView; + __weak RCTUIView *_firstVisibleView; // [macOS] RCTCustomScrollView *_scrollView; +#if !TARGET_OS_OSX // [macOS] UIView *_contentView; +#endif // [macOS] NSTimeInterval _lastScrollDispatchTime; NSMutableArray *_cachedChildFrames; BOOL _allowNextScrollNoMatterWhat; +#if TARGET_OS_OSX // [macOS + BOOL _notifyDidScroll; + NSPoint _lastScrollPosition; +#endif // macOS] CGRect _lastClippedToRect; uint16_t _coalescingKey; NSString *_lastEmittedEventName; NSHashTable *_scrollListeners; } +#if !TARGET_OS_OSX // [macOS] UIKeyboard notifications not needed on macOS - (void)_registerKeyboardListener { [[NSNotificationCenter defaultCenter] addObserver:self @@ -369,24 +486,34 @@ - (void)_keyboardWillChangeFrame:(NSNotification *)notification } completion:nil]; } +#endif // [macOS] - (instancetype)initWithEventDispatcher:(id)eventDispatcher { RCTAssertParam(eventDispatcher); if ((self = [super initWithFrame:CGRectZero])) { +#if !TARGET_OS_OSX // [macOS] [self _registerKeyboardListener]; +#endif // [macOS] _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#if !TARGET_OS_OSX // [macOS] _scrollView.delegate = self; _scrollView.delaysContentTouches = NO; +#else // [macOS + _scrollView.postsBoundsChangedNotifications = YES; + _lastScrollPosition = NSZeroPoint; +#endif // macOS] +#if !TARGET_OS_OSX // [macOS] // We set the default behavior to "never" so that iOS // doesn't do weird things to UIScrollView insets automatically // and keeps it as an opt-in behavior. _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; +#endif // [macOS] _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; @@ -403,15 +530,75 @@ - (instancetype)initWithEventDispatcher:(id)eventDis return self; } +#if TARGET_OS_OSX // [macOS +- (BOOL)canBecomeKeyView +{ + return [self focusable]; +} + +- (CGRect)focusRingMaskBounds +{ + return [self bounds]; +} + +- (void)drawFocusRingMask +{ + if (self.enableFocusRing) { + NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:2.0 yRadius:2.0]; + [borderPath stroke]; + } +} + +- (RCTBridge *)bridge +{ + return [_eventDispatcher bridge]; +} + +- (RCTUIView *)contentView // [macOS] +{ + return _scrollView.documentView; +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + [_scrollView setAccessibilityLabel:accessibilityLabel]; +} + +- (void)setAccessibilityRole:(NSAccessibilityRole)accessibilityRole +{ + [_scrollView setAccessibilityRole:accessibilityRole]; +} + +- (void)setInverted:(BOOL)inverted +{ + BOOL changed = _inverted != inverted; + _inverted = inverted; + if (changed && _onInvertedDidChange) { + _onInvertedDidChange(@{}); + } +} + +- (void)setHasOverlayStyleIndicator:(BOOL)hasOverlayStyle +{ + if (hasOverlayStyle == true) { + self.scrollView.scrollerStyle = NSScrollerStyleOverlay; + } else { + self.scrollView.scrollerStyle = NSScrollerStyleLegacy; + } +} +#endif // macOS] + RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder) static inline void RCTApplyTransformationAccordingLayoutDirection( - UIView *view, + RCTPlatformView *view, // [macOS] UIUserInterfaceLayoutDirection layoutDirection) { +#if !TARGET_OS_OSX // [macOS] view.transform = layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ? CGAffineTransformIdentity : CGAffineTransformMakeScale(-1, 1); +#endif // [macOS] } - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection @@ -419,7 +606,7 @@ - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection [super setReactLayoutDirection:layoutDirection]; RCTApplyTransformationAccordingLayoutDirection(_scrollView, layoutDirection); - RCTApplyTransformationAccordingLayoutDirection(_contentView, layoutDirection); + RCTApplyTransformationAccordingLayoutDirection(self.contentView, layoutDirection); // macOS use property instead of ivar for mac } - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews @@ -427,9 +614,10 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // Does nothing } -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(RCTUIView *)view atIndex:(NSInteger)atIndex // [macOS] { [super insertReactSubview:view atIndex:atIndex]; +#if !TARGET_OS_OSX // [macOS] if ([view conformsToProtocol:@protocol(RCTCustomRefreshControlProtocol)]) { [_scrollView setCustomRefreshControl:(UIView *)view]; if (![view isKindOfClass:[UIRefreshControl class]] && [view conformsToProtocol:@protocol(UIScrollViewDelegate)]) { @@ -444,11 +632,19 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection); [_scrollView addSubview:view]; } +#else // [macOS + RCTAssert(self.contentView == nil, @"RCTScrollView may only contain a single subview"); + + _scrollView.documentView = view; +#endif // macOS] } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTUIView *)subview // [macOS] { [super removeReactSubview:subview]; +#if TARGET_OS_OSX // [macOS + _scrollView.documentView = nil; +#else // [macOS if ([subview conformsToProtocol:@protocol(RCTCustomRefreshControlProtocol)]) { [_scrollView setCustomRefreshControl:nil]; if (![subview isKindOfClass:[UIRefreshControl class]] && @@ -459,6 +655,7 @@ - (void)removeReactSubview:(UIView *)subview RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; } +#endif // macOS] } - (void)didUpdateReactSubviews @@ -486,14 +683,20 @@ - (void)setCenterContent:(BOOL)centerContent - (void)setClipsToBounds:(BOOL)clipsToBounds { super.clipsToBounds = clipsToBounds; +#if !TARGET_OS_OSX // [macOS] _scrollView.clipsToBounds = clipsToBounds; +#endif // [macOS] } - (void)dealloc { +#if !TARGET_OS_OSX // [macOS] _scrollView.delegate = nil; +#endif // [macOS] [_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self]; +#if !TARGET_OS_OSX // [macOS] [self _unregisterKeyboardListener]; +#endif // [macOS] } - (void)layoutSubviews @@ -502,7 +705,7 @@ - (void)layoutSubviews RCTAssert(self.subviews.count == 1, @"we should only have exactly one subview"); RCTAssert([self.subviews lastObject] == _scrollView, @"our only subview should be a scrollview"); -#if !TARGET_OS_TV +#if !TARGET_OS_TV && !TARGET_OS_OSX // [macOS] // Adjust the refresh control frame if the scrollview layout changes. UIView *refreshControl = _scrollView.customRefreshControl; if (refreshControl && refreshControl.isRefreshing && ![refreshControl isKindOfClass:UIRefreshControl.class]) { @@ -517,7 +720,7 @@ - (void)layoutSubviews - (void)updateClippedSubviews { // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; + RCTPlatformView *clipView = [self react_findClipView]; // [macOS] if (!clipView) { return; } @@ -525,7 +728,11 @@ - (void)updateClippedSubviews static const CGFloat leeway = 1.0; const CGSize contentSize = _scrollView.contentSize; - const CGRect bounds = _scrollView.bounds; +#if !TARGET_OS_OSX // [macOS] + const CGRect bounds = _scrollView.bounds; +#else // [macOS + const CGRect bounds = _scrollView.contentView.bounds; +#endif // macOS] const BOOL scrollsHorizontally = contentSize.width > bounds.size.width; const BOOL scrollsVertically = contentSize.height > bounds.size.height; @@ -542,6 +749,36 @@ - (void)updateClippedSubviews } } +#if TARGET_OS_OSX // [macOS +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; + if ([self window] == nil) { + // Unregister for bounds change notifications + [defaultCenter removeObserver:self + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; + [defaultCenter removeObserver:self + name:NSPreferredScrollerStyleDidChangeNotification + object:nil]; + } else { + // Register for bounds change notifications so we can track scrolling + [defaultCenter addObserver:self + selector:@selector(scrollViewDocumentViewBoundsDidChange:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; // NSClipView + [defaultCenter addObserver:self + selector:@selector(preferredScrollerStyleDidChange:) + name:NSPreferredScrollerStyleDidChangeNotification + object:nil]; + } + + _notifyDidScroll = ([self window] != nil); +} +#endif // macOS] + - (void)setContentInset:(UIEdgeInsets)contentInset { if (UIEdgeInsetsEqualToEdgeInsets(contentInset, _contentInset)) { @@ -556,11 +793,22 @@ - (void)setContentInset:(UIEdgeInsets)contentInset _scrollView.contentOffset = contentOffset; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)isHorizontal:(UIScrollView *)scrollView +#else // [macOS +- (BOOL)isHorizontal:(RCTCustomScrollView *)scrollView +#endif // macOS] { return scrollView.contentSize.width > self.frame.size.width; } +#if TARGET_OS_OSX // [macOS +- (BOOL)isVertical:(RCTCustomScrollView *)scrollView +{ + return scrollView.contentSize.height > self.frame.size.height; +} +#endif // macOS] + - (void)scrollToOffset:(CGPoint)offset { [self scrollToOffset:offset animated:YES]; @@ -582,6 +830,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated 0.01)); // Make width and height greater than 0 // Ensure at least one scroll event will fire _allowNextScrollNoMatterWhat = YES; + if (!CGRectContainsPoint(maxRect, offset) && !self.scrollToOverflowEnabled) { CGFloat x = fmax(offset.x, CGRectGetMinX(maxRect)); x = fmin(x, CGRectGetMaxX(maxRect)); @@ -589,6 +838,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated y = fmin(y, CGRectGetMaxY(maxRect)); offset = CGPointMake(x, y); } + [_scrollView setContentOffset:offset animated:animated]; } } @@ -600,12 +850,17 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated - (void)scrollToEnd:(BOOL)animated { BOOL isHorizontal = [self isHorizontal:_scrollView]; +#if !TARGET_OS_OSX // [macOS + CGSize boundsSize = _scrollView.bounds.size; +#else + CGSize boundsSize = _scrollView.contentView.bounds.size; +#endif // macOS] CGPoint offset; if (isHorizontal) { - CGFloat offsetX = _scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right; + CGFloat offsetX = _scrollView.contentSize.width - boundsSize.width + _scrollView.contentInset.right; // [macOS] offset = CGPointMake(fmax(offsetX, 0), 0); } else { - CGFloat offsetY = _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom; + CGFloat offsetY = _scrollView.contentSize.height - boundsSize.height + _scrollView.contentInset.bottom; // [macOS] offset = CGPointMake(0, fmax(offsetY, 0)); } if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) { @@ -617,7 +872,12 @@ - (void)scrollToEnd:(BOOL)animated - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { +#if TARGET_OS_OSX // [macOS + (void) animated; + [_scrollView magnifyToFitRect:rect]; +#else // [macOS [_scrollView zoomToRect:rect animated:animated]; +#endif // macOS] } - (void)refreshContentInset @@ -625,8 +885,33 @@ - (void)refreshContentInset [RCTView autoAdjustInsetsForView:self withScrollView:_scrollView updateOffset:YES]; } +// [macOS +- (void)flashScrollIndicators +{ +#if !TARGET_OS_OSX + [_scrollView flashScrollIndicators]; +#else + [_scrollView flashScrollers]; +#endif +} +// macOS] + #pragma mark - ScrollView delegate +#if TARGET_OS_OSX // [macOS +- (void)scrollViewDocumentViewBoundsDidChange:(__unused NSNotification *)notification +{ + if (_scrollView.centerContent) { + // contentOffset setter dynamically centers content when _centerContent == YES + [_scrollView setContentOffset:_scrollView.contentOffset]; + } + + if (_notifyDidScroll) { + [self scrollViewDidScroll:_scrollView]; + } +} +#endif // macOS] + #define RCT_SEND_SCROLL_EVENT(_eventName, _userData) \ { \ NSString *eventName = NSStringFromSelector(@selector(_eventName)); \ @@ -647,6 +932,8 @@ -(void)delegateMethod : (UIScrollView *)scrollView \ RCT_FORWARD_SCROLL_EVENT(delegateMethod : scrollView); \ } +#if !TARGET_OS_OSX // [macOS] + RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin) RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll) RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop) @@ -661,10 +948,28 @@ - (void)removeScrollListener:(NSObject *)scrollListener [_scrollListeners removeObject:scrollListener]; } -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +#endif // [macOS] + +- (void)scrollViewDidScroll:(RCTCustomScrollView *)scrollView // [macOS] { NSTimeInterval now = CACurrentMediaTime(); [self updateClippedSubviews]; + +#if TARGET_OS_OSX // [macOS + /** + * To check for effective scroll position changes, the comparison with lastScrollPosition should happen + * after updateClippedSubviews. updateClippedSubviews will update the display of the vertical/horizontal + * scrollers which can change the clipview bounds. + * This change also ensures that no onScroll events are sent when the React setFrame call is running, + * which could submit onScroll events while the content view was not setup yet. + */ + BOOL didScroll = !NSEqualPoints(scrollView.contentView.bounds.origin, _lastScrollPosition); + if (!didScroll) { + return; + } + _lastScrollPosition = scrollView.contentView.bounds.origin; +#endif // macOS] + /** * TODO: this logic looks wrong, and it may be because it is. Currently, if _scrollEventThrottle * is set to zero (the default), the "didScroll" event is only sent once per scroll, instead of repeatedly @@ -681,9 +986,13 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; } +#if !TARGET_OS_OSX // [macOS] RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll : scrollView); +#endif // [macOS] } +#if !TARGET_OS_OSX // [macOS] + - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { _allowNextScrollNoMatterWhat = YES; // Ensure next scroll event is recorded, regardless of throttle @@ -888,6 +1197,8 @@ - (UIView *)viewForZoomingInScrollView:(__unused UIScrollView *)scrollView return _contentView; } +#endif // [macOS] + - (CGSize)_calculateViewportSize { CGSize viewportSize = self.bounds.size; @@ -902,7 +1213,7 @@ - (CGSize)_calculateViewportSize - (CGSize)contentSize { - return _contentView.frame.size; + return self.contentView.frame.size; // macOS use property instead of ivar for mac } - (void)updateContentSizeIfNeeded @@ -910,6 +1221,9 @@ - (void)updateContentSizeIfNeeded CGSize contentSize = self.contentSize; if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) { _scrollView.contentSize = contentSize; +#if TARGET_OS_OSX // [macOS + [_scrollView setContentOffset:_scrollView.contentOffset]; +#endif // macOS] } } @@ -930,34 +1244,33 @@ - (void)setMaintainVisibleContentPosition:(NSDictionary *)maintainVisibleContent - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager { RCTAssertUIManagerQueue(); - - [manager prependUIBlock:^( - __unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { - BOOL horz = [self isHorizontal:self->_scrollView]; - NSUInteger minIdx = [self->_maintainVisibleContentPosition[@"minIndexForVisible"] integerValue]; - for (NSUInteger ii = minIdx; ii < self->_contentView.subviews.count; ++ii) { - // Find the first entirely visible view. This must be done after we update the content offset - // or it will tend to grab rows that were made visible by the shift in position - UIView *subview = self->_contentView.subviews[ii]; - BOOL hasNewView = NO; - if (horz) { - CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; - CGFloat x = self->_scrollView.contentOffset.x + leftInset; - hasNewView = subview.frame.origin.x > x; - } else { - CGFloat bottomInset = - self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; - CGFloat y = self->_scrollView.contentOffset.y + bottomInset; - hasNewView = subview.frame.origin.y > y; - } - if (hasNewView || ii == self->_contentView.subviews.count - 1) { - self->_prevFirstVisibleFrame = subview.frame; - self->_firstVisibleView = subview; - break; - } - } - }]; - [manager addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { + [manager + prependUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { // [macOS] + BOOL horz = [self isHorizontal:self->_scrollView]; + NSUInteger minIdx = [self->_maintainVisibleContentPosition[@"minIndexForVisible"] integerValue]; + for (NSUInteger ii = minIdx; ii < self.contentView.subviews.count; ++ii) { // macOS use property instead of ivar for mac + // Find the first entirely visible view. This must be done after we update the content offset + // or it will tend to grab rows that were made visible by the shift in position + RCTUIView *subview = self.contentView.subviews[ii]; // [macOS] use property instead of ivar for mac + BOOL hasNewView = NO; + if (horz) { + CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; + CGFloat x = self->_scrollView.contentOffset.x + leftInset; + hasNewView = subview.frame.origin.x > x; + } else { + CGFloat bottomInset = + self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; + CGFloat y = self->_scrollView.contentOffset.y + bottomInset; + hasNewView = subview.frame.origin.y > y; + } + if (hasNewView || ii == self.contentView.subviews.count - 1) { // macOS use property instead of ivar for mac + self->_prevFirstVisibleFrame = subview.frame; + self->_firstVisibleView = subview; + break; + } + } + }]; + [manager addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { // [macOS] if (self->_maintainVisibleContentPosition == nil) { return; // The prop might have changed in the previous UIBlocks, so need to abort here. } @@ -997,6 +1310,73 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager }]; } +// [macOS +#pragma mark - Keyboard Events + +#if TARGET_OS_OSX +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; + + // Only post events for keys we care about + if (![RCTHandledKey event:event matchesFilter:validKeys]) { + return nil; + } + + return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; +} + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; + if (keyboardEvent) { + [_eventDispatcher sendEvent:keyboardEvent]; + return YES; + } + } + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + + // AX: if a tab key was pressed and the first responder is currently clipped by the scroll view, + // automatically scroll to make the view visible to make it navigable via keyboard. + NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + if ([key isEqualToString:@"Tab"]) { + id firstResponder = [[self window] firstResponder]; + if ([firstResponder isKindOfClass:[NSView class]] && + [firstResponder isDescendantOf:[_scrollView documentView]]) { + NSView *view = (NSView*)firstResponder; + NSRect visibleRect = ([view superview] == [_scrollView documentView]) ? NSInsetRect(view.frame, -1, -1) : + [view convertRect:view.frame toView:_scrollView.documentView]; + [[_scrollView documentView] scrollRectToVisible:visibleRect]; + } + } + } +} + +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } +} + +static NSString *RCTStringForScrollerStyle(NSScrollerStyle scrollerStyle) { + switch (scrollerStyle) { + case NSScrollerStyleLegacy: + return @"legacy"; + case NSScrollerStyleOverlay: + return @"overlay"; + } +} + +- (void)preferredScrollerStyleDidChange:(__unused NSNotification *)notification { + RCT_SEND_SCROLL_EVENT(onPreferredScrollerStyleDidChange, (@{ @"preferredScrollerStyle": RCTStringForScrollerStyle([NSScroller preferredScrollerStyle])})); +} +#endif // macOS] + // Note: setting several properties of UIScrollView has the effect of // resetting its contentOffset to {0, 0}. To prevent this, we generate // setters here that will record the contentOffset beforehand, and @@ -1016,6 +1396,7 @@ -(type)getter \ RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceHorizontal, alwaysBounceHorizontal, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceVertical, alwaysBounceVertical, BOOL) +#if !TARGET_OS_OSX // [macOS] RCT_SET_AND_PRESERVE_OFFSET(setBounces, bounces, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setBouncesZoom, bouncesZoom, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setCanCancelContentTouches, canCancelContentTouches, BOOL) @@ -1025,14 +1406,18 @@ -(type)getter \ RCT_SET_AND_PRESERVE_OFFSET(setKeyboardDismissMode, keyboardDismissMode, UIScrollViewKeyboardDismissMode) RCT_SET_AND_PRESERVE_OFFSET(setMaximumZoomScale, maximumZoomScale, CGFloat) RCT_SET_AND_PRESERVE_OFFSET(setMinimumZoomScale, minimumZoomScale, CGFloat) +#endif // [macOS] RCT_SET_AND_PRESERVE_OFFSET(setScrollEnabled, isScrollEnabled, BOOL) +#if !TARGET_OS_OSX // [macOS] RCT_SET_AND_PRESERVE_OFFSET(setPagingEnabled, isPagingEnabled, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setScrollsToTop, scrollsToTop, BOOL) +#endif // [macOS] RCT_SET_AND_PRESERVE_OFFSET(setShowsHorizontalScrollIndicator, showsHorizontalScrollIndicator, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat); RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, scrollIndicatorInsets, UIEdgeInsets); +#if !TARGET_OS_OSX // [macOS] - (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0)) { // `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13. @@ -1047,9 +1432,11 @@ - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBeh _scrollView.contentInsetAdjustmentBehavior = behavior; _scrollView.contentOffset = contentOffset; } +#endif // [macOS] +#pragma clang diagnostic pop // [macOS] - (void)sendScrollEventWithName:(NSString *)eventName - scrollView:(UIScrollView *)scrollView + scrollView:(RCTCustomScrollView *)scrollView // [macOS] userData:(NSDictionary *)userData { if (![_lastEmittedEventName isEqualToString:eventName]) { diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.h b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.h index 3709180eb41b9f..f5a0060df8123a 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.h +++ b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.h @@ -8,11 +8,13 @@ #import #import +#if !TARGET_OS_OSX // [macOS] @interface RCTConvert (UIScrollView) + (UIScrollViewKeyboardDismissMode)UIScrollViewKeyboardDismissMode:(id)json; @end +#endif // [macOS] @interface RCTScrollViewManager : RCTViewManager diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m index 03355504e0a1dc..75e2365f5d1149 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m @@ -12,6 +12,7 @@ #import "RCTShadowView.h" #import "RCTUIManager.h" +#if !TARGET_OS_OSX // [macOS] @implementation RCTConvert (UIScrollView) RCT_ENUM_CONVERTER( @@ -48,35 +49,36 @@ @implementation RCTConvert (UIScrollView) integerValue) @end +#endif // [macOS] @implementation RCTScrollViewManager RCT_EXPORT_MODULE() -- (UIView *)view +- (RCTPlatformView *)view // [macOS] { return [[RCTScrollView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(alwaysBounceHorizontal, BOOL) RCT_EXPORT_VIEW_PROPERTY(alwaysBounceVertical, BOOL) -RCT_EXPORT_VIEW_PROPERTY(bounces, BOOL) -RCT_EXPORT_VIEW_PROPERTY(bouncesZoom, BOOL) -RCT_EXPORT_VIEW_PROPERTY(canCancelContentTouches, BOOL) +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(bounces, BOOL) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(bouncesZoom, BOOL) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(canCancelContentTouches, BOOL) // [macOS] RCT_EXPORT_VIEW_PROPERTY(centerContent, BOOL) RCT_EXPORT_VIEW_PROPERTY(maintainVisibleContentPosition, NSDictionary) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) -RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustKeyboardInsets, BOOL) -RCT_EXPORT_VIEW_PROPERTY(decelerationRate, CGFloat) -RCT_EXPORT_VIEW_PROPERTY(directionalLockEnabled, BOOL) -RCT_EXPORT_VIEW_PROPERTY(indicatorStyle, UIScrollViewIndicatorStyle) -RCT_EXPORT_VIEW_PROPERTY(keyboardDismissMode, UIScrollViewKeyboardDismissMode) -RCT_EXPORT_VIEW_PROPERTY(maximumZoomScale, CGFloat) -RCT_EXPORT_VIEW_PROPERTY(minimumZoomScale, CGFloat) +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(automaticallyAdjustKeyboardInsets, BOOL) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(decelerationRate, CGFloat) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(directionalLockEnabled, BOOL) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(indicatorStyle, UIScrollViewIndicatorStyle) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(keyboardDismissMode, UIScrollViewKeyboardDismissMode) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(maximumZoomScale, CGFloat) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(minimumZoomScale, CGFloat) // [macOS] RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) -RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL) -RCT_REMAP_VIEW_PROPERTY(pinchGestureEnabled, scrollView.pinchGestureEnabled, BOOL) -RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(pagingEnabled, BOOL) // [macOS] +RCT_REMAP_NOT_OSX_VIEW_PROPERTY(pinchGestureEnabled, scrollView.pinchGestureEnabled, BOOL) // [macOS] +RCT_EXPORT_NOT_OSX_VIEW_PROPERTY(scrollsToTop, BOOL) // [macOS] RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) @@ -97,9 +99,13 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock) +RCT_EXPORT_OSX_VIEW_PROPERTY(onInvertedDidChange, RCTDirectEventBlock) // [macOS] +RCT_EXPORT_OSX_VIEW_PROPERTY(onPreferredScrollerStyleDidChange, RCTDirectEventBlock) // [macOS] RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustsScrollIndicatorInsets, BOOL) +#if !TARGET_OS_OSX // [macOS] RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) +#endif // [macOS] // overflow is used both in css-layout as well as by react-native. In css-layout // we always want to treat overflow as scroll but depending on what the overflow @@ -134,8 +140,8 @@ - (UIView *)view : (BOOL)animated) { [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[reactTag]; // [macOS] if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:(CGPoint){x, y} animated:animated]; } else { @@ -151,8 +157,8 @@ - (UIView *)view RCT_EXPORT_METHOD(scrollToEnd : (nonnull NSNumber *)reactTag animated : (BOOL)animated) { [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[reactTag]; // [macOS] if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToEnd:animated]; } else { @@ -168,8 +174,8 @@ - (UIView *)view RCT_EXPORT_METHOD(zoomToRect : (nonnull NSNumber *)reactTag withRect : (CGRect)rect animated : (BOOL)animated) { [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[reactTag]; // [macOS] if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view zoomToRect:rect animated:animated]; } else { @@ -192,7 +198,7 @@ - (UIView *)view return; } - [view.scrollView flashScrollIndicators]; + [view flashScrollIndicators]; // [macOS] }]; } diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollableProtocol.h b/packages/react-native/React/Views/ScrollView/RCTScrollableProtocol.h index d85f135c38e3a1..2bd099608d9e10 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollableProtocol.h +++ b/packages/react-native/React/Views/ScrollView/RCTScrollableProtocol.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] /** * Contains any methods related to scrolling. Any `RCTView` that has scrolling @@ -25,8 +25,10 @@ - (void)scrollToEnd:(BOOL)animated; - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated; +#if !TARGET_OS_OSX // [macOS] - (void)addScrollListener:(NSObject *)scrollListener; - (void)removeScrollListener:(NSObject *)scrollListener; +#endif // [macOS] @end @@ -38,8 +40,10 @@ @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, readonly, getter=isRefreshing) BOOL refreshing; +#if !TARGET_OS_OSX // [macOS] @optional -@property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, weak) RCTUIScrollView *scrollView; +#endif // [macOS] @end diff --git a/packages/react-native/React/Views/UIView+Private.h b/packages/react-native/React/Views/UIView+Private.h index dde159827ab6bc..e08a8d357969ba 100644 --- a/packages/react-native/React/Views/UIView+Private.h +++ b/packages/react-native/React/Views/UIView+Private.h @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] -@interface UIView (Private) +@interface RCTPlatformView (Private) // [macOS] // remove clipped subviews implementation - (void)react_remountAllSubviews; -- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; -- (UIView *)react_findClipView; +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(RCTPlatformView *)clipView; // [macOS] +- (RCTPlatformView *)react_findClipView; // [macOS] @end diff --git a/packages/react-native/React/Views/UIView+React.h b/packages/react-native/React/Views/UIView+React.h index fbc8ca0f94c80d..e053884d6d0724 100644 --- a/packages/react-native/React/Views/UIView+React.h +++ b/packages/react-native/React/Views/UIView+React.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import @@ -19,15 +19,15 @@ typedef struct { CGFloat z; } RCTTransformOrigin; -@interface UIView (React) +@interface RCTPlatformView (React) // [macOS] /** * RCTComponent interface. */ -- (NSArray *)reactSubviews NS_REQUIRES_SUPER; -- (UIView *)reactSuperview NS_REQUIRES_SUPER; -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; -- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; // [macOS] +- (RCTPlatformView *)reactSuperview NS_REQUIRES_SUPER; // [macOS] +- (void)insertReactSubview:(RCTPlatformView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; // [macOS] +- (void)removeReactSubview:(RCTPlatformView *)subview NS_REQUIRES_SUPER; // [macOS] /** * The native id of the view, used to locate view from native codes @@ -63,7 +63,7 @@ typedef struct { * Subviews sorted by z-index. Note that this method doesn't do any caching (yet) * and sorts all the views each call. */ -- (NSArray *)reactZIndexSortedSubviews; +- (NSArray *)reactZIndexSortedSubviews; // [macOS] /** * Updates the subviews array based on the reactSubviews. Default behavior is @@ -88,6 +88,7 @@ typedef struct { */ - (UIViewController *)reactViewController; +#if !TARGET_OS_OSX // [macOS] /** * This method attaches the specified controller as a child of the * the owning view controller of this view. Returns NO if no view @@ -95,6 +96,9 @@ typedef struct { * attached to the view hierarchy). */ - (void)reactAddControllerToClosestParent:(UIViewController *)controller; +#endif // [macOS] + +- (void)reactViewDidMoveToWindow; // [macOS] Github #1412 /** * Focus manipulation. @@ -125,19 +129,25 @@ typedef struct { * transparent in favour of some subview. * Defaults to `self`. */ -@property (nonatomic, readonly) UIView *reactAccessibilityElement; +@property (nonatomic, readonly) RCTPlatformView *reactAccessibilityElement; // [macOS] /** * Accessibility properties */ +#if !TARGET_OS_OSX // [macOS] @property (nonatomic, copy) NSString *accessibilityRole; +#else // [macOS renamed so it doesn't conflict with -[NSAccessibility accessibilityRole]. +@property (nonatomic, copy) NSString *accessibilityRoleInternal; +#endif // macOS] @property (nonatomic, copy) NSString *role; @property (nonatomic, copy) NSDictionary *accessibilityState; @property (nonatomic, copy) NSArray *accessibilityActions; @property (nonatomic, copy) NSDictionary *accessibilityValueInternal; @property (nonatomic, copy) NSString *accessibilityLanguage; +#if !TARGET_OS_OSX // [macOS] @property (nonatomic) UIAccessibilityTraits accessibilityRoleTraits; @property (nonatomic) UIAccessibilityTraits roleTraits; +#endif // [macOS] /** * Used in debugging to get a description of the view hierarchy rooted at diff --git a/packages/react-native/React/Views/UIView+React.m b/packages/react-native/React/Views/UIView+React.m index 2ee2c500ddb7bf..cda5a77398bd7b 100644 --- a/packages/react-native/React/Views/UIView+React.m +++ b/packages/react-native/React/Views/UIView+React.m @@ -13,7 +13,7 @@ #import "RCTLog.h" #import "RCTShadowView.h" -@implementation UIView (React) +@implementation RCTPlatformView (React) // [macOS] - (NSNumber *)reactTag { @@ -47,12 +47,18 @@ - (void)setNativeID:(NSString *)nativeID - (BOOL)shouldAccessibilityIgnoresInvertColors { +#if !TARGET_OS_OSX // [macOS] return self.accessibilityIgnoresInvertColors; +#else // [macOS + return NO; +#endif // macOS] } - (void)setShouldAccessibilityIgnoresInvertColors:(BOOL)shouldAccessibilityIgnoresInvertColors { +#if !TARGET_OS_OSX // [macOS] self.accessibilityIgnoresInvertColors = shouldAccessibilityIgnoresInvertColors; +#endif // [macOS] } - (BOOL)isReactRootView @@ -62,24 +68,24 @@ - (BOOL)isReactRootView - (NSNumber *)reactTagAtPoint:(CGPoint)point { - UIView *view = [self hitTest:point withEvent:nil]; + RCTPlatformView *view = RCTUIViewHitTestWithEvent(self, point, nil); // [macOS] while (view && !view.reactTag) { view = view.superview; } return view.reactTag; } -- (NSArray *)reactSubviews +- (NSArray *)reactSubviews // [macOS] { return objc_getAssociatedObject(self, _cmd); } -- (UIView *)reactSuperview +- (RCTPlatformView *)reactSuperview // [macOS] { return self.superview; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(RCTPlatformView *)subview atIndex:(NSInteger)atIndex // [macOS] { // We access the associated object directly here in case someone overrides // the `reactSubviews` getter method and returns an immutable array. @@ -91,7 +97,7 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex [subviews insertObject:subview atIndex:atIndex]; } -- (void)removeReactSubview:(UIView *)subview +- (void)removeReactSubview:(RCTPlatformView *)subview // [macOS] { // We access the associated object directly here in case someone overrides // the `reactSubviews` getter method and returns an immutable array. @@ -116,23 +122,37 @@ - (void)setReactDisplay:(YGDisplay)display - (UIUserInterfaceLayoutDirection)reactLayoutDirection { +#if !TARGET_OS_OSX // [macOS] if ([self respondsToSelector:@selector(semanticContentAttribute)]) { +#pragma clang diagnostic push // [macOS] +#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute]; +#pragma clang diagnostic pop // [macOS] } else { return [objc_getAssociatedObject(self, @selector(reactLayoutDirection)) integerValue]; } +#else // [macOS + return self.userInterfaceLayoutDirection; +#endif // macOS] } - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection { +#if !TARGET_OS_OSX // [macOS] if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) { +#pragma clang diagnostic push // [macOS] +#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] self.semanticContentAttribute = layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ? UISemanticContentAttributeForceLeftToRight : UISemanticContentAttributeForceRightToLeft; +#pragma clang diagnostic pop // [macOS] } else { objc_setAssociatedObject( self, @selector(reactLayoutDirection), @(layoutDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +#else // [macOS + self.userInterfaceLayoutDirection = layoutDirection; +#endif // macOS] } #pragma mark - zIndex @@ -147,17 +167,17 @@ - (void)setReactZIndex:(NSInteger)reactZIndex self.layer.zPosition = reactZIndex; } -- (NSArray *)reactZIndexSortedSubviews +- (NSArray *)reactZIndexSortedSubviews // [macOS] { // Check if sorting is required - in most cases it won't be. BOOL sortingRequired = NO; - for (UIView *subview in self.subviews) { + for (RCTUIView *subview in self.subviews) { // [macOS] if (subview.reactZIndex != 0) { sortingRequired = YES; break; } } - return sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { + return sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(RCTUIView *a, RCTUIView *b) { // [macOS] if (a.reactZIndex > b.reactZIndex) { return NSOrderedDescending; } else { @@ -171,7 +191,7 @@ - (void)setReactZIndex:(NSInteger)reactZIndex - (void)didUpdateReactSubviews { - for (UIView *subview in self.reactSubviews) { + for (RCTPlatformView *subview in self.reactSubviews) { // [macOS] [self addSubview:subview]; } } @@ -183,6 +203,7 @@ - (void)didSetProps:(__unused NSArray *)changedProps - (void)reactSetFrame:(CGRect)frame { +#if !TARGET_OS_OSX // [macOS] // These frames are in terms of anchorPoint = topLeft, but internally the // views are anchorPoint = center for easier scale and rotation animations. // Convert the frame so it works with anchorPoint = center. @@ -203,6 +224,17 @@ - (void)reactSetFrame:(CGRect)frame self.center = position; self.bounds = bounds; +#else // [macOS + // Avoid crashes due to nan coords + if (isnan(frame.origin.x) || isnan(frame.origin.y) || + isnan(frame.size.width) || isnan(frame.size.height)) { + RCTLogError(@"Invalid layout for (%@)%@. frame: %@", + self.reactTag, self, NSStringFromCGRect(frame)); + return; + } + + self.frame = frame; +#endif // macOS] id transformOrigin = objc_getAssociatedObject(self, @selector(reactTransformOrigin)); if (transformOrigin) { @@ -243,7 +275,7 @@ - (void)setReactTransformOrigin:(RCTTransformOrigin)reactTransformOrigin updateTransform(self); } -static void updateTransform(UIView *view) +static void updateTransform(RCTPlatformView *view) // [macOS] { CATransform3D transform; id rawTansformOrigin = objc_getAssociatedObject(view, @selector(reactTransformOrigin)); @@ -291,6 +323,7 @@ - (UIViewController *)reactViewController return nil; } +#if !TARGET_OS_OSX // [macOS] - (void)reactAddControllerToClosestParent:(UIViewController *)controller { if (!controller.parentViewController) { @@ -306,39 +339,63 @@ - (void)reactAddControllerToClosestParent:(UIViewController *)controller return; } } +#endif // [macOS] -/** - * Focus manipulation. - */ -- (BOOL)reactIsFocusNeeded +// [macOS Github#1412 +- (void)reactViewDidMoveToWindow { - return [(NSNumber *)objc_getAssociatedObject(self, @selector(reactIsFocusNeeded)) boolValue]; + [self reactFocusIfNeeded]; } +// macOS] -- (void)setReactIsFocusNeeded:(BOOL)isFocusNeeded -{ - objc_setAssociatedObject(self, @selector(reactIsFocusNeeded), @(isFocusNeeded), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} +/** + * Focus manipulation. + */ +static __weak RCTPlatformView *_pendingFocusView; // [macOS] - (void)reactFocus { - if (![self becomeFirstResponder]) { - self.reactIsFocusNeeded = YES; +#if !TARGET_OS_OSX // [macOS] + if (![self becomeFirstResponder]) { +#else // [macOS + if (![[self window] makeFirstResponder:self]) { +#endif // macOS] +// [macOS Github#1412 + _pendingFocusView = self; + } else { + _pendingFocusView = nil; } +// macOS] } - (void)reactFocusIfNeeded { - if (self.reactIsFocusNeeded) { - if ([self becomeFirstResponder]) { - self.reactIsFocusNeeded = NO; - } - } + if ([self isEqual:_pendingFocusView]) { // [macOS] +#if TARGET_OS_OSX // [macOS + if ([[self window] makeFirstResponder:self]) { +#else + if ([self becomeFirstResponder]) { +#endif // macOS] + _pendingFocusView = nil; // [macOS] Github#1412 + } + } } - (void)reactBlur { +#if !TARGET_OS_OSX // [macOS] [self resignFirstResponder]; +#else // [macOS + if (self == [[self window] firstResponder]) { + [[self window] makeFirstResponder:[[self window] nextResponder]]; + } +#endif // macOS] + +// [macOS Github#1412 + if ([self isEqual:_pendingFocusView]) { + _pendingFocusView = nil; + } +// macOS] } #pragma mark - Layout @@ -373,7 +430,7 @@ - (CGRect)reactContentFrame #pragma mark - Accessibility -- (UIView *)reactAccessibilityElement +- (RCTPlatformView *)reactAccessibilityElement // [macOS] { return self; } @@ -400,14 +457,26 @@ - (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage self, @selector(accessibilityLanguage), accessibilityLanguage, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +#if !TARGET_OS_OSX // [macOS] - (NSString *)accessibilityRole +#else // [macOS renamed so it doesn't conflict with -[NSAccessibility accessibilityRole]. +- (NSString *)accessibilityRoleInternal +#endif // macOS] { return objc_getAssociatedObject(self, _cmd); } +#if !TARGET_OS_OSX // [macOS] - (void)setAccessibilityRole:(NSString *)accessibilityRole +#else // [macOS renamed so it doesn't conflict with -[NSAccessibility accessibilityRole]. +- (void)setAccessibilityRoleInternal:(NSString *)accessibilityRole +#endif // macOS] { +#if !TARGET_OS_OSX // [macOS] objc_setAssociatedObject(self, @selector(accessibilityRole), accessibilityRole, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +#else // [macOS + objc_setAssociatedObject(self, @selector(accessibilityRoleInternal), accessibilityRole, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +#endif // macOS] } - (NSString *)role @@ -440,6 +509,7 @@ - (void)setAccessibilityValueInternal:(NSDictionary *)accessibil self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +#if !TARGET_OS_OSX // [macOS] - (UIAccessibilityTraits)accessibilityRoleTraits { NSNumber *traitsAsNumber = objc_getAssociatedObject(self, _cmd); @@ -466,6 +536,7 @@ - (void)setRoleTraits:(UIAccessibilityTraits)roleTraits objc_setAssociatedObject( self, @selector(roleTraits), [NSNumber numberWithUnsignedLongLong:roleTraits], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +#endif // [macOS] #pragma mark - Debug - (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level @@ -477,7 +548,7 @@ - (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel: [string appendString:self.description]; [string appendString:@"\n"]; - for (UIView *subview in self.subviews) { + for (RCTPlatformView *subview in self.subviews) { // [macOS] [subview react_addRecursiveDescriptionToString:string atLevel:level + 1]; } } @@ -489,4 +560,13 @@ - (NSString *)react_recursiveDescription return description; } +// [macOS +#pragma mark - Hit testing +#if TARGET_OS_OSX +- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + return [self hitTest:point]; +} +#endif // macOS] + @end diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h index 0f1e4ff55926e8..b37a994702d96a 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm index 8a286dcfd33665..1c3df6c577d215 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm @@ -9,7 +9,6 @@ #import #import -#import using namespace facebook::react; @@ -46,8 +45,13 @@ - (NSDictionary *)getConstants { __block NSDictionary *constants; RCTUnsafeExecuteOnMainQueueSync(^{ +#if !TARGET_OS_OSX // [macOS] UIScreen *mainScreen = UIScreen.mainScreen; CGSize screenSize = mainScreen.bounds.size; +#else // [macOS + NSScreen *mainScreen = NSScreen.mainScreen; + CGSize screenSize = mainScreen.frame.size; +#endif // macOS] constants = @{ @"const1" : @YES, diff --git a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h index 4d7289a1f94710..5f6935c9764e92 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h +++ b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h @@ -7,7 +7,7 @@ #import #import -#import +#import // [macOS] #include NS_ASSUME_NONNULL_BEGIN @@ -25,24 +25,24 @@ typedef void (^InterceptorBlock)(std::string eventName, folly::dynamic event); bridgeProxy:(nullable RCTBridgeProxy *)bridgeProxy bridgelessInteropData:(RCTBridgeModuleDecorator *)bridgelessInteropData; -- (UIView *)createPaperViewWithTag:(NSInteger)tag; +- (RCTPlatformView *)createPaperViewWithTag:(NSInteger)tag; // [macOS] - (void)addObserveForTag:(NSInteger)tag usingBlock:(InterceptorBlock)block; - (void)removeObserveForTag:(NSInteger)tag; -- (void)setProps:(const folly::dynamic &)props forView:(UIView *)view; +- (void)setProps:(const folly::dynamic &)props forView:(RCTPlatformView *)view; // [macOS] - (NSString *)componentViewName; - (void)handleCommand:(NSString *)commandName args:(NSArray *)args reactTag:(NSInteger)tag - paperView:(UIView *)paperView; + paperView:(RCTPlatformView *)paperView; // [macOS] - (void)removeViewFromRegistryWithTag:(NSInteger)tag; -- (void)addViewToRegistry:(UIView *)view withTag:(NSInteger)tag; +- (void)addViewToRegistry:(RCTPlatformView *)view withTag:(NSInteger)tag; // [macOS] @end diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp index 944d290e85e9e2..2f67bcf75f8016 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp @@ -23,6 +23,13 @@ void setTouchPayloadOnObject( object.setProperty(runtime, "target", touch.target); object.setProperty(runtime, "timestamp", touch.timestamp * 1000); object.setProperty(runtime, "force", touch.force); +#if TARGET_OS_OSX // [macOS + object.setProperty(runtime, "button", touch.button); + object.setProperty(runtime, "altKey", touch.altKey); + object.setProperty(runtime, "ctrlKey", touch.ctrlKey); + object.setProperty(runtime, "shiftKey", touch.shiftKey); + object.setProperty(runtime, "metaKey", touch.metaKey); +#endif // macOS] } #if RN_DEBUG_STRING_CONVERTIBLE @@ -42,6 +49,13 @@ std::vector getDebugProps( {"target", getDebugDescription(touch.target, options)}, {"force", getDebugDescription(touch.force, options)}, {"timestamp", getDebugDescription(touch.timestamp, options)}, +#if TARGET_OS_SX // [macOS + {"button", getDebugDescription(touch.button, options)}, + {"altKey", getDebugDescription(touch.altKey, options)}, + {"ctrlKey", getDebugDescription(touch.ctrlKey, options)}, + {"shiftKey", getDebugDescription(touch.shiftKey, options)}, + {"metaKey", getDebugDescription(touch.metaKey, options)}, +#endif // macOS] }; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h index 0d658310f27a45..55d6090719a373 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h @@ -55,6 +55,32 @@ struct BaseTouch { * The time in seconds when the touch occurred or when it was last mutated. */ Float timestamp; +#if TARGET_OS_OSX // [macOS + /* + * The button indicating which pointer is used. + */ + int button; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey; +#endif // macOS] /* * The particular implementation of `Hasher` and (especially) `Comparator` diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 1d566ac8d6c440..a5b2eca1c179f4 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -8,7 +8,7 @@ #import "RCTPlatformColorUtils.h" #import -#import +#import // [macOS] #include @@ -130,7 +130,7 @@ return dict; } -static UIColor *_UIColorFromHexValue(NSNumber *hexValue) +static RCTUIColor *_UIColorFromHexValue(NSNumber *hexValue) // [macOS] { NSUInteger hexIntValue = [hexValue unsignedIntegerValue]; @@ -139,10 +139,10 @@ CGFloat blue = ((CGFloat)((hexIntValue & 0xFF00) >> 8)) / 255.0; CGFloat alpha = ((CGFloat)(hexIntValue & 0xFF)) / 255.0; - return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; + return [RCTUIColor colorWithRed:red green:green blue:blue alpha:alpha]; // [macOS] } -static UIColor *_Nullable _UIColorFromSemanticString(NSString *semanticString) +static RCTUIColor *_Nullable _UIColorFromSemanticString(NSString *semanticString) // [macOS] { NSString *platformColorString = [semanticString hasSuffix:kColorSuffix] ? [semanticString substringToIndex:[semanticString length] - [kColorSuffix length]] @@ -151,17 +151,17 @@ NSDictionary *colorInfo = platformColorSelectorsDict[platformColorString]; if (colorInfo) { SEL objcColorSelector = NSSelectorFromString([platformColorString stringByAppendingString:kColorSuffix]); - if (![UIColor respondsToSelector:objcColorSelector]) { + if (![RCTUIColor respondsToSelector:objcColorSelector]) { // [macOS] NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey]; if (fallbackRGB) { return _UIColorFromHexValue(fallbackRGB); } } else { - Class uiColorClass = [UIColor class]; + Class uiColorClass = [RCTUIColor class]; // [macOS] IMP imp = [uiColorClass methodForSelector:objcColorSelector]; id (*getUIColor)(id, SEL) = ((id(*)(id, SEL))imp); id colorObject = getUIColor(uiColorClass, objcColorSelector); - if ([colorObject isKindOfClass:[UIColor class]]) { + if ([colorObject isKindOfClass:[RCTUIColor class]]) { // [macOS] return colorObject; } } @@ -176,9 +176,12 @@ return [NSString stringWithCString:string.c_str() encoding:encoding]; } -static inline facebook::react::ColorComponents _ColorComponentsFromUIColor(UIColor *color) +static inline facebook::react::ColorComponents _ColorComponentsFromUIColor(RCTUIColor *color) // [macOS] { CGFloat rgba[4]; +#if TARGET_OS_OSX // [macOS + color = [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; +#endif // macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; return {(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}; } @@ -187,7 +190,7 @@ { for (const auto &semanticCString : semanticItems) { NSString *semanticNSString = _NSStringFromCString(semanticCString); - UIColor *uiColor = [UIColor colorNamed:semanticNSString]; + RCTUIColor *uiColor = [RCTUIColor colorNamed:semanticNSString]; // [macOS] if (uiColor != nil) { return _ColorComponentsFromUIColor(uiColor); } diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.h index da6223a542d222..0772e7d93ae97c 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTImageManagerProtocol.h" diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h index 0aa6a75b92e0ba..b183bfa88813fd 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.h index 395f65b9c6b110..b940b43ee3225f 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "RCTImageManagerProtocol.h" diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/NSTextStorage+FontScaling.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/NSTextStorage+FontScaling.h index a87d67d192305c..5fa455d4bba336 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/NSTextStorage+FontScaling.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/NSTextStorage+FontScaling.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] @interface NSTextStorage (FontScaling) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h index 49a4353c5f011c..bfb465c2c5a34d 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #include #include diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index b3b53c80812c0c..55bb50d01ab9b3 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -56,6 +56,7 @@ inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight) return weights[(fontWeight + 50) / 100 - 1]; } +#if !TARGET_OS_OSX // [macOS] inline static UIFontTextStyle RCTUIFontTextStyleForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp) { switch (dynamicTypeRamp) { @@ -83,6 +84,7 @@ inline static UIFontTextStyle RCTUIFontTextStyleForDynamicTypeRamp(const Dynamic return UIFontTextStyleLargeTitle; } } +#endif // [macOS] inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp) { @@ -117,6 +119,7 @@ inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynam inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes) { if (textAttributes.allowFontScaling.value_or(true)) { +#if !TARGET_OS_OSX // [macOS] if (textAttributes.dynamicTypeRamp.has_value()) { DynamicTypeRamp dynamicTypeRamp = textAttributes.dynamicTypeRamp.value(); UIFontMetrics *fontMetrics = @@ -128,6 +131,9 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex } else { return textAttributes.fontSizeMultiplier; } +#else // [macOS + return textAttributes.fontSizeMultiplier; +#endif // macOS] } else { return 1.0; } @@ -154,9 +160,9 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex return RCTFontWithFontProperties(fontProperties); } -inline static UIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes) +inline static RCTUIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes) // [macOS] { - UIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [UIColor blackColor]; + RCTUIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ? RCTUIColorFromSharedColor(textAttributes.foregroundColor) : [RCTUIColor blackColor]; // [macOS] if (!isnan(textAttributes.opacity)) { effectiveForegroundColor = [effectiveForegroundColor @@ -166,16 +172,16 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex return effectiveForegroundColor; } -inline static UIColor *RCTEffectiveBackgroundColorFromTextAttributes(const TextAttributes &textAttributes) +inline static RCTUIColor *RCTEffectiveBackgroundColorFromTextAttributes(const TextAttributes &textAttributes) // [macOS] { - UIColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor); + RCTUIColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor); // [macOS] if (effectiveBackgroundColor && !isnan(textAttributes.opacity)) { effectiveBackgroundColor = [effectiveBackgroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * textAttributes.opacity]; } - return effectiveBackgroundColor ?: [UIColor clearColor]; + return effectiveBackgroundColor ? effectiveBackgroundColor : [RCTUIColor clearColor]; // [macOS] } NSDictionary *RCTNSTextAttributesFromTextAttributes(const TextAttributes &textAttributes) @@ -189,7 +195,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex } // Colors - UIColor *effectiveForegroundColor = RCTEffectiveForegroundColorFromTextAttributes(textAttributes); + RCTUIColor *effectiveForegroundColor = RCTEffectiveForegroundColorFromTextAttributes(textAttributes); // [macOS] if (textAttributes.foregroundColor || !isnan(textAttributes.opacity)) { attributes[NSForegroundColorAttributeName] = effectiveForegroundColor; @@ -251,7 +257,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex NSUnderlineStyle style = RCTNSUnderlineStyleFromTextDecorationStyle( textAttributes.textDecorationStyle.value_or(TextDecorationStyle::Solid)); - UIColor *textDecorationColor = RCTUIColorFromSharedColor(textAttributes.textDecorationColor); + RCTUIColor *textDecorationColor = RCTUIColorFromSharedColor(textAttributes.textDecorationColor); // [macOS] // Underline if (textDecorationLineType == TextDecorationLineType::Underline || @@ -329,8 +335,8 @@ static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) if (!font) { return; } - - maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight); + + maximumFontLineHeight = MAX(UIFontLineHeight(font), maximumFontLineHeight); // [macOS] }]; if (maximumLineHeight < maximumFontLineHeight) { diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontProperties.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontProperties.h index eca30e5046838a..67751f5444fb37 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontProperties.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontProperties.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.h index 5a37f1f6f993b8..7450ebce19de39 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm index 4f8d5aa0703293..b8144992ecef8d 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm @@ -118,7 +118,11 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) // the specific metrics of the standard system font as closely as possible. font = RCTDefaultFontWithFontProperties(fontProperties); } else { +#if !TARGET_OS_OSX // [macOS] NSArray *fontNames = [UIFont fontNamesForFamilyName:fontProperties.family]; +#else // [macOS + NSArray *fontNames = @[]; +#endif // macOS] if (fontNames.count == 0) { // Gracefully handle being given a font name rather than font family, for diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index b0d906ca10cea8..f94044409cdef5 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 368c3341055804..d134df88154e47 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -96,12 +96,20 @@ - (void)drawAttributedString:(AttributedString)attributedString // `rect`'s width is stored in double precesion. // `frame`'s width is also in double precesion but was stored as float in Yoga previously, precesion was lost. +#if !TARGET_OS_OSX // [macOS] if (std::abs(RCTCeilPixelValue(rect.size.width) - frame.size.width) < threshold) { +#else // [macOS + if (std::abs(RCTCeilPixelValue(rect.size.width, RCTScreenScale()) - frame.size.width) < threshold) { +#endif // macOS] // `textStorage` passed to this method was used to calculate size of frame. If that's the case, it's // width is the same as frame's width. Origin must be adjusted, otherwise glyhps will be painted in wrong // place. // We could create new `NSTextStorage` for the specific frame, but that is expensive. +#if !TARGET_OS_OSX // [macOS] origin.x -= RCTCeilPixelValue(rect.origin.x); +#else // [macOS + origin.x -= RCTCeilPixelValue(rect.origin.x, RCTScreenScale()); +#endif // macOS] } } @@ -290,8 +298,12 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; +#if !TARGET_OS_OSX // [macOS] size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)}; - +#else // [macOS + CGFloat scale = [[NSScreen mainScreen] backingScaleFactor]; + size = (CGSize){RCTCeilPixelValue(size.width, scale), RCTCeilPixelValue(size.height, scale)}; +#endif // macOS] __block auto attachments = TextMeasurement::Attachments{}; [textStorage diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h index 1921d1ca1cffed..9144ca65236088 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #include #include @@ -48,13 +48,13 @@ inline static NSLineBreakStrategy RCTNSLineBreakStrategyFromLineBreakStrategy( case facebook::react::LineBreakStrategy::PushOut: return NSLineBreakStrategyPushOut; case facebook::react::LineBreakStrategy::HangulWordPriority: - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] return NSLineBreakStrategyHangulWordPriority; } else { return NSLineBreakStrategyNone; } case facebook::react::LineBreakStrategy::Standard: - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] return NSLineBreakStrategyStandard; } else { return NSLineBreakStrategyNone; @@ -94,24 +94,24 @@ inline static NSUnderlineStyle RCTNSUnderlineStyleFromTextDecorationStyle( } } -inline static UIColor *RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) +inline static RCTUIColor *RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) // [macOS] { if (!sharedColor) { return nil; } if (*facebook::react::clearColor() == *sharedColor) { - return [UIColor clearColor]; + return [RCTUIColor clearColor]; // [macOS] } if (*facebook::react::blackColor() == *sharedColor) { - return [UIColor blackColor]; + return [RCTUIColor blackColor]; // [macOS] } if (*facebook::react::whiteColor() == *sharedColor) { - return [UIColor whiteColor]; + return [RCTUIColor whiteColor]; // [macOS] } auto components = facebook::react::colorComponentsFromColor(sharedColor); - return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; + return [RCTUIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; // [macOS] } diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/ObjCTimerRegistry.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/ObjCTimerRegistry.h index 091ac60b8db980..9770cc6c768b7e 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/ObjCTimerRegistry.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/ObjCTimerRegistry.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h index be1bc2207c89ec..a3d34d0c59f38e 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import #import diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index f73100ae5fdc53..64a0b1e186bba6 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -277,7 +277,7 @@ - (void)_start // This enables RCTViewRegistry in modules to return UIViews from its viewForReactTag method __weak RCTSurfacePresenter *weakSurfacePresenter = _surfacePresenter; - [_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^UIView *(NSNumber *reactTag) { + [_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^RCTPlatformView *(NSNumber *reactTag) { // [macOS] RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter; if (strongSurfacePresenter == nil) { return nil; diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 7149c6463b52fa..8f925eab9a9945 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -84,6 +84,8 @@ import typeof YellowBox from './Libraries/YellowBox/YellowBoxDeprecated'; // Plugins import typeof {DynamicColorIOS} from './Libraries/StyleSheet/PlatformColorValueTypesIOS'; +import typeof {DynamicColorMacOS} from './Libraries/StyleSheet/PlatformColorValueTypesMacOS'; // [macOS] +import typeof {ColorWithSystemEffectMacOS} from './Libraries/StyleSheet/PlatformColorValueTypesMacOS'; // [macOS] import typeof NativeModules from './Libraries/BatchedBridge/NativeModules'; import typeof Platform from './Libraries/Utilities/Platform'; import typeof {PlatformColor} from './Libraries/StyleSheet/PlatformColorValueTypes'; @@ -364,6 +366,15 @@ module.exports = { get processColor(): processColor { return require('./Libraries/StyleSheet/processColor').default; }, + // [macOS + get DynamicColorMacOS(): DynamicColorMacOS { + return require('./Libraries/StyleSheet/PlatformColorValueTypesMacOS') + .DynamicColorMacOS; + }, + get ColorWithSystemEffectMacOS(): ColorWithSystemEffectMacOS { + return require('./Libraries/StyleSheet/PlatformColorValueTypesMacOS') + .ColorWithSystemEffectMacOS; + }, // macOS] get requireNativeComponent(): ( uiViewClassName: string, ) => HostComponent { @@ -419,6 +430,7 @@ module.exports = { ); return require('deprecated-react-native-prop-types').ViewPropTypes; }, + // macOS] }; if (__DEV__) { diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.h b/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.h index dcfe00f2e76862..4e0840505682fc 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.h +++ b/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.h @@ -6,11 +6,11 @@ */ #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN -@interface RNTLegacyView : UIView +@interface RNTLegacyView : RCTUIView // [macOS] @property (nonatomic, copy) RCTBubblingEventBlock onColorChanged; diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.mm b/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.mm index 3577ac72e82cb2..e728caa7a42408 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.mm +++ b/packages/rn-tester/NativeComponentExample/ios/RNTLegacyView.mm @@ -9,7 +9,7 @@ @implementation RNTLegacyView -- (void)setBackgroundColor:(UIColor *)backgroundColor +- (void)setBackgroundColor:(RCTUIColor *)backgroundColor // [macOS] { super.backgroundColor = backgroundColor; [self emitEvent]; diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm b/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm index 2125e36313f678..9ce6ef6f683087 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm +++ b/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm @@ -39,8 +39,8 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(changeBackgroundColor : (nonnull NSNumber *)reactTag color : (NSString *)color) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[reactTag]; // [macOS] if (!view || ![view isKindOfClass:[RNTLegacyView class]]) { RCTLogError(@"Cannot find RNTLegacyView with tag #%@", reactTag); return; @@ -53,18 +53,20 @@ + (BOOL)requiresMainQueueSetup [scanner setScanLocation:1]; // bypass '#' character [scanner scanHexInt:&rgbValue]; - UIColor *newColor = [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 - green:((rgbValue & 0xFF00) >> 8) / 255.0 - blue:(rgbValue & 0xFF) / 255.0 - alpha:1.0]; + // [macOS + RCTUIColor *newColor = [RCTUIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; + // macOS] view.backgroundColor = newColor; }]; } -- (UIView *)view +- (RCTUIView *)view // [macOS] { RNTLegacyView *view = [[RNTLegacyView alloc] init]; - view.backgroundColor = UIColor.redColor; + view.backgroundColor = RCTUIColor.redColor; // [macOS] return view; } diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.h b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.h index 60f930debdfbed..f983c83aa11f72 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.h +++ b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.h @@ -7,7 +7,7 @@ #import #import -#import +#import // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) RCTBubblingEventBlock onIntArrayChanged; -- (UIColor *)UIColorFromHexString:(const std::string)hexString; +- (RCTUIColor *)UIColorFromHexString:(const std::string)hexString; @end diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.mm b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.mm index b3455854747091..e0e17de47802d2 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.mm +++ b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewComponentView.mm @@ -20,7 +20,7 @@ @interface RNTMyNativeViewComponentView () @end @implementation RNTMyNativeViewComponentView { - UIView *_view; + RCTUIView *_view; // [macOS] } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -42,8 +42,8 @@ - (instancetype)initWithFrame:(CGRect)frame static const auto defaultProps = std::make_shared(); _props = defaultProps; - _view = [[UIView alloc] init]; - _view.backgroundColor = [UIColor redColor]; + _view = [[RCTUIView alloc] init]; // [macOS] + _view.backgroundColor = [RCTUIColor redColor]; // [macOS] self.contentView = _view; } @@ -51,17 +51,17 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -- (UIColor *)UIColorFromHexString:(const std::string)hexString +- (RCTUIColor *)RCTUIColorFromHexString:(const std::string)hexString // [macOS] { unsigned rgbValue = 0; NSString *colorString = [NSString stringWithCString:hexString.c_str() encoding:[NSString defaultCStringEncoding]]; NSScanner *scanner = [NSScanner scannerWithString:colorString]; [scanner setScanLocation:1]; // bypass '#' character [scanner scanHexInt:&rgbValue]; - return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 - green:((rgbValue & 0xFF00) >> 8) / 255.0 - blue:(rgbValue & 0xFF) / 255.0 - alpha:1.0]; + return [RCTUIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 // [macOS] + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; } - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps @@ -107,7 +107,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & [super updateProps:props oldProps:oldProps]; } -- (void)onChange:(UIView *)sender +- (void)onChange:(RCTUIView *)sender // [macOS] { // No-op // std::dynamic_pointer_cast(_eventEmitter) @@ -123,7 +123,7 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)callNativeMethodToChangeBackgroundColor:(NSString *)colorString { - UIColor *color = [self UIColorFromHexString:std::string([colorString UTF8String])]; + RCTUIColor *color = [self RCTUIColorFromHexString:std::string([colorString UTF8String])]; // [macOS] _view.backgroundColor = color; } @end diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewManager.mm b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewManager.mm index 6729d8f12cb071..105f6c2a205d75 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewManager.mm +++ b/packages/rn-tester/NativeComponentExample/ios/RNTMyNativeViewManager.mm @@ -24,9 +24,9 @@ @implementation RNTMyNativeViewManager RCT_EXPORT_METHOD(callNativeMethodToChangeBackgroundColor : (nonnull NSNumber *)reactTag color : (NSString *)color) { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - if (!view || ![view isKindOfClass:[UIView class]]) { + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] + RCTUIView *view = viewRegistry[reactTag]; // [macOS] + if (!view || ![view isKindOfClass:[RCTPlatformView class]]) { // [macOS] RCTLogError(@"Cannot find NativeView with tag #%@", reactTag); return; } @@ -37,17 +37,16 @@ @implementation RNTMyNativeViewManager NSScanner *scanner = [NSScanner scannerWithString:colorString]; [scanner setScanLocation:1]; // bypass '#' character [scanner scanHexInt:&rgbValue]; - - view.backgroundColor = [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 - green:((rgbValue & 0xFF00) >> 8) / 255.0 - blue:(rgbValue & 0xFF) / 255.0 - alpha:1.0]; + view.backgroundColor = [RCTUIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 // [macOS + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; }]; } -- (UIView *)view +- (RCTUIView *)view // [macOS] { - return [[UIView alloc] init]; + return [[RCTUIView alloc] init]; // [macOS] } @end diff --git a/packages/rn-tester/NativeModuleExample/Screenshot.mm b/packages/rn-tester/NativeModuleExample/Screenshot.mm index ceb28ac6d0673c..ee4b318e5aac47 100644 --- a/packages/rn-tester/NativeModuleExample/Screenshot.mm +++ b/packages/rn-tester/NativeModuleExample/Screenshot.mm @@ -20,7 +20,8 @@ @implementation ScreenshotManager : (RCTPromiseRejectBlock)reject) { [self.bridge.uiManager addUIBlock:^( - __unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + __unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] +#if !TARGET_OS_OSX // [macOS] // Get view UIView *view; if (target == nil || [target isEqual:@"window"]) { @@ -81,6 +82,40 @@ @implementation ScreenshotManager // If we reached here, something went wrong reject(RCTErrorUnspecified, error.localizedDescription, error); }); +#else // [macOS + // find the key window + NSWindow *keyWindow; + for (NSWindow *window in NSApp.windows) { + if (window.keyWindow) { + keyWindow = window; + break; + } + } + + // take a snapshot of the key window + CGWindowID windowID = (CGWindowID)[keyWindow windowNumber]; + CGWindowImageOption imageOptions = kCGWindowImageDefault; + CGWindowListOption listOptions = kCGWindowListOptionIncludingWindow; + CGRect imageBounds = CGRectNull; + CGImageRef windowImage = CGWindowListCreateImage(imageBounds, listOptions, windowID, imageOptions); + NSImage *image = [[NSImage alloc] initWithCGImage:windowImage size:[keyWindow frame].size]; + CGImageRelease(windowImage); + + // save to a temp file + NSError *error = nil; + NSString *tempFilePath = RCTTempFilePath(@"jpeg", &error); + NSData *imageData = [image TIFFRepresentation]; + NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; + NSDictionary *imageProps = [NSDictionary dictionaryWithObject:@0.8 forKey:NSImageCompressionFactor]; + imageData = [imageRep representationUsingType:NSBitmapImageFileTypeJPEG properties:imageProps]; + BOOL success = [imageData writeToFile:tempFilePath atomically:NO]; + + if (success) { + resolve(tempFilePath); + } else { + reject(RCTErrorUnspecified, error.localizedDescription, error); + } +#endif // macOS] }]; } diff --git a/packages/rn-tester/RNTester/AppDelegate.h b/packages/rn-tester/RNTester/AppDelegate.h index d172167ee1d627..86d372a6d0043e 100644 --- a/packages/rn-tester/RNTester/AppDelegate.h +++ b/packages/rn-tester/RNTester/AppDelegate.h @@ -6,7 +6,7 @@ */ #import -#import +#import // [macOS] @interface AppDelegate : RCTAppDelegate diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm index 1bf36697556148..0ec5f98a0d9f46 100644 --- a/packages/rn-tester/RNTester/AppDelegate.mm +++ b/packages/rn-tester/RNTester/AppDelegate.mm @@ -26,19 +26,31 @@ #if BUNDLE_PATH NSString *kBundlePath = @"xplat/js/RKJSModules/EntryPoints/RNTesterTestBundle.js"; #else +#if TARGET_OS_OSX // [macOS] NSString *kBundlePath = @"js/RNTesterApp.ios"; +#else // [macOS +NSString *kBundlePath = @"js/RNTesterApp.macos"; +#endif // macOS] #endif @implementation AppDelegate +#if !TARGET_OS_OSX // [macOS] - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +#else // [macOS +- (void)applicationDidFinishLaunching:(NSNotification *)notification +#endif // macOS] { self.moduleName = @"RNTesterApp"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = [self prepareInitialProps]; +#if !TARGET_OS_OSX // [macOS] return [super application:application didFinishLaunchingWithOptions:launchOptions]; +#else // [macOS + [super applicationDidFinishLaunching:notification]; +#endif // macOS] } - (NSDictionary *)prepareInitialProps @@ -58,12 +70,14 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:kBundlePath]; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { return [RCTLinkingManager application:app openURL:url options:options]; } +#endif // [macOS] - (void)loadSourceForBridge:(RCTBridge *)bridge onProgress:(RCTSourceLoadProgressBlock)onProgress @@ -88,40 +102,61 @@ - (void)loadSourceForBridge:(RCTBridge *)bridge #if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_OSX // [macOS] // Required to register for notifications - (void)application:(__unused UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings]; } +#endif // [macOS] // Required for the remoteNotificationsRegistered event. -- (void)application:(__unused UIApplication *)application +- (void)application:(__unused RCTUIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } // Required for the remoteNotificationRegistrationError event. -- (void)application:(__unused UIApplication *)application +- (void)application:(__unused RCTUIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error]; } // Required for the remoteNotificationReceived event. -- (void)application:(__unused UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification +- (void)application:(__unused RCTUIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification { [RCTPushNotificationManager didReceiveRemoteNotification:notification]; } +#if !TARGET_OS_OSX // [macOS] // Required for the localNotificationReceived event. - (void)application:(__unused UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RCTPushNotificationManager didReceiveLocalNotification:notification]; } +#endif // [macOS] +#if TARGET_OS_OSX // [macOS +- (void)userNotificationCenter:(NSUserNotificationCenter *)center + didDeliverNotification:(NSUserNotification *)notification +{ +} +- (void)userNotificationCenter:(NSUserNotificationCenter *)center + didActivateNotification:(NSUserNotification *)notification +{ + [RCTPushNotificationManager didReceiveUserNotification:notification]; +} + +- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center + shouldPresentNotification:(NSUserNotification *)notification +{ + return YES; +} +#endif // macOS] #endif #pragma mark - RCTComponentViewFactoryComponentProvider diff --git a/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.h b/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.h index d798f17738a6a1..858a9b3255bae8 100644 --- a/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.h +++ b/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.mm b/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.mm index 489c3a91e33659..3faced729bedb5 100644 --- a/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.mm +++ b/packages/rn-tester/RNTester/NativeExampleViews/FlexibleSizeExampleView.mm @@ -12,7 +12,11 @@ #import #import + #import "AppDelegate.h" +#if TARGET_OS_OSX // [macOS +#define UITextView NSTextView +#endif // macOS] @interface FlexibleSizeExampleViewManager : RCTViewManager @@ -22,7 +26,7 @@ @implementation FlexibleSizeExampleViewManager RCT_EXPORT_MODULE(); -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [FlexibleSizeExampleView new]; } @@ -56,9 +60,16 @@ - (instancetype)initWithFrame:(CGRect)frame #ifndef TARGET_OS_TV _currentSizeTextView.editable = NO; #endif - _currentSizeTextView.text = @"Resizable view has not been resized yet"; - _currentSizeTextView.textColor = [UIColor blackColor]; - _currentSizeTextView.backgroundColor = [UIColor whiteColor]; + // [macOS Github#1642: Suppress analyzer error of nonlocalized string + NSString *currentSizeTextViewString = NSLocalizedString(@"Resizable view has not been resized yet", nil); +#if !TARGET_OS_OSX + _currentSizeTextView.text = currentSizeTextViewString; // [macOS] +#else + _currentSizeTextView.string = currentSizeTextViewString; +#endif // macOS] +#pragma clang diagnostic pop + _currentSizeTextView.textColor = [RCTUIColor blackColor]; // [macOS] + _currentSizeTextView.backgroundColor = [RCTUIColor whiteColor]; // [macOS] _currentSizeTextView.font = [UIFont boldSystemFontOfSize:10]; _resizableRootView.delegate = self; @@ -79,7 +90,7 @@ - (void)layoutSubviews [_currentSizeTextView setFrame:CGRectMake(0, 0, self.frame.size.width, textViewHeight)]; } -- (NSArray *> *)reactSubviews +- (NSArray *> *)reactSubviews // [macOS] { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy (void)[super reactSubviews]; @@ -95,12 +106,21 @@ - (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView if (!_sizeUpdated) { _sizeUpdated = TRUE; - _currentSizeTextView.text = [NSString - stringWithFormat: - @"RCTRootViewDelegate: content with initially unknown size has appeared, updating root view's size so the content fits."]; +#if !TARGET_OS_OSX // [macOS] + _currentSizeTextView.text = +#else // [macOS + _currentSizeTextView.string = +#endif // macOS] + [NSString + stringWithFormat: + @"RCTRootViewDelegate: content with initially unknown size has appeared, updating root view's size so the content fits."]; } else { +#if !TARGET_OS_OSX // [macOS] _currentSizeTextView.text = +#else // [macOS + _currentSizeTextView.string = +#endif // macOS] [NSString stringWithFormat: @"RCTRootViewDelegate: content size has been changed to (%ld, %ld), updating root view's size.", (long)newFrame.size.width, diff --git a/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.h b/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.h index fcf18370072b01..285645e93b4f1c 100644 --- a/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.h +++ b/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import diff --git a/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.mm b/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.mm index 60667fd3bea463..31a1ce6177d098 100644 --- a/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.mm +++ b/packages/rn-tester/RNTester/NativeExampleViews/UpdatePropertiesExampleView.mm @@ -20,7 +20,7 @@ @implementation UpdatePropertiesExampleViewManager RCT_EXPORT_MODULE(); -- (UIView *)view +- (RCTUIView *)view // [macOS] { return [UpdatePropertiesExampleView new]; } @@ -29,7 +29,11 @@ - (UIView *)view @implementation UpdatePropertiesExampleView { RCTRootView *_rootView; +#if !TARGET_OS_OSX // [macOS] UIButton *_button; +#else // [macOS + NSButton *_button; +#endif // macOS] BOOL _beige; } @@ -45,12 +49,21 @@ - (instancetype)initWithFrame:(CGRect)frame moduleName:@"SetPropertiesExampleApp" initialProperties:@{@"color" : @"beige"}]; + // [macOS Github#1642: Suppress analyzer error of nonlocalized string + NSString *buttonTitle = NSLocalizedString(@"Native Button", nil); +#if !TARGET_OS_OSX _button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; - [_button setTitle:@"Native Button" forState:UIControlStateNormal]; + [_button setTitle:buttonTitle /* [macOS] */ forState:UIControlStateNormal]; [_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_button setBackgroundColor:[UIColor grayColor]]; [_button addTarget:self action:@selector(changeColor) forControlEvents:UIControlEventTouchUpInside]; +#else + _button = [NSButton new]; + [_button setTitle:buttonTitle]; + [_button setTarget:self]; + [_button setAction:@selector(changeColor)]; +#endif // macOS] [self addSubview:_button]; [self addSubview:_rootView]; @@ -75,7 +88,7 @@ - (void)changeColor [_rootView setAppProperties:@{@"color" : _beige ? @"beige" : @"purple"}]; } -- (NSArray *> *)reactSubviews +- (NSArray *> *)reactSubviews // [macOS] { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy (void)[super reactSubviews]; diff --git a/packages/rn-tester/RNTester/main.m b/packages/rn-tester/RNTester/main.m index cc89645ead1d7b..ddc2af3c5d99f5 100644 --- a/packages/rn-tester/RNTester/main.m +++ b/packages/rn-tester/RNTester/main.m @@ -5,13 +5,22 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import "AppDelegate.h" +#if !TARGET_OS_OSX // [macOS] int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } +#else // [macOS +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} +#endif // macOS] + + diff --git a/packages/rn-tester/js/RNTesterApp.ios.js b/packages/rn-tester/js/RNTesterApp.ios.js index d997862b64425b..d550a22ad2f934 100644 --- a/packages/rn-tester/js/RNTesterApp.ios.js +++ b/packages/rn-tester/js/RNTesterApp.ios.js @@ -10,7 +10,7 @@ import type {Node} from 'react'; -import {AppRegistry} from 'react-native'; +import {AppRegistry, NativeModules, Platform, View} from 'react-native'; // [macOS] everything but AppRegistry import React from 'react'; import SnapshotViewIOS from './examples/Snapshot/SnapshotViewIOS.ios'; @@ -19,6 +19,8 @@ import RNTesterList from './utils/RNTesterList'; import RNTesterApp from './RNTesterAppShared'; import type {RNTesterModuleInfo} from './types/RNTesterTypes'; +const {TestModule} = NativeModules; // [macOS] + AppRegistry.registerComponent('SetPropertiesExampleApp', () => require('./examples/SetPropertiesExample/SetPropertiesExampleApp'), ); @@ -50,7 +52,54 @@ RNTesterList.Components.concat(RNTesterList.APIs).forEach( () => Snapshotter, ); } + + // [macOS + class LoadPageTest extends React.Component<{}> { + componentDidMount() { + requestAnimationFrame(() => { + TestModule.markTestCompleted(); + }); + } + + render(): Node { + return ; + } + } + + AppRegistry.registerComponent( + 'LoadPageTest_' + Example.key, + () => LoadPageTest, + ); + // macOS] }, ); +// [macOS +class EnumerateExamplePages extends React.Component<{}> { + render(): Node { + RNTesterList.Components.concat(RNTesterList.APIs).forEach( + (Example: RNTesterModuleInfo) => { + let skipTest = false; + if ('skipTest' in Example) { + const platforms = Example.skipTest; + skipTest = + platforms !== undefined && + (Platform.OS in platforms || 'default' in platforms); + } + if (!skipTest) { + console.trace(Example.key); + } + }, + ); + TestModule.markTestCompleted(); + return ; + } +} + +AppRegistry.registerComponent( + 'EnumerateExamplePages', + () => EnumerateExamplePages, +); +// macOS] + module.exports = RNTesterApp; diff --git a/packages/rn-tester/js/RNTesterApp.macos.js b/packages/rn-tester/js/RNTesterApp.macos.js new file mode 100644 index 00000000000000..f8f9e4d05814b5 --- /dev/null +++ b/packages/rn-tester/js/RNTesterApp.macos.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +// [macOS] + +/* $FlowFixMe allow macOS to share iOS file */ +const RNTesterApp = require('./RNTesterApp.ios'); + +module.exports = RNTesterApp; diff --git a/packages/rn-tester/js/components/ListExampleShared.js b/packages/rn-tester/js/components/ListExampleShared.js index 32ddc5633f39c3..524bb9fbe8c0de 100644 --- a/packages/rn-tester/js/components/ListExampleShared.js +++ b/packages/rn-tester/js/components/ListExampleShared.js @@ -23,6 +23,7 @@ const { Text, TextInput, View, + PlatformColor, // [macOS] } = require('react-native'); export type Item = { @@ -71,13 +72,16 @@ class ItemComponent extends React.PureComponent<{ onShowUnderlay?: () => void, onHideUnderlay?: () => void, textSelectable?: ?boolean, + isSelected?: ?boolean, // [macOS] ... }> { _onPress = () => { this.props.onPress(this.props.item.key); }; render(): React.Node { - const {fixedHeight, horizontal, item, textSelectable} = this.props; + // [macOS + const {fixedHeight, horizontal, item, textSelectable, isSelected} = + this.props; // macOS] const itemHash = Math.abs(hashCode(item.title)); const imgSource = THUMB_URLS[itemHash % THUMB_URLS.length]; return ( @@ -91,10 +95,11 @@ class ItemComponent extends React.PureComponent<{ styles.row, horizontal && {width: HORIZ_WIDTH}, fixedHeight && {height: ITEM_HEIGHT}, + isSelected && styles.selectedItem, // [macOS] ]}> {!item.noImage && } {item.title} - {item.text} @@ -346,6 +351,13 @@ const styles = StyleSheet.create({ margin: -10, transform: [{scale: 0.5}], }, + // [macOS + macos: { + top: 4, + margin: -10, + transform: [{scale: 0.5}], + }, + // macOS] }), stacked: { alignItems: 'center', @@ -371,6 +383,22 @@ const styles = StyleSheet.create({ text: { flex: 1, }, + // [macOS + selectedItem: { + backgroundColor: Platform.select({ + macos: PlatformColor('selectedContentBackgroundColor'), + default: 'blue', + }), + }, + selectedItemText: { + // This was the closest UI Element color that looked right... + // https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors + color: Platform.select({ + macos: PlatformColor('selectedMenuItemTextColor'), + default: 'white', + }), + }, + // macOS] loadingContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/packages/rn-tester/js/components/RNTPressableRow.js b/packages/rn-tester/js/components/RNTPressableRow.js index 805b7c57fd1d7b..179c4ae7c513f0 100644 --- a/packages/rn-tester/js/components/RNTPressableRow.js +++ b/packages/rn-tester/js/components/RNTPressableRow.js @@ -74,11 +74,17 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: 15, paddingVertical: 12, - marginVertical: Platform.select({ios: 4, android: 8}), + marginVertical: Platform.select({ios: 4, android: 8, macos: 4}), // [macOS] marginHorizontal: 15, overflow: 'hidden', elevation: 5, - backgroundColor: Platform.select({ios: '#FFFFFF', android: '#F3F8FF'}), + // [macOS + backgroundColor: Platform.select({ + ios: '#FFFFFF', + android: '#F3F8FF', + macos: '#FFFFFF', + }), + // macOS] }, descriptionText: { fontSize: 12, diff --git a/packages/rn-tester/js/components/RNTesterBlock.js b/packages/rn-tester/js/components/RNTesterBlock.js index 58b41c5d6b88d8..839f6613759d6d 100644 --- a/packages/rn-tester/js/components/RNTesterBlock.js +++ b/packages/rn-tester/js/components/RNTesterBlock.js @@ -10,7 +10,8 @@ import * as React from 'react'; import {RNTesterThemeContext} from './RNTesterTheme'; -import {StyleSheet, Text, View} from 'react-native'; +import {PlatformColor, StyleSheet, Text, View} from 'react-native'; +import {Platform} from 'react-native'; // [macOS] type Props = $ReadOnly<{| children?: React.Node, @@ -57,6 +58,15 @@ const styles = StyleSheet.create({ marginHorizontal: 20, }, titleText: { + ...Platform.select({ + macos: { + color: PlatformColor('labelColor'), + }, + ios: { + color: PlatformColor('labelColor'), + }, + default: undefined, + }), fontSize: 18, fontWeight: '300', }, diff --git a/packages/rn-tester/js/components/RNTesterModuleContainer.js b/packages/rn-tester/js/components/RNTesterModuleContainer.js index bcf48da376de5e..49de336ea88422 100644 --- a/packages/rn-tester/js/components/RNTesterModuleContainer.js +++ b/packages/rn-tester/js/components/RNTesterModuleContainer.js @@ -133,7 +133,7 @@ function Header(props: { props.noBottomPadding === true ? styles.headerNoBottomPadding : null, { backgroundColor: - Platform.OS === 'ios' + Platform.OS === 'ios' || Platform.OS === 'macos' // [macOS] ? props.theme.SystemBackgroundColor : props.theme.BackgroundColor, }, @@ -163,6 +163,7 @@ const styles = StyleSheet.create({ borderBottomWidth: Platform.select({ ios: StyleSheet.hairlineWidth, android: 0, + macos: StyleSheet.hairlineWidth, // [macOS] }), marginHorizontal: 15, }, diff --git a/packages/rn-tester/js/components/RNTesterModuleList.js b/packages/rn-tester/js/components/RNTesterModuleList.js index 04421e3cf229fe..faa15f18c72527 100644 --- a/packages/rn-tester/js/components/RNTesterModuleList.js +++ b/packages/rn-tester/js/components/RNTesterModuleList.js @@ -14,6 +14,7 @@ const React = require('react'); const { Platform, + PlatformColor, SectionList, StyleSheet, Text, @@ -39,6 +40,7 @@ const ExampleModuleRow = ({ const onAndroid = !platform || platform === 'android'; const rightAddOn = ( toggleBookmark({ @@ -153,6 +155,8 @@ const RNTesterModuleList: React$AbstractComponent = React.memo( extraData={filteredSections} renderItem={renderListItem} keyboardShouldPersistTaps="handled" + focusable={true} // [macOS] + enableSelectionOnKeyPress={true} // [macOS] automaticallyAdjustContentInsets={false} keyboardDismissMode="on-drag" renderSectionHeader={renderSectionHeader} @@ -171,6 +175,25 @@ const styles = StyleSheet.create({ flex: 1, }, sectionHeader: { + ...Platform.select({ + // [macOS + macos: { + backgroundColor: PlatformColor( + 'unemphasizedSelectedContentBackgroundColor', + ), + + color: PlatformColor('headerTextColor'), + }, + ios: { + backgroundColor: PlatformColor('systemGroupedBackgroundColor'), + color: PlatformColor('secondaryLabelColor'), + }, + default: { + // macOS] + backgroundColor: '#eeeeee', + color: 'black', + }, // [macOS + }), // macOS] padding: 5, fontWeight: '500', fontSize: 11, @@ -179,7 +202,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: 15, paddingVertical: 12, - marginVertical: Platform.select({ios: 4, android: 8}), + marginVertical: Platform.select({ios: 4, android: 8, macos: 4}), // [macOS] marginHorizontal: 15, overflow: 'hidden', elevation: 5, diff --git a/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js b/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js new file mode 100644 index 00000000000000..2d683631bf4dc4 --- /dev/null +++ b/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import type {Node} from 'react'; +import {NativeModules, Button} from 'react-native'; +import React from 'react'; + +const {ASANCrash} = NativeModules; + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = 'ASAN Crash'; +exports.category = 'Basic'; +exports.description = 'ASAN Crash examples.'; + +exports.examples = [ + { + title: 'Native Address Sanitizer crash', + render(): Node { + return ( +