diff --git a/.flowconfig b/.flowconfig index 4a97d9ec9e22dd..db7ce0a5e6a957 100644 --- a/.flowconfig +++ b/.flowconfig @@ -86,4 +86,4 @@ untyped-import untyped-type-import [version] -^0.126.1 +^0.127.0 diff --git a/.flowconfig.android b/.flowconfig.android index 0a2ba4482f0dc5..c4e92ddec22200 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -89,4 +89,4 @@ untyped-import untyped-type-import [version] -^0.126.1 +^0.127.0 diff --git a/.flowconfig.macos b/.flowconfig.macos index 12781b008ce5e0..02d1c53e6da1bc 100644 --- a/.flowconfig.macos +++ b/.flowconfig.macos @@ -59,10 +59,6 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_macos\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_macos\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ -suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError - experimental.well_formed_exports=true experimental.types_first=true experimental.abstract_locations=true @@ -89,4 +85,4 @@ untyped-import untyped-type-import [version] -^0.126.1 +^0.127.0 diff --git a/IntegrationTests/AccessibilityManagerTest.js b/IntegrationTests/AccessibilityManagerTest.js index dd155cec22c517..7501170f3acf89 100644 --- a/IntegrationTests/AccessibilityManagerTest.js +++ b/IntegrationTests/AccessibilityManagerTest.js @@ -10,13 +10,12 @@ 'use strict'; -const React = require('react'); -const ReactNative = require('react-native'); -const {View} = ReactNative; -const RCTDeviceEventEmitter = require('react-native/Libraries/EventEmitter/RCTDeviceEventEmitter'); -const {TestModule} = ReactNative.NativeModules; -import NativeAccessibilityManager from 'react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager'; import invariant from 'invariant'; +import NativeAccessibilityManager from 'react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager'; +import {DeviceEventEmitter, NativeModules, View} from 'react-native'; +import * as React from 'react'; + +const {TestModule} = NativeModules; class AccessibilityManagerTest extends React.Component<{...}> { componentDidMount() { @@ -39,7 +38,7 @@ class AccessibilityManagerTest extends React.Component<{...}> { accessibilityExtraExtraLarge: 11.0, accessibilityExtraExtraExtraLarge: 12.0, }); - RCTDeviceEventEmitter.addListener('didUpdateDimensions', update => { + DeviceEventEmitter.addListener('didUpdateDimensions', update => { TestModule.markTestPassed(update.window.fontScale === 4.0); }); } diff --git a/IntegrationTests/ReactContentSizeUpdateTest.js b/IntegrationTests/ReactContentSizeUpdateTest.js index eefb6681b258e9..7abcc4fb8b4627 100644 --- a/IntegrationTests/ReactContentSizeUpdateTest.js +++ b/IntegrationTests/ReactContentSizeUpdateTest.js @@ -17,7 +17,7 @@ const ReactNative = require('react-native'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; -import type EmitterSubscription from 'react-native/Libraries/vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; const reactViewWidth = 101; const reactViewHeight = 102; @@ -33,7 +33,7 @@ type State = {| class ReactContentSizeUpdateTest extends React.Component { _timeoutID: ?TimeoutID = null; - _subscription: ?EmitterSubscription = null; + _subscription: ?EventSubscription = null; state: State = { height: reactViewHeight, diff --git a/IntegrationTests/SizeFlexibilityUpdateTest.js b/IntegrationTests/SizeFlexibilityUpdateTest.js index fe7a2944853845..b2e4952b102c70 100644 --- a/IntegrationTests/SizeFlexibilityUpdateTest.js +++ b/IntegrationTests/SizeFlexibilityUpdateTest.js @@ -16,7 +16,7 @@ const ReactNative = require('react-native'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; -import type EmitterSubscription from 'react-native/Libraries/vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; const reactViewWidth = 111; const reactViewHeight = 222; @@ -31,7 +31,7 @@ type Props = $ReadOnly<{| |}>; class SizeFlexibilityUpdateTest extends React.Component { - _subscription: ?EmitterSubscription = null; + _subscription: ?EventSubscription = null; UNSAFE_componentWillMount() { this._subscription = RCTNativeAppEventEmitter.addListener( diff --git a/Libraries/Alert/NativeAlertManager.js b/Libraries/Alert/NativeAlertManager.js index 778c89083cb81f..08b2c2d648669c 100644 --- a/Libraries/Alert/NativeAlertManager.js +++ b/Libraries/Alert/NativeAlertManager.js @@ -17,7 +17,7 @@ import type {DefaultInputsArray} from './AlertMacOS'; // TODO(macOS GH#774) export type Args = {| title?: string, message?: string, - buttons?: Array, // TODO: have a better type + buttons?: Array, // TODO(T67565166): have a better type type?: string, defaultValue?: string, cancelButtonKey?: string, diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 8cc6aa25b5bd70..e24ba00097f087 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -38,6 +38,15 @@ const API = { enableQueue: function(): void { queueConnections = true; }, + getValue: function( + tag: number, + saveValueCallback: (value: number) => void, + ): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + if (NativeAnimatedModule.getValue) { + NativeAnimatedModule.getValue(tag, saveValueCallback); + } + }, disableQueue: function(): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); queueConnections = false; diff --git a/Libraries/Animated/src/NativeAnimatedModule.js b/Libraries/Animated/src/NativeAnimatedModule.js index fc76ec1953a6d5..51bb3e50d0c597 100644 --- a/Libraries/Animated/src/NativeAnimatedModule.js +++ b/Libraries/Animated/src/NativeAnimatedModule.js @@ -15,6 +15,7 @@ import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; type EndResult = {finished: boolean, ...}; type EndCallback = (result: EndResult) => void; +type SaveValueCallback = (value: number) => void; export type EventMapping = {| nativeEventPath: Array, @@ -28,6 +29,7 @@ export type AnimatingNodeConfig = Object; export interface Spec extends TurboModule { +createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void; + +getValue: (tag: number, saveValueCallback: SaveValueCallback) => void; +startListeningToAnimatedNodeValue: (tag: number) => void; +stopListeningToAnimatedNodeValue: (tag: number) => void; +connectAnimatedNodes: (parentTag: number, childTag: number) => void; diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index 4a68f78f3addb6..f2de0c3d4a6fdd 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -36,6 +36,7 @@ describe('Native Animated', () => { beforeEach(() => { Object.assign(NativeAnimatedModule, { + getValue: jest.fn(), addAnimatedEventToView: jest.fn(), connectAnimatedNodes: jest.fn(), connectAnimatedNodeToView: jest.fn(), @@ -115,6 +116,26 @@ describe('Native Animated', () => { ); }); + it('should save value on unmount', () => { + NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => { + saveCallback(1); + }); + const opacity = new Animated.Value(0); + + opacity.__makeNative(); + + const root = TestRenderer.create(); + const tag = opacity.__getNativeTag(); + + root.unmount(); + + expect(NativeAnimatedModule.getValue).toBeCalledWith( + tag, + expect.any(Function), + ); + expect(opacity.__getValue()).toBe(1); + }); + it('should extract offset', () => { const opacity = new Animated.Value(0); opacity.__makeNative(); diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js index 099e71c8faaaaf..4851399fd1bb9f 100644 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -10,6 +10,7 @@ 'use strict'; +const View = require('../../Components/View/View'); const {AnimatedEvent} = require('./AnimatedEvent'); const AnimatedProps = require('./nodes/AnimatedProps'); const React = require('react'); @@ -20,7 +21,18 @@ const setAndForwardRef = require('../../Utilities/setAndForwardRef'); export type AnimatedComponentType< Props: {+[string]: mixed, ...}, Instance, -> = React.AbstractComponent<$ObjMap any>, Instance>; +> = React.AbstractComponent< + $ObjMap< + Props & + $ReadOnly<{ + passthroughAnimatedPropExplicitValues?: React.ElementConfig< + typeof View, + >, + }>, + () => any, + >, + Instance, +>; function createAnimatedComponent( Component: React.AbstractComponent, @@ -65,6 +77,7 @@ function createAnimatedComponent( // However, setNativeProps can only be implemented on leaf native // components. If you want to animate a composite component, you need to // re-render it. In this case, we have a fallback that uses forceUpdate. + // This fallback is also called in Fabric. _animatedPropsCallback = () => { if (this._component == null) { // AnimatedProps is created in will-mount because it's used in render. @@ -119,6 +132,10 @@ function createAnimatedComponent( _attachProps(nextProps) { const oldPropsAnimated = this._propsAnimated; + if (nextProps === oldPropsAnimated) { + return; + } + this._propsAnimated = new AnimatedProps( nextProps, this._animatedPropsCallback, @@ -160,10 +177,15 @@ function createAnimatedComponent( }); render() { - const props = this._propsAnimated.__getValue(); + const {style = {}, ...props} = this._propsAnimated.__getValue() || {}; + const {style: passthruStyle = {}, ...passthruProps} = + this.props.passthroughAnimatedPropExplicitValues || {}; + const mergedStyle = {...style, ...passthruStyle}; return ( { + this._value = value; + }); + } this.stopAnimation(); super.__detach(); } diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js index b5fb59bd9fd5db..cc8a62a381ff8e 100644 --- a/Libraries/AppState/AppState.js +++ b/Libraries/AppState/AppState.js @@ -10,13 +10,11 @@ 'use strict'; -const EventEmitter = require('../vendor/emitter/EventEmitter'); -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); - -const invariant = require('invariant'); -const logError = require('../Utilities/logError'); - +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import logError from '../Utilities/logError'; +import EventEmitter from '../vendor/emitter/EventEmitter'; import NativeAppState from './NativeAppState'; +import invariant from 'invariant'; /** * `AppState` can tell you if the app is in the foreground or background, diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index 001c44c17b2ae9..d2f3c4a442a4c9 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -10,11 +10,10 @@ 'use strict'; -const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter'); - -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; -import NativeBugReporting from './NativeBugReporting'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import NativeRedBox from '../NativeModules/specs/NativeRedBox'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import NativeBugReporting from './NativeBugReporting'; type ExtraData = {[key: string]: string, ...}; type SourceCallback = () => string; @@ -39,8 +38,8 @@ function defaultExtras() { class BugReporting { static _extraSources: Map = new Map(); static _fileSources: Map = new Map(); - static _subscription: ?EmitterSubscription = null; - static _redboxSubscription: ?EmitterSubscription = null; + static _subscription: ?EventSubscription = null; + static _redboxSubscription: ?EventSubscription = null; static _maybeInit() { if (!BugReporting._subscription) { diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js index 07556bf488d3c8..e2b19d2bd1146b 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js @@ -10,9 +10,8 @@ 'use strict'; -const RCTDeviceEventEmitter = require('../../EventEmitter/RCTDeviceEventEmitter'); -const UIManager = require('../../ReactNative/UIManager'); - +import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; +import UIManager from '../../ReactNative/UIManager'; import NativeAccessibilityInfo from './NativeAccessibilityInfo'; const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index 66080bfecca0fa..157f5bc6388a59 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -10,9 +10,7 @@ 'use strict'; -const Promise = require('../../Promise'); -const RCTDeviceEventEmitter = require('../../EventEmitter/RCTDeviceEventEmitter'); - +import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; import NativeAccessibilityManager from './NativeAccessibilityManager'; const CHANGE_EVENT_NAME = { diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js index d5cd47afdce59f..d3f475a1fdb777 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -10,14 +10,13 @@ 'use strict'; -const Platform = require('../../Utilities/Platform'); -const NativeEventEmitter = require('../../EventEmitter/NativeEventEmitter'); - +import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; +import Platform from '../../Utilities/Platform'; +import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; import NativeTVNavigationEventEmitter from './NativeTVNavigationEventEmitter'; -import type EmitterSubscription from '../../vendor/emitter/EmitterSubscription'; class TVEventHandler { - __nativeTVNavigationEventListener: ?EmitterSubscription = null; + __nativeTVNavigationEventListener: ?EventSubscription = null; __nativeTVNavigationEventEmitter: ?NativeEventEmitter = null; enable(component: ?any, callback: Function): void { diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index c10fe53dad503d..a9e0a29771ddfd 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -10,13 +10,11 @@ 'use strict'; -const LayoutAnimation = require('../../LayoutAnimation/LayoutAnimation'); -const NativeEventEmitter = require('../../EventEmitter/NativeEventEmitter'); - -const dismissKeyboard = require('../../Utilities/dismissKeyboard'); -const invariant = require('invariant'); - +import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; +import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; +import dismissKeyboard from '../../Utilities/dismissKeyboard'; import NativeKeyboardObserver from './NativeKeyboardObserver'; +import invariant from 'invariant'; const KeyboardEventEmitter: NativeEventEmitter = new NativeEventEmitter( NativeKeyboardObserver, ); diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 07513e253b7041..b8a150efbaa34d 100644 --- a/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -18,7 +18,7 @@ const StyleSheet = require('../../StyleSheet/StyleSheet'); const View = require('../View/View'); import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import type EmitterSubscription from '../../vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; import type { ViewProps, ViewLayout, @@ -67,7 +67,7 @@ class KeyboardAvoidingView extends React.Component { }; _frame: ?ViewLayout = null; - _subscriptions: Array = []; + _subscriptions: Array = []; viewRef: {current: React.ElementRef | null, ...}; _initialFrameHeight: number = 0; diff --git a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js index 181eb14b77bce5..639cdd0db5f4f0 100644 --- a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js +++ b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -11,12 +11,12 @@ 'use strict'; -const Keyboard = require('../Keyboard'); -const dismissKeyboard = require('../../../Utilities/dismissKeyboard'); +const NativeModules = require('../../../BatchedBridge/NativeModules'); const LayoutAnimation = require('../../../LayoutAnimation/LayoutAnimation'); +const dismissKeyboard = require('../../../Utilities/dismissKeyboard'); +const Keyboard = require('../Keyboard'); -const NativeEventEmitter = require('../../../EventEmitter/NativeEventEmitter'); -const NativeModules = require('../../../BatchedBridge/NativeModules'); +import NativeEventEmitter from '../../../EventEmitter/NativeEventEmitter'; jest.mock('../../../LayoutAnimation/LayoutAnimation'); diff --git a/Libraries/Components/Picker/PickerAndroid.android.js b/Libraries/Components/Picker/PickerAndroid.android.js index 573618f6c0de3e..ccefd18833d214 100644 --- a/Libraries/Components/Picker/PickerAndroid.android.js +++ b/Libraries/Components/Picker/PickerAndroid.android.js @@ -100,7 +100,7 @@ function PickerAndroid(props: Props): React.Node { props.mode === 'dropdown' ? AndroidDropdownPickerCommands : AndroidDialogPickerCommands; - Commands.setNativeSelectedPosition(current, selected); + Commands.setNativeSelectedPosition(current, position); } }, [ diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.js b/Libraries/Components/SafeAreaView/SafeAreaView.js index ed8a9ed3f74c1d..8fc76bed849dc8 100644 --- a/Libraries/Components/SafeAreaView/SafeAreaView.js +++ b/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -8,9 +8,9 @@ * @format */ -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const View = require('../View/View'); +import Platform from '../../Utilities/Platform'; +import * as React from 'react'; +import View from '../View/View'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index a520d17d2875fd..527db47eb7c2f9 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -10,25 +10,25 @@ 'use strict'; -const React = require('react'); const Dimensions = require('../Utilities/Dimensions'); const FrameRateLogger = require('../Interaction/FrameRateLogger'); const Keyboard = require('./Keyboard/Keyboard'); +const Platform = require('../Utilities/Platform'); +const React = require('react'); const ReactNative = require('../Renderer/shims/ReactNative'); const TextInputState = require('./TextInput/TextInputState'); const UIManager = require('../ReactNative/UIManager'); -const Platform = require('../Utilities/Platform'); -import Commands from './ScrollView/ScrollViewCommands'; const invariant = require('invariant'); const performanceNow = require('fbjs/lib/performanceNow'); +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; import type {PressEvent, ScrollEvent} from '../Types/CoreEventTypes'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import type {KeyboardEvent} from './Keyboard/Keyboard'; import typeof ScrollView from './ScrollView/ScrollView'; import type {Props as ScrollViewProps} from './ScrollView/ScrollView'; -import type {KeyboardEvent} from './Keyboard/Keyboard'; -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import Commands from './ScrollView/ScrollViewCommands'; /** * Mixin that can be integrated in order to handle scrolling that plays well @@ -119,10 +119,10 @@ export type State = {| |}; const ScrollResponderMixin = { - _subscriptionKeyboardWillShow: (null: ?EmitterSubscription), - _subscriptionKeyboardWillHide: (null: ?EmitterSubscription), - _subscriptionKeyboardDidShow: (null: ?EmitterSubscription), - _subscriptionKeyboardDidHide: (null: ?EmitterSubscription), + _subscriptionKeyboardWillShow: (null: ?EventSubscription), + _subscriptionKeyboardWillHide: (null: ?EventSubscription), + _subscriptionKeyboardDidShow: (null: ?EventSubscription), + _subscriptionKeyboardDidHide: (null: ?EventSubscription), scrollResponderMixinGetInitialState: function(): State { return { isTouching: false, diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index c8d5426679e7de..0491f3aa90bb92 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -308,15 +308,6 @@ type IOSProps = $ReadOnly<{| * @platform macos */ verticalLineScroll?: number, // TODO(macOS GH#774) - /** - * When true, ScrollView will emit updateChildFrames data in scroll events, - * otherwise will not compute or emit child frame data. This only exists - * to support legacy issues, `onLayout` should be used instead to retrieve - * frame data. - * The default value is false. - * @platform ios - */ - DEPRECATED_sendUpdatedChildFrames?: ?boolean, |}>; type AndroidProps = $ReadOnly<{| @@ -1187,6 +1178,7 @@ class ScrollView extends React.Component { return ( this._setStickyHeaderRef(key, ref)} nextHeaderLayoutY={this._headerLayoutYs.get( this._getKeyForIndex(nextIndex, childArray), @@ -1248,9 +1240,6 @@ class ScrollView extends React.Component { ? this.props.alwaysBounceVertical : !this.props.horizontal; - const DEPRECATED_sendUpdatedChildFrames = !!this.props - .DEPRECATED_sendUpdatedChildFrames; - const baseStyle = this.props.horizontal === true ? styles.baseHorizontal @@ -1301,7 +1290,6 @@ class ScrollView extends React.Component { this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd ? true : false, - DEPRECATED_sendUpdatedChildFrames, // default to true snapToStart: this.props.snapToStart !== false, // default to true diff --git a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index 4004a6bfb9dc9e..a6366e8b62b347 100644 --- a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -77,7 +77,6 @@ export type ScrollViewNativeProps = $ReadOnly<{ snapToOffsets?: ?$ReadOnlyArray, snapToStart?: ?boolean, zoomScale?: ?number, - DEPRECATED_sendUpdatedChildFrames?: ?boolean, // Overrides style?: {...ViewStyleProp, ...} | DangerouslyImpreciseStyle, onResponderGrant?: ?(e: any) => void | boolean, diff --git a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index 0bc51bbfaecbec..172be92dfcab14 100644 --- a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -14,6 +14,7 @@ const AnimatedImplementation = require('../../Animated/src/AnimatedImplementatio const React = require('react'); const StyleSheet = require('../../StyleSheet/StyleSheet'); const View = require('../View/View'); +const Platform = require('../../Utilities/Platform'); import type {LayoutEvent} from '../../Types/CoreEventTypes'; @@ -29,6 +30,7 @@ export type Props = { inverted: ?boolean, // The height of the parent ScrollView. Currently only set when inverted. scrollViewHeight: ?number, + nativeID?: ?string, ... }; @@ -37,6 +39,7 @@ type State = { layoutY: number, layoutHeight: number, nextHeaderLayoutY: ?number, + translateY: ?number, ... }; @@ -46,17 +49,121 @@ class ScrollViewStickyHeader extends React.Component { layoutY: 0, layoutHeight: 0, nextHeaderLayoutY: this.props.nextHeaderLayoutY, + translateY: null, }; + _translateY: ?AnimatedImplementation.Interpolation = null; + _shouldRecreateTranslateY: boolean = true; + _haveReceivedInitialZeroTranslateY: boolean = true; + _ref: any; // TODO T53738161: flow type this, and the whole file + + // Fabric-only: + _timer: ?TimeoutID; + _animatedValueListenerId: string; + _animatedValueListener: (valueObject: $ReadOnly<{|value: number|}>) => void; + _debounceTimeout: number = Platform.OS === 'android' ? 15 : 64; + setNextHeaderY(y: number) { this.setState({nextHeaderLayoutY: y}); } + UNSAFE_componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.scrollViewHeight !== this.props.scrollViewHeight || + nextProps.scrollAnimatedValue !== this.props.scrollAnimatedValue || + nextProps.inverted !== this.props.inverted + ) { + this._shouldRecreateTranslateY = true; + } + } + + updateTranslateListener( + translateY: AnimatedImplementation.Interpolation, + isFabric: boolean, + ) { + if (this._translateY != null && this._animatedValueListenerId != null) { + this._translateY.removeListener(this._animatedValueListenerId); + } + + this._translateY = translateY; + this._shouldRecreateTranslateY = false; + + if (!isFabric) { + return; + } + + if (!this._animatedValueListener) { + // This is called whenever the (Interpolated) Animated Value + // updates, which is several times per frame during scrolling. + // To ensure that the Fabric ShadowTree has the most recent + // translate style of this node, we debounce the value and then + // pass it through to the underlying node during render. + // This is: + // 1. Only an issue in Fabric. + // 2. Worse in Android than iOS. In Android, but not iOS, you + // can touch and move your finger slightly and still trigger + // a "tap" event. In iOS, moving will cancel the tap in + // both Fabric and non-Fabric. On Android when you move + // your finger, the hit-detection moves from the Android + // platform to JS, so we need the ShadowTree to have knowledge + // of the current position. + this._animatedValueListener = ({value}) => { + // When the AnimatedInterpolation is recreated, it always initializes + // to a value of zero and emits a value change of 0 to its listeners. + if (value === 0 && !this._haveReceivedInitialZeroTranslateY) { + this._haveReceivedInitialZeroTranslateY = true; + return; + } + if (this._timer) { + clearTimeout(this._timer); + } + this._timer = setTimeout(() => { + if (value !== this.state.translateY) { + this.setState({ + translateY: value, + }); + // This fixes jank on iOS, especially around paging, + // but causes jank on Android. + // It seems that Native Animated Driver on iOS has + // more conflicts with values passed through the ShadowTree + // especially when connecting new Animated nodes + disconnecting + // old ones, compared to Android where that process seems fine. + if (Platform.OS === 'ios') { + setTimeout(() => { + this.setState({ + translateY: null, + }); + }, 0); + } + } + }, this._debounceTimeout); + }; + } + if (this.state.translateY !== 0 && this.state.translateY != null) { + this._haveReceivedInitialZeroTranslateY = false; + } + this._animatedValueListenerId = translateY.addListener( + this._animatedValueListener, + ); + } + _onLayout = event => { + const layoutY = event.nativeEvent.layout.y; + const layoutHeight = event.nativeEvent.layout.height; + const measured = true; + + if ( + layoutY !== this.state.layoutY || + layoutHeight !== this.state.layoutHeight || + measured !== this.state.measured + ) { + this._shouldRecreateTranslateY = true; + } + this.setState({ - measured: true, - layoutY: event.nativeEvent.layout.y, - layoutHeight: event.nativeEvent.layout.height, + measured, + layoutY, + layoutHeight, }); this.props.onLayout(event); @@ -66,85 +173,125 @@ class ScrollViewStickyHeader extends React.Component { } }; + _setComponentRef = ref => { + this._ref = ref; + }; + render(): React.Node { - const {inverted, scrollViewHeight} = this.props; - const {measured, layoutHeight, layoutY, nextHeaderLayoutY} = this.state; - const inputRange: Array = [-1, 0]; - const outputRange: Array = [0, 0]; - - if (measured) { - if (inverted) { - // The interpolation looks like: - // - Negative scroll: no translation - // - `stickStartPoint` is the point at which the header will start sticking. - // It is calculated using the ScrollView viewport height so it is a the bottom. - // - Headers that are in the initial viewport will never stick, `stickStartPoint` - // will be negative. - // - From 0 to `stickStartPoint` no translation. This will cause the header - // to scroll normally until it reaches the top of the scroll view. - // - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate - // equally to scroll. This will cause the header to stay at the top of the scroll view. - // - Past the collision with the next header y: no more translation. This will cause the - // header to continue scrolling up and make room for the next sticky header. - // In the case that there is no next header just translate equally to - // scroll indefinitely. - if (scrollViewHeight != null) { - const stickStartPoint = layoutY + layoutHeight - scrollViewHeight; - if (stickStartPoint > 0) { - inputRange.push(stickStartPoint); - outputRange.push(0); - inputRange.push(stickStartPoint + 1); - outputRange.push(1); - // If the next sticky header has not loaded yet (probably windowing) or is the last - // we can just keep it sticked forever. - const collisionPoint = - (nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight; - if (collisionPoint > stickStartPoint) { - inputRange.push(collisionPoint, collisionPoint + 1); - outputRange.push( - collisionPoint - stickStartPoint, - collisionPoint - stickStartPoint, - ); + // Fabric Detection + const isFabric = !!( + // An internal transform mangles variables with leading "_" as private. + // eslint-disable-next-line dot-notation + (this._ref && this._ref['_internalInstanceHandle']?.stateNode?.canonical) + ); + + // Initially and in the case of updated props or layout, we + // recreate this interpolated value. Otherwise, we do not recreate + // when there are state changes. + if (this._shouldRecreateTranslateY) { + const {inverted, scrollViewHeight} = this.props; + const {measured, layoutHeight, layoutY, nextHeaderLayoutY} = this.state; + const inputRange: Array = [-1, 0]; + const outputRange: Array = [0, 0]; + + if (measured) { + if (inverted) { + // The interpolation looks like: + // - Negative scroll: no translation + // - `stickStartPoint` is the point at which the header will start sticking. + // It is calculated using the ScrollView viewport height so it is a the bottom. + // - Headers that are in the initial viewport will never stick, `stickStartPoint` + // will be negative. + // - From 0 to `stickStartPoint` no translation. This will cause the header + // to scroll normally until it reaches the top of the scroll view. + // - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate + // equally to scroll. This will cause the header to stay at the top of the scroll view. + // - Past the collision with the next header y: no more translation. This will cause the + // header to continue scrolling up and make room for the next sticky header. + // In the case that there is no next header just translate equally to + // scroll indefinitely. + if (scrollViewHeight != null) { + const stickStartPoint = layoutY + layoutHeight - scrollViewHeight; + if (stickStartPoint > 0) { + inputRange.push(stickStartPoint); + outputRange.push(0); + inputRange.push(stickStartPoint + 1); + outputRange.push(1); + // If the next sticky header has not loaded yet (probably windowing) or is the last + // we can just keep it sticked forever. + const collisionPoint = + (nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight; + if (collisionPoint > stickStartPoint) { + inputRange.push(collisionPoint, collisionPoint + 1); + outputRange.push( + collisionPoint - stickStartPoint, + collisionPoint - stickStartPoint, + ); + } } } - } - } else { - // The interpolation looks like: - // - Negative scroll: no translation - // - From 0 to the y of the header: no translation. This will cause the header - // to scroll normally until it reaches the top of the scroll view. - // - From header y to when the next header y hits the bottom edge of the header: translate - // equally to scroll. This will cause the header to stay at the top of the scroll view. - // - Past the collision with the next header y: no more translation. This will cause the - // header to continue scrolling up and make room for the next sticky header. - // In the case that there is no next header just translate equally to - // scroll indefinitely. - inputRange.push(layoutY); - outputRange.push(0); - // If the next sticky header has not loaded yet (probably windowing) or is the last - // we can just keep it sticked forever. - const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight; - if (collisionPoint >= layoutY) { - inputRange.push(collisionPoint, collisionPoint + 1); - outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY); } else { - inputRange.push(layoutY + 1); - outputRange.push(1); + // The interpolation looks like: + // - Negative scroll: no translation + // - From 0 to the y of the header: no translation. This will cause the header + // to scroll normally until it reaches the top of the scroll view. + // - From header y to when the next header y hits the bottom edge of the header: translate + // equally to scroll. This will cause the header to stay at the top of the scroll view. + // - Past the collision with the next header y: no more translation. This will cause the + // header to continue scrolling up and make room for the next sticky header. + // In the case that there is no next header just translate equally to + // scroll indefinitely. + inputRange.push(layoutY); + outputRange.push(0); + // If the next sticky header has not loaded yet (probably windowing) or is the last + // we can just keep it sticked forever. + const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight; + if (collisionPoint >= layoutY) { + inputRange.push(collisionPoint, collisionPoint + 1); + outputRange.push( + collisionPoint - layoutY, + collisionPoint - layoutY, + ); + } else { + inputRange.push(layoutY + 1); + outputRange.push(1); + } } } + + this.updateTranslateListener( + this.props.scrollAnimatedValue.interpolate({ + inputRange, + outputRange, + }), + isFabric, + ); } - const translateY = this.props.scrollAnimatedValue.interpolate({ - inputRange, - outputRange, - }); const child = React.Children.only(this.props.children); + // TODO T68319535: remove this if NativeAnimated is rewritten for Fabric + const passthroughAnimatedPropExplicitValues = + isFabric && this.state.translateY != null + ? { + style: {transform: [{translateY: this.state.translateY}]}, + } + : null; + return ( + ref={this._setComponentRef} + style={[ + child.props.style, + styles.header, + {transform: [{translateY: this._translateY}]}, + ]} + passthroughAnimatedPropExplicitValues={ + passthroughAnimatedPropExplicitValues + }> {React.cloneElement(child, { style: styles.fill, // We transfer the child style to the wrapper. onLayout: undefined, // we call this manually through our this._onLayout diff --git a/Libraries/Components/ScrollView/ScrollViewViewConfig.js b/Libraries/Components/ScrollView/ScrollViewViewConfig.js index 90adc39f12883a..b2a3a9246b343b 100644 --- a/Libraries/Components/ScrollView/ScrollViewViewConfig.js +++ b/Libraries/Components/ScrollView/ScrollViewViewConfig.js @@ -69,8 +69,6 @@ const ScrollViewViewConfig = { snapToOffsets: true, snapToStart: true, zoomScale: true, - - DEPRECATED_sendUpdatedChildFrames: true, }, }; diff --git a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap index 3f7620bc348fcb..47a15d64c9348e 100644 --- a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap +++ b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap @@ -14,7 +14,6 @@ exports[` should render as expected: should deep render when mocke exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` ; -export default (codegenNativeComponent( - 'RCTInputAccessoryView', -): HostComponent); +export default (codegenNativeComponent('InputAccessory', { + interfaceOnly: true, + paperComponentName: 'RCTInputAccessoryView', + excludedPlatforms: ['android'], +}): HostComponent); diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js index c8514296721c89..1b92392c1b6b86 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -285,6 +285,7 @@ class TouchableNativeFeedback extends React.Component { this.props.useForeground === true, ), accessible: this.props.accessible !== false, + accessibilityHint: this.props.accessibilityHint, accessibilityLabel: this.props.accessibilityLabel, accessibilityRole: this.props.accessibilityRole, accessibilityState: this.props.accessibilityState, diff --git a/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js b/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js index d6ea59f56f9c3f..ad2542dfa2844c 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js +++ b/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js @@ -19,6 +19,12 @@ const ReactNativeViewViewConfigAndroid = { captured: 'onSelectCapture', }, }, + topAssetDidLoad: { + phasedRegistrationNames: { + bubbled: 'onAssetDidLoad', + captured: 'onAssetDidLoadCapture', + }, + }, }, directEventTypes: { topClick: { @@ -57,6 +63,9 @@ const ReactNativeViewViewConfigAndroid = { topSelectionChange: { registrationName: 'onSelectionChange', }, + onAssetDidLoad: { + registrationName: 'onAssetDidLoad', + }, }, validAttributes: { hasTVPreferredFocus: true, diff --git a/Libraries/Core/ExceptionsManager.js b/Libraries/Core/ExceptionsManager.js index b1abe891f18f0b..ba35831eeb8596 100644 --- a/Libraries/Core/ExceptionsManager.js +++ b/Libraries/Core/ExceptionsManager.js @@ -75,7 +75,8 @@ function reportException( message = e.jsEngine == null ? message : `${message}, js engine: ${e.jsEngine}`; - const isHandledByLogBox = e.forceRedbox !== true && !global.RN$Bridgeless; + const isHandledByLogBox = + e.forceRedbox !== true && !global.RN$Bridgeless && !global.RN$Express; const data = preprocessException({ message, @@ -113,7 +114,7 @@ function reportException( NativeExceptionsManager.reportException(data); - if (__DEV__) { + if (__DEV__ && !global.RN$Express) { if (e.preventSymbolication === true) { return; } diff --git a/Libraries/Core/NativeExceptionsManager.js b/Libraries/Core/NativeExceptionsManager.js index db982abc037c6f..b36eb0cd74370b 100644 --- a/Libraries/Core/NativeExceptionsManager.js +++ b/Libraries/Core/NativeExceptionsManager.js @@ -47,7 +47,6 @@ export interface Spec extends TurboModule { stack: Array, exceptionId: number, ) => void; - // TODO(T53311281): This is a noop on iOS now. Implement it. +reportException?: (data: ExceptionData) => void; +updateExceptionMessage: ( message: string, diff --git a/Libraries/EventEmitter/NativeEventEmitter.js b/Libraries/EventEmitter/NativeEventEmitter.js index e2650878b186e7..8e4409973f0f0c 100644 --- a/Libraries/EventEmitter/NativeEventEmitter.js +++ b/Libraries/EventEmitter/NativeEventEmitter.js @@ -10,13 +10,11 @@ 'use strict'; -const EventEmitter = require('../vendor/emitter/EventEmitter'); -const Platform = require('../Utilities/Platform'); -const RCTDeviceEventEmitter = require('./RCTDeviceEventEmitter'); - -const invariant = require('invariant'); - -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; +import Platform from '../Utilities/Platform'; +import EventEmitter from '../vendor/emitter/EventEmitter'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import RCTDeviceEventEmitter from './RCTDeviceEventEmitter'; +import invariant from 'invariant'; type NativeModule = { +addListener: (eventType: string) => void, @@ -46,7 +44,7 @@ class NativeEventEmitter extends EventEmitter { eventType: string, listener: Function, context: ?Object, - ): EmitterSubscription { + ): EventSubscription { if (this._nativeModule != null) { this._nativeModule.addListener(eventType); } @@ -62,7 +60,7 @@ class NativeEventEmitter extends EventEmitter { super.removeAllListeners(eventType); } - removeSubscription(subscription: EmitterSubscription) { + removeSubscription(subscription: EventSubscription) { if (this._nativeModule != null) { this._nativeModule.removeListeners(1); } diff --git a/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/Libraries/EventEmitter/RCTDeviceEventEmitter.js index 0b4c15d501d6b8..8c0f02328759ff 100644 --- a/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -10,10 +10,9 @@ 'use strict'; -const EventEmitter = require('../vendor/emitter/EventEmitter'); -const EventSubscriptionVendor = require('../vendor/emitter/EventSubscriptionVendor'); - -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; +import EventEmitter from '../vendor/emitter/EventEmitter'; +import type EmitterSubscription from '../vendor/emitter/_EmitterSubscription'; +import EventSubscriptionVendor from '../vendor/emitter/_EventSubscriptionVendor'; function checkNativeEventModule(eventType: ?string) { if (eventType) { diff --git a/Libraries/EventEmitter/RCTNativeAppEventEmitter.js b/Libraries/EventEmitter/RCTNativeAppEventEmitter.js index c0cbef506efb11..0e3c4faf51ccc1 100644 --- a/Libraries/EventEmitter/RCTNativeAppEventEmitter.js +++ b/Libraries/EventEmitter/RCTNativeAppEventEmitter.js @@ -10,7 +10,7 @@ 'use strict'; -const RCTDeviceEventEmitter = require('./RCTDeviceEventEmitter'); +import RCTDeviceEventEmitter from './RCTDeviceEventEmitter'; /** * Deprecated - subclass NativeEventEmitter to create granular event modules instead of diff --git a/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js b/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js index 5a8eadc2795302..35f46742abef41 100644 --- a/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js +++ b/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js @@ -10,8 +10,8 @@ 'use strict'; -const EventEmitter = require('../../vendor/emitter/EventEmitter'); -const RCTDeviceEventEmitter = require('../RCTDeviceEventEmitter'); +import EventEmitter from '../../vendor/emitter/EventEmitter'; +import RCTDeviceEventEmitter from '../RCTDeviceEventEmitter'; /** * Mock the NativeEventEmitter as a normal JS EventEmitter. diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index 2276aeeed11a71..5fb45d2153da68 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -233,6 +233,10 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "createAnimatedNode", @selector(createAnimatedNode:config:), args, count); } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_getValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "getValue", @selector(getValue:saveValueCallback:), args, count); + } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "startListeningToAnimatedNodeValue", @selector(startListeningToAnimatedNodeValue:), args, count); } @@ -312,6 +316,9 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json methodMap_["createAnimatedNode"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_createAnimatedNode}; + methodMap_["getValue"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_getValue}; + + methodMap_["startListeningToAnimatedNodeValue"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue}; @@ -904,6 +911,26 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso + } + + } // namespace react +} // namespace facebook +namespace facebook { + namespace react { + + + static facebook::jsi::Value __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, "loadBundle", @selector(loadBundle:resolve:reject:), args, count); + } + + + NativeDevSplitBundleLoaderSpecJSI::NativeDevSplitBundleLoaderSpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + + methodMap_["loadBundle"] = MethodMetadata {1, __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle}; + + + } } // namespace react diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 29bd6a93843cf8..2dde2e571ba2d9 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -276,6 +276,8 @@ namespace JS { - (void)createAnimatedNode:(double)tag config:(NSDictionary *)config; +- (void)getValue:(double)tag +saveValueCallback:(RCTResponseSenderBlock)saveValueCallback; - (void)startListeningToAnimatedNodeValue:(double)tag; - (void)stopListeningToAnimatedNodeValue:(double)tag; - (void)connectAnimatedNodes:(double)parentTag @@ -763,6 +765,26 @@ namespace facebook { }; } // namespace react } // namespace facebook +@protocol NativeDevSplitBundleLoaderSpec + +- (void)loadBundle:(NSString *)bundlePath + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; + +@end +namespace facebook { + namespace react { + /** + * ObjC++ class for module 'DevSplitBundleLoader' + */ + + class JSI_EXPORT NativeDevSplitBundleLoaderSpecJSI : public ObjCTurboModule { + public: + NativeDevSplitBundleLoaderSpecJSI(const ObjCTurboModule::InitParams ¶ms); + + }; + } // namespace react +} // namespace facebook @protocol NativeDeviceEventManagerSpec - (void)invokeDefaultBackPressHandler; diff --git a/Libraries/Image/ImageProps.js b/Libraries/Image/ImageProps.js index 2218270ec0f89b..6e3c138cee2572 100644 --- a/Libraries/Image/ImageProps.js +++ b/Libraries/Image/ImageProps.js @@ -21,9 +21,8 @@ export type ImageLoadEvent = SyntheticEvent< source: $ReadOnly<{| width: number, height: number, - url: string, + uri: string, |}>, - uri?: string, // Only on Android |}>, >; diff --git a/Libraries/Image/RCTImageView.mm b/Libraries/Image/RCTImageView.mm index 8a14c4c137ae95..2835e857095b57 100644 --- a/Libraries/Image/RCTImageView.mm +++ b/Libraries/Image/RCTImageView.mm @@ -91,9 +91,9 @@ static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSi static NSDictionary *onLoadParamsForSource(RCTImageSource *source) { NSDictionary *dict = @{ + @"uri": source.request.URL.absoluteString, @"width": @(source.size.width), @"height": @(source.size.height), - @"url": source.request.URL.absoluteString, }; return @{ @"source": dict }; } diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index 02912bbc3cf0b9..1bf5cbf032db4b 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -11,13 +11,14 @@ 'use strict'; const BatchedBridge = require('../BatchedBridge/BatchedBridge'); -const EventEmitter = require('../vendor/emitter/EventEmitter'); const TaskQueue = require('./TaskQueue'); const infoLog = require('../Utilities/infoLog'); const invariant = require('invariant'); const keyMirror = require('fbjs/lib/keyMirror'); +import EventEmitter from '../vendor/emitter/EventEmitter'; + export type Handle = number; import type {Task} from './TaskQueue'; diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js index dd6fc76f152fe4..7a0ab925a6d8c8 100644 --- a/Libraries/Linking/Linking.js +++ b/Libraries/Linking/Linking.js @@ -10,13 +10,11 @@ 'use strict'; -const InteractionManager = require('../Interaction/InteractionManager'); -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); -const Platform = require('../Utilities/Platform'); - -const invariant = require('invariant'); - +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import InteractionManager from '../Interaction/InteractionManager'; +import Platform from '../Utilities/Platform'; import NativeLinking from './NativeLinking'; +import invariant from 'invariant'; /** * `Linking` gives you a general interface to interact with both incoming diff --git a/Libraries/Linking/NativeLinking.js b/Libraries/Linking/NativeLinking.js index a73a19fe8fb878..18b3e7128cd570 100644 --- a/Libraries/Linking/NativeLinking.js +++ b/Libraries/Linking/NativeLinking.js @@ -26,7 +26,7 @@ export interface Spec extends TurboModule { action: string, extras: ?Array<{ key: string, - value: string | number | boolean, + value: string | number | boolean, // TODO(T67672788): Union types are not type safe ... }>, ) => Promise; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index dcc524da53ca06..9f34234ffecb69 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -1175,9 +1175,12 @@ class VirtualizedList extends React.PureComponent { this.context == null ) { // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 + // TODO (GitHub #818): Use console.error (as per 646605b90e666c4b0d1c1200a137eacf62b46f87) + // instead of console.warn after resolving problems3 in RNTester's MultiColumn and SectionList example pages console.warn( 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + - 'orientation - use another VirtualizedList-backed container instead.', + 'orientation because it can break windowing and other functionality - use another ' + + 'VirtualizedList-backed container instead.', ); this._hasWarned.nesting = true; } @@ -1652,10 +1655,12 @@ class VirtualizedList extends React.PureComponent { this.props.initialScrollIndex > 0 && !this._hasDoneInitialScroll ) { - this.scrollToIndex({ - animated: false, - index: this.props.initialScrollIndex, - }); + if (this.props.contentOffset == null) { + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex, + }); + } this._hasDoneInitialScroll = true; } if (this.props.onContentSizeChange) { diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index de52591c90090b..ef1d75c3639eb5 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -11,7 +11,6 @@ 'use strict'; const AppContainer = require('../ReactNative/AppContainer'); -const {RootTagContext} = require('../ReactNative/RootTag'); const I18nManager = require('../ReactNative/I18nManager'); const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); import NativeModalManager from './NativeModalManager'; @@ -22,12 +21,14 @@ const ScrollView = require('../Components/ScrollView/ScrollView'); const StyleSheet = require('../StyleSheet/StyleSheet'); const View = require('../Components/View/View'); -import type {RootTag} from '../ReactNative/RootTag'; +const {RootTagContext} = require('../ReactNative/RootTag'); + import type {ViewProps} from '../Components/View/ViewPropTypes'; +import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js'; +import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; -import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js'; const ModalEventEmitter = Platform.OS === 'ios' && NativeModalManager != null @@ -161,7 +162,7 @@ class Modal extends React.Component { static contextType: React.Context = RootTagContext; _identifier: number; - _eventSubscription: ?EmitterSubscription; + _eventSubscription: ?EventSubscription; constructor(props: Props) { super(props); diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm index 5727f0bf308307..60897f2f48c713 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm @@ -15,9 +15,6 @@ typedef void (^AnimatedOperation)(RCTNativeAnimatedNodesManager *nodesManager); -@interface RCTNativeAnimatedModule() -@end - @implementation RCTNativeAnimatedModule { RCTNativeAnimatedNodesManager *_nodesManager; @@ -96,16 +93,16 @@ - (void)setBridge:(RCTBridge *)bridge [nodesManager startAnimatingNode:[NSNumber numberWithDouble:animationId] nodeTag:[NSNumber numberWithDouble:nodeTag] config:config endCallback:callBack]; }]; - RCTExecuteOnMainQueue(^{ - if (![self->_nodesManager isNodeManagedByFabric:[NSNumber numberWithDouble:nodeTag]]) { - return; - } + RCTExecuteOnMainQueue(^{ + if (![self->_nodesManager isNodeManagedByFabric:[NSNumber numberWithDouble:nodeTag]]) { + return; + } - RCTExecuteOnUIManagerQueue(^{ - self->_animIdIsManagedByFabric[[NSNumber numberWithDouble:animationId]] = @YES; - [self flushOperationQueues]; - }); - }); + RCTExecuteOnUIManagerQueue(^{ + self->_animIdIsManagedByFabric[[NSNumber numberWithDouble:animationId]] = @YES; + [self flushOperationQueues]; + }); + }); } RCT_EXPORT_METHOD(stopAnimation:(double)animationId) @@ -219,6 +216,12 @@ - (void)setBridge:(RCTBridge *)bridge }]; } +RCT_EXPORT_METHOD(getValue:(double)nodeTag saveCallback:(RCTResponseSenderBlock)saveCallback) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager getValue:[NSNumber numberWithDouble:nodeTag] saveCallback:saveCallback]; + }]; +} + #pragma mark -- Batch handling - (void)addOperationBlock:(AnimatedOperation)operation @@ -303,7 +306,6 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)uiManager operation(self->_nodesManager); } }]; - [uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { // TODO(macOS ISS#3536887) for (AnimatedOperation operation in operations) { operation(self->_nodesManager); @@ -335,11 +337,6 @@ - (void)eventDispatcherWillDispatchEvent:(id)event }); } -- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); -} - @end Class RCTNativeAnimatedModuleCls(void) { diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index 7038a40b6820c6..a6cc4363862c5d 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h @@ -22,6 +22,9 @@ - (BOOL)isNodeManagedByFabric:(nonnull NSNumber *)tag; +- (void)getValue:(nonnull NSNumber *)nodeTag + saveCallback:(nullable RCTResponseSenderBlock)saveCallback; + // graph - (void)createAnimatedNode:(nonnull NSNumber *)tag diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m index 776f20cac0eb9f..37ddea0fdbd8f7 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m @@ -242,6 +242,17 @@ - (void)extractAnimatedNodeOffset:(nonnull NSNumber *)nodeTag [valueNode extractOffset]; } +- (void)getValue:(NSNumber *)nodeTag saveCallback:(RCTResponseSenderBlock)saveCallback +{ + RCTAnimatedNode *node = _animationNodes[nodeTag]; + if (![node isKindOfClass:[RCTValueAnimatedNode class]]) { + RCTLogError(@"Not a value node."); + return; + } + RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)node;; + saveCallback(@[@(valueNode.value)]); +} + #pragma mark -- Drivers - (void)startAnimatingNode:(nonnull NSNumber *)animationId diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js index 9321f490d55d22..12864c9c9af7da 100644 --- a/Libraries/Network/RCTNetworking.android.js +++ b/Libraries/Network/RCTNetworking.android.js @@ -12,10 +12,9 @@ // Do not require the native RCTNetworking module directly! Use this wrapper module instead. // It will add the necessary requestId, so that you don't have to generate it yourself. -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); -const convertRequestBody = require('./convertRequestBody'); - +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import NativeNetworkingAndroid from './NativeNetworkingAndroid'; +import convertRequestBody from './convertRequestBody'; import type {RequestBody} from './convertRequestBody'; type Header = [string, string]; diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js index 73a33ab1d9fbe5..ec3706bf0ec253 100644 --- a/Libraries/Network/RCTNetworking.ios.js +++ b/Libraries/Network/RCTNetworking.ios.js @@ -10,12 +10,10 @@ 'use strict'; -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); - -const convertRequestBody = require('./convertRequestBody'); - +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import NativeNetworkingIOS from './NativeNetworkingIOS'; import type {NativeResponseType} from './XMLHttpRequest'; +import convertRequestBody from './convertRequestBody'; import type {RequestBody} from './convertRequestBody'; class RCTNetworking extends NativeEventEmitter { diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index c52b9be5f570f2..fa6262057ed5a9 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -10,10 +10,10 @@ 'use strict'; -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); import {Platform} from 'react-native'; // TODO(macOS GH#774) +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import NativePushNotificationManagerIOS from './NativePushNotificationManagerIOS'; -const invariant = require('invariant'); +import invariant from 'invariant'; const PushNotificationEmitter = new NativeEventEmitter( NativePushNotificationManagerIOS, diff --git a/Libraries/ReactNative/AppContainer.js b/Libraries/ReactNative/AppContainer.js index 2c2b0fc157fd94..68dd3169dd612e 100644 --- a/Libraries/ReactNative/AppContainer.js +++ b/Libraries/ReactNative/AppContainer.js @@ -10,13 +10,13 @@ 'use strict'; -const EmitterSubscription = require('../vendor/emitter/EmitterSubscription'); -const PropTypes = require('prop-types'); -const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter'); -const React = require('react'); +import View from '../Components/View/View'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import {RootTagContext, createRootTag} from './RootTag'; -const StyleSheet = require('../StyleSheet/StyleSheet'); -const View = require('../Components/View/View'); +import PropTypes from 'prop-types'; +import * as React from 'react'; type Context = {rootTag: number, ...}; @@ -43,7 +43,7 @@ class AppContainer extends React.Component { hasError: false, }; _mainRef: ?React.ElementRef; - _subscription: ?EmitterSubscription = null; + _subscription: ?EventSubscription = null; static getDerivedStateFromError: any = undefined; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.ios.js index cba39734037eeb..cac0b3babd7ee8 100644 --- a/Libraries/Settings/Settings.ios.js +++ b/Libraries/Settings/Settings.ios.js @@ -10,11 +10,9 @@ 'use strict'; -const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter'); - -const invariant = require('invariant'); - +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import NativeSettingsManager from './NativeSettingsManager'; +import invariant from 'invariant'; const subscriptions: Array<{ keys: Array, diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 8a33e8558f0aed..0fcce05b874464 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -69,6 +69,7 @@ const viewConfig = { onInlineViewLayout: true, dataDetectorType: true, tooltip: true, + android_hyphenationFrequency: true, }, directEventTypes: { topTextLayout: { diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.h b/Libraries/Text/TextInput/Multiline/RCTUITextView.h index 0025987e0197b1..144bd810f9e5a9 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.h +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.h @@ -45,6 +45,8 @@ NS_ASSUME_NONNULL_BEGIN // it's declared here only to conform to the interface. @property (nonatomic, assign) BOOL caretHidden; +@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; + #if TARGET_OS_OSX // [TODO(macOS GH#774) @property (nonatomic, assign) BOOL scrollEnabled; @property (nonatomic, strong, nullable) RCTUIColor *selectionColor; // TODO(OSS Candidate ISS#2710739) diff --git a/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h b/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index b593c4c863476b..1eedc681c12134 100644 --- a/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -55,6 +55,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; #endif // TODO(macOS GH#774) @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; // This protocol disallows direct access to `selectedTextRange` property because // unwise usage of it can break the `delegate` behavior. So, we always have to diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/Libraries/Text/TextInput/Singleline/RCTUITextField.h index 7fa470862adb1d..d02fef0bc4fe6f 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN @property (assign, getter=isEditable) BOOL editable; #endif // ]TODO(macOS GH#774) @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; #if TARGET_OS_OSX // [TODO(macOS GH#774) @property (nonatomic, copy, nullable) NSString *text; diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 191c5df185a7c5..fdde9ad3438139 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -57,6 +57,18 @@ export type TextProps = $ReadOnly<{| * See https://reactnative.dev/docs/text.html#allowfontscaling */ allowFontScaling?: ?boolean, + + /** + * Set hyphenation strategy on Android. + * + */ + android_hyphenationFrequency?: ?( + | 'normal' + | 'none' + | 'full' + | 'high' + | 'balanced' + ), children?: ?Node, /** diff --git a/Libraries/Utilities/NativeDevSplitBundleLoader.js b/Libraries/Utilities/NativeDevSplitBundleLoader.js new file mode 100644 index 00000000000000..505ae1c21081e5 --- /dev/null +++ b/Libraries/Utilities/NativeDevSplitBundleLoader.js @@ -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. + * + * @flow + * @format + */ + +'use strict'; + +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +loadBundle: (bundlePath: string) => Promise; +} + +export default (TurboModuleRegistry.get('DevSplitBundleLoader'): ?Spec); diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js index 0b051c48e4cd9b..65cfd09c52c335 100644 --- a/Libraries/WebSocket/WebSocket.js +++ b/Libraries/WebSocket/WebSocket.js @@ -10,18 +10,16 @@ 'use strict'; -const Blob = require('../Blob/Blob'); -const BlobManager = require('../Blob/BlobManager'); -const EventTarget = require('event-target-shim'); -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); -const WebSocketEvent = require('./WebSocketEvent'); - -const base64 = require('base64-js'); -const binaryToBase64 = require('../Utilities/binaryToBase64'); -const invariant = require('invariant'); - -import type EventSubscription from '../vendor/emitter/EventSubscription'; +import Blob from '../Blob/Blob'; +import BlobManager from '../Blob/BlobManager'; +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import binaryToBase64 from '../Utilities/binaryToBase64'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import NativeWebSocketModule from './NativeWebSocketModule'; +import WebSocketEvent from './WebSocketEvent'; +import base64 from 'base64-js'; +import EventTarget from 'event-target-shim'; +import invariant from 'invariant'; type ArrayBufferView = | Int8Array diff --git a/Libraries/WebSocket/WebSocketInterceptor.js b/Libraries/WebSocket/WebSocketInterceptor.js index 6c371c1028c485..7882c46f3fdff5 100644 --- a/Libraries/WebSocket/WebSocketInterceptor.js +++ b/Libraries/WebSocket/WebSocketInterceptor.js @@ -9,11 +9,9 @@ 'use strict'; -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); - +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import NativeWebSocketModule from './NativeWebSocketModule'; - -const base64 = require('base64-js'); +import base64 from 'base64-js'; const originalRCTWebSocketConnect = NativeWebSocketModule.connect; const originalRCTWebSocketSend = NativeWebSocketModule.send; diff --git a/Libraries/polyfills/console.js b/Libraries/polyfills/console.js index 0816a6fd940155..f82fb4d05bc113 100644 --- a/Libraries/polyfills/console.js +++ b/Libraries/polyfills/console.js @@ -580,16 +580,7 @@ if (global.nativeLoggingHook) { const reactNativeMethod = console[methodName]; if (originalConsole[methodName]) { console[methodName] = function() { - // TODO(T43930203): remove this special case once originalConsole.assert properly checks - // the condition - if (methodName === 'assert') { - if (!arguments[0] && originalConsole.hasOwnProperty('assert')) { - // TODO(macOS GH#774) - originalConsole.assert(...arguments); - } - } else { - originalConsole[methodName](...arguments); - } + originalConsole[methodName](...arguments); reactNativeMethod.apply(console, arguments); }; } diff --git a/Libraries/vendor/emitter/EventEmitter.js b/Libraries/vendor/emitter/EventEmitter.js index f3c18c91058d8e..3057ea4cb5642e 100644 --- a/Libraries/vendor/emitter/EventEmitter.js +++ b/Libraries/vendor/emitter/EventEmitter.js @@ -4,225 +4,17 @@ * 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 - * @noflow - * @typecheck */ 'use strict'; -const EmitterSubscription = require('./EmitterSubscription'); -const EventSubscriptionVendor = require('./EventSubscriptionVendor'); +// $FlowFixMe - EventEmitter Type Safety +const EventEmitter = require('./_EventEmitter'); -const invariant = require('invariant'); +export default EventEmitter; -const sparseFilterPredicate = () => true; - -/** - * @class EventEmitter - * @description - * An EventEmitter is responsible for managing a set of listeners and publishing - * events to them when it is told that such events happened. In addition to the - * data for the given event it also sends a event control object which allows - * the listeners/handlers to prevent the default behavior of the given event. - * - * The emitter is designed to be generic enough to support all the different - * contexts in which one might want to emit events. It is a simple multicast - * mechanism on top of which extra functionality can be composed. For example, a - * more advanced emitter may use an EventHolder and EventFactory. - */ -class EventEmitter { - _subscriber: EventSubscriptionVendor; - _currentSubscription: ?EmitterSubscription; - - /** - * @constructor - * - * @param {EventSubscriptionVendor} subscriber - Optional subscriber instance - * to use. If omitted, a new subscriber will be created for the emitter. - */ - constructor(subscriber: ?EventSubscriptionVendor) { - this._subscriber = subscriber || new EventSubscriptionVendor(); - } - - /** - * Adds a listener to be invoked when events of the specified type are - * emitted. An optional calling context may be provided. The data arguments - * emitted will be passed to the listener function. - * - * TODO: Annotate the listener arg's type. This is tricky because listeners - * can be invoked with varargs. - * - * @param {string} eventType - Name of the event to listen to - * @param {function} listener - Function to invoke when the specified event is - * emitted - * @param {*} context - Optional context object to use when invoking the - * listener - */ - addListener( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { - return (this._subscriber.addSubscription( - eventType, - new EmitterSubscription(this, this._subscriber, listener, context), - ): any); - } - - /** - * Similar to addListener, except that the listener is removed after it is - * invoked once. - * - * @param {string} eventType - Name of the event to listen to - * @param {function} listener - Function to invoke only once when the - * specified event is emitted - * @param {*} context - Optional context object to use when invoking the - * listener - */ - once( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { - return this.addListener(eventType, (...args) => { - this.removeCurrentListener(); - listener.apply(context, args); - }); - } - - /** - * Removes all of the registered listeners, including those registered as - * listener maps. - * - * @param {?string} eventType - Optional name of the event whose registered - * listeners to remove - */ - removeAllListeners(eventType: ?string) { - this._subscriber.removeAllSubscriptions(eventType); - } - - /** - * Provides an API that can be called during an eventing cycle to remove the - * last listener that was invoked. This allows a developer to provide an event - * object that can remove the listener (or listener map) during the - * invocation. - * - * If it is called when not inside of an emitting cycle it will throw. - * - * @throws {Error} When called not during an eventing cycle - * - * @example - * var subscription = emitter.addListenerMap({ - * someEvent: function(data, event) { - * console.log(data); - * emitter.removeCurrentListener(); - * } - * }); - * - * emitter.emit('someEvent', 'abc'); // logs 'abc' - * emitter.emit('someEvent', 'def'); // does not log anything - */ - removeCurrentListener() { - invariant( - !!this._currentSubscription, - 'Not in an emitting cycle; there is no current subscription', - ); - this.removeSubscription(this._currentSubscription); - } - - /** - * Removes a specific subscription. Called by the `remove()` method of the - * subscription itself to ensure any necessary cleanup is performed. - */ - removeSubscription(subscription: EmitterSubscription) { - invariant( - subscription.emitter === this, - 'Subscription does not belong to this emitter.', - ); - this._subscriber.removeSubscription(subscription); - } - - /** - * Returns an array of listeners that are currently registered for the given - * event. - * - * @param {string} eventType - Name of the event to query - * @returns {array} - */ - listeners(eventType: string): [EmitterSubscription] { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - return subscriptions - ? subscriptions - // We filter out missing entries because the array is sparse. - // "callbackfn is called only for elements of the array which actually - // exist; it is not called for missing elements of the array." - // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter - .filter(sparseFilterPredicate) - .map(subscription => subscription.listener) - : []; - } - - /** - * Emits an event of the given type with the given data. All handlers of that - * particular type will be notified. - * - * @param {string} eventType - Name of the event to emit - * @param {...*} Arbitrary arguments to be passed to each registered listener - * - * @example - * emitter.addListener('someEvent', function(message) { - * console.log(message); - * }); - * - * emitter.emit('someEvent', 'abc'); // logs 'abc' - */ - emit(eventType: string) { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - if (subscriptions) { - for (let i = 0, l = subscriptions.length; i < l; i++) { - const subscription = subscriptions[i]; - - // The subscription may have been removed during this event loop. - if (subscription && subscription.listener) { - this._currentSubscription = subscription; - subscription.listener.apply( - subscription.context, - Array.prototype.slice.call(arguments, 1), - ); - } - } - this._currentSubscription = null; - } - } - - /** - * Removes the given listener for event of specific type. - * - * @param {string} eventType - Name of the event to emit - * @param {function} listener - Function to invoke when the specified event is - * emitted - * - * @example - * emitter.removeListener('someEvent', function(message) { - * console.log(message); - * }); // removes the listener if already registered - * - */ - removeListener(eventType: String, listener) { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - if (subscriptions) { - for (let i = 0, l = subscriptions.length; i < l; i++) { - const subscription = subscriptions[i]; - - // The subscription may have been removed during this event loop. - // its listener matches the listener in method parameters - if (subscription && subscription.listener === listener) { - subscription.remove(); - } - } - } - } +export interface EventSubscription { + remove(): void; } - -module.exports = EventEmitter; diff --git a/Libraries/vendor/emitter/EmitterSubscription.js b/Libraries/vendor/emitter/_EmitterSubscription.js similarity index 92% rename from Libraries/vendor/emitter/EmitterSubscription.js rename to Libraries/vendor/emitter/_EmitterSubscription.js index cc3138305b3490..5a767c823dd9f6 100644 --- a/Libraries/vendor/emitter/EmitterSubscription.js +++ b/Libraries/vendor/emitter/_EmitterSubscription.js @@ -10,10 +10,9 @@ 'use strict'; -const EventSubscription = require('./EventSubscription'); - import type EventEmitter from './EventEmitter'; -import type EventSubscriptionVendor from './EventSubscriptionVendor'; +import EventSubscription from './_EventSubscription'; +import type EventSubscriptionVendor from './_EventSubscriptionVendor'; /** * EmitterSubscription represents a subscription with listener and context data. diff --git a/Libraries/vendor/emitter/_EventEmitter.js b/Libraries/vendor/emitter/_EventEmitter.js new file mode 100644 index 00000000000000..8ec88d27d02246 --- /dev/null +++ b/Libraries/vendor/emitter/_EventEmitter.js @@ -0,0 +1,175 @@ +/** + * 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 + * @noflow + * @typecheck + */ + +'use strict'; + +const invariant = require('invariant'); + +import EmitterSubscription from './_EmitterSubscription'; +import EventSubscriptionVendor from './_EventSubscriptionVendor'; + +const sparseFilterPredicate = () => true; + +/** + * @class EventEmitter + * @description + * An EventEmitter is responsible for managing a set of listeners and publishing + * events to them when it is told that such events happened. In addition to the + * data for the given event it also sends a event control object which allows + * the listeners/handlers to prevent the default behavior of the given event. + * + * The emitter is designed to be generic enough to support all the different + * contexts in which one might want to emit events. It is a simple multicast + * mechanism on top of which extra functionality can be composed. For example, a + * more advanced emitter may use an EventHolder and EventFactory. + */ +class EventEmitter { + _subscriber: EventSubscriptionVendor; + + /** + * @constructor + * + * @param {EventSubscriptionVendor} subscriber - Optional subscriber instance + * to use. If omitted, a new subscriber will be created for the emitter. + */ + constructor(subscriber: ?EventSubscriptionVendor) { + this._subscriber = subscriber || new EventSubscriptionVendor(); + } + + /** + * Adds a listener to be invoked when events of the specified type are + * emitted. An optional calling context may be provided. The data arguments + * emitted will be passed to the listener function. + * + * TODO: Annotate the listener arg's type. This is tricky because listeners + * can be invoked with varargs. + * + * @param {string} eventType - Name of the event to listen to + * @param {function} listener - Function to invoke when the specified event is + * emitted + * @param {*} context - Optional context object to use when invoking the + * listener + */ + addListener( + eventType: string, + listener: Function, + context: ?Object, + ): EmitterSubscription { + return (this._subscriber.addSubscription( + eventType, + new EmitterSubscription(this, this._subscriber, listener, context), + ): any); + } + + /** + * Removes all of the registered listeners, including those registered as + * listener maps. + * + * @param {?string} eventType - Optional name of the event whose registered + * listeners to remove + */ + removeAllListeners(eventType: ?string) { + this._subscriber.removeAllSubscriptions(eventType); + } + + /** + * Removes a specific subscription. Called by the `remove()` method of the + * subscription itself to ensure any necessary cleanup is performed. + */ + removeSubscription(subscription: EmitterSubscription) { + invariant( + subscription.emitter === this, + 'Subscription does not belong to this emitter.', + ); + this._subscriber.removeSubscription(subscription); + } + + /** + * Returns an array of listeners that are currently registered for the given + * event. + * + * @param {string} eventType - Name of the event to query + * @returns {array} + */ + listeners(eventType: string): [EmitterSubscription] { + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + return subscriptions + ? subscriptions + // We filter out missing entries because the array is sparse. + // "callbackfn is called only for elements of the array which actually + // exist; it is not called for missing elements of the array." + // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter + .filter(sparseFilterPredicate) + .map(subscription => subscription.listener) + : []; + } + + /** + * Emits an event of the given type with the given data. All handlers of that + * particular type will be notified. + * + * @param {string} eventType - Name of the event to emit + * @param {...*} Arbitrary arguments to be passed to each registered listener + * + * @example + * emitter.addListener('someEvent', function(message) { + * console.log(message); + * }); + * + * emitter.emit('someEvent', 'abc'); // logs 'abc' + */ + emit(eventType: string) { + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + if (subscription && subscription.listener) { + subscription.listener.apply( + subscription.context, + Array.prototype.slice.call(arguments, 1), + ); + } + } + } + } + + /** + * Removes the given listener for event of specific type. + * + * @param {string} eventType - Name of the event to emit + * @param {function} listener - Function to invoke when the specified event is + * emitted + * + * @example + * emitter.removeListener('someEvent', function(message) { + * console.log(message); + * }); // removes the listener if already registered + * + */ + removeListener(eventType: String, listener) { + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + // its listener matches the listener in method parameters + if (subscription && subscription.listener === listener) { + subscription.remove(); + } + } + } + } +} + +module.exports = EventEmitter; diff --git a/Libraries/vendor/emitter/EventSubscription.js b/Libraries/vendor/emitter/_EventSubscription.js similarity index 92% rename from Libraries/vendor/emitter/EventSubscription.js rename to Libraries/vendor/emitter/_EventSubscription.js index 8f767e060c405b..54ec8f2f2ddc41 100644 --- a/Libraries/vendor/emitter/EventSubscription.js +++ b/Libraries/vendor/emitter/_EventSubscription.js @@ -10,7 +10,7 @@ 'use strict'; -import type EventSubscriptionVendor from './EventSubscriptionVendor'; +import type EventSubscriptionVendor from './_EventSubscriptionVendor'; /** * EventSubscription represents a subscription to a particular event. It can diff --git a/Libraries/vendor/emitter/EventSubscriptionVendor.js b/Libraries/vendor/emitter/_EventSubscriptionVendor.js similarity index 97% rename from Libraries/vendor/emitter/EventSubscriptionVendor.js rename to Libraries/vendor/emitter/_EventSubscriptionVendor.js index db44c6f3ec7adb..5ea0aa821e0360 100644 --- a/Libraries/vendor/emitter/EventSubscriptionVendor.js +++ b/Libraries/vendor/emitter/_EventSubscriptionVendor.js @@ -12,7 +12,7 @@ const invariant = require('invariant'); -import type EventSubscription from './EventSubscription'; +import type EventSubscription from './_EventSubscription'; /** * EventSubscriptionVendor stores a set of EventSubscriptions that are diff --git a/RNTester/Podfile.lock b/RNTester/Podfile.lock index c412112d0c12dd..2127bed0cb8f74 100644 --- a/RNTester/Podfile.lock +++ b/RNTester/Podfile.lock @@ -524,8 +524,8 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f DoubleConversion: 2b45d0f8e156a5b02354c8a4062de64d41ccb4e0 - FBLazyVector: 473624d6d8a95dddea531181b5fe074296b849d4 - FBReactNativeSpec: aa5face1bbc5054a321b7a71a7a9f63fb9b9ed7f + FBLazyVector: 25db8fc930c080720dd9f1d1a26d539ce9515858 + FBReactNativeSpec: 357b56d66b8d511a9f812b4c630bd4966a433371 Flipper: be611d4b742d8c87fbae2ca5f44603a02539e365 Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41 Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3 @@ -536,34 +536,34 @@ SPEC CHECKSUMS: glog: 789873d01e4b200777d0a09bc23d548446758699 OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355 RCT-Folly: 55d0039b24e192081ec0b2257f7bd9f42e382fb7 - RCTRequired: c2b55a638af545470baa7e1953525cbdd249f071 - RCTTypeSafety: 6f8075941c3b0a18fe2dc4b343aea938d0bad5e2 - React: 8ebf7e7f105fbcea86199badd55a6bb67a5b8afa - React-ART: a75138364a2681f5189fe24af2a645a4f9069353 - React-callinvoker: 497cecd63f6e72c269c808e8529b36e31816175f - React-Core: 21186234ce1a0dfdd68bb885f36953f03f6ad595 - React-CoreModules: 20deecbc9727971b94d8b7e36addba9ee5b3df7b - React-cxxreact: 78dadb6f1c308aac81c14cc5fa55eee1bf4e509a - React-jsi: 04aa49b53eb6627f22a208b7a453b7c695c23f13 - React-jsiexecutor: 68b861e21c43a19980374229f98003860e389eb7 - React-jsinspector: b75196b3c0ed12a6608798366cd627fd93d8aef5 - React-perflogger: 1e95f386e69d2531429be35018b9e3a7e7319f0d - React-RCTActionSheet: 2f5024fba47f6bbae7cd7035bcfe3a2a9ceab828 - React-RCTAnimation: c5daba3acc6601ed49befc73314f3a4a3dffac7e - React-RCTBlob: e88442441bf43cb1733faf5ee0369cb77ef9a9c5 - React-RCTImage: 0d6613af0b99b24bb2f10d951c36d682b15ba90f - React-RCTLinking: b47cd2eda4ad15424f1ad02f4eb9ff027f212f6c - React-RCTNetwork: 1bdb44306b36686fcdf51fb353cd5bea1910c9dc - React-RCTPushNotification: 0d9ef4d1acc93d00828de47763834ef9f7e825c7 - React-RCTSettings: 606a93f1f422bb1e33e8178715edcbeb00ef0e29 - React-RCTTest: 61a59b3db0d728bd3f2e4c477aa39f5308f58d3d - React-RCTText: 377bcb96f1626badf03d4e7cb08efc1e49ca8654 - React-RCTVibration: a68e8de67fd7d6fe24f027965291dff20a7e1ecf - React-runtimeexecutor: b5c501cbb838fb190272567117469ba89ca4095c + RCTRequired: ce43f9f9167af58d0e1eecf5605634ab016faa81 + RCTTypeSafety: c9b0498fb46d5d1d756e0ce90dd5e60f1b9d5cdd + React: 9a7fd7a316d91eaabfb9b6fbe3456d873e1d92b2 + React-ART: c2a752357958d214b16806c18fd812e3eda5165e + React-callinvoker: 66b62bab1170e0359d46a8af0cb61a9a1a8d18ad + React-Core: 312e62d070297a0cf4c693eb9400145149b1fc70 + React-CoreModules: 68d668bfd30a500e49c83c68e2fc75524f2437f8 + React-cxxreact: 6e2e98c6b87bc02437806a9498c582a03a10c9f5 + React-jsi: 827382a7da620a02cb890accfcf84de279f272a6 + React-jsiexecutor: dbc72f3190c3f3bf7d2ac548ce17e12d357e122b + React-jsinspector: 320d5cb76361b00a6a87a8b5a67280c200712604 + React-perflogger: 0cac631583b97716913aafd5d5c07ac133bab771 + React-RCTActionSheet: c15c33157199c398d405a5cbc2715d9dba17bbf8 + React-RCTAnimation: bffa5450d36cae700b6c97bfdd617986810b6f33 + React-RCTBlob: 169d4c1f011c5fe60f43cbde36f25391c5bd62a7 + React-RCTImage: 3a129461152c25115f67a3052e086b8277c36938 + React-RCTLinking: 2f4d467f6449bfa6f35ef108ea4eada0c110e020 + React-RCTNetwork: 66bdde9026a6fe29b06b6027b847de96153653a7 + React-RCTPushNotification: c2b605a89e385917bb764017d0e8e5c6d4f4a206 + React-RCTSettings: 8d536a656c3d2f34ea155d24244488a0c39ec3b2 + React-RCTTest: 991feac2784e473e8b466091259cefe6d33bc72f + React-RCTText: 6be029a3b38a2b98f875b84a4bd182eebd048218 + React-RCTVibration: 655e32ab1393718662b8a6f75eef9f3d2ceedf96 + React-runtimeexecutor: b529b2ffeac2f15b775a37226471a2523a3f7844 React-TurboModuleCxx-RNW: 18bb71af41fe34c8b12a56bef60aae7ee32b0817 - React-TurboModuleCxx-WinRTPort: bbf5afaa8aa712919a56b270bee8a83a74d7c6f5 - ReactCommon: d4fccf43589bf37d7e3b94c32ce3722a0889d98c - Yoga: 4fec242f984a794825ad52bc24af3c0c91da2d7d + React-TurboModuleCxx-WinRTPort: 29b59397f335c90defd1c74eaf0982ccd13c52be + ReactCommon: 989cfe7b74513b6bed09cf75e733bc631ec963c7 + Yoga: ba2f886e24a03fa7dad871b727b5de2a7205dd8a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: 7d43a928a9b9ad27329da110adbfadd923a39ba8 diff --git a/RNTester/RNTesterPods.xcodeproj/project.pbxproj b/RNTester/RNTesterPods.xcodeproj/project.pbxproj index 563893e2e592da..8191ffef0023b3 100644 --- a/RNTester/RNTesterPods.xcodeproj/project.pbxproj +++ b/RNTester/RNTesterPods.xcodeproj/project.pbxproj @@ -1056,7 +1056,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\nexport PROJECT_ROOT=$SRCROOT/..\nexport SOURCEMAP_FILE=sourcemap.macOS.map\n# export FORCE_BUNDLING=true\n$SRCROOT/../scripts/react-native-xcode.sh RNTester/js/RNTesterApp.macos.js\n"; + shellScript = "export NODE_BINARY=node\nexport PROJECT_ROOT=$SRCROOT/..\nexport SOURCEMAP_FILE=sourcemap.macOS.map\n# export FORCE_BUNDLING=true\n\"$SRCROOT/../scripts/react-native-xcode.sh\" RNTester/js/RNTesterApp.macos.js\n"; }; 4BDC0C5271A785332898B818 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; @@ -1121,7 +1121,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\nexport PROJECT_ROOT=$SRCROOT/..\nexport SOURCEMAP_FILE=../sourcemap.ios.map\n# export FORCE_BUNDLING=true\nPROJECT_ROOT=$SRCROOT/.. $SRCROOT/../scripts/react-native-xcode.sh RNTester/js/RNTesterApp.ios.js\n"; + shellScript = "export NODE_BINARY=node\nexport PROJECT_ROOT=$SRCROOT/..\nexport SOURCEMAP_FILE=../sourcemap.ios.map\n# export FORCE_BUNDLING=true\n\"$SRCROOT/../scripts/react-native-xcode.sh\" RNTester/js/RNTesterApp.ios.js\n"; }; 79A2B74F37E4F8AF8B3D0A7A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/RNTester/RNTesterUnitTests/RCTBundleURLProviderTests.m b/RNTester/RNTesterUnitTests/RCTBundleURLProviderTests.m index e54e9435109d94..df71469ac09ccd 100644 --- a/RNTester/RNTesterUnitTests/RCTBundleURLProviderTests.m +++ b/RNTester/RNTesterUnitTests/RCTBundleURLProviderTests.m @@ -24,7 +24,7 @@ URLWithString: [NSString stringWithFormat: - @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runMdoule=true&app=com.apple.dt.xctest.tool", + @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName]]; // TODO(macOS GH#774) } @@ -35,7 +35,7 @@ URLWithString: [NSString stringWithFormat: - @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runMdoule=true&app=com.apple.dt.xctest.tool", + @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName]]; // TODO(macOS GH#774) } diff --git a/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m b/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m index 827f882077a9ab..c24b461e7626c2 100644 --- a/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m +++ b/RNTester/RNTesterUnitTests/RCTMultipartStreamReaderTests.m @@ -15,84 +15,102 @@ @interface RCTMultipartStreamReaderTests : XCTestCase @implementation RCTMultipartStreamReaderTests -- (void)testSimpleCase { +- (void)testSimpleCase +{ NSString *response = - @"preable, should be ignored\r\n" - @"--sample_boundary\r\n" - @"Content-Type: application/json; charset=utf-8\r\n" - @"Content-Length: 2\r\n\r\n" - @"{}\r\n" - @"--sample_boundary--\r\n" - @"epilogue, should be ignored"; + @"preamble, should be ignored\r\n" + @"--sample_boundary\r\n" + @"Content-Type: application/json; charset=utf-8\r\n" + @"Content-Length: 2\r\n\r\n" + @"{}\r\n" + @"--sample_boundary--\r\n" + @"epilogue, should be ignored"; NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; - RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; + RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream + boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllPartsWithCompletionCallback:^(NSDictionary *headers, NSData *content, BOOL done) { - XCTAssertTrue(done); - XCTAssertEqualObjects(headers[@"Content-Type"], @"application/json; charset=utf-8"); - XCTAssertEqualObjects([[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding], @"{}"); - count++; - } progressCallback: nil]; + BOOL success = [reader + readAllPartsWithCompletionCallback:^(NSDictionary *headers, NSData *content, BOOL done) { + XCTAssertTrue(done); + XCTAssertEqualObjects(headers[@"Content-Type"], @"application/json; charset=utf-8"); + XCTAssertEqualObjects([[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding], @"{}"); + count++; + } + progressCallback:nil]; XCTAssertTrue(success); XCTAssertEqual(count, 1); } -- (void)testMultipleParts { +- (void)testMultipleParts +{ NSString *response = - @"preable, should be ignored\r\n" - @"--sample_boundary\r\n" - @"1\r\n" - @"--sample_boundary\r\n" - @"2\r\n" - @"--sample_boundary\r\n" - @"3\r\n" - @"--sample_boundary--\r\n" - @"epilogue, should be ignored"; + @"preamble, should be ignored\r\n" + @"--sample_boundary\r\n" + @"1\r\n" + @"--sample_boundary\r\n" + @"2\r\n" + @"--sample_boundary\r\n" + @"3\r\n" + @"--sample_boundary--\r\n" + @"epilogue, should be ignored"; NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; - RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; + RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream + boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, NSData *content, BOOL done) { - count++; - XCTAssertEqual(done, count == 3); - NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count]; - NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]; - XCTAssertEqualObjects(actualBody, expectedBody); - } progressCallback:nil]; + BOOL success = [reader + readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, NSData *content, BOOL done) { + count++; + XCTAssertEqual(done, count == 3); + NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count]; + NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(actualBody, expectedBody); + } + progressCallback:nil]; XCTAssertTrue(success); XCTAssertEqual(count, 3); } -- (void)testNoDelimiter { +- (void)testNoDelimiter +{ NSString *response = @"Yolo"; NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; - RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; + RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream + boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { - count++; - } progressCallback:nil]; + BOOL success = [reader + readAllPartsWithCompletionCallback:^( + __unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { + count++; + } + progressCallback:nil]; XCTAssertFalse(success); XCTAssertEqual(count, 0); } -- (void)testNoCloseDelimiter { +- (void)testNoCloseDelimiter +{ NSString *response = - @"preable, should be ignored\r\n" - @"--sample_boundary\r\n" - @"Content-Type: application/json; charset=utf-8\r\n" - @"Content-Length: 2\r\n\r\n" - @"{}\r\n" - @"--sample_boundary\r\n" - @"incomplete message..."; + @"preamble, should be ignored\r\n" + @"--sample_boundary\r\n" + @"Content-Type: application/json; charset=utf-8\r\n" + @"Content-Length: 2\r\n\r\n" + @"{}\r\n" + @"--sample_boundary\r\n" + @"incomplete message..."; NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]]; - RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:@"sample_boundary"]; + RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream + boundary:@"sample_boundary"]; __block NSInteger count = 0; - BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { - count++; - } progressCallback:nil]; + BOOL success = [reader + readAllPartsWithCompletionCallback:^( + __unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) { + count++; + } + progressCallback:nil]; XCTAssertFalse(success); XCTAssertEqual(count, 1); } diff --git a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m index 804f5e5976bcda..9ec98adab37c9c 100644 --- a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m +++ b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m @@ -866,6 +866,23 @@ - (void)testNativeAnimatedEventDoNotUpdate [_uiManager verify]; } +- (void) testGetValue +{ + __block NSInteger saveValueCallbackCalls = 0; + NSNumber *nodeTag = @100; + [_nodesManager createAnimatedNode:nodeTag + config:@{@"type": @"value", @"value": @1, @"offset": @0}]; + RCTResponseSenderBlock saveValueCallback = ^(NSArray *response) { + saveValueCallbackCalls++; + XCTAssertEqualObjects(response, @[@1]); + }; + + XCTAssertEqual(saveValueCallbackCalls, 0); + + [_nodesManager getValue:nodeTag saveCallback:saveValueCallback]; + XCTAssertEqual(saveValueCallbackCalls, 1); +} + /** * Creates a following graph of nodes: * Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag) diff --git a/RNTester/js/components/RNTesterExampleList.js b/RNTester/js/components/RNTesterExampleList.js index b9d7c20bb688aa..9fef66c83d6c90 100644 --- a/RNTester/js/components/RNTesterExampleList.js +++ b/RNTester/js/components/RNTesterExampleList.js @@ -117,8 +117,7 @@ const renderSectionHeader = ({section}) => ( class RNTesterExampleList extends React.Component { render(): React.Node { const filter = ({example, filterRegex}) => - filterRegex.test(example.module.title) && - (!Platform.isTV || example.supportsTVOS); + filterRegex.test(example.module.title) && !Platform.isTV; const sections = [ { diff --git a/RNTester/js/examples/Animated/AnimatedExample.js b/RNTester/js/examples/Animated/AnimatedExample.js index 7ef971ca5ee704..76c546dc9e06b1 100644 --- a/RNTester/js/examples/Animated/AnimatedExample.js +++ b/RNTester/js/examples/Animated/AnimatedExample.js @@ -335,6 +335,115 @@ exports.examples = [ ); }, }, + { + title: 'Moving box example', + description: ('Click arrow buttons to move the box.' + + 'Then hide the box and reveal it again.' + + 'After that the box position will reset to initial position.': string), + render: (): React.Node => { + const containerWidth = 200; + const boxSize = 50; + + const movingBoxStyles = StyleSheet.create({ + container: { + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + backgroundColor: '#fff', + padding: 30, + }, + boxContainer: { + backgroundColor: '#d3d3d3', + height: boxSize, + width: containerWidth, + }, + box: { + width: boxSize, + height: boxSize, + margin: 0, + }, + buttonsContainer: { + marginTop: 20, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + width: containerWidth, + }, + }); + type Props = $ReadOnly<{||}>; + type State = {|boxVisible: boolean|}; + + class MovingBoxExample extends React.Component { + x: Animated.Value; + constructor(props) { + super(props); + this.x = new Animated.Value(0); + this.state = { + boxVisible: true, + }; + } + + render() { + const {boxVisible} = this.state; + const toggleText = boxVisible ? 'Hide' : 'Show'; + return ( + + {this.renderBox()} + + this.moveTo(0)}> + {'<-'} + + + {toggleText} + + this.moveTo(containerWidth - boxSize)}> + {'->'} + + + + ); + } + + renderBox = () => { + if (this.state.boxVisible) { + const horizontalLocation = {transform: [{translateX: this.x}]}; + return ( + + + + ); + } else { + return ( + + The box view is not being rendered + + ); + } + }; + + moveTo = x => { + Animated.timing(this.x, { + toValue: x, + duration: 1000, + useNativeDriver: true, + }).start(); + }; + + toggleVisibility = () => { + const {boxVisible} = this.state; + this.setState({boxVisible: !boxVisible}); + }; + } + return ; + }, + }, { title: 'Continuous Interactions', description: ('Gesture events, chaining, 2D ' + diff --git a/RNTester/js/examples/Image/ImageExample.js b/RNTester/js/examples/Image/ImageExample.js index 7430390cb960ac..11ac6c16e8389a 100644 --- a/RNTester/js/examples/Image/ImageExample.js +++ b/RNTester/js/examples/Image/ImageExample.js @@ -37,7 +37,7 @@ type ImageSource = $ReadOnly<{| type NetworkImageCallbackExampleState = {| events: Array, startLoadPrefetched: boolean, - mountTime: Date, + mountTime: number, |}; type NetworkImageCallbackExampleProps = $ReadOnly<{| @@ -52,11 +52,11 @@ class NetworkImageCallbackExample extends React.Component< state = { events: [], startLoadPrefetched: false, - mountTime: new Date(), + mountTime: Date.now(), }; UNSAFE_componentWillMount() { - this.setState({mountTime: new Date()}); + this.setState({mountTime: Date.now()}); } _loadEventFired = (event: string) => { @@ -73,43 +73,50 @@ class NetworkImageCallbackExample extends React.Component< source={this.props.source} style={[styles.base, {overflow: 'visible'}]} onLoadStart={() => - this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`) + this._loadEventFired(`✔ onLoadStart (+${Date.now() - mountTime}ms)`) } + onProgress={event => { + const {loaded, total} = event.nativeEvent; + const percent = Math.round((loaded / total) * 100); + this._loadEventFired( + `✔ onProgress ${percent}% (+${Date.now() - mountTime}ms)`, + ); + }} onLoad={event => { if (event.nativeEvent.source) { - const url = event.nativeEvent.source.url; + const url = event.nativeEvent.source.uri; this._loadEventFired( - `✔ onLoad (+${new Date() - mountTime}ms) for URL ${url}`, + `✔ onLoad (+${Date.now() - mountTime}ms) for URL ${url}`, ); } else { - this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`); + this._loadEventFired(`✔ onLoad (+${Date.now() - mountTime}ms)`); } }} onLoadEnd={() => { - this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`); + this._loadEventFired(`✔ onLoadEnd (+${Date.now() - mountTime}ms)`); this.setState({startLoadPrefetched: true}, () => { prefetchTask.then( () => { this._loadEventFired( - `✔ Prefetch OK (+${new Date() - mountTime}ms)`, + `✔ Prefetch OK (+${Date.now() - mountTime}ms)`, ); Image.queryCache([IMAGE_PREFETCH_URL]).then(map => { const result = map[IMAGE_PREFETCH_URL]; if (result) { this._loadEventFired( - `✔ queryCache "${result}" (+${new Date() - + `✔ queryCache "${result}" (+${Date.now() - mountTime}ms)`, ); } else { this._loadEventFired( - `✘ queryCache (+${new Date() - mountTime}ms)`, + `✘ queryCache (+${Date.now() - mountTime}ms)`, ); } }); }, error => { this._loadEventFired( - `✘ Prefetch failed (+${new Date() - mountTime}ms)`, + `✘ Prefetch failed (+${Date.now() - mountTime}ms)`, ); }, ); @@ -122,26 +129,26 @@ class NetworkImageCallbackExample extends React.Component< style={[styles.base, {overflow: 'visible'}]} onLoadStart={() => this._loadEventFired( - `✔ (prefetched) onLoadStart (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoadStart (+${Date.now() - mountTime}ms)`, ) } onLoad={event => { // Currently this image source feature is only available on iOS. if (event.nativeEvent.source) { - const url = event.nativeEvent.source.url; + const url = event.nativeEvent.source.uri; this._loadEventFired( - `✔ (prefetched) onLoad (+${new Date() - + `✔ (prefetched) onLoad (+${Date.now() - mountTime}ms) for URL ${url}`, ); } else { this._loadEventFired( - `✔ (prefetched) onLoad (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoad (+${Date.now() - mountTime}ms)`, ); } }} onLoadEnd={() => this._loadEventFired( - `✔ (prefetched) onLoadEnd (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoadEnd (+${Date.now() - mountTime}ms)`, ) } /> @@ -153,9 +160,9 @@ class NetworkImageCallbackExample extends React.Component< } type NetworkImageExampleState = {| - error: boolean, + error: ?string, loading: boolean, - progress: number, + progress: $ReadOnlyArray, |}; type NetworkImageExampleProps = $ReadOnly<{| @@ -167,38 +174,38 @@ class NetworkImageExample extends React.Component< NetworkImageExampleState, > { state = { - error: false, + error: null, loading: false, - progress: 0, + progress: [], }; render() { - const loader = this.state.loading ? ( - - {this.state.progress}% - - - ) : null; - return this.state.error ? ( + return this.state.error != null ? ( {this.state.error} ) : ( - this.setState({loading: true})} - onError={e => - this.setState({error: e.nativeEvent.error, loading: false}) - } - onProgress={e => - this.setState({ - progress: Math.round( - (100 * e.nativeEvent.loaded) / e.nativeEvent.total, - ), - }) - } - onLoad={() => this.setState({loading: false, error: false})}> - {loader} - + <> + this.setState({loading: true})} + onError={e => + this.setState({error: e.nativeEvent.error, loading: false}) + } + onProgress={e => { + const {loaded, total} = e.nativeEvent; + this.setState(prevState => ({ + progress: [ + ...prevState.progress, + Math.round((100 * loaded) / total), + ], + })); + }} + onLoad={() => this.setState({loading: false, error: null})} + /> + + {this.state.progress.map(progress => `${progress}%`).join('\n')} + + ); } } @@ -347,12 +354,6 @@ const styles = StyleSheet.create({ width: 38, height: 38, }, - progress: { - flex: 1, - alignItems: 'center', - flexDirection: 'row', - width: 100, - }, leftMargin: { marginLeft: 10, }, @@ -466,7 +467,6 @@ exports.examples = [ /> ); }, - platform: 'ios', }, { title: 'Image Download Progress', @@ -479,7 +479,6 @@ exports.examples = [ /> ); }, - platform: 'ios', }, { title: 'defaultSource', diff --git a/RNTester/js/examples/OrientationChange/OrientationChangeExample.js b/RNTester/js/examples/OrientationChange/OrientationChangeExample.js index 98e3de6348b264..d7ddd4f4274329 100644 --- a/RNTester/js/examples/OrientationChange/OrientationChangeExample.js +++ b/RNTester/js/examples/OrientationChange/OrientationChangeExample.js @@ -14,10 +14,10 @@ const React = require('react'); const {DeviceEventEmitter, Text, View} = require('react-native'); -import type EmitterSubscription from '../../../../Libraries/vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from '../../../../Libraries/vendor/emitter/EventEmitter'; class OrientationChangeExample extends React.Component<{...}, $FlowFixMeState> { - _orientationSubscription: EmitterSubscription; + _orientationSubscription: EventSubscription; state = { currentOrientation: '', diff --git a/RNTester/js/examples/PlatformColor/PlatformColorExample.js b/RNTester/js/examples/PlatformColor/PlatformColorExample.js index 5a1358a22ed8c4..c211f16f482646 100644 --- a/RNTester/js/examples/PlatformColor/PlatformColorExample.js +++ b/RNTester/js/examples/PlatformColor/PlatformColorExample.js @@ -210,7 +210,10 @@ function FallbackColorsExample() { color: PlatformColor('bogus', '@color/catalyst_redbox_background'), }; } else { - throw 'Unexpected Platform.OS: ' + Platform.OS; + color = { + label: 'Unexpected Platform.OS: ' + Platform.OS, + color: 'red', + }; } return ( @@ -312,6 +315,7 @@ function VariantColorsExample() { ios: "DynamicColorIOS({light: 'red', dark: 'blue'})", android: "PlatformColor('?attr/colorAccent')", macos: "DynamicColorMacOS({light: 'red', dark: 'blue'})", + default: 'Unexpected Platform.OS: ' + Platform.OS, }) // ]TODO(OSS Candidate ISS#2710739) } @@ -326,7 +330,9 @@ function VariantColorsExample() { Platform.OS === 'macos' ? DynamicColorMacOS({light: 'red', dark: 'blue'}) : // ]TODO(macOS GH#774) - PlatformColor('?attr/colorAccent'), + Platform.OS === 'android' + ? PlatformColor('?attr/colorAccent') + : 'red', }} /> diff --git a/RNTester/js/examples/Text/TextExample.android.js b/RNTester/js/examples/Text/TextExample.android.js index f9631821f9900f..cec4b348d19fe4 100644 --- a/RNTester/js/examples/Text/TextExample.android.js +++ b/RNTester/js/examples/Text/TextExample.android.js @@ -146,6 +146,7 @@ class AdjustingFontSize extends React.Component< {'Multiline text component shrinking is supported, watch as this reeeeaaaally loooooong teeeeeeext grooooows and then shriiiinks as you add text to me! ioahsdia soady auydoa aoisyd aosdy ' + ' ' + @@ -207,6 +208,28 @@ class TextExample extends React.Component<{...}> { going to the next line. + + + Normal: + WillHaveAnHyphenWhenBreakingForNewLine + + + None: + WillNotHaveAnHyphenWhenBreakingForNewLine + + + Full: + WillHaveAnHyphenWhenBreakingForNewLine + + + High: + WillHaveAnHyphenWhenBreakingForNewLine + + + Balanced: + WillHaveAnHyphenWhenBreakingForNewLine + + This text is indented by 10px padding on all sides. diff --git a/RNTester/js/types/RNTesterTypes.js b/RNTester/js/types/RNTesterTypes.js index f4db40d65d6dd7..99d86c93b62f27 100644 --- a/RNTester/js/types/RNTesterTypes.js +++ b/RNTester/js/types/RNTesterTypes.js @@ -43,7 +43,6 @@ export type RNTesterExampleModule = $ReadOnly<{| export type RNTesterExample = $ReadOnly<{| key: string, module: RNTesterExampleModule, - supportsTVOS?: boolean, skipTest?: { ios?: string, macos?: string, diff --git a/RNTester/js/utils/RNTesterList.ios.js b/RNTester/js/utils/RNTesterList.ios.js index 821f69e724ab1f..43091cfb418cb8 100644 --- a/RNTester/js/utils/RNTesterList.ios.js +++ b/RNTester/js/utils/RNTesterList.ios.js @@ -16,60 +16,49 @@ const ComponentExamples: Array = [ { key: 'ActivityIndicatorExample', module: require('../examples/ActivityIndicator/ActivityIndicatorExample'), - supportsTVOS: true, }, { key: 'ButtonExample', module: require('../examples/Button/ButtonExample'), - supportsTVOS: true, }, // [TODO(OSS Candidate ISS#2710739) { key: 'DarkModeExample', module: require('../examples/DarkModeExample/DarkModeExample'), - supportsTVOS: false, }, // ]TODO(OSS Candidate ISS#2710739) { key: 'DatePickerIOSExample', module: require('../examples/DatePicker/DatePickerIOSExample'), - supportsTVOS: false, }, // [TODO(macOS GH#774) { key: 'DatePickerMacOSExample', module: require('../examples/DatePicker/DatePickerMacOSExample'), - supportsTVOS: false, }, // ]TODO(macOS GH#774) { key: 'FlatListExample', module: require('../examples/FlatList/FlatListExample'), - supportsTVOS: true, }, // [TODO(OSS Candidate ISS#2710739) { key: 'FocusEvents', module: require('../examples/FocusEventsExample/FocusEventsExample'), - supportsTVOS: true, }, // ]TODO(OSS Candidate ISS#2710739) { key: 'KeyboardEvents', module: require('../examples/KeyboardEventsExample/KeyboardEventsExample'), - supportsTVOS: false, }, // ]TODO(OSS Candidate ISS#2710739) { key: 'Key-View Accessibility Looping', module: require('../examples/KeyViewLoopExample/KeyViewLoopExample'), - supportsTVOS: false, }, // ]TODO(OSS Candidate GH#768) { key: 'AccessibilityShowMenu', module: require('../examples/AccessibilityShowMenu/AccessibilityShowMenu'), - supportsTVOS: false, }, // ]TODO(OSS Candidate ISS#2710739) { key: 'ImageExample', module: require('../examples/Image/ImageExample'), - supportsTVOS: true, skipTest: { // [TODO(OSS Candidate ISS#2710739) ios: @@ -83,87 +72,70 @@ const ComponentExamples: Array = [ { key: 'InputAccessoryViewExample', module: require('../examples/InputAccessoryView/InputAccessoryViewExample'), - supportsTVOS: true, }, { key: 'KeyboardAvoidingViewExample', module: require('../examples/KeyboardAvoidingView/KeyboardAvoidingViewExample'), - supportsTVOS: false, }, { key: 'LayoutEventsExample', module: require('../examples/Layout/LayoutEventsExample'), - supportsTVOS: true, }, { key: 'MaskedViewExample', module: require('../examples/MaskedView/MaskedViewExample'), - supportsTVOS: true, }, { key: 'ModalExample', module: require('../examples/Modal/ModalExample'), - supportsTVOS: true, }, { key: 'MultiColumnExample', module: require('../examples/MultiColumn/MultiColumnExample'), - supportsTVOS: true, }, { key: 'NewAppScreenExample', module: require('../examples/NewAppScreen/NewAppScreenExample'), - supportsTVOS: false, }, { key: 'PickerExample', module: require('../examples/Picker/PickerExample'), - supportsTVOS: false, }, { key: 'PickerIOSExample', module: require('../examples/Picker/PickerIOSExample'), - supportsTVOS: false, }, { key: 'PressableExample', module: require('../examples/Pressable/PressableExample'), - supportsTVOS: true, }, { key: 'ProgressViewIOSExample', module: require('../examples/ProgressViewIOS/ProgressViewIOSExample'), - supportsTVOS: true, }, { key: 'RefreshControlExample', module: require('../examples/RefreshControl/RefreshControlExample'), - supportsTVOS: false, }, { key: 'ScrollViewSimpleExample', module: require('../examples/ScrollView/ScrollViewSimpleExample'), - supportsTVOS: true, }, { key: 'SafeAreaViewExample', module: require('../examples/SafeAreaView/SafeAreaViewExample'), - supportsTVOS: true, }, { key: 'ScrollViewExample', module: require('../examples/ScrollView/ScrollViewExample'), - supportsTVOS: true, }, { key: 'ScrollViewAnimatedExample', module: require('../examples/ScrollView/ScrollViewAnimatedExample'), - supportsTVOS: true, }, { key: 'SectionListExample', module: require('../examples/SectionList/SectionListExample'), - supportsTVOS: true, skipTest: { // [TODO(OSS Candidate ISS#2710739) ios: 'Reason: RedBox shown on failure to load an image.', @@ -172,54 +144,44 @@ const ComponentExamples: Array = [ { key: 'SegmentedControlIOSExample', module: require('../examples/SegmentedControlIOS/SegmentedControlIOSExample'), - supportsTVOS: false, }, { key: 'SliderExample', module: require('../examples/Slider/SliderExample'), - supportsTVOS: false, }, { key: 'StatusBarExample', module: require('../examples/StatusBar/StatusBarExample'), - supportsTVOS: false, }, { key: 'SwitchExample', module: require('../examples/Switch/SwitchExample'), - supportsTVOS: false, }, { key: 'TextExample', /* $FlowFixMe TODO(macOS GH#774): allow macOS to share iOS test */ module: require('../examples/Text/TextExample.ios'), - supportsTVOS: true, }, { key: 'TextInputExample', /* $FlowFixMe TODO(macOS GH#774): allow macOS to share iOS test */ module: require('../examples/TextInput/TextInputExample.ios'), - supportsTVOS: true, }, { key: 'TooltipExample', module: require('../examples/Tooltip/TooltipExample'), - supportsTVOS: true, }, { key: 'TouchableExample', module: require('../examples/Touchable/TouchableExample'), - supportsTVOS: true, }, { key: 'TransparentHitTestExample', module: require('../examples/TransparentHitTest/TransparentHitTestExample'), - supportsTVOS: false, }, { key: 'ViewExample', module: require('../examples/View/ViewExample'), - supportsTVOS: true, }, ]; @@ -227,73 +189,59 @@ const APIExamples: Array = [ { key: 'AccessibilityExample', module: require('../examples/Accessibility/AccessibilityExample'), - supportsTVOS: false, }, { key: 'AccessibilityIOSExample', module: require('../examples/Accessibility/AccessibilityIOSExample'), - supportsTVOS: false, }, { key: 'ActionSheetIOSExample', module: require('../examples/ActionSheetIOS/ActionSheetIOSExample'), - supportsTVOS: true, }, { key: 'AlertIOSExample', module: require('../examples/Alert/AlertIOSExample'), - supportsTVOS: true, }, // [TODO(macOS GH#774) { key: 'AlertMacOSExample', module: require('../examples/Alert/AlertMacOSExample'), - supportsTVOS: true, }, // ]TODO(macOS GH#774) { key: 'AnimatedExample', module: require('../examples/Animated/AnimatedExample'), - supportsTVOS: true, }, { key: 'AnExApp', module: require('../examples/Animated/AnimatedGratuitousApp/AnExApp'), - supportsTVOS: true, }, { key: 'AppearanceExample', module: require('../examples/Appearance/AppearanceExample'), - supportsTVOS: false, }, { key: 'AppStateExample', module: require('../examples/AppState/AppStateExample'), - supportsTVOS: true, }, { key: 'AsyncStorageExample', module: require('../examples/AsyncStorage/AsyncStorageExample'), - supportsTVOS: true, }, { key: 'BorderExample', module: require('../examples/Border/BorderExample'), - supportsTVOS: true, }, { key: 'BoxShadowExample', module: require('../examples/BoxShadow/BoxShadowExample'), - supportsTVOS: true, }, { key: 'ClipboardExample', module: require('../examples/Clipboard/ClipboardExample'), - supportsTVOS: false, }, { key: 'CrashExample', module: require('../examples/Crash/CrashExample'), - supportsTVOS: false, }, { key: 'DevSettings', @@ -302,52 +250,42 @@ const APIExamples: Array = [ { key: 'Dimensions', module: require('../examples/Dimensions/DimensionsExample'), - supportsTVOS: true, }, { key: 'LayoutAnimationExample', module: require('../examples/Layout/LayoutAnimationExample'), - supportsTVOS: true, }, { key: 'LayoutExample', module: require('../examples/Layout/LayoutExample'), - supportsTVOS: true, }, { key: 'LinkingExample', module: require('../examples/Linking/LinkingExample'), - supportsTVOS: true, }, { key: 'NativeAnimationsExample', module: require('../examples/NativeAnimation/NativeAnimationsExample'), - supportsTVOS: true, }, { key: 'OrientationChangeExample', module: require('../examples/OrientationChange/OrientationChangeExample'), - supportsTVOS: false, }, { key: 'PanResponderExample', module: require('../examples/PanResponder/PanResponderExample'), - supportsTVOS: false, }, { key: 'PlatformColorExample', module: require('../examples/PlatformColor/PlatformColorExample'), - supportsTVOS: true, }, { key: 'PointerEventsExample', module: require('../examples/PointerEvents/PointerEventsExample'), - supportsTVOS: false, }, { key: 'PushNotificationIOSExample', module: require('../examples/PushNotificationIOS/PushNotificationIOSExample'), - supportsTVOS: false, // [TODO(OSS Candidate ISS#2710739) skipTest: { ios: @@ -357,7 +295,6 @@ const APIExamples: Array = [ { key: 'RCTRootViewIOSExample', module: require('../examples/RCTRootView/RCTRootViewIOSExample'), - supportsTVOS: true, // [TODO(OSS Candidate ISS#2710739) skipTest: { default: @@ -367,27 +304,22 @@ const APIExamples: Array = [ { key: 'RTLExample', module: require('../examples/RTL/RTLExample'), - supportsTVOS: true, }, { key: 'ShareExample', module: require('../examples/Share/ShareExample'), - supportsTVOS: true, }, { key: 'SnapshotExample', module: require('../examples/Snapshot/SnapshotExample'), - supportsTVOS: true, }, { key: 'TimerExample', module: require('../examples/Timer/TimerExample'), - supportsTVOS: true, }, { key: 'TransformExample', module: require('../examples/Transform/TransformExample'), - supportsTVOS: true, // [TODO(OSS Candidate ISS#2710739) skipTest: { default: 'Reason: Stack overflow in jsi, upstream issue.', @@ -396,7 +328,6 @@ const APIExamples: Array = [ { key: 'TurboModuleExample', module: require('../examples/TurboModule/TurboModuleExample'), - supportsTVOS: false, // [TODO(OSS Candidate ISS#2710739) skipTest: { default: 'Reason: requires TurboModule to be configured in host app.', @@ -405,22 +336,18 @@ const APIExamples: Array = [ { key: 'TVEventHandlerExample', module: require('../examples/TVEventHandler/TVEventHandlerExample'), - supportsTVOS: true, }, { key: 'VibrationExample', module: require('../examples/Vibration/VibrationExample'), - supportsTVOS: false, }, { key: 'WebSocketExample', module: require('../examples/WebSocket/WebSocketExample'), - supportsTVOS: true, }, { key: 'XHRExample', module: require('../examples/XHR/XHRExample'), - supportsTVOS: true, }, ]; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index ab394f12e5dc61..e1e4065f277bd6 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -33,6 +33,11 @@ RCT_EXTERN NSString *const RCTJavaScriptWillStartExecutingNotification; */ RCT_EXTERN NSString *const RCTJavaScriptDidLoadNotification; +/** + * This notification fires every time the bridge has finished loading an additional JS bundle. + */ +RCT_EXTERN NSString *const RCTAdditionalJavaScriptDidLoadNotification; + /** * This notification fires when the bridge failed to load the JS bundle. The * `error` key can be used to determine the error that occurred. @@ -135,6 +140,12 @@ RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotificationBridgeDescripti */ typedef NSArray> * (^RCTBridgeModuleListProvider)(void); +/** + * These blocks are used to report whether an additional bundle + * fails or succeeds loading. + */ +typedef void (^RCTLoadAndExecuteErrorBlock)(NSError *error); + /** * This function returns the module name for a given class. */ @@ -290,8 +301,15 @@ RCT_EXTERN void RCTEnableTurboModule(BOOL enabled); - (void)requestReload __deprecated_msg("Use RCTReloadCommand instead"); /** - * Says whether bridge has started receiving calls from javascript. + * Says whether bridge has started receiving calls from JavaScript. */ - (BOOL)isBatchActive; +/** + * Loads and executes additional bundles in the VM for development. + */ +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete; + @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 2d8754ff3dc4e7..64792e726164d6 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -16,6 +16,7 @@ #if RCT_ENABLE_INSPECTOR #import "RCTInspectorDevServerHelper.h" #endif +#import "RCTDevLoadingViewProtocol.h" #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTPerformanceLogger.h" @@ -23,10 +24,11 @@ #import "RCTReloadCommand.h" #import "RCTUtils.h" -NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification"; -NSString *const RCTJavaScriptWillStartExecutingNotification = @"RCTJavaScriptWillStartExecutingNotification"; -NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTAdditionalJavaScriptDidLoadNotification = @"RCTAdditionalJavaScriptDidLoadNotification"; NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; +NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptWillStartExecutingNotification = @"RCTJavaScriptWillStartExecutingNotification"; +NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification"; NSString *const RCTDidInitializeModuleNotification = @"RCTDidInitializeModuleNotification"; NSString *const RCTDidSetupModuleNotification = @"RCTDidSetupModuleNotification"; NSString *const RCTDidSetupModuleNotificationModuleNameKey = @"moduleName"; @@ -389,4 +391,11 @@ - (void)registerSegmentWithId:(NSUInteger)segmentId path:(NSString *)path [self.batchedBridge registerSegmentWithId:segmentId path:path]; } +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ + [self.batchedBridge loadAndExecuteSplitBundleURL:bundleURL onError:onError onComplete:onComplete]; +} + @end diff --git a/React/Base/RCTBundleURLProvider.m b/React/Base/RCTBundleURLProvider.mm similarity index 95% rename from React/Base/RCTBundleURLProvider.m rename to React/Base/RCTBundleURLProvider.mm index fd02477c4f3ddf..64cf9f7b851efe 100644 --- a/React/Base/RCTBundleURLProvider.m +++ b/React/Base/RCTBundleURLProvider.mm @@ -10,6 +10,11 @@ #import "RCTConvert.h" #import "RCTDefines.h" +#if __has_include("hermes.h") || __has_include() +#import +#define HAS_BYTECODE_VERSION +#endif + NSString *const RCTBundleURLProviderUpdatedNotification = @"RCTBundleURLProviderUpdatedNotification"; const NSUInteger kRCTBundleURLProviderDefaultPort = RCT_METRO_PORT; @@ -220,13 +225,22 @@ + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot runModule:(BOOL)runModule { NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; +#ifdef HAS_BYTECODE_VERSION + NSString *runtimeBytecodeVersion = + [NSString stringWithFormat:@"&runtimeBytecodeVersion=%u", facebook::hermes::HermesRuntime::getBytecodeVersion()]; +#else + NSString *runtimeBytecodeVersion = @""; +#endif + // When we support only iOS 8 and above, use queryItems for a better API. - NSString *query = [NSString stringWithFormat:@"platform=%@&dev=%@&minify=%@&modulesOnly=%@&runMdoule=%@", + NSString *query = [NSString stringWithFormat:@"platform=%@&dev=%@&minify=%@&modulesOnly=%@&runModule=%@%@", kRCTPlatformName, // TODO(macOS GH#774) enableDev ? @"true" : @"false", enableMinification ? @"true" : @"false", modulesOnly ? @"true" : @"false", - runModule ? @"true" : @"false"]; + runModule ? @"true" : @"false", + runtimeBytecodeVersion]; + NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]; if (bundleID) { query = [NSString stringWithFormat:@"%@&app=%@", query, bundleID]; diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index d51e2d5222448c..0a5230af620904 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -11,6 +11,11 @@ extern NSString *const RCTJavaScriptLoaderErrorDomain; +extern const UInt32 RCT_BYTECODE_ALIGNMENT; + +UInt32 RCTReadUInt32LE(NSData *script, UInt32 offset); +bool RCTIsBytecodeBundle(NSData *script); + NS_ENUM(NSInteger){ RCTJavaScriptLoaderErrorNoScriptURL = 1, RCTJavaScriptLoaderErrorFailedOpeningFile = 2, @@ -87,7 +92,6 @@ typedef void (^RCTSourceLoadBlock)(NSError *error, RCTSource *source); * RCTJavaScriptLoaderErrorDomain and the code RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously. */ + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL - runtimeBCVersion:(int32_t)runtimeBCVersion sourceLength:(int64_t *)sourceLength error:(NSError **)error; diff --git a/React/Base/RCTJavaScriptLoader.mm b/React/Base/RCTJavaScriptLoader.mm index d15738c549eabe..d902f0c92d0002 100755 --- a/React/Base/RCTJavaScriptLoader.mm +++ b/React/Base/RCTJavaScriptLoader.mm @@ -19,7 +19,19 @@ NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain"; -static const int32_t JSNoBytecodeFileFormatVersion = -1; +const UInt32 RCT_BYTECODE_ALIGNMENT = 4; +UInt32 RCTReadUInt32LE(NSData *script, UInt32 offset) +{ + return [script length] < offset + 4 ? 0 : CFSwapInt32LittleToHost(*(((uint32_t *)[script bytes]) + offset / 4)); +} + +bool RCTIsBytecodeBundle(NSData *script) +{ + static const UInt32 BYTECODE_BUNDLE_MAGIC_NUMBER = 0xffe7c3c3; + return ( + [script length] > 8 && RCTReadUInt32LE(script, 0) == BYTECODE_BUNDLE_MAGIC_NUMBER && + RCTReadUInt32LE(script, 4) > 0); +} @interface RCTSource () { @public @@ -37,7 +49,10 @@ @implementation RCTSource { RCTSource *source = [RCTSource new]; source->_url = url; - source->_data = data; + // Multipart responses may give us an unaligned view into the buffer. This ensures memory is aligned. + source->_data = (RCTIsBytecodeBundle(data) && ((long)[data bytes] % RCT_BYTECODE_ALIGNMENT)) + ? [[NSData alloc] initWithData:data] + : data; source->_length = length; source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler; return source; @@ -75,10 +90,7 @@ + (void)loadBundleAtURL:(NSURL *)scriptURL { int64_t sourceLength; NSError *error; - NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL - runtimeBCVersion:JSNoBytecodeFileFormatVersion - sourceLength:&sourceLength - error:&error]; + NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL sourceLength:&sourceLength error:&error]; if (data) { onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength)); return; @@ -95,7 +107,6 @@ + (void)loadBundleAtURL:(NSURL *)scriptURL } + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL - runtimeBCVersion:(int32_t)runtimeBCVersion sourceLength:(int64_t *)sourceLength error:(NSError **)error { @@ -186,27 +197,6 @@ + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL return nil; #endif } - case facebook::react::ScriptTag::BCBundle: - if (runtimeBCVersion == JSNoBytecodeFileFormatVersion || runtimeBCVersion < 0) { - if (error) { - *error = [NSError - errorWithDomain:RCTJavaScriptLoaderErrorDomain - code:RCTJavaScriptLoaderErrorBCNotSupported - userInfo:@{NSLocalizedDescriptionKey : @"Bytecode bundles are not supported by this runtime."}]; - } - return nil; - } else if ((uint32_t)runtimeBCVersion != header.version) { - if (error) { - NSString *errDesc = [NSString - stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u", runtimeBCVersion, header.version]; - - *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain - code:RCTJavaScriptLoaderErrorBCVersion - userInfo:@{NSLocalizedDescriptionKey : errDesc}]; - } - return nil; - } - break; } struct stat statInfo; @@ -300,10 +290,25 @@ static void attemptAsynchronousLoadOfBundleAtURL( // Validate that the packager actually returned javascript. NSString *contentType = headers[@"Content-Type"]; NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; - if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) { - NSString *description = [NSString - stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", - mimeType]; + if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"] && + ![mimeType isEqualToString:@"application/x-metro-bytecode-bundle"]) { + NSString *description; + if ([mimeType isEqualToString:@"application/json"]) { + NSError *parseError; + NSDictionary *jsonError = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError]; + if (!parseError && [jsonError isKindOfClass:[NSDictionary class]] && + [[jsonError objectForKey:@"message"] isKindOfClass:[NSString class]] && + [[jsonError objectForKey:@"message"] length]) { + description = [jsonError objectForKey:@"message"]; + } else { + description = [NSString stringWithFormat:@"Unknown error fetching '%@'.", scriptURL.absoluteString]; + } + } else { + description = [NSString + stringWithFormat: + @"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType]; + } + error = [NSError errorWithDomain:@"JSServer" code:NSURLErrorCannotParseResponse @@ -318,7 +323,8 @@ static void attemptAsynchronousLoadOfBundleAtURL( } progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { // Only care about download progress events for the javascript bundle part. - if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) { + if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"] || + [headers[@"Content-Type"] isEqualToString:@"application/x-metro-bytecode-bundle"]) { onProgress(progressEventFromDownloadProgress(loaded, total)); } }]; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 2595fbcf6659f9..a20cfcb38f176a 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -42,6 +42,7 @@ RCT_EXTERN void RCTUnsafeExecuteOnMainQueueSync(dispatch_block_t block); // Get screen metrics in a thread-safe way RCT_EXTERN CGFloat RCTScreenScale(void); +RCT_EXTERN CGFloat RCTFontSizeMultiplier(void); RCT_EXTERN CGSize RCTScreenSize(void); // Round float coordinates to nearest whole screen pixel (not point) diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index beee44e90ed135..19d88c78467d15 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -305,6 +305,30 @@ CGFloat RCTScreenScale() return scale; } +CGFloat RCTFontSizeMultiplier() +{ + static NSDictionary *mapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mapping = @{ + UIContentSizeCategoryExtraSmall : @0.823, + UIContentSizeCategorySmall : @0.882, + UIContentSizeCategoryMedium : @0.941, + UIContentSizeCategoryLarge : @1.0, + UIContentSizeCategoryExtraLarge : @1.118, + UIContentSizeCategoryExtraExtraLarge : @1.235, + UIContentSizeCategoryExtraExtraExtraLarge : @1.353, + UIContentSizeCategoryAccessibilityMedium : @1.786, + UIContentSizeCategoryAccessibilityLarge : @2.143, + UIContentSizeCategoryAccessibilityExtraLarge : @2.643, + UIContentSizeCategoryAccessibilityExtraExtraLarge : @3.143, + UIContentSizeCategoryAccessibilityExtraExtraExtraLarge : @3.571 + }; + }); + + return mapping[RCTSharedApplication().preferredContentSizeCategory].floatValue; +} + CGSize RCTScreenSize() { // FIXME: this caches the bounds at app start, whatever those were, and then diff --git a/React/Base/Surface/RCTSurfaceRootShadowView.m b/React/Base/Surface/RCTSurfaceRootShadowView.m index 97525cba479fbd..455af97b1ac215 100644 --- a/React/Base/Surface/RCTSurfaceRootShadowView.m +++ b/React/Base/Surface/RCTSurfaceRootShadowView.m @@ -46,7 +46,6 @@ - (void)layoutWithAffectedShadowViews:(NSHashTable *)affectedSh NSHashTable *other = [NSHashTable new]; RCTLayoutContext layoutContext = {}; - layoutContext.absolutePosition = CGPointZero; layoutContext.affectedShadowViews = affectedShadowViews; layoutContext.other = other; diff --git a/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h b/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h index b10476a72e9b07..32c43d6ec1b4a5 100644 --- a/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h +++ b/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h @@ -45,9 +45,6 @@ NS_ASSUME_NONNULL_BEGIN initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions; -- (instancetype)initWithSurface:(RCTSurface *)surface - sizeMeasureMode:(RCTSurfaceSizeMeasureMode)sizeMeasureMode NS_UNAVAILABLE; - - (void)cancelTouches; @end diff --git a/React/CoreModules/BUCK b/React/CoreModules/BUCK index 274a89b115fdd6..2f9902478139d5 100644 --- a/React/CoreModules/BUCK +++ b/React/CoreModules/BUCK @@ -116,6 +116,9 @@ rn_apple_library( ) + react_module_plugin_providers( name = "DevLoadingView", native_class_func = "RCTDevLoadingViewCls", + ) + react_module_plugin_providers( + name = "DevSplitBundleLoader", + native_class_func = "RCTDevSplitBundleLoaderCls", ), plugins_header = "FBCoreModulesPlugins.h", preprocessor_flags = OBJC_ARC_PREPROCESSOR_FLAGS + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ diff --git a/React/CoreModules/CoreModulesPlugins.h b/React/CoreModules/CoreModulesPlugins.h index 478c7473a512ba..509eb5af2f8dbc 100644 --- a/React/CoreModules/CoreModulesPlugins.h +++ b/React/CoreModules/CoreModulesPlugins.h @@ -53,6 +53,7 @@ Class RCTTVNavigationEventEmitterCls(void) __attribute__((used)); Class RCTWebSocketExecutorCls(void) __attribute__((used)); Class RCTWebSocketModuleCls(void) __attribute__((used)); Class RCTDevLoadingViewCls(void) __attribute__((used)); +Class RCTDevSplitBundleLoaderCls(void) __attribute__((used)); #ifdef __cplusplus } diff --git a/React/CoreModules/CoreModulesPlugins.mm b/React/CoreModules/CoreModulesPlugins.mm index b75ee76c74f22e..50e232f43c2cd2 100644 --- a/React/CoreModules/CoreModulesPlugins.mm +++ b/React/CoreModules/CoreModulesPlugins.mm @@ -48,6 +48,7 @@ Class RCTCoreModulesClassProvider(const char *name) { {"WebSocketExecutor", RCTWebSocketExecutorCls}, {"WebSocketModule", RCTWebSocketModuleCls}, {"DevLoadingView", RCTDevLoadingViewCls}, + {"DevSplitBundleLoader", RCTDevSplitBundleLoaderCls}, }; auto p = sCoreModuleClassMap.find(name); diff --git a/React/CoreModules/RCTDevSettings.h b/React/CoreModules/RCTDevSettings.h index 0143776cd52009..77dabfc7da7848 100644 --- a/React/CoreModules/RCTDevSettings.h +++ b/React/CoreModules/RCTDevSettings.h @@ -105,9 +105,14 @@ - (void)toggleElementInspector; /** - * If loading bundle from metro, sets up HMRClient. + * Set up the HMRClient if loading the bundle from Metro. */ -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL; +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL; + +/** + * Register additional bundles with the HMRClient. + */ +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL; #if RCT_DEV_MENU - (void)addHandler:(id)handler diff --git a/React/CoreModules/RCTDevSettings.mm b/React/CoreModules/RCTDevSettings.mm index e58719f7aa905d..a3166b00c832f9 100644 --- a/React/CoreModules/RCTDevSettings.mm +++ b/React/CoreModules/RCTDevSettings.mm @@ -445,7 +445,7 @@ - (void)addHandler:(id)handler forPackagerMethod:(NSStr #endif } -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL { if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check NSString *const path = [bundleURL.path substringFromIndex:1]; // Strip initial slash. @@ -463,6 +463,20 @@ - (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL } } +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL +{ + if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check + if (self.bridge) { + [self.bridge enqueueJSCall:@"HMRClient" + method:@"registerBundle" + args:@[ [bundleURL absoluteString] ] + completion:NULL]; + } else { + self.invokeJS(@"HMRClient", @"registerBundle", @[ [bundleURL absoluteString] ]); + } + } +} + #pragma mark - Internal /** @@ -554,7 +568,10 @@ - (void)setProfilingEnabled:(BOOL)isProfilingEnabled - (void)toggleElementInspector { } -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL +{ +} +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL { } - (void)addMenuItem:(NSString *)title diff --git a/React/CoreModules/RCTDevSplitBundleLoader.h b/React/CoreModules/RCTDevSplitBundleLoader.h new file mode 100644 index 00000000000000..d86f6244bb4fce --- /dev/null +++ b/React/CoreModules/RCTDevSplitBundleLoader.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. + */ + +#import +#import // TODO(macOS GH#774) + +@interface RCTDevSplitBundleLoader : NSObject +@end diff --git a/React/CoreModules/RCTDevSplitBundleLoader.mm b/React/CoreModules/RCTDevSplitBundleLoader.mm new file mode 100644 index 00000000000000..73d0c98e692dff --- /dev/null +++ b/React/CoreModules/RCTDevSplitBundleLoader.mm @@ -0,0 +1,88 @@ +/* + * 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 +#import +#import +#import +#import +#import + +#import "CoreModulesPlugins.h" + +using namespace facebook::react; + +@interface RCTDevSplitBundleLoader () +@end + +#if RCT_DEV_MENU + +@implementation RCTDevSplitBundleLoader { +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; +} + +RCT_EXPORT_METHOD(loadBundle + : (NSString *)bundlePath resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + NSURL *sourceURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForSplitBundleRoot:bundlePath]; + [_bridge loadAndExecuteSplitBundleURL:sourceURL + onError:^(NSError *error) { + reject(@"E_BUNDLE_LOAD_ERROR", [error localizedDescription], error); + } + onComplete:^() { + resolve(@YES); + }]; +} + +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end + +#else + +@implementation RCTDevSplitBundleLoader + ++ (NSString *)moduleName +{ + return nil; +} +- (void)loadBundle:(NSString *)bundlePath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +{ +} +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end + +#endif + +Class RCTDevSplitBundleLoaderCls(void) +{ + return RCTDevSplitBundleLoader.class; +} diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 549613b12b2d2b..dcb22df6660f7f 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -999,10 +999,50 @@ - (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync } if (self.devSettings.isDevModeEnabled) { // TODO(OSS Candidate ISS#2710739) - [self.devSettings setupHotModuleReloadClientIfApplicableForURL:self.bundleURL]; + [self.devSettings setupHMRClientWithBundleURL:self.bundleURL]; } } +#if RCT_DEV_MENU +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ + __weak __typeof(self) weakSelf = self; + [RCTJavaScriptLoader loadBundleAtURL:bundleURL + onProgress:^(RCTLoadingProgress *progressData) { +#if (RCT_DEV_MENU | RCT_ENABLE_LOADING_VIEW) && __has_include() + id loadingView = [weakSelf moduleForName:@"DevLoadingView" + lazilyLoadIfNecessary:YES]; + [loadingView updateProgress:progressData]; +#endif + } + onComplete:^(NSError *error, RCTSource *source) { + if (error) { + onError(error); + return; + } + + [self enqueueApplicationScript:source.data + url:source.url + onComplete:^{ + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTAdditionalJavaScriptDidLoadNotification + object:self->_parentBridge + userInfo:@{@"bridge" : self}]; + [self.devSettings setupHMRClientWithAdditionalBundleURL:source.url]; + onComplete(); + }]; + }]; +} +#else +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ +} +#endif + - (void)handleError:(NSError *)error { // This is generally called when the infrastructure throws an @@ -1408,7 +1448,15 @@ - (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)a // hold a local reference to reactInstance in case a parallel thread // resets it between null check and usage auto reactInstance = self->_reactInstance; - if (isRAMBundle(script)) { + if (reactInstance && RCTIsBytecodeBundle(script)) { + UInt32 offset = 8; + while (offset < script.length) { + UInt32 fileLength = RCTReadUInt32LE(script, offset); + NSData *unit = [script subdataWithRange:NSMakeRange(offset + 4, fileLength)]; + reactInstance->loadScriptFromString(std::make_unique(unit), sourceUrlStr.UTF8String, false); + offset += ((fileLength + RCT_BYTECODE_ALIGNMENT - 1) & ~(RCT_BYTECODE_ALIGNMENT - 1)) + 4; + } + } else if (isRAMBundle(script)) { [self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad]; auto ramBundle = std::make_unique(sourceUrlStr.UTF8String); std::unique_ptr scriptStr = ramBundle->getStartupCode(); diff --git a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h new file mode 100644 index 00000000000000..37abe305e0d0e1 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.h @@ -0,0 +1,21 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * UIView class for root component. + */ +@interface RCTInputAccessoryComponentView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm new file mode 100644 index 00000000000000..cf24358e274c57 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm @@ -0,0 +1,153 @@ +/* + * 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 "RCTInputAccessoryComponentView.h" + +#import +#import +#import +#import +#import +#import +#import "RCTInputAccessoryContentView.h" + +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +static UIView *_Nullable RCTFindTextInputWithNativeId(UIView *view, NSString *nativeId) +{ + if ([view respondsToSelector:@selector(inputAccessoryViewID)] && + [view respondsToSelector:@selector(setInputAccessoryView:)]) { + UIView *typed = (UIView *)view; + if (!nativeId || [typed.inputAccessoryViewID isEqualToString:nativeId]) { + return typed; + } + } + + for (UIView *subview in view.subviews) { + UIView *result = RCTFindTextInputWithNativeId(subview, nativeId); + if (result) { + return result; + } + } + + return nil; +} + +@implementation RCTInputAccessoryComponentView { + InputAccessoryShadowNode::ConcreteState::Shared _state; + RCTInputAccessoryContentView *_contentView; + RCTSurfaceTouchHandler *_touchHandler; + UIView __weak *_textInput; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + _contentView = [RCTInputAccessoryContentView new]; + _touchHandler = [RCTSurfaceTouchHandler new]; + [_touchHandler attachToView:_contentView]; + } + + return self; +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + + if (self.window && !_textInput) { + if (self.nativeId) { + _textInput = RCTFindTextInputWithNativeId(self.window, self.nativeId); + _textInput.inputAccessoryView = _contentView; + } else { + _textInput = RCTFindTextInputWithNativeId(_contentView, nil); + } + + if (!self.nativeId) { + [self becomeFirstResponder]; + } + } +} + +- (BOOL)canBecomeFirstResponder +{ + return true; +} + +- (UIView *)inputAccessoryView +{ + return _contentView; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + [_contentView insertSubview:childComponentView atIndex:index]; +} + +- (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index +{ + [childComponentView removeFromSuperview]; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + auto const &oldInputAccessoryProps = *std::static_pointer_cast(_props); + auto const &newInputAccessoryProps = *std::static_pointer_cast(props); + + if (newInputAccessoryProps.backgroundColor != oldInputAccessoryProps.backgroundColor) { + _contentView.backgroundColor = RCTUIColorFromSharedColor(newInputAccessoryProps.backgroundColor); + } + + [super updateProps:props oldProps:oldProps]; + self.hidden = true; +} + +- (void)updateState:(const facebook::react::State::Shared &)state + oldState:(const facebook::react::State::Shared &)oldState +{ + _state = std::static_pointer_cast(state); + CGSize oldScreenSize = RCTCGSizeFromSize(_state->getData().screenSize); + CGSize screenSize = [[UIScreen mainScreen] bounds].size; + screenSize.height = std::nan(""); + if (oldScreenSize.width != screenSize.width) { + auto stateData = InputAccessoryState{RCTSizeFromCGSize(screenSize)}; + _state->updateState(std::move(stateData)); + } +} + +- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics + oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics +{ + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + + [_contentView setFrame:RCTCGRectFromRect(layoutMetrics.getContentFrame())]; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + _state.reset(); + _textInput = nil; +} + +@end + +Class RCTInputAccessoryCls(void) +{ + return RCTInputAccessoryComponentView.class; +} diff --git a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.h new file mode 100644 index 00000000000000..15c80e6a0245a0 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.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. + */ + +#import + +@interface RCTInputAccessoryContentView : UIView + +@end diff --git a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm new file mode 100644 index 00000000000000..d9c4fb231b2493 --- /dev/null +++ b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm @@ -0,0 +1,70 @@ +/* + * 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 "RCTInputAccessoryContentView.h" + +@implementation RCTInputAccessoryContentView { + UIView *_safeAreaContainer; + NSLayoutConstraint *_heightConstraint; +} + +- (instancetype)init +{ + if (self = [super init]) { + self.autoresizingMask = UIViewAutoresizingFlexibleHeight; + + _safeAreaContainer = [UIView new]; + _safeAreaContainer.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_safeAreaContainer]; + + _heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0]; + _heightConstraint.active = YES; + + if (@available(iOS 11.0, tvOS 11.0, *)) { + [NSLayoutConstraint activateConstraints:@[ + [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor], + [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor], + [_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor], + [_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor] + ]]; + } else { + [NSLayoutConstraint activateConstraints:@[ + [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], + [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.topAnchor], + [_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.leadingAnchor], + [_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.trailingAnchor] + ]]; + } + } + return self; +} + +- (CGSize)intrinsicContentSize +{ + // This is needed so the view size is based on autolayout constraints. + return CGSizeZero; +} + +- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index +{ + [_safeAreaContainer insertSubview:view atIndex:index]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + [_safeAreaContainer setFrame:frame]; + _heightConstraint.constant = frame.size.height; + [self layoutIfNeeded]; +} + +- (BOOL)canBecomeFirstResponder +{ + return true; +} + +@end diff --git a/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm index afd1873bacfe4d..10e78de2937607 100644 --- a/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm @@ -87,6 +87,7 @@ - (void)prepareForRecycle [_viewsToBeMounted removeAllObjects]; [_viewsToBeUnmounted removeAllObjects]; _state.reset(); + self.contentView = nil; [super prepareForRecycle]; } diff --git a/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h b/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h index 7b002cbf8bf66f..573e7b1fc6f786 100644 --- a/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h +++ b/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h @@ -40,6 +40,7 @@ Class RCTModalHostViewCls(void) __attribute__((used)); Class RCTImageCls(void) __attribute__((used)); Class RCTParagraphCls(void) __attribute__((used)); Class RCTTextInputCls(void) __attribute__((used)); +Class RCTInputAccessoryCls(void) __attribute__((used)); Class RCTViewCls(void) __attribute__((used)); #ifdef __cplusplus diff --git a/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm b/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm index 33cc39126fe494..6107f1297fea21 100644 --- a/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm +++ b/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm @@ -29,6 +29,7 @@ {"Image", RCTImageCls}, {"Paragraph", RCTParagraphCls}, {"TextInput", RCTTextInputCls}, + {"InputAccessoryView", RCTInputAccessoryCls}, {"View", RCTViewCls}, }; diff --git a/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm b/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm index bff3b25519802f..48603345fc547b 100644 --- a/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm @@ -29,10 +29,10 @@ @implementation RCTSliderComponentView { UIImage *_maximumTrackImage; UIImage *_thumbImage; - const ImageResponseObserverCoordinator *_trackImageCoordinator; - const ImageResponseObserverCoordinator *_minimumTrackImageCoordinator; - const ImageResponseObserverCoordinator *_maximumTrackImageCoordinator; - const ImageResponseObserverCoordinator *_thumbImageCoordinator; + ImageResponseObserverCoordinator const *_trackImageCoordinator; + ImageResponseObserverCoordinator const *_minimumTrackImageCoordinator; + ImageResponseObserverCoordinator const *_maximumTrackImageCoordinator; + ImageResponseObserverCoordinator const *_thumbImageCoordinator; RCTImageResponseObserverProxy _trackImageResponseObserverProxy; RCTImageResponseObserverProxy _minimumTrackImageResponseObserverProxy; @@ -81,20 +81,19 @@ - (void)prepareForRecycle // need to make sure that image properties are reset here [_sliderView setMinimumTrackImage:nil forState:UIControlStateNormal]; [_sliderView setMaximumTrackImage:nil forState:UIControlStateNormal]; - [_sliderView setThumbImage:nil forState:UIControlStateNormal]; + + if (_thumbImage) { + [_sliderView setThumbImage:nil forState:UIControlStateNormal]; + } _trackImage = nil; _minimumTrackImage = nil; _maximumTrackImage = nil; _thumbImage = nil; -} -- (void)dealloc -{ - self.trackImageCoordinator = nullptr; - self.minimumTrackImageCoordinator = nullptr; - self.maximumTrackImageCoordinator = nullptr; - self.thumbImageCoordinator = nullptr; + const auto &props = *std::static_pointer_cast(_props); + _sliderView.value = props.value; + _previousValue = props.value; } #pragma mark - RCTComponentViewProtocol diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 8d936feba22625..f23be7a94636dd 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -15,6 +15,7 @@ #import #import #import +#import #import "RCTConversions.h" #import "RCTTextInputNativeCommands.h" @@ -54,8 +55,11 @@ @implementation RCTTextInputComponentView { * In multiline text input this is undesirable as we don't want to be sending events for changes that JS triggered. */ BOOL _comingFromJS; + BOOL _didMoveToWindow; } +#pragma mark - UIView overrides + - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { @@ -67,12 +71,33 @@ - (instancetype)initWithFrame:(CGRect)frame _backedTextInputView.textInputDelegate = self; _ignoreNextTextInputCall = NO; _comingFromJS = NO; + _didMoveToWindow = NO; [self addSubview:_backedTextInputView]; } return self; } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + + if (self.window && !_didMoveToWindow) { + auto const &props = *std::static_pointer_cast(_props); + if (props.autoFocus) { + [_backedTextInputView becomeFirstResponder]; + } + _didMoveToWindow = YES; + } +} + +#pragma mark - RCTViewComponentView overrides + +- (NSObject *)accessibilityElement +{ + return _backedTextInputView; +} + #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider @@ -175,13 +200,17 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) { _backedTextInputView.defaultTextAttributes = - RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes()); + RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier())); } if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) { _backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor); } + if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { + _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); + } + [super updateProps:props oldProps:oldProps]; } @@ -214,20 +243,6 @@ - (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth); } -- (void)_setAttributedString:(NSAttributedString *)attributedString -{ - UITextRange *selectedRange = [_backedTextInputView selectedTextRange]; - _backedTextInputView.attributedText = attributedString; - if (_lastStringStateWasUpdatedWith.length == attributedString.length) { - // Calling `[_backedTextInputView setAttributedText]` moves caret - // to the end of text input field. This cancels any selection as well - // as position in the text input field. In case the length of string - // doesn't change, selection and caret position is maintained. - [_backedTextInputView setSelectedTextRange:selectedRange notifyDelegate:NO]; - } - _lastStringStateWasUpdatedWith = attributedString; -} - - (void)prepareForRecycle { [super prepareForRecycle]; @@ -237,19 +252,7 @@ - (void)prepareForRecycle _comingFromJS = NO; _lastStringStateWasUpdatedWith = nil; _ignoreNextTextInputCall = NO; -} - -#pragma mark - RCTComponentViewProtocol - -- (void)_setMultiline:(BOOL)multiline -{ - [_backedTextInputView removeFromSuperview]; - UIView *backedTextInputView = - multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; - backedTextInputView.frame = _backedTextInputView.frame; - RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); - _backedTextInputView = backedTextInputView; - [self addSubview:_backedTextInputView]; + _didMoveToWindow = NO; } #pragma mark - RCTBackedTextInputDelegate @@ -376,41 +379,6 @@ - (void)textInputDidChangeSelection } } -#pragma mark - Other - -- (TextInputMetrics)_textInputMetrics -{ - TextInputMetrics metrics; - metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string); - metrics.selectionRange = [self _selectionRange]; - metrics.eventCount = _mostRecentEventCount; - return metrics; -} - -- (void)_updateState -{ - if (!_state) { - return; - } - NSAttributedString *attributedString = _backedTextInputView.attributedText; - auto data = _state->getData(); - _lastStringStateWasUpdatedWith = attributedString; - data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); - _mostRecentEventCount += _comingFromJS ? 0 : 1; - data.mostRecentEventCount = _mostRecentEventCount; - _state->updateState(std::move(data)); -} - -- (AttributedString::Range)_selectionRange -{ - 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)}; -} - #pragma mark - Native Commands - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args @@ -458,6 +426,66 @@ - (void)setTextAndSelection:(NSInteger)eventCount _comingFromJS = NO; } +#pragma mark - Other + +- (TextInputMetrics)_textInputMetrics +{ + TextInputMetrics metrics; + metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string); + metrics.selectionRange = [self _selectionRange]; + metrics.eventCount = _mostRecentEventCount; + return metrics; +} + +- (void)_updateState +{ + if (!_state) { + return; + } + NSAttributedString *attributedString = _backedTextInputView.attributedText; + auto data = _state->getData(); + _lastStringStateWasUpdatedWith = attributedString; + data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); + _mostRecentEventCount += _comingFromJS ? 0 : 1; + data.mostRecentEventCount = _mostRecentEventCount; + _state->updateState(std::move(data)); +} + +- (AttributedString::Range)_selectionRange +{ + 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)}; +} + +- (void)_setAttributedString:(NSAttributedString *)attributedString +{ + UITextRange *selectedRange = [_backedTextInputView selectedTextRange]; + _backedTextInputView.attributedText = attributedString; + if (_lastStringStateWasUpdatedWith.length == attributedString.length) { + // Calling `[_backedTextInputView setAttributedText]` moves caret + // to the end of text input field. This cancels any selection as well + // as position in the text input field. In case the length of string + // doesn't change, selection and caret position is maintained. + [_backedTextInputView setSelectedTextRange:selectedRange notifyDelegate:NO]; + } + _lastStringStateWasUpdatedWith = attributedString; +} + +- (void)_setMultiline:(BOOL)multiline +{ + [_backedTextInputView removeFromSuperview]; + UIView *backedTextInputView = + multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; + backedTextInputView.frame = _backedTextInputView.frame; + RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); + _backedTextInputView = backedTextInputView; + [self addSubview:_backedTextInputView]; +} + @end Class RCTTextInputCls(void) diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 09bc95cb33d57d..dfec3a599371db 100644 --- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -53,18 +53,6 @@ - (void)setContentView:(UIView *)contentView } } -- (void)layoutSubviews -{ - [super layoutSubviews]; - // Consider whether using `updateLayoutMetrics:oldLayoutMetrics` - // isn't more appropriate for your use case. `layoutSubviews` is called - // by UIKit while `updateLayoutMetrics:oldLayoutMetrics` is called - // by React Native Renderer within `CATransaction`. - // If you are calling `setFrame:` or other methods that cause - // `layoutSubviews` to be triggered, `_contentView`'s and `_borderLayout`'s - // frames might get out of sync with `self.bounds`. -} - - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { @@ -228,11 +216,23 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } + // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { self.accessibilityElement.accessibilityTraits = RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits); } + // `accessibilityState` + if (oldViewProps.accessibilityState != newViewProps.accessibilityState) { + self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected); + if (newViewProps.accessibilityState.selected) { + self.accessibilityTraits |= UIAccessibilityTraitSelected; + } + if (newViewProps.accessibilityState.disabled) { + self.accessibilityTraits |= UIAccessibilityTraitNotEnabled; + } + } + // `accessibilityIgnoresInvertColors` if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) { #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ diff --git a/React/Fabric/Mounting/RCTComponentViewRegistry.mm b/React/Fabric/Mounting/RCTComponentViewRegistry.mm index b0dbe553b03ff4..870f49352cf980 100644 --- a/React/Fabric/Mounting/RCTComponentViewRegistry.mm +++ b/React/Fabric/Mounting/RCTComponentViewRegistry.mm @@ -18,60 +18,6 @@ using namespace facebook::react; -#define LEGACY_UIMANAGER_INTEGRATION_ENABLED 1 - -#ifdef LEGACY_UIMANAGER_INTEGRATION_ENABLED - -#import -#import - -/** - * Warning: This is a total hack and temporary solution. - * Unless we have a pure Fabric-based implementation of UIManager commands - * delivery pipeline, we have to leverage existing infra. This code tricks - * legacy UIManager by registering all Fabric-managed views in it, - * hence existing command-delivery infra can reach "foreign" views using - * the old pipeline. - */ -@interface RCTUIManager () -- (NSMutableDictionary *)viewRegistry; -@end - -@interface RCTUIManager (Hack) - -+ (void)registerView:(UIView *)view; -+ (void)unregisterView:(UIView *)view; - -@end - -@implementation RCTUIManager (Hack) - -+ (void)registerView:(UIView *)view -{ - if (!view) { - return; - } - - RCTUIManager *uiManager = [[RCTBridge currentBridge] uiManager]; - view.reactTag = @(view.tag); - [uiManager.viewRegistry setObject:view forKey:@(view.tag)]; -} - -+ (void)unregisterView:(UIView *)view -{ - if (!view) { - return; - } - - RCTUIManager *uiManager = [[RCTBridge currentBridge] uiManager]; - view.reactTag = nil; - [uiManager.viewRegistry removeObjectForKey:@(view.tag)]; -} - -@end - -#endif - const NSInteger RCTComponentViewRegistryRecyclePoolMaxSize = 1024; @implementation RCTComponentViewRegistry { @@ -133,10 +79,6 @@ - (RCTComponentViewDescriptor)dequeueComponentViewWithComponentHandle:(Component _registry.insert({tag, componentViewDescriptor}); -#ifdef LEGACY_UIMANAGER_INTEGRATION_ENABLED - [RCTUIManager registerView:componentViewDescriptor.view]; -#endif - return componentViewDescriptor; } @@ -149,10 +91,6 @@ - (void)enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandle RCTAssert( _registry.find(tag) != _registry.end(), @"RCTComponentViewRegistry: Attempt to enqueue unregistered component."); -#ifdef LEGACY_UIMANAGER_INTEGRATION_ENABLED - [RCTUIManager unregisterView:componentViewDescriptor.view]; -#endif - _registry.erase(tag); componentViewDescriptor.view.tag = 0; [self _enqueueComponentViewWithComponentHandle:componentHandle componentViewDescriptor:componentViewDescriptor]; diff --git a/React/Fabric/RCTConversions.h b/React/Fabric/RCTConversions.h index 5136eef321b87a..421ff298eb5ecd 100644 --- a/React/Fabric/RCTConversions.h +++ b/React/Fabric/RCTConversions.h @@ -30,9 +30,9 @@ inline NSString *_Nullable RCTNSStringFromStringNilIfEmpty( return string.empty() ? nil : RCTNSStringFromString(string, encoding); } -inline std::string RCTStringFromNSString(NSString *string, const NSStringEncoding &encoding = NSUTF8StringEncoding) +inline std::string RCTStringFromNSString(NSString *string) { - return [string cStringUsingEncoding:encoding]; + return std::string([string UTF8String]); } inline UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) diff --git a/React/Fabric/RCTScheduler.mm b/React/Fabric/RCTScheduler.mm index 67afffa3b0fcbf..da0a3a5ca5f96e 100644 --- a/React/Fabric/RCTScheduler.mm +++ b/React/Fabric/RCTScheduler.mm @@ -95,7 +95,7 @@ void activityDidChange(RunLoopObserver::Delegate const *delegate, RunLoopObserve }; @implementation RCTScheduler { - std::shared_ptr _scheduler; + std::unique_ptr _scheduler; std::shared_ptr _animationDriver; std::shared_ptr _delegateProxy; std::shared_ptr _layoutAnimationDelegateProxy; @@ -120,7 +120,7 @@ - (instancetype)initWithToolbox:(facebook::react::SchedulerToolbox)toolbox _uiRunLoopObserver->setDelegate(_layoutAnimationDelegateProxy.get()); } - _scheduler = std::make_shared( + _scheduler = std::make_unique( toolbox, (_animationDriver ? _animationDriver.get() : nullptr), _delegateProxy.get()); } @@ -138,8 +138,6 @@ - (void)dealloc _animationDriver->setLayoutAnimationStatusDelegate(nullptr); } _animationDriver = nullptr; - - _scheduler->setDelegate(nullptr); } - (void)startSurfaceWithSurfaceId:(SurfaceId)surfaceId diff --git a/React/Fabric/RCTSurfacePresenter.h b/React/Fabric/RCTSurfacePresenter.h index 38538abc99730b..061ed0da81683b 100644 --- a/React/Fabric/RCTSurfacePresenter.h +++ b/React/Fabric/RCTSurfacePresenter.h @@ -74,6 +74,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeObserver:(id)observer; +/* + * Please do not use this, this will be deleted soon. + */ +- (nullable UIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; + @end NS_ASSUME_NONNULL_END diff --git a/React/Fabric/RCTSurfacePresenter.mm b/React/Fabric/RCTSurfacePresenter.mm index 1f9cbc8d75de7e..79a0edd2a498b6 100644 --- a/React/Fabric/RCTSurfacePresenter.mm +++ b/React/Fabric/RCTSurfacePresenter.mm @@ -54,7 +54,8 @@ static inline LayoutContext RCTGetLayoutContext() { return {.pointScaleFactor = RCTScreenScale(), .swapLeftAndRightInRTL = - [[RCTI18nUtil sharedInstance] isRTL] && [[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]}; + [[RCTI18nUtil sharedInstance] isRTL] && [[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL], + .fontSizeMultiplier = RCTFontSizeMultiplier()}; } @interface RCTSurfacePresenter () @@ -190,6 +191,13 @@ - (void)setMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize surfa surfaceId:surface.rootTag]; } +- (UIView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag +{ + UIView *componentView = + [_mountingManager.componentViewRegistry findComponentViewWithTag:tag]; + return componentView; +} + - (BOOL)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag props:(NSDictionary *)props { RCTScheduler *scheduler = [self _scheduler]; diff --git a/React/Modules/RCTSurfacePresenterStub.h b/React/Modules/RCTSurfacePresenterStub.h index 402453e1bbb23b..f1bcc4c440ac13 100644 --- a/React/Modules/RCTSurfacePresenterStub.h +++ b/React/Modules/RCTSurfacePresenterStub.h @@ -8,6 +8,7 @@ #import #import +#import // TODO(macOS GH#774) NS_ASSUME_NONNULL_BEGIN @@ -26,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol RCTSurfacePresenterStub +- (nullable RCTPlatformView *)findComponentViewWithTag_DO_NOT_USE_DEPRECATED:(NSInteger)tag; // TODO(macOS GH#774) - (BOOL)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag props:(NSDictionary *)props; - (void)addObserver:(id)observer; - (void)removeObserver:(id)observer; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 76c55be3c57922..7368a270574529 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -8,6 +8,7 @@ #import "RCTUIManager.h" #import +#import #import // TODO(macOS GH#774) @@ -369,7 +370,11 @@ - (NSString *)viewNameForReactTag:(NSNumber *)reactTag - (RCTPlatformView *)viewForReactTag:(NSNumber *)reactTag // TODO(macOS GH#774) { RCTAssertMainQueue(); - return _viewRegistry[reactTag]; + RCTPlatformView *view = [_bridge.surfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue]; // TODO(macOS GH#774) + if (!view) { + view = _viewRegistry[reactTag]; + } + return view; } - (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag @@ -1475,7 +1480,8 @@ static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTRe { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // TODO(macOS GH#774) _jsResponder = viewRegistry[reactTag]; - if (!_jsResponder) { + // Fabric view's are not stored in viewRegistry. We avoid logging a warning in that case. + if (!_jsResponder && !RCTUIManagerTypeForTagIsFabric(reactTag)) { RCTLogWarn(@"Invalid view set to be the JS responder - tag %@", reactTag); } }]; diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index c4b3b22168fc1e..2d74542d5f430d 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -195,6 +195,87 @@ - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection #endif // ]TODO(macOS GH#774) } +#pragma mark - Hit Testing + +- (void)setPointerEvents:(RCTPointerEvents)pointerEvents +{ + _pointerEvents = pointerEvents; + self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); +#if !TARGET_OS_OSX // TODO(macOS GH#774) + if (pointerEvents == RCTPointerEventsBoxNone) { + self.accessibilityViewIsModal = NO; + } +#endif // TODO(macOS GH#774) +} + +- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // TODO(macOS GH#774) +{ + BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]); + if (!canReceiveTouchEvents) { + return nil; + } + + // `hitSubview` is the topmost subview which was hit. The hit point can + // be outside the bounds of `view` (e.g., if -clipsToBounds is NO). + RCTPlatformView *hitSubview = nil; // TODO(macOS GH#774) + 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]; // TODO(macOS ISS#3536887) + + // 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 + // the hit point. By doing hit testing directly on the subviews, we bypass + // 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 (RCTUIView *subview in [sortedSubviews reverseObjectEnumerator]) { // TODO(macOS ISS#3536887) + CGPoint pointForHitTest = CGPointZero; // [TODO(macOS GH#774) +#if TARGET_OS_OSX + if ([subview isKindOfClass:[RCTView class]]) { + pointForHitTest = [subview convertPoint:point fromView:self]; + } else { + pointForHitTest = point; + } +#else + pointForHitTest = [subview convertPoint:point fromView:self]; +#endif + hitSubview = RCTUIViewHitTestWithEvent(subview, pointForHitTest, event); // ]TODO(macOS GH#774) and TODO(macOS ISS#3536887) + if (hitSubview != nil) { + break; + } + } + } + + RCTPlatformView *hitView = (isPointInside ? self : nil); // TODO(macOS GH#774) + + switch (_pointerEvents) { + case RCTPointerEventsNone: + return nil; + case RCTPointerEventsUnspecified: + return hitSubview ?: hitView; + case RCTPointerEventsBoxOnly: + return hitView; + case RCTPointerEventsBoxNone: + return hitSubview; + default: + RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self); + return hitSubview ?: hitView; + } +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { + return [super pointInside:point withEvent:event]; + } + CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); + return CGRectContainsPoint(hitFrame, point); +} + +#pragma mark - Accessibility + - (NSString *)accessibilityLabel { NSString *label = super.accessibilityLabel; @@ -239,10 +320,8 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti if (!_onAccessibilityAction || !accessibilityActionsLabelMap) { return NO; } - - // iOS defines the name as the localized label, so use our map to convert this back to the non-localized action namne + // iOS defines the name as the localized label, so use our map to convert this back to the non-localized action name // when passing to JS. This allows for standard action names across platforms. - NSDictionary *actionObject = accessibilityActionsLabelMap[action.name]; if (actionObject) { _onAccessibilityAction(@{@"actionName" : actionObject[@"name"], @"actionTarget" : self.reactTag}); @@ -270,6 +349,7 @@ - (NSString *)accessibilityValue } } if (rolesAndStatesDescription == nil) { + // Falling back to hardcoded English list. NSLog(@"Cannot load localized accessibility strings."); rolesAndStatesDescription = @{ @"alert" : @"alert", @@ -298,6 +378,7 @@ - (NSString *)accessibilityValue } }); + // Handle Switch. if ((self.accessibilityTraits & SwitchAccessibilityTrait) == SwitchAccessibilityTrait) { for (NSString *state in self.accessibilityState) { id val = self.accessibilityState[state]; @@ -314,6 +395,8 @@ - (NSString *)accessibilityValue if (roleDescription) { [valueComponents addObject:roleDescription]; } + + // Handle states which haven't already been handled in RCTViewManager. for (NSString *state in self.accessibilityState) { id val = self.accessibilityState[state]; if (!val) { @@ -334,8 +417,7 @@ - (NSString *)accessibilityValue } } - // handle accessibilityValue - + // Handle accessibilityValue. if (self.accessibilityValueInternal) { id min = self.accessibilityValueInternal[@"min"]; id now = self.accessibilityValueInternal[@"now"]; @@ -519,83 +601,6 @@ - (id)accessibilityMaxValue { #endif // ]TODO(macOS GH#774) -- (void)setPointerEvents:(RCTPointerEvents)pointerEvents -{ - _pointerEvents = pointerEvents; - self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); -#if !TARGET_OS_OSX // TODO(macOS GH#774) - if (pointerEvents == RCTPointerEventsBoxNone) { - self.accessibilityViewIsModal = NO; - } -#endif // TODO(macOS GH#774) -} - -- (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // TODO(macOS GH#774) -{ - BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]); - if (!canReceiveTouchEvents) { - return nil; - } - - // `hitSubview` is the topmost subview which was hit. The hit point can - // be outside the bounds of `view` (e.g., if -clipsToBounds is NO). - RCTPlatformView *hitSubview = nil; // TODO(macOS GH#774) - 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]; // TODO(macOS ISS#3536887) - - // 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 - // the hit point. By doing hit testing directly on the subviews, we bypass - // 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 (RCTUIView *subview in [sortedSubviews reverseObjectEnumerator]) { // TODO(macOS ISS#3536887) - CGPoint pointForHitTest = CGPointZero; // [TODO(macOS GH#774) -#if TARGET_OS_OSX - if ([subview isKindOfClass:[RCTView class]]) { - pointForHitTest = [subview convertPoint:point fromView:self]; - } else { - pointForHitTest = point; - } -#else - pointForHitTest = [subview convertPoint:point fromView:self]; -#endif - hitSubview = RCTUIViewHitTestWithEvent(subview, pointForHitTest, event); // ]TODO(macOS GH#774) and TODO(macOS ISS#3536887) - if (hitSubview != nil) { - break; - } - } - } - - RCTPlatformView *hitView = (isPointInside ? self : nil); // TODO(macOS GH#774) - - switch (_pointerEvents) { - case RCTPointerEventsNone: - return nil; - case RCTPointerEventsUnspecified: - return hitSubview ?: hitView; - case RCTPointerEventsBoxOnly: - return hitView; - case RCTPointerEventsBoxNone: - return hitSubview; - default: - RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self); - return hitSubview ?: hitView; - } -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { - return [super pointInside:point withEvent:event]; - } - CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); - return CGRectContainsPoint(hitFrame, point); -} - - (RCTPlatformView *)reactAccessibilityElement // TODO(macOS GH#774) { return self; @@ -825,7 +830,7 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view } #endif // TODO(macOS GH#774) -#pragma mark - View unmounting +#pragma mark - View Unmounting - (void)react_remountAllSubviews { diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 28c37e0a0c692f..79c97a925bbb69 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -413,7 +413,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher #pragma clang diagnostic pop // TODO(OSS Candidate ISS#2710739) _automaticallyAdjustContentInsets = YES; - _DEPRECATED_sendUpdatedChildFrames = NO; _contentInset = UIEdgeInsetsZero; _contentSize = CGSizeZero; _lastClippedToRect = CGRectNull; @@ -865,13 +864,7 @@ - (void)scrollViewDidScroll:(RCTCustomScrollView *)scrollView // TODO(macOS GH#7 */ if (_allowNextScrollNoMatterWhat || (_scrollEventThrottle > 0 && _scrollEventThrottle < MAX(0.017, now - _lastScrollDispatchTime))) { - if (_DEPRECATED_sendUpdatedChildFrames) { - // Calculate changed frames - RCT_SEND_SCROLL_EVENT(onScroll, (@{@"updatedChildFrames" : [self calculateChildFramesData]})); - } else { - RCT_SEND_SCROLL_EVENT(onScroll, nil); - } - + RCT_SEND_SCROLL_EVENT(onScroll, nil); // Update dispatch time _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; @@ -883,38 +876,6 @@ - (void)scrollViewDidScroll:(RCTCustomScrollView *)scrollView // TODO(macOS GH#7 #endif // TODO(macOS GH#774) } -- (NSArray *)calculateChildFramesData -{ - NSMutableArray *updatedChildFrames = [NSMutableArray new]; - [[self.contentView reactSubviews] enumerateObjectsUsingBlock: // TODO(OSS Candidate ISS#2710739) use p - ^(RCTPlatformView *subview, NSUInteger idx, __unused BOOL *stop) { // TODO(macOS GH#774) - - // Check if new or changed - CGRect newFrame = subview.frame; - BOOL frameChanged = NO; - if (self->_cachedChildFrames.count <= idx) { - frameChanged = YES; - [self->_cachedChildFrames addObject:NSValueWithCGRect(newFrame)]; // TODO(macOS GH#774) - } else if (!CGRectEqualToRect(newFrame, CGRectValue(self->_cachedChildFrames[idx]))) { // TODO(macOS GH#774) - frameChanged = YES; - self->_cachedChildFrames[idx] = NSValueWithCGRect(newFrame); // TODO(macOS GH#774) - } - - // Create JS frame object - if (frameChanged) { - [updatedChildFrames addObject:@{ - @"index" : @(idx), - @"x" : @(newFrame.origin.x), - @"y" : @(newFrame.origin.y), - @"width" : @(newFrame.size.width), - @"height" : @(newFrame.size.height), - }]; - } - }]; - - return updatedChildFrames; -} - #if !TARGET_OS_OSX // TODO(macOS GH#774) - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index cabd0ccd224886..acc59de59e96c6 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -12,12 +12,6 @@ #import "RCTShadowView.h" #import "RCTUIManager.h" -@interface RCTScrollView (Private) - -- (NSArray *)calculateChildFramesData; - -@end - #if !TARGET_OS_OSX // TODO(macOS GH#774) @implementation RCTConvert (UIScrollView) @@ -111,7 +105,6 @@ - (RCTPlatformView *)view // TODO(macOS GH#774) RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollBegin, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(DEPRECATED_sendUpdatedChildFrames, BOOL) RCT_EXPORT_OSX_VIEW_PROPERTY(onScrollKeyDown, RCTDirectEventBlock) // TODO(macOS GH#774) RCT_EXPORT_OSX_VIEW_PROPERTY(onPreferredScrollerStyleDidChange, RCTDirectEventBlock) // TODO(macOS GH#774) RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL) @@ -145,22 +138,6 @@ - (RCTPlatformView *)view // TODO(macOS GH#774) }]; } -RCT_EXPORT_METHOD(calculateChildFrames : (nonnull NSNumber *)reactTag callback : (RCTResponseSenderBlock)callback) -{ - [self.bridge.uiManager - addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTScrollView *view = viewRegistry[reactTag]; - if (!view || ![view isKindOfClass:[RCTScrollView class]]) { - return; - } - - NSArray *childFrames = [view calculateChildFramesData]; - if (childFrames) { - callback(@[ childFrames ]); - } - }]; -} - RCT_EXPORT_METHOD(scrollTo : (nonnull NSNumber *)reactTag offsetX : (CGFloat)x offsetY diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java index 91710dbc5ebe0b..0bfc02aaaf2eca 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java @@ -66,6 +66,9 @@ public abstract void removeAnimatedEventFromView(double viewTag, String eventNam public abstract void startAnimatingNode(double animationId, double nodeTag, ReadableMap config, Callback endCallback); + @ReactMethod + public abstract void getValue(double tag, Callback saveValueCallback); + @ReactMethod public abstract void stopListeningToAnimatedNodeValue(double tag); diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java new file mode 100644 index 00000000000000..979b134c908d27 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java @@ -0,0 +1,29 @@ +/** + * 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. + * + *

Generated by an internal genrule from Flow types. + * + * @generated + * @nolint + */ + +package com.facebook.fbreact.specs; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; + +public abstract class NativeDevSplitBundleLoaderSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { + public NativeDevSplitBundleLoaderSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @ReactMethod + public abstract void loadBundle(String bundlePath, Promise promise); +} diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp index 62189bc3aba3e3..732ca58778364a 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp @@ -186,6 +186,10 @@ namespace facebook { return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, "createAnimatedNode", "(DLcom/facebook/react/bridge/ReadableMap;)V", args, count); } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_getValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, "getValue", "(DLcom/facebook/react/bridge/Callback;)V", args, count); + } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, "startListeningToAnimatedNodeValue", "(D)V", args, count); } @@ -265,6 +269,9 @@ namespace facebook { methodMap_["createAnimatedNode"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_createAnimatedNode}; + methodMap_["getValue"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_getValue}; + + methodMap_["startListeningToAnimatedNodeValue"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue}; @@ -785,6 +792,26 @@ namespace facebook { + } + + } // namespace react +} // namespace facebook +namespace facebook { + namespace react { + + + static facebook::jsi::Value __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, "loadBundle", "(Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count); + } + + + NativeDevSplitBundleLoaderSpecJSI::NativeDevSplitBundleLoaderSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + + methodMap_["loadBundle"] = MethodMetadata {1, __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle}; + + + } } // namespace react diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h index bb12df936070cd..c94b5f5ef7d214 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h @@ -241,6 +241,20 @@ namespace facebook { } // namespace react } // namespace facebook +namespace facebook { + namespace react { + /** + * C++ class for module 'DevSplitBundleLoader' + */ + + class JSI_EXPORT NativeDevSplitBundleLoaderSpecJSI : public JavaTurboModule { + public: + NativeDevSplitBundleLoaderSpecJSI(const JavaTurboModule::InitParams ¶ms); + + }; + } // namespace react +} // namespace facebook + namespace facebook { namespace react { /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index f18cc78f8f02c7..e2a345df18109c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -35,6 +35,7 @@ rn_android_library( react_native_target("java/com/facebook/react/module/model:model"), react_native_target("java/com/facebook/react/modules/appregistry:appregistry"), react_native_target("java/com/facebook/react/modules/appearance:appearance"), + react_native_target("java/com/facebook/react/modules/bundleloader:bundleloader"), react_native_target("java/com/facebook/react/modules/debug:debug"), react_native_target("java/com/facebook/react/modules/fabric:fabric"), react_native_target("java/com/facebook/react/modules/debug:interfaces"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 754d86ec08d5c4..36fb15b0a52d41 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -21,6 +21,7 @@ import com.facebook.react.module.annotations.ReactModuleList; import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.modules.bundleloader.NativeDevSplitBundleLoaderModule; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ExceptionsManagerModule; @@ -56,6 +57,7 @@ SourceCodeModule.class, TimingModule.class, UIManagerModule.class, + NativeDevSplitBundleLoaderModule.class, }) public class CoreModulesPackage extends TurboReactPackage implements ReactPackageLogger { @@ -101,7 +103,8 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { HeadlessJsTaskSupportModule.class, SourceCodeModule.class, TimingModule.class, - UIManagerModule.class + UIManagerModule.class, + NativeDevSplitBundleLoaderModule.class, }; final Map reactModuleInfoMap = new HashMap<>(); @@ -158,6 +161,9 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext) return createUIManager(reactContext); case DeviceInfoModule.NAME: return new DeviceInfoModule(reactContext); + case NativeDevSplitBundleLoaderModule.NAME: + return new NativeDevSplitBundleLoaderModule( + reactContext, mReactInstanceManager.getDevSupportManager()); default: throw new IllegalArgumentException( "In CoreModulesPackage, could not find Native module for " + name); diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index c1247471729ba2..45049cb5a65366 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -39,6 +39,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.modules.appregistry.AppRegistry; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.deviceinfo.DeviceInfoModule; @@ -431,6 +432,24 @@ private void updateRootLayoutSpecs(final int widthMeasureSpec, final int heightM @ThreadConfined(UI) public void unmountReactApplication() { UiThreadUtil.assertOnUiThread(); + // Stop surface in Fabric. + // Calling FabricUIManager.stopSurface causes the C++ Binding.stopSurface + // to be called synchronously over the JNI, which causes an empty tree + // to be committed via the Scheduler, which will cause mounting instructions + // to be queued up and synchronously executed to delete and remove + // all the views in the hierarchy. + if (mReactInstanceManager != null && ReactFeatureFlags.enableStopSurfaceOnRootViewUnmount) { + final ReactContext reactApplicationContext = mReactInstanceManager.getCurrentReactContext(); + if (reactApplicationContext != null && getUIManagerType() == FABRIC) { + @Nullable + UIManager uiManager = + UIManagerHelper.getUIManager(reactApplicationContext, getUIManagerType()); + if (uiManager != null) { + FLog.e(TAG, "stopSurface for surfaceId: " + this.getId()); + uiManager.stopSurface(this.getId()); + } + } + } if (mReactInstanceManager != null && mIsAttachedToInstance) { mReactInstanceManager.detachRootView(this); diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java index 3c3b595d2faa1f..550073f7924591 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/DivisionAnimatedNode.java @@ -42,12 +42,12 @@ public void update() { } if (value == 0) { throw new JSApplicationCausedNativeException( - "Detected a division by zero in " + "Animated.divide node"); + "Detected a division by zero in Animated.divide node with Animated ID " + mTag); } mValue /= value; } else { throw new JSApplicationCausedNativeException( - "Illegal node ID set as an input for " + "Animated.divide node"); + "Illegal node ID set as an input for Animated.divide node with Animated ID " + mTag); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index 1c05e52b92c67a..f85a092952b16f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.facebook.common.logging.FLog; import com.facebook.fbreact.specs.NativeAnimatedModuleSpec; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; @@ -27,10 +28,13 @@ import com.facebook.react.uimanager.GuardedFrameCallback; import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.UIManagerModule; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import com.facebook.react.uimanager.common.UIManagerType; +import com.facebook.react.uimanager.common.ViewUtil; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Module that exposes interface for creating and managing animated nodes on the "native" side. @@ -81,6 +85,7 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec implements LifecycleEventListener, UIManagerListener { public static final String NAME = "NativeAnimatedModule"; + public static final boolean ANIMATED_MODULE_DEBUG = false; private interface UIThreadOperation { void execute(NativeAnimatedNodesManager animatedNodesManager); @@ -88,14 +93,21 @@ private interface UIThreadOperation { @NonNull private final GuardedFrameCallback mAnimatedFrameCallback; private final ReactChoreographer mReactChoreographer; - @NonNull private List mOperations = new ArrayList<>(); - @NonNull private List mPreOperations = new ArrayList<>(); - @NonNull private List mPreOperationsUIBlock = new ArrayList<>(); - @NonNull private List mOperationsUIBlock = new ArrayList<>(); + @NonNull + private ConcurrentLinkedQueue mOperations = new ConcurrentLinkedQueue<>(); + + @NonNull + private ConcurrentLinkedQueue mPreOperations = new ConcurrentLinkedQueue<>(); private @Nullable NativeAnimatedNodesManager mNodesManager; + private volatile boolean mFabricBatchCompleted = false; + private boolean mInitializedForFabric = false; + private @UIManagerType int mUIManagerType = UIManagerType.DEFAULT; + private int mNumFabricAnimations = 0; + private int mNumNonFabricAnimations = 0; + public NativeAnimatedModule(ReactApplicationContext reactContext) { super(reactContext); @@ -147,68 +159,69 @@ public void onHostResume() { enqueueFrameCallback(); } - // For FabricUIManager + // For FabricUIManager only @Override - @UiThread - public void willDispatchPreMountItems() { - if (mPreOperationsUIBlock.size() != 0) { - List preOperations = mPreOperationsUIBlock; - mPreOperationsUIBlock = new ArrayList<>(); - - for (UIBlock op : preOperations) { - op.execute(null); - } + public void didScheduleMountItems(UIManager uiManager) { + if (mUIManagerType != UIManagerType.FABRIC) { + return; } + + mFabricBatchCompleted = true; } - // For FabricUIManager + // For FabricUIManager only @Override @UiThread - public void willDispatchMountItems() { - if (mOperationsUIBlock.size() != 0) { - List operations = mOperationsUIBlock; - mOperationsUIBlock = new ArrayList<>(); + public void didDispatchMountItems(UIManager uiManager) { + if (mUIManagerType != UIManagerType.FABRIC) { + return; + } - for (UIBlock op : operations) { - op.execute(null); - } + if (mFabricBatchCompleted) { + // This will execute all operations and preOperations queued + // since the last time this was run, and will race with anything + // being queued from the JS thread. That is, if the JS thread + // is still queuing operations, we might execute some of them + // at the very end until we exhaust the queue faster than the + // JS thread can queue up new items. + executeAllOperations(mPreOperations); + executeAllOperations(mOperations); + mFabricBatchCompleted = false; } } - // For non-FabricUIManager + private void executeAllOperations(Queue operationQueue) { + NativeAnimatedNodesManager nodesManager = getNodesManager(); + while (!operationQueue.isEmpty()) { + operationQueue.poll().execute(nodesManager); + } + } + + // For non-FabricUIManager only @Override @UiThread public void willDispatchViewUpdates(final UIManager uiManager) { if (mOperations.isEmpty() && mPreOperations.isEmpty()) { return; } + if (mUIManagerType == UIManagerType.FABRIC) { + return; + } - final AtomicBoolean hasRunPreOperations = new AtomicBoolean(false); - final AtomicBoolean hasRunOperations = new AtomicBoolean(false); - final List preOperations = mPreOperations; - final List operations = mOperations; - mPreOperations = new ArrayList<>(); - mOperations = new ArrayList<>(); - - // This is kind of a hack. Basically UIManagerListener cannot import UIManagerModule - // (that would cause an import cycle) and they're not in the same package. But, - // UIManagerModule is the only thing that calls `willDispatchViewUpdates` so we - // know this is safe. - // This goes away entirely in Fabric/Venice. - UIManagerModule uiManagerModule = (UIManagerModule) uiManager; + final Queue preOperations = new LinkedList<>(); + final Queue operations = new LinkedList<>(); + while (!mPreOperations.isEmpty()) { + preOperations.add(mPreOperations.poll()); + } + while (!mOperations.isEmpty()) { + operations.add(mOperations.poll()); + } UIBlock preOperationsUIBlock = new UIBlock() { @Override public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { - if (!hasRunPreOperations.compareAndSet(false, true)) { - return; - } - - NativeAnimatedNodesManager nodesManager = getNodesManager(); - for (UIThreadOperation operation : preOperations) { - operation.execute(nodesManager); - } + executeAllOperations(preOperations); } }; @@ -216,23 +229,13 @@ public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { new UIBlock() { @Override public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { - if (!hasRunOperations.compareAndSet(false, true)) { - return; - } - NativeAnimatedNodesManager nodesManager = getNodesManager(); - for (UIThreadOperation operation : operations) { - operation.execute(nodesManager); - } + executeAllOperations(operations); } }; - // Queue up operations for Fabric - mPreOperationsUIBlock.add(preOperationsUIBlock); - mOperationsUIBlock.add(operationsUIBlock); - - // Here we queue up the UI Blocks for the old, non-Fabric UIManager. - // We queue them in both systems, let them race, and see which wins. + assert (uiManager instanceof UIManagerModule); + UIManagerModule uiManagerModule = (UIManagerModule) uiManager; uiManagerModule.prependUIBlock(preOperationsUIBlock); uiManagerModule.addUIBlock(operationsUIBlock); } @@ -265,10 +268,7 @@ private NativeAnimatedNodesManager getNodesManager() { ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); if (reactApplicationContext != null) { - UIManagerModule uiManager = - Assertions.assertNotNull( - reactApplicationContext.getNativeModule(UIManagerModule.class)); - mNodesManager = new NativeAnimatedNodesManager(uiManager); + mNodesManager = new NativeAnimatedNodesManager(reactApplicationContext); } } @@ -295,11 +295,23 @@ public void setNodesManager(NativeAnimatedNodesManager nodesManager) { @Override public void createAnimatedNode(final double tagDouble, final ReadableMap config) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue createAnimatedNode: " + tag + " config: " + config.toHashMap().toString()); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute createAnimatedNode: " + + tag + + " config: " + + config.toHashMap().toString()); + } animatedNodesManager.createAnimatedNode(tag, config); } }); @@ -308,6 +320,9 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void startListeningToAnimatedNodeValue(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue startListeningToAnimatedNodeValue: " + tag); + } final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() { @@ -330,6 +345,9 @@ public void onValueUpdate(double value) { new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute startListeningToAnimatedNodeValue: " + tag); + } animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener); } }); @@ -338,11 +356,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void stopListeningToAnimatedNodeValue(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue stopListeningToAnimatedNodeValue: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute stopListeningToAnimatedNodeValue: " + tag); + } animatedNodesManager.stopListeningToAnimatedNodeValue(tag); } }); @@ -351,11 +375,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void dropAnimatedNode(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue dropAnimatedNode: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute dropAnimatedNode: " + tag); + } animatedNodesManager.dropAnimatedNode(tag); } }); @@ -364,11 +394,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void setAnimatedNodeValue(final double tagDouble, final double value) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue setAnimatedNodeValue: " + tag + " value: " + value); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute setAnimatedNodeValue: " + tag + " value: " + value); + } animatedNodesManager.setAnimatedNodeValue(tag, value); } }); @@ -377,11 +413,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void setAnimatedNodeOffset(final double tagDouble, final double value) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue setAnimatedNodeOffset: " + tag + " offset: " + value); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute setAnimatedNodeOffset: " + tag + " offset: " + value); + } animatedNodesManager.setAnimatedNodeOffset(tag, value); } }); @@ -390,11 +432,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void flattenAnimatedNodeOffset(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue flattenAnimatedNodeOffset: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute flattenAnimatedNodeOffset: " + tag); + } animatedNodesManager.flattenAnimatedNodeOffset(tag); } }); @@ -403,11 +451,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void extractAnimatedNodeOffset(final double tagDouble) { final int tag = (int) tagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue extractAnimatedNodeOffset: " + tag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute extractAnimatedNodeOffset: " + tag); + } animatedNodesManager.extractAnimatedNodeOffset(tag); } }); @@ -421,11 +475,19 @@ public void startAnimatingNode( final Callback endCallback) { final int animationId = (int) animationIdDouble; final int animatedNodeTag = (int) animatedNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute startAnimatingNode: ID: " + animationId + " tag: " + animatedNodeTag); + } animatedNodesManager.startAnimatingNode( animationId, animatedNodeTag, animationConfig, endCallback); } @@ -435,11 +497,17 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void stopAnimation(final double animationIdDouble) { final int animationId = (int) animationIdDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "queue stopAnimation: ID: " + animationId); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d(NAME, "execute stopAnimation: ID: " + animationId); + } animatedNodesManager.stopAnimation(animationId); } }); @@ -450,11 +518,23 @@ public void connectAnimatedNodes( final double parentNodeTagDouble, final double childNodeTagDouble) { final int parentNodeTag = (int) parentNodeTagDouble; final int childNodeTag = (int) childNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue connectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodes: parent: " + + parentNodeTag + + " child: " + + childNodeTag); + } animatedNodesManager.connectAnimatedNodes(parentNodeTag, childNodeTag); } }); @@ -465,11 +545,24 @@ public void disconnectAnimatedNodes( final double parentNodeTagDouble, final double childNodeTagDouble) { final int parentNodeTag = (int) parentNodeTagDouble; final int childNodeTag = (int) childNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue disconnectAnimatedNodes: parent: " + parentNodeTag + " child: " + childNodeTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute disconnectAnimatedNodes: parent: " + + parentNodeTag + + " child: " + + childNodeTag); + } animatedNodesManager.disconnectAnimatedNodes(parentNodeTag, childNodeTag); } }); @@ -480,11 +573,27 @@ public void connectAnimatedNodeToView( final double animatedNodeTagDouble, final double viewTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue connectAnimatedNodeToView: animatedNodeTag: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodeToView: animatedNodeTag: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } animatedNodesManager.connectAnimatedNodeToView(animatedNodeTag, viewTag); } }); @@ -495,11 +604,27 @@ public void disconnectAnimatedNodeFromView( final double animatedNodeTagDouble, final double viewTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue connectAnimatedNodeToView: disconnectAnimatedNodeFromView: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute connectAnimatedNodeToView: disconnectAnimatedNodeFromView: " + + animatedNodeTag + + " viewTag: " + + viewTag); + } animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag); } }); @@ -508,11 +633,21 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @Override public void restoreDefaultValues(final double animatedNodeTagDouble) { final int animatedNodeTag = (int) animatedNodeTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, "queue restoreDefaultValues: disconnectAnimatedNodeFromView: " + animatedNodeTag); + } mPreOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute restoreDefaultValues: disconnectAnimatedNodeFromView: " + + animatedNodeTag); + } animatedNodesManager.restoreDefaultValues(animatedNodeTag); } }); @@ -522,11 +657,52 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { public void addAnimatedEventToView( final double viewTagDouble, final String eventName, final ReadableMap eventMapping) { final int viewTag = (int) viewTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue addAnimatedEventToView: " + + viewTag + + " eventName: " + + eventName + + " eventMapping: " + + eventMapping.toHashMap().toString()); + } + + mUIManagerType = ViewUtil.getUIManagerType(viewTag); + if (mUIManagerType == UIManagerType.FABRIC) { + mNumFabricAnimations++; + } else { + mNumNonFabricAnimations++; + } + + // Subscribe to FabricUIManager lifecycle events if we haven't yet + if (!mInitializedForFabric && mUIManagerType == UIManagerType.FABRIC) { + ReactApplicationContext reactApplicationContext = getReactApplicationContext(); + if (reactApplicationContext != null) { + @Nullable + UIManager uiManager = + UIManagerHelper.getUIManager(reactApplicationContext, UIManagerType.FABRIC); + if (uiManager != null) { + uiManager.addUIManagerEventListener(this); + mInitializedForFabric = true; + } + } + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute addAnimatedEventToView: " + + viewTag + + " eventName: " + + eventName + + " eventMapping: " + + eventMapping.toHashMap().toString()); + } animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping); } }); @@ -537,11 +713,53 @@ public void removeAnimatedEventFromView( final double viewTagDouble, final String eventName, final double animatedValueTagDouble) { final int viewTag = (int) viewTagDouble; final int animatedValueTag = (int) animatedValueTagDouble; + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "queue addAnimatedEventToView: removeAnimatedEventFromView: " + + viewTag + + " eventName: " + + eventName + + " animatedValueTag: " + + animatedValueTag); + } + + @UIManagerType int animationManagerType = ViewUtil.getUIManagerType(viewTag); + if (animationManagerType == UIManagerType.FABRIC) { + mNumFabricAnimations--; + } else { + mNumNonFabricAnimations--; + } + + // Should we switch to a different animation mode? + // This can be useful when navigating between Fabric and non-Fabric screens: + // If there are ongoing Fabric animations from a previous screen, + // and we tear down the current non-Fabric screen, we should expect + // the animation mode to switch back - and vice-versa. + if (mNumNonFabricAnimations == 0 + && mNumFabricAnimations > 0 + && mUIManagerType != UIManagerType.FABRIC) { + mUIManagerType = UIManagerType.FABRIC; + } else if (mNumFabricAnimations == 0 + && mNumNonFabricAnimations > 0 + && mUIManagerType != UIManagerType.DEFAULT) { + mUIManagerType = UIManagerType.DEFAULT; + } mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { + if (ANIMATED_MODULE_DEBUG) { + FLog.d( + NAME, + "execute addAnimatedEventToView: removeAnimatedEventFromView: " + + viewTag + + " eventName: " + + eventName + + " animatedValueTag: " + + animatedValueTag); + } animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName, animatedValueTag); } }); @@ -556,4 +774,16 @@ public void addListener(String eventName) { public void removeListeners(double count) { // iOS only } + + @Override + public void getValue(final double animatedValueNodeTagDouble, final Callback callback) { + final int animatedValueNodeTag = (int) animatedValueNodeTagDouble; + mOperations.add( + new UIThreadOperation() { + @Override + public void execute(NativeAnimatedNodesManager animatedNodesManager) { + animatedNodesManager.getValue(animatedValueNodeTag, callback); + } + }); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 2c99f40d655a5f..f1c76b780d10e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -10,16 +10,21 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationCausedNativeException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactNoCrashSoftException; +import com.facebook.react.bridge.ReactSoftException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; @@ -48,6 +53,9 @@ */ /*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener { + private static final String TAG = "NativeAnimatedNodesManager"; + private static final int MAX_INCONSISTENT_FRAMES = 64; + private final SparseArray mAnimatedNodes = new SparseArray<>(); private final SparseArray mActiveAnimations = new SparseArray<>(); private final SparseArray mUpdatedNodes = new SparseArray<>(); @@ -55,14 +63,19 @@ // there will be only one driver per mapping so all code code should be optimized around that. private final Map> mEventDrivers = new HashMap<>(); private final UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver; - private final UIManager mUIManager; + private final ReactApplicationContext mReactApplicationContext; private int mAnimatedGraphBFSColor = 0; + private int mNumInconsistentFrames = 0; // Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`. private final List mRunUpdateNodeList = new LinkedList<>(); - public NativeAnimatedNodesManager(UIManagerModule uiManager) { - mUIManager = uiManager; - mUIManager.getEventDispatcher().addListener(this); + public NativeAnimatedNodesManager(ReactApplicationContext reactApplicationContext) { + mReactApplicationContext = reactApplicationContext; + + UIManagerModule uiManager = + Assertions.assertNotNull(reactApplicationContext.getNativeModule(UIManagerModule.class)); + + uiManager.getEventDispatcher().addListener(this); // TODO T64216139 Remove dependency of UIManagerModule when the Constants are not in Native // anymore mCustomEventNamesResolver = uiManager.getDirectEventNamesResolver(); @@ -89,7 +102,7 @@ public void createAnimatedNode(int tag, ReadableMap config) { } else if ("value".equals(type)) { node = new ValueAnimatedNode(config); } else if ("props".equals(type)) { - node = new PropsAnimatedNode(config, this, mUIManager); + node = new PropsAnimatedNode(config, this); } else if ("interpolation".equals(type)) { node = new InterpolationAnimatedNode(config); } else if ("addition".equals(type)) { @@ -125,7 +138,7 @@ public void startListeningToAnimatedNodeValue(int tag, AnimatedNodeValueListener AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } ((ValueAnimatedNode) node).setValueListener(listener); } @@ -134,7 +147,7 @@ public void stopListeningToAnimatedNodeValue(int tag) { AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } ((ValueAnimatedNode) node).setValueListener(null); } @@ -143,7 +156,7 @@ public void setAnimatedNodeValue(int tag, double value) { AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } stopAnimationsForNode(node); ((ValueAnimatedNode) node).mValue = value; @@ -154,7 +167,7 @@ public void setAnimatedNodeOffset(int tag, double offset) { AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } ((ValueAnimatedNode) node).mOffset = offset; mUpdatedNodes.put(tag, node); @@ -164,7 +177,7 @@ public void flattenAnimatedNodeOffset(int tag) { AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } ((ValueAnimatedNode) node).flattenOffset(); } @@ -173,7 +186,7 @@ public void extractAnimatedNodeOffset(int tag) { AnimatedNode node = mAnimatedNodes.get(tag); if (node == null || !(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + "Animated node with tag " + tag + " does not exist, or is not a 'value' node"); } ((ValueAnimatedNode) node).extractOffset(); } @@ -183,7 +196,7 @@ public void startAnimatingNode( AnimatedNode node = mAnimatedNodes.get(animatedNodeTag); if (node == null) { throw new JSApplicationIllegalArgumentException( - "Animated node with tag " + animatedNodeTag + " does not exists"); + "Animated node with tag " + animatedNodeTag + " does not exist"); } if (!(node instanceof ValueAnimatedNode)) { throw new JSApplicationIllegalArgumentException( @@ -301,8 +314,25 @@ public void connectAnimatedNodeToView(int animatedNodeTag, int viewTag) { + "of type " + PropsAnimatedNode.class.getName()); } + if (mReactApplicationContext == null) { + throw new IllegalStateException( + "Animated node could not be connected, no ReactApplicationContext: " + viewTag); + } + + @Nullable + UIManager uiManager = + UIManagerHelper.getUIManagerForReactTag(mReactApplicationContext, viewTag); + if (uiManager == null) { + ReactSoftException.logSoftException( + TAG, + new ReactNoCrashSoftException( + "Animated node could not be connected to UIManager - uiManager disappeared for tag: " + + viewTag)); + return; + } + PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node; - propsAnimatedNode.connectToView(viewTag); + propsAnimatedNode.connectToView(viewTag, uiManager); mUpdatedNodes.put(animatedNodeTag, node); } @@ -322,6 +352,15 @@ public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) { propsAnimatedNode.disconnectFromView(viewTag); } + public void getValue(int tag, Callback callback) { + AnimatedNode node = mAnimatedNodes.get(tag); + if (node == null || !(node instanceof ValueAnimatedNode)) { + throw new JSApplicationIllegalArgumentException( + "Animated node with tag " + tag + " does not exists or is not a 'value' node"); + } + callback.invoke(((ValueAnimatedNode) node).getValue()); + } + public void restoreDefaultValues(int animatedNodeTag) { AnimatedNode node = mAnimatedNodes.get(animatedNodeTag); // Restoring default values needs to happen before UIManager operations so it is @@ -543,26 +582,43 @@ private void updateNodes(List nodes) { } // Run main "update" loop + boolean errorsCaught = false; while (!nodesQueue.isEmpty()) { AnimatedNode nextNode = nodesQueue.poll(); - nextNode.update(); - if (nextNode instanceof PropsAnimatedNode) { - // Send property updates to native view manager - try { + try { + nextNode.update(); + if (nextNode instanceof PropsAnimatedNode) { + // Send property updates to native view manager ((PropsAnimatedNode) nextNode).updateView(); - } catch (IllegalViewOperationException e) { - // An exception is thrown if the view hasn't been created yet. This can happen because - // views are - // created in batches. If this particular view didn't make it into a batch yet, the view - // won't - // exist and an exception will be thrown when attempting to start an animation on it. - // - // Eat the exception rather than crashing. The impact is that we may drop one or more - // frames of the - // animation. + } + } catch (IllegalViewOperationException e) { + // An exception is thrown if the view hasn't been created yet. This can happen because + // views are + // created in batches. If this particular view didn't make it into a batch yet, the view + // won't + // exist and an exception will be thrown when attempting to start an animation on it. + // + // Eat the exception rather than crashing. The impact is that we may drop one or more + // frames of the + // animation. + FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e); + } catch (JSApplicationCausedNativeException e) { + // In Fabric there can be race conditions between the JS thread setting up or tearing down + // animated nodes, and Fabric executing them on the UI thread, leading to temporary + // inconsistent + // states. We require that the inconsistency last for N frames before throwing these + // exceptions. + if (!errorsCaught) { + errorsCaught = true; + mNumInconsistentFrames++; + } + if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) { + throw new IllegalStateException(e); + } else { FLog.e( - ReactConstants.TAG, - "Native animation workaround, frame lost as result of race condition", + TAG, + "Swallowing exception due to potential race between JS and UI threads: inconsistent frame counter: " + + mNumInconsistentFrames, e); } } @@ -586,13 +642,23 @@ private void updateNodes(List nodes) { // Verify that we've visited *all* active nodes. Throw otherwise as this would mean there is a // cycle in animated node graph. We also take advantage of the fact that all active nodes are // visited in the step above so that all the nodes properties `mActiveIncomingNodes` are set to - // zero + // zero. + // In Fabric there can be race conditions between the JS thread setting up or tearing down + // animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent + // states. We require that the inconsistency last for 64 frames before throwing this exception. if (activeNodesCount != updatedNodesCount) { - throw new IllegalStateException( - "Looks like animated nodes graph has cycles, there are " - + activeNodesCount - + " but toposort visited only " - + updatedNodesCount); + if (!errorsCaught) { + mNumInconsistentFrames++; + } + if (mNumInconsistentFrames > MAX_INCONSISTENT_FRAMES) { + throw new IllegalStateException( + "Looks like animated nodes graph has cycles, there are " + + activeNodesCount + + " but toposort visited only " + + updatedNodesCount); + } + } else if (!errorsCaught) { + mNumInconsistentFrames = 0; } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java index 2eea21b4954266..42cb57a7ec3a8e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -25,14 +25,11 @@ private int mConnectedViewTag = -1; private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; - private final UIManager mUIManager; private final Map mPropNodeMapping; private final JavaOnlyMap mPropMap; + @Nullable private UIManager mUIManager; - PropsAnimatedNode( - ReadableMap config, - NativeAnimatedNodesManager nativeAnimatedNodesManager, - UIManager uiManager) { + PropsAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { ReadableMap props = config.getMap("props"); ReadableMapKeySetIterator iter = props.keySetIterator(); mPropNodeMapping = new HashMap<>(); @@ -43,28 +40,36 @@ } mPropMap = new JavaOnlyMap(); mNativeAnimatedNodesManager = nativeAnimatedNodesManager; - mUIManager = uiManager; } - public void connectToView(int viewTag) { + public void connectToView(int viewTag, UIManager uiManager) { if (mConnectedViewTag != -1) { throw new JSApplicationIllegalArgumentException( - "Animated node " + mTag + " is " + "already attached to a view"); + "Animated node " + mTag + " is " + "already attached to a view: " + mConnectedViewTag); } mConnectedViewTag = viewTag; + mUIManager = uiManager; } public void disconnectFromView(int viewTag) { - if (mConnectedViewTag != viewTag) { + if (mConnectedViewTag != viewTag && mConnectedViewTag != -1) { throw new JSApplicationIllegalArgumentException( "Attempting to disconnect view that has " - + "not been connected with the given animated node"); + + "not been connected with the given animated node: " + + viewTag + + " but is connected to view " + + mConnectedViewTag); } mConnectedViewTag = -1; } public void restoreDefaultValues() { + // Cannot restore default values if this view has already been disconnected. + if (mConnectedViewTag == -1) { + return; + } + ReadableMapKeySetIterator it = mPropMap.keySetIterator(); while (it.hasNextKey()) { mPropMap.putNull(it.nextKey()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index 0a9e52b8f958a1..dae969346aa9be 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -240,6 +240,11 @@ public void loadScriptFromFile(String fileName, String sourceURL, boolean loadSy jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously); } + @Override + public void loadSplitBundleFromFile(String fileName, String sourceURL) { + jniLoadScriptFromFile(fileName, sourceURL, false); + } + private native void jniSetSourceURL(String sourceURL); private native void jniRegisterSegment(int segmentId, String path); @@ -335,16 +340,6 @@ public void destroy() { FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() start"); UiThreadUtil.assertOnUiThread(); - if (ReactFeatureFlags.useCatalystTeardownV2) { - destroyV2(); - } else { - destroyV1(); - } - } - - @ThreadConfined(UI) - public void destroyV1() { - FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.destroyV1() start"); UiThreadUtil.assertOnUiThread(); if (mDestroyed) { @@ -422,101 +417,6 @@ public void run() { Systrace.unregisterListener(mTraceListener); } - /** - * Destroys this catalyst instance, waiting for any other threads in ReactQueueConfiguration - * (besides the UI thread) to finish running. Must be called from the UI thread so that we can - * fully shut down other threads. - */ - @ThreadConfined(UI) - public void destroyV2() { - FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.destroyV2() start"); - UiThreadUtil.assertOnUiThread(); - - if (mDestroyed) { - return; - } - - // TODO: tell all APIs to shut down - ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_START); - mDestroyed = true; - mNativeModulesThreadDestructionComplete = false; - mJSThreadDestructionComplete = false; - - mNativeModulesQueueThread.runOnQueue( - new Runnable() { - @Override - public void run() { - FLog.d("CatalystInstanceImpl", ".destroy on native modules thread"); - mNativeModuleRegistry.notifyJSInstanceDestroy(); - - // Notifies all JSI modules that they are being destroyed, including the FabricUIManager - // and Fabric Scheduler - mJSIModuleRegistry.notifyJSInstanceDestroy(); - boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); - if (!mBridgeIdleListeners.isEmpty()) { - for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { - if (!wasIdle) { - listener.onTransitionToBridgeIdle(); - } - listener.onBridgeDestroyed(); - } - } - - mNativeModulesThreadDestructionComplete = true; - FLog.d("CatalystInstanceImpl", ".destroy on native modules thread finished"); - } - }); - - getReactQueueConfiguration() - .getJSQueueThread() - .runOnQueue( - new Runnable() { - @Override - public void run() { - FLog.d("CatalystInstanceImpl", ".destroy on JS thread"); - // We need to destroy the TurboModuleManager on the JS Thread - if (mTurboModuleManagerJSIModule != null) { - mTurboModuleManagerJSIModule.onCatalystInstanceDestroy(); - } - - mJSThreadDestructionComplete = true; - FLog.d("CatalystInstanceImpl", ".destroy on JS thread finished"); - } - }); - - // Wait until destruction is complete - long waitStartTime = System.currentTimeMillis(); - while (!mNativeModulesThreadDestructionComplete || !mJSThreadDestructionComplete) { - // Never wait here, blocking the UI thread, for more than 100ms - if ((System.currentTimeMillis() - waitStartTime) > 100) { - FLog.w( - ReactConstants.TAG, - "CatalystInstanceImpl.destroy() timed out waiting for Native Modules and JS thread teardown"); - break; - } - } - - // Kill non-UI threads from neutral third party - // potentially expensive, so don't run on UI thread - - // contextHolder is used as a lock to guard against - // other users of the JS VM having the VM destroyed - // underneath them, so notify them before we reset - // Native - mJavaScriptContextHolder.clear(); - - // Imperatively destruct the C++ CatalystInstance rather than - // wait for the JVM's GC to free it. - mHybridData.resetNative(); - - getReactQueueConfiguration().destroy(); - FLog.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end"); - ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END); - - // This is a noop if the listener was not yet registered. - Systrace.unregisterListener(mTraceListener); - } - @Override public boolean isDestroyed() { return mDestroyed; diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java index d8d577b1aa6ea6..5ff5d4a305113c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java @@ -73,6 +73,25 @@ public String loadScript(JSBundleLoaderDelegate delegate) { }; } + /** + * Same as {{@link JSBundleLoader#createCachedBundleFromNetworkLoader(String, String)}}, but for + * split bundles in development. + */ + public static JSBundleLoader createCachedSplitBundleFromNetworkLoader( + final String sourceURL, final String cachedFileLocation) { + return new JSBundleLoader() { + @Override + public String loadScript(JSBundleLoaderDelegate delegate) { + try { + delegate.loadSplitBundleFromFile(cachedFileLocation, sourceURL); + return sourceURL; + } catch (Exception e) { + throw DebugServerException.makeGeneric(sourceURL, e.getMessage(), e); + } + } + }; + } + /** * This loader is used when proxy debugging is enabled. In that case there is no point in fetching * the bundle from device as remote executor will have to do it anyway. diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoaderDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoaderDelegate.java index 7b2121f5a26438..f03cd14ae18502 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoaderDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoaderDelegate.java @@ -33,6 +33,12 @@ public interface JSBundleLoaderDelegate { */ void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously); + /** + * Load a split JS bundle from the filesystem. See {@link + * JSBundleLoader#createCachedSplitBundleFromNetworkLoader(String, String)}. + */ + void loadSplitBundleFromFile(String fileName, String sourceURL); + /** * This API is used in situations where the JS bundle is being executed not on the device, but on * a host machine. In that case, we must provide two source URLs for the JS bundle: One to be used diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java index 126237e74e8e31..27d44870176762 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java @@ -296,9 +296,6 @@ public void destroy() { mDestroyed = true; if (mCatalystInstance != null) { mCatalystInstance.destroy(); - if (ReactFeatureFlags.nullifyCatalystInstanceOnDestroy) { - mCatalystInstance = null; - } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java index 4edebdcf00ea54..1707c623edbbc8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java @@ -32,6 +32,13 @@ int startSurface( int widthMeasureSpec, int heightMeasureSpec); + /** + * Stop a surface from running in JS and clears up native memory usage. Assumes that the native + * View hierarchy has already been cleaned up. Fabric-only. + */ + @AnyThread + void stopSurface(final int surfaceId); + /** * Updates the layout specs of the RootShadowNode based on the Measure specs received by * parameters. @@ -106,4 +113,14 @@ int startSurface( * @param listener */ void removeUIManagerEventListener(UIManagerListener listener); + + /** + * This method dispatches events from RN Android code to JS. The delivery of this event will not + * be queued in EventDispatcher class. + * + * @param reactTag tag + * @param eventName name of the event + * @param event parameters + */ + void receiveEvent(int reactTag, String eventName, @Nullable WritableMap event); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java index 216d38b00d6495..c190e0a8c02d14 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManagerListener.java @@ -14,8 +14,8 @@ public interface UIManagerListener { * module needs to add UIBlocks to the queue before it is flushed. */ void willDispatchViewUpdates(UIManager uiManager); - /** Called on the UI thread right before normal mount items are executed. */ - void willDispatchMountItems(); - /** Called on the UI thread right before premount items are executed. */ - void willDispatchPreMountItems(); + /* Called right after view updates are dispatched for a frame. */ + void didDispatchMountItems(UIManager uiManager); + /* Called right after scheduleMountItems is called in Fabric, after a new tree is committed. */ + void didScheduleMountItems(UIManager uiManager); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/DebugServerException.java b/ReactAndroid/src/main/java/com/facebook/react/common/DebugServerException.java index 7f8fe6d409902d..00f52a84c09a0b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/DebugServerException.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/DebugServerException.java @@ -40,16 +40,25 @@ public static DebugServerException makeGeneric( return new DebugServerException(reason + message + extra, t); } + private final String mOriginalMessage; + private DebugServerException(String description, String fileName, int lineNumber, int column) { super(description + "\n at " + fileName + ":" + lineNumber + ":" + column); + mOriginalMessage = description; } public DebugServerException(String description) { super(description); + mOriginalMessage = description; } public DebugServerException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); + mOriginalMessage = detailMessage; + } + + public String getOriginalMessage() { + return mOriginalMessage; } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 26025c3b4aa9a3..94e6349153074a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -41,28 +41,7 @@ public class ReactFeatureFlags { * inside view manager will be called instead. */ public static boolean useViewManagerDelegatesForCommands = false; - - /** - * Should this application use Catalyst Teardown V2? This is an experiment to use a V2 of the - * CatalystInstanceImpl `destroy` method. - */ - public static boolean useCatalystTeardownV2 = false; - - /** - * When the ReactContext is destroyed, should the CatalystInstance immediately be nullified? This - * is the safest thing to do since the CatalystInstance shouldn't be used, and should be - * garbage-collected after it's destroyed, but this is a breaking change in that many native - * modules assume that a ReactContext will always have a CatalystInstance. This will be deleted - * and the CatalystInstance will always be destroyed in some future release. - */ - public static boolean nullifyCatalystInstanceOnDestroy = false; - - /** - * Temporary flag. See UIImplementation: if this flag is enabled, ViewCommands will be queued and - * executed before any other types of UI operations. - */ - public static boolean allowEarlyViewCommandExecution = false; - + /** * This react flag enables a custom algorithm for the getChildVisibleRect() method in the classes * ReactViewGroup, ReactHorizontalScrollView and ReactScrollView. @@ -87,6 +66,6 @@ public class ReactFeatureFlags { /** Feature flag to configure initialization of Fabric surfaces. */ public static boolean enableFabricStartSurfaceWithLayoutMetrics = true; - /** Feature flag to have FabricUIManager teardown stop all active surfaces. */ - public static boolean enableFabricStopAllSurfacesOnTeardown = false; + /** Feature flag to use stopSurface when ReactRootView is unmounted. */ + public static boolean enableStopSurfaceOnRootViewUnmount = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java index 17235dd9d46356..ec4aa94fa3708f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java @@ -107,10 +107,7 @@ public void downloadBundleFromURL( Request.Builder requestBuilder) { final Request request = - requestBuilder - .url(formatBundleUrl(bundleURL)) - .addHeader("Accept", "multipart/mixed") - .build(); + requestBuilder.url(bundleURL).addHeader("Accept", "multipart/mixed").build(); mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request)); mDownloadBundleFromURLCall.enqueue( new Callback() { @@ -165,10 +162,6 @@ public void onResponse(Call call, final Response response) throws IOException { }); } - private String formatBundleUrl(String bundleURL) { - return bundleURL; - } - private void processMultipartResponse( final String url, final Response response, @@ -207,10 +200,8 @@ public void onChunkComplete( try { JSONObject progress = new JSONObject(body.readUtf8()); - String status = null; - if (progress.has("status")) { - status = progress.getString("status"); - } + String status = + progress.has("status") ? progress.getString("status") : "Bundling"; Integer done = null; if (progress.has("done")) { done = progress.getInt("done"); @@ -227,11 +218,9 @@ public void onChunkComplete( } @Override - public void onChunkProgress(Map headers, long loaded, long total) - throws IOException { + public void onChunkProgress(Map headers, long loaded, long total) { if ("application/javascript".equals(headers.get("Content-Type"))) { - callback.onProgress( - "Downloading JavaScript bundle", (int) (loaded / 1024), (int) (total / 1024)); + callback.onProgress("Downloading", (int) (loaded / 1024), (int) (total / 1024)); } } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevLoadingViewController.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevLoadingViewController.java index aec05670334acf..b718751f52971a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevLoadingViewController.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevLoadingViewController.java @@ -37,8 +37,7 @@ public static void setDevLoadingEnabled(boolean enabled) { sEnabled = enabled; } - public DevLoadingViewController( - Context context, ReactInstanceManagerDevHelper reactInstanceManagerHelper) { + public DevLoadingViewController(ReactInstanceManagerDevHelper reactInstanceManagerHelper) { mReactInstanceManagerHelper = reactInstanceManagerHelper; } @@ -98,12 +97,7 @@ public void run() { message.append(status != null ? status : "Loading"); if (done != null && total != null && total > 0) { message.append( - String.format( - Locale.getDefault(), - " %.1f%% (%d/%d)", - (float) done / total * 100, - done, - total)); + String.format(Locale.getDefault(), " %.1f%%", (float) done / total * 100)); } message.append("\u2026"); // `...` character if (mDevLoadingView != null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index b539282d635215..83e7ca48cb1558 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -419,15 +419,26 @@ private boolean getJSMinifyMode() { } private String createBundleURL(String mainModuleID, BundleType type, String host) { + return createBundleURL(mainModuleID, type, host, false, true); + } + + private String createSplitBundleURL(String mainModuleID, String host) { + return createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false); + } + + private String createBundleURL( + String mainModuleID, BundleType type, String host, boolean modulesOnly, boolean runModule) { return String.format( Locale.US, - "http://%s/%s.%s?platform=android&dev=%s&minify=%s&app=%s", + "http://%s/%s.%s?platform=android&dev=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s", host, mainModuleID, type.typeID(), getDevMode(), getJSMinifyMode(), - mPackageName); + mPackageName, + modulesOnly ? "true" : "false", + runModule ? "true" : "false"); } private String createBundleURL(String mainModuleID, BundleType type) { @@ -454,6 +465,11 @@ public String getDevServerBundleURL(final String jsModulePath) { mSettings.getPackagerConnectionSettings().getDebugServerHost()); } + public String getDevServerSplitBundleURL(String jsModulePath) { + return createSplitBundleURL( + jsModulePath, mSettings.getPackagerConnectionSettings().getDebugServerHost()); + } + public void isPackagerRunning(final PackagerStatusCallback callback) { String statusURL = createPackagerStatusURL(mSettings.getPackagerConnectionSettings().getDebugServerHost()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java index e93b645cb83bd5..2e118ffa1ac93e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java @@ -23,12 +23,14 @@ import android.widget.EditText; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import com.facebook.common.logging.FLog; import com.facebook.debug.holder.PrinterHolder; import com.facebook.debug.tags.ReactDebugOverlayTags; import com.facebook.infer.annotation.Assertions; import com.facebook.react.R; import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; +import com.facebook.react.bridge.JSBundleLoader; import com.facebook.react.bridge.JavaJSExecutor; import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.ReactContext; @@ -43,6 +45,7 @@ import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener; import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevOptionHandler; +import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.devsupport.interfaces.ErrorCustomizer; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; @@ -70,6 +73,7 @@ public abstract class DevSupportManagerBase private static final int JAVA_ERROR_COOKIE = -1; private static final int JSEXCEPTION_ERROR_COOKIE = -1; private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + private static final String JS_SPLIT_BUNDLES_DIR_NAME = "dev_js_split_bundles"; private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; private static final String FLIPPER_DEBUGGER_URL = "flipper://null/Hermesdebuggerrn?device=React%20Native"; @@ -97,6 +101,7 @@ private enum ErrorType { private final ReactInstanceManagerDevHelper mReactInstanceManagerHelper; private final @Nullable String mJSAppBundleName; private final File mJSBundleTempFile; + private final File mJSSplitBundlesDir; private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; private final DevLoadingViewController mDevLoadingViewController; @@ -104,6 +109,7 @@ private enum ErrorType { private @Nullable AlertDialog mDevOptionsDialog; private @Nullable DebugOverlayController mDebugOverlayController; private boolean mDevLoadingViewVisible = false; + private int mPendingJSSplitBundleRequests = 0; private @Nullable ReactContext mCurrentContext; private DevInternalSettings mDevSettings; private boolean mIsReceiverRegistered = false; @@ -113,7 +119,6 @@ private enum ErrorType { private @Nullable String mLastErrorTitle; private @Nullable StackFrame[] mLastErrorStack; private int mLastErrorCookie = 0; - private @Nullable ErrorType mLastErrorType; private @Nullable DevBundleDownloadListener mBundleDownloadListener; private @Nullable List mErrorCustomizers; private @Nullable PackagerLocationCustomizer mPackagerLocationCustomizer; @@ -204,13 +209,15 @@ public void onReceive(Context context, Intent intent) { // TODO(6418010): Fix readers-writers problem in debug reload from HTTP server mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME); + mJSSplitBundlesDir = + mApplicationContext.getDir(JS_SPLIT_BUNDLES_DIR_NAME, Context.MODE_PRIVATE); + mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); setDevSupportEnabled(enableOnCreate); mRedBoxHandler = redBoxHandler; - mDevLoadingViewController = - new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); + mDevLoadingViewController = new DevLoadingViewController(reactInstanceManagerHelper); mExceptionLoggers.add(new JSExceptionLogger()); @@ -776,26 +783,6 @@ public boolean hasUpToDateJSBundleInCache() { return false; } - /** - * @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case {@link - * com.facebook.react.ReactInstanceManager} should use that file from assets instead of - * downloading bundle from dev server - */ - public boolean hasBundleInAssets(String bundleAssetName) { - try { - String[] assets = mApplicationContext.getAssets().list(""); - for (int i = 0; i < assets.length; i++) { - if (assets[i].equals(bundleAssetName)) { - return true; - } - } - } catch (IOException e) { - // Ignore this error and just fallback to downloading JS from devserver - FLog.e(ReactConstants.TAG, "Error while loading assets list"); - } - return false; - } - private void resetCurrentContext(@Nullable ReactContext reactContext) { if (mCurrentContext == reactContext) { // new context is the same as the old one - do nothing @@ -875,6 +862,82 @@ public void handleReloadJS() { } } + @Override + public void loadSplitBundleFromServer(String bundlePath, final DevSplitBundleCallback callback) { + final String bundleUrl = mDevServerHelper.getDevServerSplitBundleURL(bundlePath); + // The bundle path may contain the '/' character, which is not allowed in file names. + final File bundleFile = + new File(mJSSplitBundlesDir, bundlePath.replaceAll("/", "_") + ".jsbundle"); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + showSplitBundleDevLoadingView(bundleUrl); + mDevServerHelper.downloadBundleFromURL( + new DevBundleDownloadListener() { + @Override + public void onSuccess() { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + hideSplitBundleDevLoadingView(); + } + }); + + @Nullable ReactContext context = mCurrentContext; + if (context == null || !context.hasActiveCatalystInstance()) { + return; + } + + JSBundleLoader.createCachedSplitBundleFromNetworkLoader( + bundleUrl, bundleFile.getAbsolutePath()) + .loadScript(context.getCatalystInstance()); + context.getJSModule(HMRClient.class).registerBundle(bundleUrl); + + callback.onSuccess(); + } + + @Override + public void onProgress( + @Nullable String status, @Nullable Integer done, @Nullable Integer total) { + mDevLoadingViewController.updateProgress(status, done, total); + } + + @Override + public void onFailure(Exception cause) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + hideSplitBundleDevLoadingView(); + } + }); + callback.onError(bundleUrl, cause); + } + }, + bundleFile, + bundleUrl, + null); + } + }); + } + + @UiThread + private void showSplitBundleDevLoadingView(String bundleUrl) { + mDevLoadingViewController.showForUrl(bundleUrl); + mDevLoadingViewVisible = true; + mPendingJSSplitBundleRequests++; + } + + @UiThread + private void hideSplitBundleDevLoadingView() { + if (--mPendingJSSplitBundleRequests == 0) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + } + } + @Override public void isPackagerRunning(final PackagerStatusCallback callback) { Runnable checkPackagerRunning = @@ -988,7 +1051,6 @@ private void updateLastErrorInfo( mLastErrorTitle = message; mLastErrorStack = stack; mLastErrorCookie = errorCookie; - mLastErrorType = errorType; } private void reloadJSInProxyMode() { @@ -1107,19 +1169,7 @@ public void onFailure(final Exception cause) { mBundleDownloadListener.onFailure(cause); } FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (cause instanceof DebugServerException) { - DebugServerException debugServerException = (DebugServerException) cause; - showNewJavaError(debugServerException.getMessage(), cause); - } else { - showNewJavaError( - mApplicationContext.getString(R.string.catalyst_reload_error), cause); - } - } - }); + reportBundleLoadingFailure(cause); } }, mJSBundleTempFile, @@ -1127,6 +1177,22 @@ public void run() { bundleInfo); } + private void reportBundleLoadingFailure(final Exception cause) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (cause instanceof DebugServerException) { + DebugServerException debugServerException = (DebugServerException) cause; + showNewJavaError(debugServerException.getMessage(), cause); + } else { + showNewJavaError( + mApplicationContext.getString(R.string.catalyst_reload_error), cause); + } + } + }); + } + @Override public void startInspector() { if (mIsDevSupportEnabled) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index df0ca843ba22a2..298de68ce389f4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.devsupport.interfaces.DevOptionHandler; +import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; import com.facebook.react.devsupport.interfaces.DevSupportManager; import com.facebook.react.devsupport.interfaces.ErrorCustomizer; import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; @@ -129,6 +130,9 @@ public void handleReloadJS() {} @Override public void reloadJSFromServer(String bundleURL) {} + @Override + public void loadSplitBundleFromServer(String bundlePath, DevSplitBundleCallback callback) {} + @Override public void isPackagerRunning(final PackagerStatusCallback callback) {} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java index 4837c0d06e5e0c..97469c319d92ba 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java @@ -29,6 +29,9 @@ public interface HMRClient extends JavaScriptModule { */ void setup(String platform, String bundleEntry, String host, int port, boolean isEnabled); + /** Registers an additional JS bundle with HMRClient. */ + void registerBundle(String bundleUrl); + /** * Sets up a connection to the packager when called the first time. Ensures code updates received * from the packager are applied. diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSplitBundleCallback.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSplitBundleCallback.java new file mode 100644 index 00000000000000..f9672ccf13efa0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSplitBundleCallback.java @@ -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. + */ + +package com.facebook.react.devsupport.interfaces; + +/** Callback class for loading split JS bundles from Metro in development. */ +public interface DevSplitBundleCallback { + /** Called when the split JS bundle has been downloaded and evaluated. */ + void onSuccess(); + /** Called when the split JS bundle failed to load. */ + void onError(String url, Throwable cause); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java index 10684ce3d4f9b5..92181077ab4c4a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java @@ -69,6 +69,8 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { void reloadJSFromServer(final String bundleURL); + void loadSplitBundleFromServer(String bundlePath, DevSplitBundleCallback callback); + void isPackagerRunning(PackagerStatusCallback callback); void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricComponents.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricComponents.java index c091ed2e381a1c..9f0f32c169d2a6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricComponents.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricComponents.java @@ -39,6 +39,7 @@ public class FabricComponents { sComponentNames.put("StickerInputView", "RCTStickerInputView"); sComponentNames.put("Map", "RCTMap"); sComponentNames.put("WebView", "RCTWebView"); + sComponentNames.put("Keyframes", "RCTKeyframes"); } /** @return the name of component in the Fabric environment */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java index 8b5b7263fccbdd..136da3a752a3c1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricJSIModuleProvider.java @@ -21,7 +21,6 @@ import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; import com.facebook.react.fabric.mounting.mountitems.CreateMountItem; -import com.facebook.react.fabric.mounting.mountitems.DeleteMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; @@ -29,7 +28,6 @@ import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; import com.facebook.react.fabric.mounting.mountitems.RemoveDeleteMultiMountItem; -import com.facebook.react.fabric.mounting.mountitems.RemoveMountItem; import com.facebook.react.fabric.mounting.mountitems.SendAccessibilityEvent; import com.facebook.react.fabric.mounting.mountitems.UpdateEventEmitterMountItem; import com.facebook.react.fabric.mounting.mountitems.UpdateLayoutMountItem; @@ -112,7 +110,6 @@ private static void loadClasses() { FabricEventEmitter.class.getClass(); BatchMountItem.class.getClass(); CreateMountItem.class.getClass(); - DeleteMountItem.class.getClass(); DispatchCommandMountItem.class.getClass(); DispatchIntCommandMountItem.class.getClass(); DispatchStringCommandMountItem.class.getClass(); @@ -120,7 +117,6 @@ private static void loadClasses() { MountItem.class.getClass(); PreAllocateViewMountItem.class.getClass(); RemoveDeleteMultiMountItem.class.getClass(); - RemoveMountItem.class.getClass(); SendAccessibilityEvent.class.getClass(); UpdateEventEmitterMountItem.class.getClass(); UpdateLayoutMountItem.class.getClass(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 4c863b702096f8..3f3eb338d13b17 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -14,6 +14,10 @@ import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getMinSize; import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getYogaMeasureMode; import static com.facebook.react.fabric.mounting.LayoutMetricsConversions.getYogaSize; +import static com.facebook.react.uimanager.UIManagerHelper.PADDING_BOTTOM_INDEX; +import static com.facebook.react.uimanager.UIManagerHelper.PADDING_END_INDEX; +import static com.facebook.react.uimanager.UIManagerHelper.PADDING_START_INDEX; +import static com.facebook.react.uimanager.UIManagerHelper.PADDING_TOP_INDEX; import static com.facebook.react.uimanager.common.UIManagerType.FABRIC; import android.annotation.SuppressLint; @@ -52,7 +56,6 @@ import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.mountitems.BatchMountItem; import com.facebook.react.fabric.mounting.mountitems.CreateMountItem; -import com.facebook.react.fabric.mounting.mountitems.DeleteMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchIntCommandMountItem; import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountItem; @@ -60,7 +63,6 @@ import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; import com.facebook.react.fabric.mounting.mountitems.RemoveDeleteMultiMountItem; -import com.facebook.react.fabric.mounting.mountitems.RemoveMountItem; import com.facebook.react.fabric.mounting.mountitems.SendAccessibilityEvent; import com.facebook.react.fabric.mounting.mountitems.UpdateEventEmitterMountItem; import com.facebook.react.fabric.mounting.mountitems.UpdateLayoutMountItem; @@ -73,6 +75,7 @@ import com.facebook.react.uimanager.ReactRootViewTagGenerator; import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.ViewManagerPropertyUpdater; import com.facebook.react.uimanager.ViewManagerRegistry; import com.facebook.react.uimanager.events.EventDispatcher; @@ -134,13 +137,19 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { @GuardedBy("mPreMountItemsLock") @NonNull - private ArrayDeque mPreMountItems = + private ArrayDeque mPreMountItems = new ArrayDeque<>(PRE_MOUNT_ITEMS_INITIAL_SIZE_ARRAY); @ThreadConfined(UI) @NonNull private final DispatchUIFrameCallback mDispatchUIFrameCallback; + @ThreadConfined(UI) + private int mLastExecutedMountItemSurfaceId = -1; + + @ThreadConfined(UI) + private boolean mLastExecutedMountItemSurfaceIdActive = false; + /** * This is used to keep track of whether or not the FabricUIManager has been destroyed. Once the * Catalyst instance is being destroyed, we should cease all operation here. @@ -242,9 +251,10 @@ public void onRequestEventBeat() { @AnyThread @ThreadConfined(ANY) + @Override public void stopSurface(int surfaceID) { - mBinding.stopSurface(surfaceID); mReactContextForRootTag.remove(surfaceID); + mBinding.stopSurface(surfaceID); } @Override @@ -287,15 +297,6 @@ public void onCatalystInstanceDestroy() { // memory immediately. mDispatchUIFrameCallback.stop(); - // Stop all attached surfaces - if (ReactFeatureFlags.enableFabricStopAllSurfacesOnTeardown) { - FLog.e(TAG, "stop all attached surfaces"); - for (int surfaceId : mReactContextForRootTag.keySet()) { - FLog.e(TAG, "stop attached surface: " + surfaceId); - stopSurface(surfaceId); - } - } - mBinding.unregister(); mBinding = null; @@ -314,6 +315,7 @@ private void preallocateView( @Nullable Object stateWrapper, boolean isLayoutable) { ThemedReactContext context = mReactContextForRootTag.get(rootTag); + String component = getFabricComponentName(componentName); synchronized (mPreMountItemsLock) { mPreMountItems.add( @@ -354,14 +356,6 @@ private MountItem createMountItem( isLayoutable); } - @DoNotStrip - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private MountItem removeMountItem(int reactTag, int parentReactTag, int index) { - return new RemoveMountItem(reactTag, parentReactTag, index); - } - @DoNotStrip @SuppressWarnings("unused") @AnyThread @@ -370,14 +364,6 @@ private MountItem insertMountItem(int reactTag, int parentReactTag, int index) { return new InsertMountItem(reactTag, parentReactTag, index); } - @DoNotStrip - @SuppressWarnings("unused") - @AnyThread - @ThreadConfined(ANY) - private MountItem deleteMountItem(int reactTag) { - return new DeleteMountItem(reactTag); - } - @DoNotStrip @SuppressWarnings("unused") @AnyThread @@ -489,6 +475,32 @@ private long measure( attachmentsPositions); } + /** + * @param surfaceID {@link int} surface ID + * @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme + * padding used by RN Android TextInput. + * @return if theme data is available in the output parameters. + */ + @DoNotStrip + public boolean getThemeData(int surfaceID, float[] defaultTextInputPadding) { + ThemedReactContext themedReactContext = mReactContextForRootTag.get(surfaceID); + if (themedReactContext == null) { + // TODO T68526882: Review if this should cause a crash instead. + ReactSoftException.logSoftException( + TAG, + new ReactNoCrashSoftException( + "Unable to find ThemedReactContext associated to surfaceID: " + surfaceID)); + return false; + } + float[] defaultTextInputPaddingForTheme = + UIManagerHelper.getDefaultTextInputPadding(themedReactContext); + defaultTextInputPadding[0] = defaultTextInputPaddingForTheme[PADDING_START_INDEX]; + defaultTextInputPadding[1] = defaultTextInputPaddingForTheme[PADDING_END_INDEX]; + defaultTextInputPadding[2] = defaultTextInputPaddingForTheme[PADDING_TOP_INDEX]; + defaultTextInputPadding[3] = defaultTextInputPaddingForTheme[PADDING_BOTTOM_INDEX]; + return true; + } + @Override @UiThread @ThreadConfined(UI) @@ -505,7 +517,11 @@ public void synchronouslyUpdateViewOnUIThread(int reactTag, @NonNull ReadableMap ReactMarker.logFabricMarker( ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, null, commitNumber); if (ENABLE_FABRIC_LOGS) { - FLog.d(TAG, "SynchronouslyUpdateViewOnUIThread for tag %d", reactTag); + FLog.d( + TAG, + "SynchronouslyUpdateViewOnUIThread for tag %d: %s", + reactTag, + (IS_DEVELOPMENT_ENVIRONMENT ? props.toHashMap().toString() : "")); } updatePropsMountItem(reactTag, props).execute(mMountingManager); @@ -531,7 +547,8 @@ public void removeUIManagerEventListener(UIManagerListener listener) { /** * This method enqueues UI operations directly to the UI thread. This might change in the future - * to enforce execution order using {@link ReactChoreographer#CallbackType}. + * to enforce execution order using {@link ReactChoreographer#CallbackType}. This method should + * only be called as the result of a new tree being committed. */ @DoNotStrip @SuppressWarnings("unused") @@ -551,6 +568,13 @@ private void scheduleMountItem( // a BatchMountItem. No other sites call into this with a BatchMountItem, and Binding.cpp only // calls scheduleMountItems with a BatchMountItem. boolean isBatchMountItem = mountItem instanceof BatchMountItem; + boolean shouldSchedule = !(isBatchMountItem && ((BatchMountItem) mountItem).getSize() == 0); + + // In case of sync rendering, this could be called on the UI thread. Otherwise, + // it should ~always be called on the JS thread. + for (UIManagerListener listener : mListeners) { + listener.didScheduleMountItems(this); + } if (isBatchMountItem) { mCommitStartTime = commitStartTime; @@ -560,13 +584,15 @@ private void scheduleMountItem( mDispatchViewUpdatesTime = SystemClock.uptimeMillis(); } - synchronized (mMountItemsLock) { - mMountItems.add(mountItem); - } + if (shouldSchedule) { + synchronized (mMountItemsLock) { + mMountItems.add(mountItem); + } - if (UiThreadUtil.isOnUiThread()) { - // We only read these flags on the UI thread. - tryDispatchMountItems(); + if (UiThreadUtil.isOnUiThread()) { + // We only read these flags on the UI thread. + tryDispatchMountItems(); + } } // Post markers outside of lock and after sync mounting finishes its execution @@ -610,21 +636,22 @@ private void tryDispatchMountItems() { return; } - for (UIManagerListener listener : mListeners) { - listener.willDispatchMountItems(); - } - final boolean didDispatchItems; try { didDispatchItems = dispatchMountItems(); } catch (Throwable e) { mReDispatchCounter = 0; + mLastExecutedMountItemSurfaceId = -1; throw e; } finally { // Clean up after running dispatchMountItems - even if an exception was thrown mInDispatch = false; } + for (UIManagerListener listener : mListeners) { + listener.didDispatchMountItems(this); + } + // Decide if we want to try reentering if (mReDispatchCounter < 10 && didDispatchItems) { // Executing twice in a row is normal. Only log after that point. @@ -641,15 +668,12 @@ private void tryDispatchMountItems() { tryDispatchMountItems(); } mReDispatchCounter = 0; + mLastExecutedMountItemSurfaceId = -1; } @UiThread @ThreadConfined(UI) private List getAndResetViewCommandMountItems() { - if (!ReactFeatureFlags.allowEarlyViewCommandExecution) { - return null; - } - synchronized (mViewCommandMountItemsLock) { List result = mViewCommandMountItems; if (result.isEmpty()) { @@ -673,9 +697,9 @@ private List getAndResetMountItems() { } } - private ArrayDeque getAndResetPreMountItems() { + private ArrayDeque getAndResetPreMountItems() { synchronized (mPreMountItemsLock) { - ArrayDeque result = mPreMountItems; + ArrayDeque result = mPreMountItems; if (result.isEmpty()) { return null; } @@ -684,6 +708,38 @@ private ArrayDeque getAndResetPreMountItems() { } } + /** + * Check if a surfaceId is active and ready for MountItems to be executed against it. It is safe + * and cheap to call this repeatedly because we expect many operations to be batched with the same + * surfaceId in a row and we memoize the parameters and results. + * + * @param surfaceId + * @param context + * @return + */ + @UiThread + @ThreadConfined(UI) + private boolean surfaceActiveForExecution(int surfaceId, String context) { + if (mLastExecutedMountItemSurfaceId != surfaceId) { + mLastExecutedMountItemSurfaceId = surfaceId; + mLastExecutedMountItemSurfaceIdActive = mReactContextForRootTag.get(surfaceId) != null; + + // If there are many MountItems with the same SurfaceId, we only + // log a warning for the first one that is skipped. + if (!mLastExecutedMountItemSurfaceIdActive) { + ReactSoftException.logSoftException( + TAG, + new ReactNoCrashSoftException( + "dispatchMountItems: skipping " + + context + + ", because surface not available: " + + surfaceId)); + } + } + + return mLastExecutedMountItemSurfaceIdActive; + } + @UiThread @ThreadConfined(UI) /** Nothing should call this directly except for `tryDispatchMountItems`. */ @@ -755,7 +811,7 @@ private boolean dispatchMountItems() { // If there are MountItems to dispatch, we make sure all the "pre mount items" are executed // first - ArrayDeque mPreMountItemsToDispatch = getAndResetPreMountItems(); + ArrayDeque mPreMountItemsToDispatch = getAndResetPreMountItems(); if (mPreMountItemsToDispatch != null) { Systrace.beginSection( @@ -764,7 +820,11 @@ private boolean dispatchMountItems() { + mPreMountItemsToDispatch.size()); while (!mPreMountItemsToDispatch.isEmpty()) { - mPreMountItemsToDispatch.pollFirst().execute(mMountingManager); + PreAllocateViewMountItem mountItem = mPreMountItemsToDispatch.pollFirst(); + if (surfaceActiveForExecution( + mountItem.getRootTag(), "dispatchMountItems PreAllocateViewMountItem")) { + mountItem.execute(mMountingManager); + } } Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -791,41 +851,14 @@ private boolean dispatchMountItems() { // TODO T68118357: clean up this logic and simplify this method overall if (mountItem instanceof BatchMountItem) { BatchMountItem batchMountItem = (BatchMountItem) mountItem; - int rootTag = batchMountItem.getRootTag(); - if (mReactContextForRootTag.get(rootTag) == null) { - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "dispatchMountItems: skipping batched item: surface not available [" - + rootTag - + "]")); + if (!surfaceActiveForExecution( + batchMountItem.getRootTag(), "dispatchMountItems BatchMountItem")) { + batchMountItem.executeDeletes(mMountingManager); continue; } } - // TODO: if early ViewCommand dispatch ships 100% as a feature, this can be removed. - // This try/catch catches Retryable errors that can only be thrown by ViewCommands, which - // won't be executed here in Early Dispatch mode. - try { - mountItem.execute(mMountingManager); - } catch (RetryableMountingLayerException e) { - // It's very common for commands to be executed on views that no longer exist - for - // example, a blur event on TextInput being fired because of a navigation event away - // from the current screen. If the exception is marked as Retryable, we log a soft - // exception but never crash in debug. - // It's not clear that logging this is even useful, because these events are very - // common, mundane, and there's not much we can do about them currently. - if (mountItem instanceof DispatchCommandMountItem) { - ReactSoftException.logSoftException( - TAG, - new ReactNoCrashSoftException( - "Caught exception executing retryable mounting layer instruction: " - + mountItem.toString(), - e)); - } else { - throw e; - } - } + mountItem.execute(mMountingManager); } mBatchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime; } @@ -843,10 +876,6 @@ private void dispatchPreMountItems(long frameTimeNanos) { // reentering during dispatchPreMountItems mInDispatch = true; - for (UIManagerListener listener : mListeners) { - listener.willDispatchPreMountItems(); - } - try { while (true) { long timeLeftInFrame = FRAME_TIME_MS - ((System.nanoTime() - frameTimeNanos) / 1000000); @@ -854,18 +883,22 @@ private void dispatchPreMountItems(long frameTimeNanos) { break; } - MountItem preMountItemsToDispatch; + PreAllocateViewMountItem preMountItemToDispatch; synchronized (mPreMountItemsLock) { if (mPreMountItems.isEmpty()) { break; } - preMountItemsToDispatch = mPreMountItems.pollFirst(); + preMountItemToDispatch = mPreMountItems.pollFirst(); } - preMountItemsToDispatch.execute(mMountingManager); + if (surfaceActiveForExecution( + preMountItemToDispatch.getRootTag(), "dispatchPreMountItems")) { + preMountItemToDispatch.execute(mMountingManager); + } } } finally { mInDispatch = false; + mLastExecutedMountItemSurfaceId = -1; } Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -906,6 +939,7 @@ public void updateRootLayoutSpecs( doLeftAndRightSwapInRTL); } + @Override public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap params) { EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(reactTag); if (eventEmitter == null) { @@ -959,14 +993,8 @@ public void dispatchCommand( @AnyThread @ThreadConfined(ANY) private void dispatchCommandMountItem(DispatchCommandMountItem command) { - if (ReactFeatureFlags.allowEarlyViewCommandExecution) { - synchronized (mViewCommandMountItemsLock) { - mViewCommandMountItems.add(command); - } - } else { - synchronized (mMountItemsLock) { - mMountItems.add(command); - } + synchronized (mViewCommandMountItemsLock) { + mViewCommandMountItems.add(command); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index 46a91c37761173..715cf1bea97beb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -43,9 +43,6 @@ struct JMountItem : public JavaClass { "Lcom/facebook/react/fabric/mounting/mountitems/MountItem;"; }; -static constexpr auto UIManagerJavaDescriptor = - "com/facebook/react/fabric/FabricUIManager"; - struct RemoveDeleteMetadata { Tag tag; Tag parentTag; @@ -283,15 +280,13 @@ void Binding::installFabricUIManager( // Keep reference to config object and cache some feature flags here reactNativeConfig_ = config; - shouldCollateRemovesAndDeletes_ = reactNativeConfig_->getBool( - "react_fabric:enable_removedelete_collation_android"); collapseDeleteCreateMountingInstructions_ = reactNativeConfig_->getBool( "react_fabric:enabled_collapse_delete_create_mounting_instructions"); disablePreallocateViews_ = reactNativeConfig_->getBool( "react_fabric:disabled_view_preallocation_android"); - bool enableLayoutAnimations_ = reactNativeConfig_->getBool( + bool enableLayoutAnimations = reactNativeConfig_->getBool( "react_fabric:enabled_layout_animations_android"); auto toolbox = SchedulerToolbox{}; @@ -301,7 +296,7 @@ void Binding::installFabricUIManager( toolbox.synchronousEventBeatFactory = synchronousBeatFactory; toolbox.asynchronousEventBeatFactory = asynchronousBeatFactory; - if (enableLayoutAnimations_) { + if (enableLayoutAnimations) { animationDriver_ = std::make_shared(this); } scheduler_ = std::make_shared( @@ -320,6 +315,7 @@ void Binding::uninstallFabricUIManager() { std::lock_guard uiManagerLock( javaUIManagerMutex_, std::adopt_lock); + animationDriver_ = nullptr; scheduler_ = nullptr; javaUIManager_ = nullptr; reactNativeConfig_ = nullptr; @@ -365,7 +361,7 @@ local_ref createUpdateEventEmitterMountItem( cEventEmitter->eventEmitter = eventEmitter; static auto updateEventEmitterInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, jobject)>( "updateEventEmitterMountItem"); @@ -386,7 +382,7 @@ local_ref createUpdatePropsMountItem( local_ref readableMap = castReadableMap(ReadableNativeMap::newObjectCxxArgs(newProps)); static auto updatePropsInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, ReadableMap::javaobject)>( "updatePropsMountItem"); @@ -403,7 +399,7 @@ local_ref createUpdateLayoutMountItem( if (newChildShadowView.layoutMetrics != EmptyLayoutMetrics && oldChildShadowView.layoutMetrics != newChildShadowView.layoutMetrics) { static auto updateLayoutInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod( jint, jint, jint, jint, jint, jint)>("updateLayoutMountItem"); auto layoutMetrics = newChildShadowView.layoutMetrics; @@ -435,7 +431,7 @@ local_ref createUpdatePaddingMountItem( } static auto updateLayoutInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, jint, jint, jint, jint)>( "updatePaddingMountItem"); @@ -456,7 +452,7 @@ local_ref createInsertMountItem( const jni::global_ref &javaUIManager, const ShadowViewMutation &mutation) { static auto insertInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, jint, jint)>( "insertMountItem"); @@ -471,7 +467,7 @@ local_ref createUpdateStateMountItem( const jni::global_ref &javaUIManager, const ShadowViewMutation &mutation) { static auto updateStateInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, jobject)>( "updateStateMountItem"); @@ -493,31 +489,6 @@ local_ref createUpdateStateMountItem( (javaStateWrapper != nullptr ? javaStateWrapper.get() : nullptr)); } -local_ref createRemoveMountItem( - const jni::global_ref &javaUIManager, - const ShadowViewMutation &mutation) { - static auto removeInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) - ->getMethod(jint, jint, jint)>( - "removeMountItem"); - - return removeInstruction( - javaUIManager, - mutation.oldChildShadowView.tag, - mutation.parentShadowView.tag, - mutation.index); -} - -local_ref createDeleteMountItem( - const jni::global_ref &javaUIManager, - const ShadowViewMutation &mutation) { - static auto deleteInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) - ->getMethod(jint)>("deleteMountItem"); - - return deleteInstruction(javaUIManager, mutation.oldChildShadowView.tag); -} - local_ref createRemoveAndDeleteMultiMountItem( const jni::global_ref &javaUIManager, const std::vector &metadata) { @@ -535,7 +506,7 @@ local_ref createRemoveAndDeleteMultiMountItem( } static auto removeDeleteMultiInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jintArray)>( "removeDeleteMultiMountItem"); @@ -560,7 +531,7 @@ local_ref createCreateMountItem( const ShadowViewMutation &mutation, const Tag surfaceId) { static auto createJavaInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod( jstring, ReadableMap::javaobject, jobject, jint, jint, jboolean)>( "createMountItem"); @@ -687,8 +658,7 @@ void Binding::schedulerDidFinishTransaction( oldChildShadowView.layoutMetrics == EmptyLayoutMetrics; // Handle accumulated removals/deletions - if (shouldCollateRemovesAndDeletes_ && - mutation.type != ShadowViewMutation::Remove && + if (mutation.type != ShadowViewMutation::Remove && mutation.type != ShadowViewMutation::Delete) { if (toRemove.size() > 0) { mountItems[position++] = @@ -710,39 +680,29 @@ void Binding::schedulerDidFinishTransaction( } case ShadowViewMutation::Remove: { if (!isVirtual) { - if (shouldCollateRemovesAndDeletes_) { - toRemove.push_back( - RemoveDeleteMetadata{mutation.oldChildShadowView.tag, - mutation.parentShadowView.tag, - mutation.index, - true, - false}); - } else { - mountItems[position++] = - createRemoveMountItem(localJavaUIManager, mutation); - } + toRemove.push_back( + RemoveDeleteMetadata{mutation.oldChildShadowView.tag, + mutation.parentShadowView.tag, + mutation.index, + true, + false}); } break; } case ShadowViewMutation::Delete: { - if (shouldCollateRemovesAndDeletes_) { - // It is impossible to delete without removing node first - const auto &it = std::find_if( - std::begin(toRemove), - std::end(toRemove), - [&mutation](const auto &x) { - return x.tag == mutation.oldChildShadowView.tag; - }); - - if (it != std::end(toRemove)) { - it->shouldDelete = true; - } else { - toRemove.push_back(RemoveDeleteMetadata{ - mutation.oldChildShadowView.tag, -1, -1, false, true}); - } + // It is impossible to delete without removing node first + const auto &it = std::find_if( + std::begin(toRemove), + std::end(toRemove), + [&mutation](const auto &x) { + return x.tag == mutation.oldChildShadowView.tag; + }); + + if (it != std::end(toRemove)) { + it->shouldDelete = true; } else { - mountItems[position++] = - createDeleteMountItem(localJavaUIManager, mutation); + toRemove.push_back(RemoveDeleteMetadata{ + mutation.oldChildShadowView.tag, -1, -1, false, true}); } deletedViewTags.insert(mutation.oldChildShadowView.tag); @@ -761,17 +721,21 @@ void Binding::schedulerDidFinishTransaction( createUpdateStateMountItem(localJavaUIManager, mutation); } - auto updateLayoutMountItem = - createUpdateLayoutMountItem(localJavaUIManager, mutation); - if (updateLayoutMountItem) { - mountItems[position++] = updateLayoutMountItem; - } - + // Padding: padding mountItems must be executed before layout props + // are updated in the view. This is necessary to ensure that events + // (resulting from layout changes) are dispatched with the correct + // padding information. auto updatePaddingMountItem = createUpdatePaddingMountItem(localJavaUIManager, mutation); if (updatePaddingMountItem) { mountItems[position++] = updatePaddingMountItem; } + + auto updateLayoutMountItem = + createUpdateLayoutMountItem(localJavaUIManager, mutation); + if (updateLayoutMountItem) { + mountItems[position++] = updateLayoutMountItem; + } } if (mutation.oldChildShadowView.eventEmitter != @@ -804,19 +768,22 @@ void Binding::schedulerDidFinishTransaction( createUpdateStateMountItem(localJavaUIManager, mutation); } + // Padding: padding mountItems must be executed before layout props + // are updated in the view. This is necessary to ensure that events + // (resulting from layout changes) are dispatched with the correct + // padding information. + auto updatePaddingMountItem = + createUpdatePaddingMountItem(localJavaUIManager, mutation); + if (updatePaddingMountItem) { + mountItems[position++] = updatePaddingMountItem; + } + // Layout auto updateLayoutMountItem = createUpdateLayoutMountItem(localJavaUIManager, mutation); if (updateLayoutMountItem) { mountItems[position++] = updateLayoutMountItem; } - - // Padding - auto updatePaddingMountItem = - createUpdatePaddingMountItem(localJavaUIManager, mutation); - if (updatePaddingMountItem) { - mountItems[position++] = updatePaddingMountItem; - } } // EventEmitter @@ -835,42 +802,39 @@ void Binding::schedulerDidFinishTransaction( } // Handle remaining removals and deletions - if (shouldCollateRemovesAndDeletes_ && toRemove.size() > 0) { + if (toRemove.size() > 0) { mountItems[position++] = createRemoveAndDeleteMultiMountItem(localJavaUIManager, toRemove); toRemove.clear(); } - if (position <= 0) { - // If there are no mountItems to be sent to the platform, then it is not - // necessary to even call. - return; - } - static auto createMountItemsBatchContainer = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod( jint, jtypeArray, jint, jint)>( "createBatchMountItem"); + // If there are no items, we pass a nullptr instead of passing the object + // through the JNI auto batch = createMountItemsBatchContainer( localJavaUIManager, surfaceId, - mountItemsArray.get(), + position == 0 ? nullptr : mountItemsArray.get(), position, commitNumber); - static auto scheduleMountItem = jni::findClassStatic(UIManagerJavaDescriptor) - ->getMethod("scheduleMountItem"); + static auto scheduleMountItem = + jni::findClassStatic(Binding::UIManagerJavaDescriptor) + ->getMethod("scheduleMountItem"); auto finishTransactionEndTime = telemetryTimePointNow(); @@ -899,7 +863,7 @@ void Binding::onAnimationStarted() { } static auto layoutAnimationsStartedJNI = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod("onAnimationStarted"); layoutAnimationsStartedJNI(localJavaUIManager); @@ -912,7 +876,7 @@ void Binding::onAllAnimationsComplete() { } static auto allAnimationsCompleteJNI = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod("onAllAnimationsComplete"); allAnimationsCompleteJNI(localJavaUIManager); @@ -943,7 +907,7 @@ void Binding::schedulerDidRequestPreliminaryViewAllocation( } static auto preallocateView = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod( "preallocateView"); @@ -984,7 +948,7 @@ void Binding::schedulerDidDispatchCommand( } static auto dispatchCommand = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod( "dispatchCommand"); @@ -1009,7 +973,7 @@ void Binding::schedulerDidSetJSResponder( } static auto setJSResponder = - jni::findClassStatic(UIManagerJavaDescriptor) + jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod("setJSResponder"); setJSResponder( @@ -1027,8 +991,9 @@ void Binding::schedulerDidClearJSResponder() { return; } - static auto clearJSResponder = jni::findClassStatic(UIManagerJavaDescriptor) - ->getMethod("clearJSResponder"); + static auto clearJSResponder = + jni::findClassStatic(Binding::UIManagerJavaDescriptor) + ->getMethod("clearJSResponder"); clearJSResponder(localJavaUIManager); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h index 27f0fba1b9fb51..3d1d439f5e539b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h @@ -32,6 +32,9 @@ class Binding : public jni::HybridClass, constexpr static const char *const kJavaDescriptor = "Lcom/facebook/react/fabric/Binding;"; + constexpr static auto UIManagerJavaDescriptor = + "com/facebook/react/fabric/FabricUIManager"; + static void registerNatives(); private: @@ -122,7 +125,6 @@ class Binding : public jni::HybridClass, float pointScaleFactor_ = 1; std::shared_ptr reactNativeConfig_{nullptr}; - bool shouldCollateRemovesAndDeletes_{false}; bool collapseDeleteCreateMountingInstructions_{false}; bool disablePreallocateViews_{false}; bool disableVirtualNodePreallocation_{false}; diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java index e48192e8963119..3c33450fc14860 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java @@ -28,6 +28,7 @@ import com.facebook.react.bridge.RetryableMountingLayerException; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.mounting.mountitems.MountItem; @@ -50,6 +51,7 @@ */ public class MountingManager { public static final String TAG = MountingManager.class.getSimpleName(); + private static final boolean SHOW_CHANGED_VIEW_HIERARCHIES = ReactBuildConfig.DEBUG && false; @NonNull private final ConcurrentHashMap mTagToViewState; @NonNull private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); @@ -61,6 +63,21 @@ public MountingManager(@NonNull ViewManagerRegistry viewManagerRegistry) { mViewManagerRegistry = viewManagerRegistry; } + private static void logViewHierarchy(ViewGroup parent) { + int parentTag = parent.getId(); + FLog.e(TAG, " "); + for (int i = 0; i < parent.getChildCount(); i++) { + FLog.e( + TAG, + " "); + } + FLog.e(TAG, " "); + } + /** * This mutates the rootView, which is an Android View, so this should only be called on the UI * thread. @@ -132,7 +149,20 @@ public void addViewAt(int parentTag, int tag, int index) { throw new IllegalStateException( "Unable to find view for viewState " + viewState + " and tag " + tag); } + + // Display children before inserting + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + FLog.e(TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); + logViewHierarchy(parentView); + } + getViewGroupManager(parentViewState).addView(parentView, view, index); + + // Display children after inserting + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + FLog.e(TAG, "addViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " AFTER"); + logViewHierarchy(parentView); + } } private @NonNull ViewState getViewState(int tag) { @@ -224,7 +254,7 @@ public void sendAccessibilityEvent(int reactTag, int eventType) { } @UiThread - public void removeViewAt(int parentTag, int index) { + public void removeViewAt(int tag, int parentTag, int index) { UiThreadUtil.assertOnUiThread(); ViewState viewState = getNullableViewState(parentTag); @@ -242,8 +272,28 @@ public void removeViewAt(int parentTag, int index) { throw new IllegalStateException("Unable to find view for tag " + parentTag); } + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + // Display children before deleting any + FLog.e(TAG, "removeViewAt: [" + tag + "] -> [" + parentTag + "] idx: " + index + " BEFORE"); + logViewHierarchy(parentView); + } + ViewGroupManager viewGroupManager = getViewGroupManager(viewState); + // Verify that the view we're about to remove has the same tag we expect + View view = viewGroupManager.getChildAt(parentView, index); + if (view != null && view.getId() != tag) { + throw new IllegalStateException( + "Tried to delete view [" + + tag + + "] of parent [" + + parentTag + + "] at index " + + index + + ", but got view tag " + + view.getId()); + } + try { viewGroupManager.removeViewAt(parentView, index); } catch (RuntimeException e) { @@ -274,6 +324,12 @@ public void removeViewAt(int parentTag, int index) { + " children in parent. Warning: childCount may be incorrect!", e); } + + // Display children after deleting any + if (SHOW_CHANGED_VIEW_HIERARCHIES) { + FLog.e(TAG, "removeViewAt: [" + parentTag + "] idx: " + index + " AFTER"); + logViewHierarchy(parentView); + } } @UiThread @@ -397,6 +453,7 @@ public void deleteView(int reactTag) { } View view = viewState.mView; + if (view != null) { dropView(view); } else { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java index 85034aa043247e..7aebdd3d071aef 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java @@ -34,12 +34,10 @@ public class BatchMountItem implements MountItem { private final int mCommitNumber; public BatchMountItem(int rootTag, MountItem[] items, int size, int commitNumber) { - if (items == null) { - throw new NullPointerException(); - } - if (size < 0 || size > items.length) { + int itemsLength = (items == null ? 0 : items.length); + if (size < 0 || size > itemsLength) { throw new IllegalArgumentException( - "Invalid size received by parameter size: " + size + " items.size = " + items.length); + "Invalid size received by parameter size: " + size + " items.size = " + itemsLength); } mRootTag = rootTag; mMountItems = items; @@ -47,33 +45,59 @@ public BatchMountItem(int rootTag, MountItem[] items, int size, int commitNumber mCommitNumber = commitNumber; } - @Override - public void execute(@NonNull MountingManager mountingManager) { + private void beginMarkers(String reason) { Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "FabricUIManager::mountViews - " + mSize + " items"); + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "FabricUIManager::" + reason + " - " + mSize + " items"); if (mCommitNumber > 0) { ReactMarker.logFabricMarker( ReactMarkerConstants.FABRIC_BATCH_EXECUTION_START, null, mCommitNumber); } + } + + private void endMarkers() { + if (mCommitNumber > 0) { + ReactMarker.logFabricMarker( + ReactMarkerConstants.FABRIC_BATCH_EXECUTION_END, null, mCommitNumber); + } + + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + @Override + public void execute(@NonNull MountingManager mountingManager) { + beginMarkers("mountViews"); for (int mountItemIndex = 0; mountItemIndex < mSize; mountItemIndex++) { MountItem mountItem = mMountItems[mountItemIndex]; mountItem.execute(mountingManager); } - if (mCommitNumber > 0) { - ReactMarker.logFabricMarker( - ReactMarkerConstants.FABRIC_BATCH_EXECUTION_END, null, mCommitNumber); + endMarkers(); + } + + public void executeDeletes(@NonNull MountingManager mountingManager) { + beginMarkers("deleteViews"); + + for (int mountItemIndex = 0; mountItemIndex < mSize; mountItemIndex++) { + MountItem mountItem = mMountItems[mountItemIndex]; + if (mountItem instanceof RemoveDeleteMultiMountItem) { + ((RemoveDeleteMultiMountItem) mountItem).executeDeletes(mountingManager, true); + } } - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + endMarkers(); } public int getRootTag() { return mRootTag; } + public int getSize() { + return mSize; + } + @Override public String toString() { StringBuilder s = new StringBuilder(); @@ -81,7 +105,7 @@ public String toString() { if (s.length() > 0) { s.append("\n"); } - s.append("BatchMountItem (") + s.append("BatchMountItem [S:" + mRootTag + "] (") .append(i + 1) .append("/") .append(mSize) diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DeleteMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DeleteMountItem.java deleted file mode 100644 index 312dcc1ca4eaab..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DeleteMountItem.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.fabric.mounting.mountitems; - -import androidx.annotation.NonNull; -import com.facebook.react.fabric.mounting.MountingManager; - -public class DeleteMountItem implements MountItem { - - private int mReactTag; - - public DeleteMountItem(int reactTag) { - mReactTag = reactTag; - } - - @Override - public void execute(@NonNull MountingManager mountingManager) { - mountingManager.deleteView(mReactTag); - } - - @Override - public String toString() { - return "DeleteMountItem [" + mReactTag + "]"; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java index fad513a9ea0b67..7f519e9bc6f2f2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/PreAllocateViewMountItem.java @@ -47,6 +47,10 @@ public PreAllocateViewMountItem( mIsLayoutable = isLayoutable; } + public int getRootTag() { + return mRootTag; + } + @Override public void execute(@NonNull MountingManager mountingManager) { if (ENABLE_FABRIC_LOGS) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java index c11cb4e4412533..8a25f84b743b4e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java @@ -49,17 +49,37 @@ public void execute(@NonNull MountingManager mountingManager) { int flags = mMetadata[i + FLAGS_INDEX]; if ((flags & REMOVE_FLAG) != 0) { int parentTag = mMetadata[i + PARENT_TAG_INDEX]; + int tag = mMetadata[i + TAG_INDEX]; int index = mMetadata[i + VIEW_INDEX_INDEX]; - mountingManager.removeViewAt(parentTag, index); + mountingManager.removeViewAt(tag, parentTag, index); } } // After removing all views, delete all views marked for deletion. + executeDeletes(mountingManager, false); + } + + /** + * Execute only deletion operations. When being executed as part of shutdown/stopping surface, + * deletion failures can be ignored. For example: if there is a batch of MountItems being executed + * as part of stopSurface, a "Create" that is not executed may have a matching "Delete". The + * Delete will fail but we can safely ignore it in those cases. + * + * @param mountingManager + * @param ignoreFailures + */ + public void executeDeletes(@NonNull MountingManager mountingManager, boolean ignoreFailures) { for (int i = 0; i < mMetadata.length; i += 4) { int flags = mMetadata[i + FLAGS_INDEX]; if ((flags & DELETE_FLAG) != 0) { int tag = mMetadata[i + TAG_INDEX]; - mountingManager.deleteView(tag); + try { + mountingManager.deleteView(tag); + } catch (IllegalStateException e) { + if (!ignoreFailures) { + throw e; + } + } } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveMountItem.java deleted file mode 100644 index f09f91e3f98b94..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveMountItem.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.fabric.mounting.mountitems; - -import androidx.annotation.NonNull; -import com.facebook.react.fabric.mounting.MountingManager; - -public class RemoveMountItem implements MountItem { - - private int mReactTag; - private int mParentReactTag; - private int mIndex; - - public RemoveMountItem(int reactTag, int parentReactTag, int index) { - mReactTag = reactTag; - mParentReactTag = parentReactTag; - mIndex = index; - } - - @Override - public void execute(@NonNull MountingManager mountingManager) { - mountingManager.removeViewAt(mParentReactTag, mIndex); - } - - public int getParentReactTag() { - return mParentReactTag; - } - - public int getIndex() { - return mIndex; - } - - @Override - public String toString() { - return "RemoveMountItem [" - + mReactTag - + "] - parentTag: " - + mParentReactTag - + " - index: " - + mIndex; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/BUCK new file mode 100644 index 00000000000000..64fb396b12a083 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/BUCK @@ -0,0 +1,21 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library") + +rn_android_library( + name = "bundleloader", + srcs = glob(["*.java"]), + is_androidx = True, + labels = ["supermodule:xplat/default/public.react_native.infra"], + provided_deps = [ + react_native_dep("third-party/android/androidx:annotation"), + ], + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/devsupport:interfaces"), + react_native_target("java/com/facebook/react/module/annotations:annotations"), + ], + exported_deps = [react_native_target("java/com/facebook/fbreact/specs:FBReactNativeSpec")], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/NativeDevSplitBundleLoaderModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/NativeDevSplitBundleLoaderModule.java new file mode 100644 index 00000000000000..0a491f172a6925 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/bundleloader/NativeDevSplitBundleLoaderModule.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package com.facebook.react.modules.bundleloader; + +import androidx.annotation.NonNull; +import com.facebook.fbreact.specs.NativeDevSplitBundleLoaderSpec; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.common.DebugServerException; +import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import com.facebook.react.module.annotations.ReactModule; + +@ReactModule(name = NativeDevSplitBundleLoaderModule.NAME) +public class NativeDevSplitBundleLoaderModule extends NativeDevSplitBundleLoaderSpec { + public static final String NAME = "DevSplitBundleLoader"; + private static final String REJECTION_CODE = "E_BUNDLE_LOAD_ERROR"; + + private final DevSupportManager mDevSupportManager; + + public NativeDevSplitBundleLoaderModule( + ReactApplicationContext reactContext, DevSupportManager devSupportManager) { + super(reactContext); + mDevSupportManager = devSupportManager; + } + + @Override + public void loadBundle(String bundlePath, final Promise promise) { + mDevSupportManager.loadSplitBundleFromServer( + bundlePath, + new DevSplitBundleCallback() { + @Override + public void onSuccess() { + promise.resolve(true); + } + + @Override + public void onError(String url, Throwable cause) { + String message = + cause instanceof DebugServerException + ? ((DebugServerException) cause).getOriginalMessage() + : "Unknown error fetching '" + url + "'."; + promise.reject(REJECTION_CODE, message, cause); + } + }); + } + + @NonNull + @Override + public String getName() { + return NAME; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/ImageEditingManager.java b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/ImageEditingManager.java deleted file mode 100644 index 105d4d3c153fa6..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/ImageEditingManager.java +++ /dev/null @@ -1,525 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.modules.camera; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapRegionDecoder; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.media.ExifInterface; -import android.net.Uri; -import android.os.AsyncTask; -import android.provider.MediaStore; -import android.text.TextUtils; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.fbreact.specs.NativeImageEditorSpec; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.GuardedAsyncTask; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.module.annotations.ReactModule; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.util.Arrays; -import java.util.List; - -/** Native module that provides image cropping functionality. */ -@ReactModule(name = ImageEditingManager.NAME) -public class ImageEditingManager extends NativeImageEditorSpec { - - public static final String NAME = "ImageEditingManager"; - - private static final List LOCAL_URI_PREFIXES = Arrays.asList("file://", "content://"); - - private static final String TEMP_FILE_PREFIX = "ReactNative_cropped_image_"; - - /** Compress quality of the output file. */ - private static final int COMPRESS_QUALITY = 90; - - @SuppressLint("InlinedApi") - private static final String[] EXIF_ATTRIBUTES = - new String[] { - ExifInterface.TAG_APERTURE, - ExifInterface.TAG_DATETIME, - ExifInterface.TAG_DATETIME_DIGITIZED, - ExifInterface.TAG_EXPOSURE_TIME, - ExifInterface.TAG_FLASH, - ExifInterface.TAG_FOCAL_LENGTH, - ExifInterface.TAG_GPS_ALTITUDE, - ExifInterface.TAG_GPS_ALTITUDE_REF, - ExifInterface.TAG_GPS_DATESTAMP, - ExifInterface.TAG_GPS_LATITUDE, - ExifInterface.TAG_GPS_LATITUDE_REF, - ExifInterface.TAG_GPS_LONGITUDE, - ExifInterface.TAG_GPS_LONGITUDE_REF, - ExifInterface.TAG_GPS_PROCESSING_METHOD, - ExifInterface.TAG_GPS_TIMESTAMP, - ExifInterface.TAG_IMAGE_LENGTH, - ExifInterface.TAG_IMAGE_WIDTH, - ExifInterface.TAG_ISO, - ExifInterface.TAG_MAKE, - ExifInterface.TAG_MODEL, - ExifInterface.TAG_ORIENTATION, - ExifInterface.TAG_SUBSEC_TIME, - ExifInterface.TAG_SUBSEC_TIME_DIG, - ExifInterface.TAG_SUBSEC_TIME_ORIG, - ExifInterface.TAG_WHITE_BALANCE - }; - - public ImageEditingManager(ReactApplicationContext reactContext) { - super(reactContext); - new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public void onCatalystInstanceDestroy() { - new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - /** - * Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped - * image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting - * down) and when the module is instantiated, to handle the case where the app crashed. - */ - private static class CleanTask extends GuardedAsyncTask { - private final Context mContext; - - private CleanTask(ReactContext context) { - super(context); - mContext = context; - } - - @Override - protected void doInBackgroundGuarded(Void... params) { - cleanDirectory(mContext.getCacheDir()); - File externalCacheDir = mContext.getExternalCacheDir(); - if (externalCacheDir != null) { - cleanDirectory(externalCacheDir); - } - } - - private void cleanDirectory(File directory) { - File[] toDelete = - directory.listFiles( - new FilenameFilter() { - @Override - public boolean accept(File dir, String filename) { - return filename.startsWith(TEMP_FILE_PREFIX); - } - }); - if (toDelete != null) { - for (File file : toDelete) { - file.delete(); - } - } - } - } - - /** - * Crop an image. If all goes well, the success callback will be called with the file:// URI of - * the new image as the only argument. This is a temporary file - consider using - * CameraRollManager.saveImageWithTag to save it in the gallery. - * - * @param uri the MediaStore URI of the image to crop - * @param options crop parameters specified as {@code {offset: {x, y}, size: {width, height}}}. - * Optionally this also contains {@code {targetSize: {width, height}}}. If this is specified, - * the cropped image will be resized to that size. All units are in pixels (not DPs). - * @param success callback to be invoked when the image has been cropped; the only argument that - * is passed to this callback is the file:// URI of the new image - * @param error callback to be invoked when an error occurs (e.g. can't create file etc.) - */ - @Override - public void cropImage( - String uri, ReadableMap options, final Callback success, final Callback error) { - ReadableMap offset = options.hasKey("offset") ? options.getMap("offset") : null; - ReadableMap size = options.hasKey("size") ? options.getMap("size") : null; - boolean allowExternalStorage = - options.hasKey("allowExternalStorage") ? options.getBoolean("allowExternalStorage") : true; - - if (offset == null - || size == null - || !offset.hasKey("x") - || !offset.hasKey("y") - || !size.hasKey("width") - || !size.hasKey("height")) { - throw new JSApplicationIllegalArgumentException("Please specify offset and size"); - } - if (uri == null || uri.isEmpty()) { - throw new JSApplicationIllegalArgumentException("Please specify a URI"); - } - - CropTask cropTask = - new CropTask( - getReactApplicationContext(), - uri, - (int) offset.getDouble("x"), - (int) offset.getDouble("y"), - (int) size.getDouble("width"), - (int) size.getDouble("height"), - allowExternalStorage, - success, - error); - if (options.hasKey("displaySize")) { - ReadableMap targetSize = options.getMap("displaySize"); - cropTask.setTargetSize( - (int) targetSize.getDouble("width"), (int) targetSize.getDouble("height")); - } - cropTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private static class CropTask extends GuardedAsyncTask { - final Context mContext; - final String mUri; - final int mX; - final int mY; - final int mWidth; - final int mHeight; - final boolean mAllowExternalStorage; - int mTargetWidth = 0; - int mTargetHeight = 0; - final Callback mSuccess; - final Callback mError; - - private CropTask( - ReactContext context, - String uri, - int x, - int y, - int width, - int height, - boolean allowExternalStorage, - Callback success, - Callback error) { - super(context); - if (x < 0 || y < 0 || width <= 0 || height <= 0) { - throw new JSApplicationIllegalArgumentException( - String.format("Invalid crop rectangle: [%d, %d, %d, %d]", x, y, width, height)); - } - mContext = context; - mUri = uri; - mX = x; - mY = y; - mWidth = width; - mHeight = height; - mAllowExternalStorage = allowExternalStorage; - mSuccess = success; - mError = error; - } - - public void setTargetSize(int width, int height) { - if (width <= 0 || height <= 0) { - throw new JSApplicationIllegalArgumentException( - String.format("Invalid target size: [%d, %d]", width, height)); - } - mTargetWidth = width; - mTargetHeight = height; - } - - private InputStream openBitmapInputStream() throws IOException { - InputStream stream; - if (isLocalUri(mUri)) { - stream = mContext.getContentResolver().openInputStream(Uri.parse(mUri)); - } else { - URLConnection connection = new URL(mUri).openConnection(); - stream = connection.getInputStream(); - } - if (stream == null) { - throw new IOException("Cannot open bitmap: " + mUri); - } - return stream; - } - - @Override - protected void doInBackgroundGuarded(Void... params) { - try { - BitmapFactory.Options outOptions = new BitmapFactory.Options(); - - // If we're downscaling, we can decode the bitmap more efficiently, using less memory - boolean hasTargetSize = (mTargetWidth > 0) && (mTargetHeight > 0); - - Bitmap cropped; - if (hasTargetSize) { - cropped = cropAndResize(mTargetWidth, mTargetHeight, outOptions); - } else { - cropped = crop(outOptions); - } - - String mimeType = outOptions.outMimeType; - if (mimeType == null || mimeType.isEmpty()) { - throw new IOException("Could not determine MIME type"); - } - - File tempFile; - try { - tempFile = writeBitmapToInternalCache(mContext, cropped, mimeType); - } catch (Exception e) { - if (mAllowExternalStorage) { - tempFile = writeBitmapToExternalCache(mContext, cropped, mimeType); - } else { - throw new SecurityException( - "We couldn't create file in internal cache and external cache is disabled. Did you forget to pass allowExternalStorage=true?"); - } - } - - if (mimeType.equals("image/jpeg")) { - copyExif(mContext, Uri.parse(mUri), tempFile); - } - - mSuccess.invoke(Uri.fromFile(tempFile).toString()); - } catch (Exception e) { - mError.invoke(e.getMessage()); - } - } - - /** - * Reads and crops the bitmap. - * - * @param outOptions Bitmap options, useful to determine {@code outMimeType}. - */ - private Bitmap crop(BitmapFactory.Options outOptions) throws IOException { - InputStream inputStream = openBitmapInputStream(); - // Efficiently crops image without loading full resolution into memory - // https://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html - BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false); - try { - Rect rect = new Rect(mX, mY, mX + mWidth, mY + mHeight); - return decoder.decodeRegion(rect, outOptions); - } finally { - if (inputStream != null) { - inputStream.close(); - } - decoder.recycle(); - } - } - - /** - * Crop the rectangle given by {@code mX, mY, mWidth, mHeight} within the source bitmap and - * scale the result to {@code targetWidth, targetHeight}. - * - * @param outOptions Bitmap options, useful to determine {@code outMimeType}. - */ - private Bitmap cropAndResize( - int targetWidth, int targetHeight, BitmapFactory.Options outOptions) throws IOException { - Assertions.assertNotNull(outOptions); - - // Loading large bitmaps efficiently: - // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html - - // Just decode the dimensions - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - InputStream inputStream = openBitmapInputStream(); - try { - BitmapFactory.decodeStream(inputStream, null, options); - } finally { - if (inputStream != null) { - inputStream.close(); - } - } - - // This uses scaling mode COVER - - // Where would the crop rect end up within the scaled bitmap? - float newWidth, newHeight, newX, newY, scale; - float cropRectRatio = mWidth / (float) mHeight; - float targetRatio = targetWidth / (float) targetHeight; - if (cropRectRatio > targetRatio) { - // e.g. source is landscape, target is portrait - newWidth = mHeight * targetRatio; - newHeight = mHeight; - newX = mX + (mWidth - newWidth) / 2; - newY = mY; - scale = targetHeight / (float) mHeight; - } else { - // e.g. source is landscape, target is portrait - newWidth = mWidth; - newHeight = mWidth / targetRatio; - newX = mX; - newY = mY + (mHeight - newHeight) / 2; - scale = targetWidth / (float) mWidth; - } - - // Decode the bitmap. We have to open the stream again, like in the example linked above. - // Is there a way to just continue reading from the stream? - outOptions.inSampleSize = getDecodeSampleSize(mWidth, mHeight, targetWidth, targetHeight); - options.inJustDecodeBounds = false; - inputStream = openBitmapInputStream(); - - Bitmap bitmap; - try { - // This can use significantly less memory than decoding the full-resolution bitmap - bitmap = BitmapFactory.decodeStream(inputStream, null, outOptions); - if (bitmap == null) { - throw new IOException("Cannot decode bitmap: " + mUri); - } - } finally { - if (inputStream != null) { - inputStream.close(); - } - } - - int cropX = (int) Math.floor(newX / (float) outOptions.inSampleSize); - int cropY = (int) Math.floor(newY / (float) outOptions.inSampleSize); - int cropWidth = (int) Math.floor(newWidth / (float) outOptions.inSampleSize); - int cropHeight = (int) Math.floor(newHeight / (float) outOptions.inSampleSize); - float cropScale = scale * outOptions.inSampleSize; - - Matrix scaleMatrix = new Matrix(); - scaleMatrix.setScale(cropScale, cropScale); - boolean filter = true; - - return Bitmap.createBitmap(bitmap, cropX, cropY, cropWidth, cropHeight, scaleMatrix, filter); - } - } - - // Utils - - private static void copyExif(Context context, Uri oldImage, File newFile) throws IOException { - File oldFile = getFileFromUri(context, oldImage); - if (oldFile == null) { - FLog.w(ReactConstants.TAG, "Couldn't get real path for uri: " + oldImage); - return; - } - - ExifInterface oldExif = new ExifInterface(oldFile.getAbsolutePath()); - ExifInterface newExif = new ExifInterface(newFile.getAbsolutePath()); - for (String attribute : EXIF_ATTRIBUTES) { - String value = oldExif.getAttribute(attribute); - if (value != null) { - newExif.setAttribute(attribute, value); - } - } - newExif.saveAttributes(); - } - - private static @Nullable File getFileFromUri(Context context, Uri uri) { - if (uri.getScheme().equals("file")) { - return new File(uri.getPath()); - } else if (uri.getScheme().equals("content")) { - Cursor cursor = - context - .getContentResolver() - .query(uri, new String[] {MediaStore.MediaColumns.DATA}, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - String path = cursor.getString(0); - if (!TextUtils.isEmpty(path)) { - return new File(path); - } - } - } finally { - cursor.close(); - } - } - } - - return null; - } - - private static boolean isLocalUri(String uri) { - for (String localPrefix : LOCAL_URI_PREFIXES) { - if (uri.startsWith(localPrefix)) { - return true; - } - } - return false; - } - - private static String getFileExtensionForType(@Nullable String mimeType) { - if ("image/png".equals(mimeType)) { - return ".png"; - } - if ("image/webp".equals(mimeType)) { - return ".webp"; - } - return ".jpg"; - } - - private static Bitmap.CompressFormat getCompressFormatForType(String type) { - if ("image/png".equals(type)) { - return Bitmap.CompressFormat.PNG; - } - if ("image/webp".equals(type)) { - return Bitmap.CompressFormat.WEBP; - } - return Bitmap.CompressFormat.JPEG; - } - - private static File writeBitmapToInternalCache(Context context, Bitmap cropped, String mimeType) - throws IOException { - File tempFile = createTempFile(context.getCacheDir(), mimeType); - writeCompressedBitmapToFile(cropped, mimeType, tempFile); - return tempFile; - } - - private static File writeBitmapToExternalCache(Context context, Bitmap cropped, String mimeType) - throws IOException { - File tempFile = createTempFile(context.getExternalCacheDir(), mimeType); - writeCompressedBitmapToFile(cropped, mimeType, tempFile); - return tempFile; - } - - private static void writeCompressedBitmapToFile(Bitmap cropped, String mimeType, File tempFile) - throws IOException { - OutputStream out = new FileOutputStream(tempFile); - cropped.compress(getCompressFormatForType(mimeType), COMPRESS_QUALITY, out); - } - - /** - * Create a temporary file in internal / external storage to use for image scaling and caching. - * - * @param mimeType the MIME type of the file to create (image/*) - */ - private static File createTempFile(@Nullable File cacheDir, @Nullable String mimeType) - throws IOException { - if (cacheDir == null) { - throw new IOException("No cache directory available"); - } - return File.createTempFile(TEMP_FILE_PREFIX, getFileExtensionForType(mimeType), cacheDir); - } - - /** - * When scaling down the bitmap, decode only every n-th pixel in each dimension. Calculate the - * largest {@code inSampleSize} value that is a power of 2 and keeps both {@code width, height} - * larger or equal to {@code targetWidth, targetHeight}. This can significantly reduce memory - * usage. - */ - private static int getDecodeSampleSize(int width, int height, int targetWidth, int targetHeight) { - int inSampleSize = 1; - if (height > targetWidth || width > targetHeight) { - int halfHeight = height / 2; - int halfWidth = width / 2; - while ((halfWidth / inSampleSize) >= targetWidth - && (halfHeight / inSampleSize) >= targetHeight) { - inSampleSize *= 2; - } - } - return inSampleSize; - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 3cd6defd986b28..669718e63cc353 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -22,7 +22,6 @@ import com.facebook.react.modules.blob.BlobModule; import com.facebook.react.modules.blob.FileReaderModule; import com.facebook.react.modules.camera.CameraRollManager; -import com.facebook.react.modules.camera.ImageEditingManager; import com.facebook.react.modules.camera.ImageStoreManager; import com.facebook.react.modules.clipboard.ClipboardModule; import com.facebook.react.modules.datepicker.DatePickerDialogModule; @@ -80,7 +79,6 @@ DialogModule.class, FrescoModule.class, I18nManagerModule.class, - ImageEditingManager.class, ImageLoaderModule.class, ImageStoreManager.class, IntentModule.class, @@ -132,8 +130,6 @@ public MainReactPackage(MainPackageConfig config) { return new FrescoModule(context, true, mConfig != null ? mConfig.getFrescoConfig() : null); case I18nManagerModule.NAME: return new I18nManagerModule(context); - case ImageEditingManager.NAME: - return new ImageEditingManager(context); case ImageLoaderModule.NAME: return new ImageLoaderModule(context); case ImageStoreManager.NAME: @@ -213,7 +209,6 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { DialogModule.class, FrescoModule.class, I18nManagerModule.class, - ImageEditingManager.class, ImageLoaderModule.class, ImageStoreManager.class, IntentModule.class, diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 749202eeaad64a..a5e9e13ace4c8a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -284,11 +284,16 @@ private static String constructManageChildrenErrorMessage( StringBuilder stringBuilder = new StringBuilder(); if (null != viewToManage) { - stringBuilder.append("View tag:" + viewToManage.getId() + "\n"); + stringBuilder.append( + "View tag:" + + viewToManage.getId() + + " View Type:" + + viewToManage.getClass().toString() + + "\n"); stringBuilder.append(" children(" + viewManager.getChildCount(viewToManage) + "): [\n"); - for (int index = 0; index < viewManager.getChildCount(viewToManage); index += 16) { + for (int index = 0; viewManager.getChildAt(viewToManage, index) != null; index += 16) { for (int innerOffset = 0; - ((index + innerOffset) < viewManager.getChildCount(viewToManage)) && innerOffset < 16; + viewManager.getChildAt(viewToManage, index + innerOffset) != null && innerOffset < 16; innerOffset++) { stringBuilder.append( viewManager.getChildAt(viewToManage, index + innerOffset).getId() + ","); @@ -396,7 +401,7 @@ public synchronized void manageChildren( + constructManageChildrenErrorMessage( viewToManage, viewManager, indicesToRemove, viewsToAdd, tagsToDelete)); } - if (indexToRemove >= viewManager.getChildCount(viewToManage)) { + if (viewManager.getChildAt(viewToManage, indexToRemove) == null) { if (mRootTags.get(tag) && viewManager.getChildCount(viewToManage) == 0) { // This root node has already been removed (likely due to a threading issue caused by // async js execution). Ignore this root removal. diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 912fcbcc756969..22b82bed0ca1d5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -31,7 +31,10 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.RCTEventEmitter; import java.util.HashMap; @@ -42,6 +45,7 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat { private static final String TAG = "ReactAccessibilityDelegate"; + public static final String TOP_ACCESSIBILITY_ACTION_EVENT = "topAccessibilityAction"; private static int sCounter = 0x3f000000; private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; private static final int SEND_EVENT = 1; @@ -281,9 +285,24 @@ public boolean performAccessibilityAction(View host, int action, Bundle args) { event.putString("actionName", mAccessibilityActionsMap.get(action)); ReactContext reactContext = (ReactContext) host.getContext(); if (reactContext.hasActiveCatalystInstance()) { - reactContext - .getJSModule(RCTEventEmitter.class) - .receiveEvent(host.getId(), "topAccessibilityAction", event); + final int reactTag = host.getId(); + UIManager uiManager = UIManagerHelper.getUIManager(reactContext, reactTag); + if (uiManager != null) { + uiManager + .getEventDispatcher() + .dispatchEvent( + new Event(reactTag) { + @Override + public String getEventName() { + return TOP_ACCESSIBILITY_ACTION_EVENT; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(reactTag, TOP_ACCESSIBILITY_ACTION_EVENT, event); + } + }); + } } else { ReactSoftException.logSoftException( TAG, new ReactNoCrashSoftException("Cannot get RCTEventEmitter, no CatalystInstance")); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java index bd978336c2d0c8..30133a5388815b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.java @@ -13,7 +13,9 @@ import android.content.Context; import android.content.ContextWrapper; import android.view.View; +import android.widget.EditText; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.JSIModuleType; import com.facebook.react.bridge.ReactContext; @@ -27,6 +29,11 @@ /** Helper class for {@link UIManager}. */ public class UIManagerHelper { + public static final int PADDING_START_INDEX = 0; + public static final int PADDING_END_INDEX = 1; + public static final int PADDING_TOP_INDEX = 2; + public static final int PADDING_BOTTOM_INDEX = 3; + /** @return a {@link UIManager} that can handle the react tag received by parameter. */ @Nullable public static UIManager getUIManagerForReactTag(ReactContext context, int reactTag) { @@ -110,4 +117,18 @@ public static ReactContext getReactContext(View view) { } return (ReactContext) context; } + + /** + * @return the default padding used by Android EditText's. This method returns the padding in an + * array to avoid extra classloading during hot-path of RN Android. + */ + public static float[] getDefaultTextInputPadding(ThemedReactContext context) { + EditText editText = new EditText(context); + float[] padding = new float[4]; + padding[PADDING_START_INDEX] = PixelUtil.toDIPFromPixel(ViewCompat.getPaddingStart(editText)); + padding[PADDING_END_INDEX] = PixelUtil.toDIPFromPixel(ViewCompat.getPaddingEnd(editText)); + padding[PADDING_TOP_INDEX] = PixelUtil.toDIPFromPixel(editText.getPaddingTop()); + padding[PADDING_BOTTOM_INDEX] = PixelUtil.toDIPFromPixel(editText.getPaddingBottom()); + return padding; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 1f51e272ac9179..52750844ed8d26 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -437,6 +437,11 @@ public int startSurface( throw new UnsupportedOperationException(); } + @Override + public void stopSurface(final int surfaceId) { + throw new UnsupportedOperationException(); + } + /** Unregisters a new root view. */ @ReactMethod public void removeRootView(int rootViewTag) { @@ -945,4 +950,11 @@ public View resolveView(int tag) { .getNativeViewHierarchyManager() .resolveView(tag); } + + @Override + public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event) { + getReactApplicationContext() + .getJSModule(RCTEventEmitter.class) + .receiveEvent(targetTag, eventName, event); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index 4f8844cba117ad..69530675e32b31 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -25,7 +25,6 @@ import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ReactConstants; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.modules.core.ReactChoreographer; import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; import com.facebook.systrace.Systrace; @@ -596,7 +595,6 @@ public void execute() { private final DispatchUIFrameCallback mDispatchUIFrameCallback; private final ReactApplicationContext mReactApplicationContext; - private final boolean mAllowViewCommandsQueue; private ArrayList mViewCommandOperations = new ArrayList<>(); // Only called from the UIManager queue? @@ -637,7 +635,6 @@ public UIViewOperationQueue( ? DEFAULT_MIN_TIME_LEFT_IN_FRAME_FOR_NONBATCHED_OPERATION_MS : minTimeLeftInFrameForNonBatchedOperationMs); mReactApplicationContext = reactContext; - mAllowViewCommandsQueue = ReactFeatureFlags.allowEarlyViewCommandExecution; } /*package*/ NativeViewHierarchyManager getNativeViewHierarchyManager() { @@ -709,22 +706,14 @@ public void enqueueDispatchCommand( int reactTag, int commandId, @Nullable ReadableArray commandArgs) { final DispatchCommandOperation command = new DispatchCommandOperation(reactTag, commandId, commandArgs); - if (mAllowViewCommandsQueue) { - mViewCommandOperations.add(command); - } else { - mOperations.add(command); - } + mViewCommandOperations.add(command); } public void enqueueDispatchCommand( int reactTag, String commandId, @Nullable ReadableArray commandArgs) { final DispatchStringCommandOperation command = new DispatchStringCommandOperation(reactTag, commandId, commandArgs); - if (mAllowViewCommandsQueue) { - mViewCommandOperations.add(command); - } else { - mOperations.add(command); - } + mViewCommandOperations.add(command); } public void enqueueUpdateExtraData(int reactTag, Object extraData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java index b1326335e3d622..69778f028f5a94 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java @@ -21,8 +21,6 @@ public class ImageLoadEvent extends Event { @Retention(RetentionPolicy.SOURCE) @interface ImageEventType {} - // Currently ON_PROGRESS is not implemented, these can be added - // easily once support exists in fresco. public static final int ON_ERROR = 1; public static final int ON_LOAD = 2; public static final int ON_LOAD_END = 3; @@ -30,41 +28,61 @@ public class ImageLoadEvent extends Event { public static final int ON_PROGRESS = 5; private final int mEventType; - private final @Nullable String mImageUri; + private final @Nullable String mErrorMessage; + private final @Nullable String mSourceUri; private final int mWidth; private final int mHeight; - private final @Nullable String mImageError; + private final int mLoaded; + private final int mTotal; - public ImageLoadEvent(int viewId, @ImageEventType int eventType) { - this(viewId, eventType, null); + public static final ImageLoadEvent createLoadStartEvent(int viewId) { + return new ImageLoadEvent(viewId, ON_LOAD_START); } - public ImageLoadEvent(int viewId, @ImageEventType int eventType, boolean error, String message) { - this(viewId, eventType, null, 0, 0, message); + /** + * @param loaded Amount of the image that has been loaded. It should be number of bytes, but + * Fresco does not currently provides that information. + * @param total Amount that `loaded` will be when the image is fully loaded. + */ + public static final ImageLoadEvent createProgressEvent( + int viewId, @Nullable String imageUri, int loaded, int total) { + return new ImageLoadEvent(viewId, ON_PROGRESS, null, imageUri, 0, 0, loaded, total); } - public ImageLoadEvent(int viewId, @ImageEventType int eventType, String imageUri) { - this(viewId, eventType, imageUri, 0, 0, null); + public static final ImageLoadEvent createLoadEvent( + int viewId, @Nullable String imageUri, int width, int height) { + return new ImageLoadEvent(viewId, ON_LOAD, null, imageUri, width, height, 0, 0); } - public ImageLoadEvent( - int viewId, @ImageEventType int eventType, @Nullable String imageUri, int width, int height) { - this(viewId, eventType, imageUri, width, height, null); + public static final ImageLoadEvent createErrorEvent(int viewId, Throwable throwable) { + return new ImageLoadEvent(viewId, ON_ERROR, throwable.getMessage(), null, 0, 0, 0, 0); } - public ImageLoadEvent( + public static final ImageLoadEvent createLoadEndEvent(int viewId) { + return new ImageLoadEvent(viewId, ON_LOAD_END); + } + + private ImageLoadEvent(int viewId, @ImageEventType int eventType) { + this(viewId, eventType, null, null, 0, 0, 0, 0); + } + + private ImageLoadEvent( int viewId, @ImageEventType int eventType, - @Nullable String imageUri, + @Nullable String errorMessage, + @Nullable String sourceUri, int width, int height, - @Nullable String message) { + int loaded, + int total) { super(viewId); mEventType = eventType; - mImageUri = imageUri; + mErrorMessage = errorMessage; + mSourceUri = sourceUri; mWidth = width; mHeight = height; - mImageError = message; + mLoaded = loaded; + mTotal = total; } public static String eventNameForType(@ImageEventType int eventType) { @@ -100,26 +118,30 @@ public short getCoalescingKey() { public void dispatch(RCTEventEmitter rctEventEmitter) { WritableMap eventData = null; - if (mImageUri != null || (mEventType == ON_LOAD || mEventType == ON_ERROR)) { - eventData = Arguments.createMap(); - - if (mImageUri != null) { - eventData.putString("uri", mImageUri); - } - - if (mEventType == ON_LOAD) { - WritableMap source = Arguments.createMap(); - source.putDouble("width", mWidth); - source.putDouble("height", mHeight); - if (mImageUri != null) { - source.putString("url", mImageUri); - } - eventData.putMap("source", source); - } else if (mEventType == ON_ERROR) { - eventData.putString("error", mImageError); - } + switch (mEventType) { + case ON_PROGRESS: + eventData = Arguments.createMap(); + eventData.putInt("loaded", mLoaded); + eventData.putInt("total", mTotal); + break; + case ON_LOAD: + eventData = Arguments.createMap(); + eventData.putMap("source", createEventDataSource()); + break; + case ON_ERROR: + eventData = Arguments.createMap(); + eventData.putString("error", mErrorMessage); + break; } rctEventEmitter.receiveEvent(getViewTag(), getEventName(), eventData); } + + private WritableMap createEventDataSource() { + WritableMap source = Arguments.createMap(); + source.putString("uri", mSourceUri); + source.putDouble("width", mWidth); + source.putDouble("height", mHeight); + return source; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java new file mode 100644 index 00000000000000..336738e5d43b97 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java @@ -0,0 +1,78 @@ +/* + * 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. + */ + +package com.facebook.react.views.image; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import com.facebook.drawee.controller.ControllerListener; +import com.facebook.drawee.drawable.ForwardingDrawable; +import javax.annotation.Nullable; + +public class ReactImageDownloadListener extends ForwardingDrawable + implements ControllerListener { + + private static final int MAX_LEVEL = 10000; + + public ReactImageDownloadListener() { + super(new EmptyDrawable()); + } + + public void onProgressChange(int loaded, int total) {} + + @Override + protected boolean onLevelChange(int level) { + onProgressChange(level, MAX_LEVEL); + return super.onLevelChange(level); + } + + @Override + public void onSubmit(String id, Object callerContext) {} + + @Override + public void onFinalImageSet( + String id, @Nullable INFO imageInfo, @Nullable Animatable animatable) {} + + @Override + public void onIntermediateImageSet(String id, @Nullable INFO imageInfo) {} + + @Override + public void onIntermediateImageFailed(String id, Throwable throwable) {} + + @Override + public void onFailure(String id, Throwable throwable) {} + + @Override + public void onRelease(String id) {} + + /** A {@link Drawable} that renders nothing. */ + private static final class EmptyDrawable extends Drawable { + + @Override + public void draw(Canvas canvas) { + // Do nothing. + } + + @Override + public void setAlpha(int alpha) { + // Do nothing. + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // Do nothing. + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java index 7077ceb7ad9030..397a8378d603f4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -242,13 +242,15 @@ public void setHeaders(ReactImageView view, ReadableMap headers) { public @Nullable Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_START), - MapBuilder.of("registrationName", "onLoadStart"), + MapBuilder.of("registrationName", "onLoadStart"), + ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_PROGRESS), + MapBuilder.of("registrationName", "onProgress"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD), - MapBuilder.of("registrationName", "onLoad"), + MapBuilder.of("registrationName", "onLoad"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_ERROR), - MapBuilder.of("registrationName", "onError"), + MapBuilder.of("registrationName", "onError"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_END), - MapBuilder.of("registrationName", "onLoadEnd")); + MapBuilder.of("registrationName", "onLoadEnd")); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 515b2c4c419c20..68ada3dfc70627 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -26,7 +26,6 @@ import com.facebook.common.references.CloseableReference; import com.facebook.common.util.UriUtil; import com.facebook.drawee.controller.AbstractDraweeControllerBuilder; -import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.controller.ControllerListener; import com.facebook.drawee.controller.ForwardingControllerListener; import com.facebook.drawee.drawable.AutoRotateDrawable; @@ -199,7 +198,7 @@ public CloseableReference process(Bitmap source, PlatformBitmapFactory b private final RoundedCornerPostprocessor mRoundedCornerPostprocessor; private final TilePostprocessor mTilePostprocessor; private @Nullable IterativeBoxBlurPostProcessor mIterativeBoxBlurPostProcessor; - private @Nullable ControllerListener mControllerListener; + private @Nullable ReactImageDownloadListener mDownloadListener; private @Nullable ControllerListener mControllerForTesting; private @Nullable GlobalImageLoadListener mGlobalImageLoadListener; private @Nullable Object mCallerContext; @@ -231,17 +230,24 @@ public ReactImageView( public void setShouldNotifyLoadEvents(boolean shouldNotify) { if (!shouldNotify) { - mControllerListener = null; + mDownloadListener = null; } else { final EventDispatcher mEventDispatcher = UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId()); - mControllerListener = - new BaseControllerListener() { + mDownloadListener = + new ReactImageDownloadListener() { @Override - public void onSubmit(String id, Object callerContext) { + public void onProgressChange(int loaded, int total) { + // TODO: Somehow get image size and convert `loaded` and `total` to image bytes. mEventDispatcher.dispatchEvent( - new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_START)); + ImageLoadEvent.createProgressEvent( + getId(), mImageSource.getSource(), loaded, total)); + } + + @Override + public void onSubmit(String id, Object callerContext) { + mEventDispatcher.dispatchEvent(ImageLoadEvent.createLoadStartEvent(getId())); } @Override @@ -249,22 +255,18 @@ public void onFinalImageSet( String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { mEventDispatcher.dispatchEvent( - new ImageLoadEvent( + ImageLoadEvent.createLoadEvent( getId(), - ImageLoadEvent.ON_LOAD, mImageSource.getSource(), imageInfo.getWidth(), imageInfo.getHeight())); - mEventDispatcher.dispatchEvent( - new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_END)); + mEventDispatcher.dispatchEvent(ImageLoadEvent.createLoadEndEvent(getId())); } } @Override public void onFailure(String id, Throwable throwable) { - mEventDispatcher.dispatchEvent( - new ImageLoadEvent( - getId(), ImageLoadEvent.ON_ERROR, true, throwable.getMessage())); + mEventDispatcher.dispatchEvent(ImageLoadEvent.createErrorEvent(getId(), throwable)); } }; } @@ -273,11 +275,12 @@ public void onFailure(String id, Throwable throwable) { } public void setBlurRadius(float blurRadius) { - int pixelBlurRadius = (int) PixelUtil.toPixelFromDIP(blurRadius); + // Divide `blurRadius` by 2 to more closely match other platforms. + int pixelBlurRadius = (int) PixelUtil.toPixelFromDIP(blurRadius) / 2; if (pixelBlurRadius == 0) { mIterativeBoxBlurPostProcessor = null; } else { - mIterativeBoxBlurPostProcessor = new IterativeBoxBlurPostProcessor(pixelBlurRadius); + mIterativeBoxBlurPostProcessor = new IterativeBoxBlurPostProcessor(2, pixelBlurRadius); } mIsDirty = true; } @@ -543,15 +546,19 @@ public void maybeUpdateView() { mDraweeControllerBuilder.setLowResImageRequest(cachedImageRequest); } - if (mControllerListener != null && mControllerForTesting != null) { + if (mDownloadListener != null && mControllerForTesting != null) { ForwardingControllerListener combinedListener = new ForwardingControllerListener(); - combinedListener.addListener(mControllerListener); + combinedListener.addListener(mDownloadListener); combinedListener.addListener(mControllerForTesting); mDraweeControllerBuilder.setControllerListener(combinedListener); } else if (mControllerForTesting != null) { mDraweeControllerBuilder.setControllerListener(mControllerForTesting); - } else if (mControllerListener != null) { - mDraweeControllerBuilder.setControllerListener(mControllerListener); + } else if (mDownloadListener != null) { + mDraweeControllerBuilder.setControllerListener(mDownloadListener); + } + + if (mDownloadListener != null) { + hierarchy.setProgressBarImage(mDownloadListener); } setController(mDraweeControllerBuilder.build()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java index 00e0066b97a125..03a60ed27e1c90 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java @@ -17,7 +17,7 @@ import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.UIManagerHelper; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewManagerDelegate; import com.facebook.react.uimanager.annotations.ReactProp; @@ -105,26 +105,24 @@ public void setIdentifier(ReactModalHostView view, int value) {} @Override protected void addEventEmitters(ThemedReactContext reactContext, final ReactModalHostView view) { - UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class); - if (uiManager == null) { - return; + final EventDispatcher dispatcher = + UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId()); + if (dispatcher != null) { + view.setOnRequestCloseListener( + new ReactModalHostView.OnRequestCloseListener() { + @Override + public void onRequestClose(DialogInterface dialog) { + dispatcher.dispatchEvent(new RequestCloseEvent(view.getId())); + } + }); + view.setOnShowListener( + new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + dispatcher.dispatchEvent(new ShowEvent(view.getId())); + } + }); } - - final EventDispatcher dispatcher = uiManager.getEventDispatcher(); - view.setOnRequestCloseListener( - new ReactModalHostView.OnRequestCloseListener() { - @Override - public void onRequestClose(DialogInterface dialog) { - dispatcher.dispatchEvent(new RequestCloseEvent(view.getId())); - } - }); - view.setOnShowListener( - new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - dispatcher.dispatchEvent(new ShowEvent(view.getId())); - } - }); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index d5312acdb18672..e404e0d4e9730f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -17,6 +17,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStructure; +import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; @@ -347,24 +348,26 @@ private void updateProperties() { Assertions.assertNotNull(mDialog, "mDialog must exist when we call updateProperties"); Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - int activityWindowFlags = currentActivity.getWindow().getAttributes().flags; - if ((activityWindowFlags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) { - mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } else { - mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } + + Window window = mDialog.getWindow(); + if (currentActivity == null || currentActivity.isFinishing() || !window.isActive()) { + // If the activity has disappeared, then we shouldn't update the window associated to the + // Dialog. + return; + } + int activityWindowFlags = currentActivity.getWindow().getAttributes().flags; + if ((activityWindowFlags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) { + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } if (mTransparent) { - mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); } else { - mDialog.getWindow().setDimAmount(0.5f); - mDialog - .getWindow() - .setFlags( - WindowManager.LayoutParams.FLAG_DIM_BEHIND, - WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.setDimAmount(0.5f); + window.setFlags( + WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index 7f11c77abf3a22..8b7215532a6424 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -307,6 +307,8 @@ public void setContentOffset(ReactHorizontalScrollView view, ReadableMap value) double x = value.hasKey("x") ? value.getDouble("x") : 0; double y = value.hasKey("y") ? value.getDouble("y") : 0; view.reactScrollTo((int) PixelUtil.toPixelFromDIP(x), (int) PixelUtil.toPixelFromDIP(y)); + } else { + view.reactScrollTo(0, 0); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index adf04118c3f437..8e6f93ec28dd90 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -311,6 +311,8 @@ public void setContentOffset(ReactScrollView view, ReadableMap value) { double x = value.hasKey("x") ? value.getDouble("x") : 0; double y = value.hasKey("y") ? value.getDouble("y") : 0; view.reactScrollTo((int) PixelUtil.toPixelFromDIP(x), (int) PixelUtil.toPixelFromDIP(y)); + } else { + view.reactScrollTo(0, 0); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSlider.java b/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSlider.java index d2bac91e0b5759..03d527b02d938e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSlider.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSlider.java @@ -53,7 +53,7 @@ public ReactSlider(Context context, @Nullable AttributeSet attrs, int style) { disableStateListAnimatorIfNeeded(); } - private void disableStateListAnimatorIfNeeded() { + /* package */ void disableStateListAnimatorIfNeeded() { // We disable the state list animator for Android 6 and 7; this is a hack to prevent T37452851 // and https://github.com/facebook/react-native/issues/9979 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSliderManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSliderManager.java index ff5da0ccb6943c..9c8bc9ced0bca5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSliderManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/slider/ReactSliderManager.java @@ -72,11 +72,11 @@ public long measure( float height, YogaMeasureMode heightMode) { if (!mMeasured) { - SeekBar reactSlider = new ReactSlider(getThemedContext(), null, STYLE); + ReactSlider reactSlider = new ReactSlider(getThemedContext(), null, STYLE); // reactSlider is used for measurements purposes, it is not necessary to set a // StateListAnimator. // It is not safe to access StateListAnimator from a background thread. - reactSlider.setStateListAnimator(null); + reactSlider.disableStateListAnimatorIfNeeded(); final int spec = View.MeasureSpec.makeMeasureSpec( ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java index 401c8a0fa34a74..4fdbd76ababee3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import android.text.Layout; import android.text.Spannable; import android.text.TextUtils; import android.text.util.Linkify; @@ -96,6 +97,24 @@ public void setSelectionColor(ReactTextView view, @Nullable Integer color) { } } + @ReactProp(name = "android_hyphenationFrequency") + public void setAndroidHyphenationFrequency(ReactTextView view, @Nullable String frequency) { + if (frequency == null || frequency.equals("none")) { + view.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE); + } else if (frequency.equals("full")) { + view.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } else if (frequency.equals("balanced")) { + view.setHyphenationFrequency(Layout.BREAK_STRATEGY_BALANCED); + } else if (frequency.equals("high")) { + view.setHyphenationFrequency(Layout.BREAK_STRATEGY_HIGH_QUALITY); + } else if (frequency.equals("normal")) { + view.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + } else { + throw new JSApplicationIllegalArgumentException( + "Invalid android_hyphenationFrequency: " + frequency); + } + } + @ReactPropGroup( names = { ViewProps.BORDER_RADIUS, diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 2f13b1ee6ab8fd..31a1139b697a76 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -142,7 +142,6 @@ public ReactTextUpdate( public static ReactTextUpdate buildReactTextUpdateFromState( Spannable text, int jsEventCounter, - boolean containsImages, int textAlign, int textBreakStrategy, int justificationMode, @@ -150,7 +149,7 @@ public static ReactTextUpdate buildReactTextUpdateFromState( ReactTextUpdate textUpdate = new ReactTextUpdate( - text, jsEventCounter, containsImages, textAlign, textBreakStrategy, justificationMode); + text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode); textUpdate.mAttributedString = attributedString; return textUpdate; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 3d7988087ffaeb..f7a0a18995fe75 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -37,7 +37,6 @@ import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.common.UIManagerType; import com.facebook.react.uimanager.common.ViewUtil; -import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.views.view.ReactViewBackgroundManager; import java.util.ArrayList; import java.util.Collections; @@ -100,8 +99,9 @@ private ReactContext getReactContext() { protected void onLayout( boolean changed, int textViewLeft, int textViewTop, int textViewRight, int textViewBottom) { // TODO T62882314: Delete this method when Fabric is fully released in OSS + int reactTag = getId(); if (!(getText() instanceof Spanned) - || ViewUtil.getUIManagerType(getId()) == UIManagerType.FABRIC) { + || ViewUtil.getUIManagerType(reactTag) == UIManagerType.FABRIC) { /** * In general, {@link #setText} is called via {@link ReactTextViewManager#updateExtraData} * before we are laid out. This ordering is a requirement because we utilize the data from @@ -257,9 +257,9 @@ public int compare(Object o1, Object o2) { WritableMap event = Arguments.createMap(); event.putArray("inlineViews", inlineViewInfoArray2); - reactContext - .getJSModule(RCTEventEmitter.class) - .receiveEvent(getId(), "topInlineViewLayout", event); + if (uiManager != null) { + uiManager.receiveEvent(reactTag, "topInlineViewLayout", event); + } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index f5a83221375f67..dff2565c36ff30 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -31,7 +31,6 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Dynamic; @@ -58,7 +57,6 @@ import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerHelper; -import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; @@ -1094,7 +1092,7 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) { }); } - private class ReactContentSizeWatcher implements ContentSizeWatcher { + private static class ReactContentSizeWatcher implements ContentSizeWatcher { private ReactEditText mEditText; private @Nullable EventDispatcher mEventDispatcher; private int mPreviousContentWidth = 0; @@ -1103,8 +1101,7 @@ private class ReactContentSizeWatcher implements ContentSizeWatcher { public ReactContentSizeWatcher(ReactEditText editText) { mEditText = editText; ReactContext reactContext = getReactContext(editText); - UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class); - mEventDispatcher = uiManager != null ? uiManager.getEventDispatcher() : null; + mEventDispatcher = getEventDispatcher(reactContext, editText); } @Override @@ -1176,7 +1173,7 @@ public void onSelectionChanged(int start, int end) { } } - private class ReactScrollWatcher implements ScrollWatcher { + private static class ReactScrollWatcher implements ScrollWatcher { private ReactEditText mReactEditText; private EventDispatcher mEventDispatcher; @@ -1234,7 +1231,7 @@ public void setPadding(ReactEditText view, int left, int top, int right, int bot } /** - * May be overriden by subclasses that would like to provide their own instance of the internal + * May be overridden by subclasses that would like to provide their own instance of the internal * {@code EditText} this class uses to determine the expected size of the view. */ protected EditText createInternalEditText(ThemedReactContext themedReactContext) { @@ -1244,45 +1241,18 @@ protected EditText createInternalEditText(ThemedReactContext themedReactContext) @Override public Object updateState( ReactEditText view, ReactStylesDiffMap props, @Nullable StateWrapper stateWrapper) { - ReadableNativeMap state = stateWrapper.getState(); - - // Do we need to communicate theme back to C++? - // If so, this should only need to be done once per surface. - if (!state.getBoolean("hasThemeData")) { - WritableNativeMap update = new WritableNativeMap(); - - ReactContext reactContext = UIManagerHelper.getReactContext(view); - if (reactContext instanceof ThemedReactContext) { - ThemedReactContext themedReactContext = (ThemedReactContext) reactContext; - EditText editText = createInternalEditText(themedReactContext); - - // Even though we check `data["textChanged"].empty()` before using the value in C++, - // state updates crash without this value on key exception. It's unintuitive why - // folly::dynamic is crashing there and if there's any way to fix on the native side, - // so leave this here until we can figure out a better way of key-existence-checking in C++. - update.putNull("textChanged"); - - update.putDouble( - "themePaddingStart", PixelUtil.toDIPFromPixel(ViewCompat.getPaddingStart(editText))); - update.putDouble( - "themePaddingEnd", PixelUtil.toDIPFromPixel(ViewCompat.getPaddingEnd(editText))); - update.putDouble("themePaddingTop", PixelUtil.toDIPFromPixel(editText.getPaddingTop())); - update.putDouble( - "themePaddingBottom", PixelUtil.toDIPFromPixel(editText.getPaddingBottom())); - - stateWrapper.updateState(update); - } else { - ReactSoftException.logSoftException( - TAG, - new IllegalStateException( - "ReactContext is not a ThemedReactContent: " - + (reactContext != null ? reactContext.getClass().getName() : "null"))); - } + if (stateWrapper == null) { + throw new IllegalArgumentException("Unable to update a NULL state."); } + ReadableNativeMap state = stateWrapper.getState(); ReadableMap attributedString = state.getMap("attributedString"); ReadableMap paragraphAttributes = state.getMap("paragraphAttributes"); + if (attributedString == null || paragraphAttributes == null) { + throw new IllegalArgumentException("Invalid TextInput State was received as a parameters"); + } + Spannable spanned = TextLayoutManager.getOrCreateSpannableForText( view.getContext(), attributedString, mReactTextViewManagerCallback); @@ -1291,11 +1261,9 @@ public Object updateState( TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy")); view.mStateWrapper = stateWrapper; - return ReactTextUpdate.buildReactTextUpdateFromState( spanned, state.getInt("mostRecentEventCount"), - false, // TODO add this into local Data TextAttributeProps.getTextAlignment(props, TextLayoutManager.isRTL(attributedString)), textBreakStrategy, TextAttributeProps.getJustificationMode(props), diff --git a/ReactAndroid/src/main/res/devsupport/layout/dev_loading_view.xml b/ReactAndroid/src/main/res/devsupport/layout/dev_loading_view.xml index 9bf8bca6aabdaf..b7e6b8cc0df6d2 100644 --- a/ReactAndroid/src/main/res/devsupport/layout/dev_loading_view.xml +++ b/ReactAndroid/src/main/res/devsupport/layout/dev_loading_view.xml @@ -4,7 +4,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="#035900" + android:background="#404040" android:ellipsize="end" android:gravity="center" android:paddingTop="5dp" diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 0a854969495375..30a9a3bc41e675 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -20,8 +20,11 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JSIModuleType; import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.UIManagerModule; @@ -54,6 +57,8 @@ public class NativeAnimatedNodeTraversalTest { @Rule public PowerMockRule rule = new PowerMockRule(); private long mFrameTimeNanos; + private ReactApplicationContext mReactApplicationContextMock; + private CatalystInstance mCatalystInstanceMock; private UIManagerModule mUIManagerMock; private EventDispatcher mEventDispatcherMock; private NativeAnimatedNodesManager mNativeAnimatedNodesManager; @@ -83,6 +88,59 @@ public Object answer(InvocationOnMock invocation) throws Throwable { }); mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS; + + mReactApplicationContextMock = mock(ReactApplicationContext.class); + PowerMockito.when(mReactApplicationContextMock.hasActiveCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + return true; + } + }); + PowerMockito.when(mReactApplicationContextMock.hasCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + return true; + } + }); + PowerMockito.when(mReactApplicationContextMock.getCatalystInstance()) + .thenAnswer( + new Answer() { + @Override + public CatalystInstance answer(InvocationOnMock invocationOnMock) throws Throwable { + return mCatalystInstanceMock; + } + }); + PowerMockito.when(mReactApplicationContextMock.getNativeModule(any(Class.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + + mCatalystInstanceMock = mock(CatalystInstance.class); + PowerMockito.when(mCatalystInstanceMock.getJSIModule(any(JSIModuleType.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + PowerMockito.when(mCatalystInstanceMock.getNativeModule(any(Class.class))) + .thenAnswer( + new Answer() { + @Override + public UIManagerModule answer(InvocationOnMock invocationOnMock) throws Throwable { + return mUIManagerMock; + } + }); + mUIManagerMock = mock(UIManagerModule.class); mEventDispatcherMock = mock(EventDispatcher.class); PowerMockito.when(mUIManagerMock.getEventDispatcher()) @@ -125,7 +183,7 @@ public String resolveCustomEventName(String eventName) { }; } }); - mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mReactApplicationContextMock); } /** @@ -802,6 +860,19 @@ public void testHandleStoppingAnimation() { verifyNoMoreInteractions(animationCallback); } + @Test + public void testGetValue() { + int tag = 1; + mNativeAnimatedNodesManager.createAnimatedNode( + tag, JavaOnlyMap.of("type", "value", "value", 1d, "offset", 0d)); + + Callback saveValueCallbackMock = mock(Callback.class); + + mNativeAnimatedNodesManager.getValue(tag, saveValueCallbackMock); + + verify(saveValueCallbackMock, times(1)).invoke(1d); + } + @Test public void testInterpolationNode() { mNativeAnimatedNodesManager.createAnimatedNode( @@ -931,7 +1002,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { MapBuilder.of("topScroll", MapBuilder.of("registrationName", "onScroll"))); } }); - mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mReactApplicationContextMock); createSimpleAnimatedViewWithOpacity(viewTag, 0d); diff --git a/ReactCommon/React-Fabric.podspec b/ReactCommon/React-Fabric.podspec index 15e1d2aadb2747..5175727d808b10 100644 --- a/ReactCommon/React-Fabric.podspec +++ b/ReactCommon/React-Fabric.podspec @@ -96,6 +96,15 @@ Pod::Spec.new do |s| sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } end + ss.subspec "legacyviewmanagerinterop" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "fabric/components/legacyviewmanagerinterop/**/*.{m,mm,cpp,h}" + sss.exclude_files = "**/tests/*" + sss.header_dir = "react/components/legacyviewmanagerinterop" + sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/Folly\" \"$(PODS_ROOT)/Headers/Private/React-Core\"" } + end + ss.subspec "modal" do |sss| sss.dependency folly_dep_name, folly_version sss.compiler_flags = folly_compiler_flags @@ -109,7 +118,9 @@ Pod::Spec.new do |s| sss.dependency folly_dep_name, folly_version sss.compiler_flags = folly_compiler_flags sss.source_files = "fabric/components/rncore/*.{m,mm,cpp,h}" - sss.exclude_files = "**/tests/*", "fabric/components/rncore/*Tests.{h,cpp}" + sss.exclude_files = "**/tests/*", "fabric/components/rncore/*Tests.{h,cpp}", + # TODO: These should be re-enabled later when Codegen Native Module support is needed. + "fabric/components/rncore/rncore-generated.mm", "fabric/components/rncore/NativeModules.{h,cpp}" sss.header_dir = "react/components/rncore" sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } end @@ -123,6 +134,15 @@ Pod::Spec.new do |s| sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } end + ss.subspec "safeareaview" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "fabric/components/safeareaview/**/*.{m,mm,cpp,h}" + sss.exclude_files = "**/tests/*" + sss.header_dir = "react/components/safeareaview" + sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/Folly\"" } + end + ss.subspec "scrollview" do |sss| sss.dependency folly_dep_name, folly_version sss.compiler_flags = folly_compiler_flags @@ -151,6 +171,24 @@ Pod::Spec.new do |s| sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } end + ss.subspec "textinput" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "fabric/components/textinput/iostextinput/**/*.{m,mm,cpp,h}" + sss.exclude_files = "**/tests/*" + sss.header_dir = "react/components/iostextinput" + sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/Folly\"" } + end + + ss.subspec "unimplementedview" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "fabric/components/unimplementedview/**/*.{m,mm,cpp,h}" + sss.exclude_files = "**/tests/*" + sss.header_dir = "react/components/unimplementedview" + sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/Folly\"" } + end + ss.subspec "view" do |sss| sss.dependency folly_dep_name, folly_version sss.dependency "Yoga" diff --git a/ReactCommon/cxxreact/BUCK b/ReactCommon/cxxreact/BUCK index 0db1caf4366450..9fffd1a414cc2a 100644 --- a/ReactCommon/cxxreact/BUCK +++ b/ReactCommon/cxxreact/BUCK @@ -84,6 +84,7 @@ rn_xplat_cxx_library( CXXREACT_PUBLIC_HEADERS = [ "CxxNativeModule.h", + "ErrorUtils.h", "Instance.h", "JSBundleType.h", "JSDeltaBundleClient.h", diff --git a/ReactCommon/cxxreact/ErrorUtils.h b/ReactCommon/cxxreact/ErrorUtils.h new file mode 100644 index 00000000000000..3ab715b2efb0ba --- /dev/null +++ b/ReactCommon/cxxreact/ErrorUtils.h @@ -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. + */ + +#include + +namespace facebook { +namespace react { + +inline static void +handleJSError(jsi::Runtime &runtime, const jsi::JSError &error, bool isFatal) { + auto errorUtils = runtime.global().getProperty(runtime, "ErrorUtils"); + if (errorUtils.isUndefined() || !errorUtils.isObject() || + !errorUtils.getObject(runtime).hasProperty(runtime, "reportFatalError") || + !errorUtils.getObject(runtime).hasProperty(runtime, "reportError")) { + // ErrorUtils was not set up. This probably means the bundle didn't + // load properly. + throw jsi::JSError( + runtime, + "ErrorUtils is not set up properly. Something probably went wrong trying to load the JS bundle. Trying to report error " + + error.getMessage(), + error.getStack()); + } + + // TODO(janzer): Rewrite this function to return the processed error + // instead of just reporting it through the native module + if (isFatal) { + auto func = errorUtils.asObject(runtime).getPropertyAsFunction( + runtime, "reportFatalError"); + + func.call(runtime, error.value()); + } else { + auto func = errorUtils.asObject(runtime).getPropertyAsFunction( + runtime, "reportError"); + + func.call(runtime, error.value()); + } +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/cxxreact/JSBigString.cpp b/ReactCommon/cxxreact/JSBigString.cpp index 834a7ca6900f8d..77e22800fd8e6a 100644 --- a/ReactCommon/cxxreact/JSBigString.cpp +++ b/ReactCommon/cxxreact/JSBigString.cpp @@ -126,6 +126,13 @@ const char *JSBigFileString::c_str() const { } #endif // WITH_FBREMAP } + static const size_t kMinPageSize = 4096; + CHECK(!(reinterpret_cast(m_data) & (kMinPageSize - 1))) + << "mmap address misaligned, likely corrupted" + << " m_data: " << (const void *)m_data; + CHECK(m_pageOff <= m_size) + << "offset impossibly large, likely corrupted" + << " m_pageOff: " << m_pageOff << " m_size: " << m_size; return m_data + m_pageOff; } diff --git a/ReactCommon/cxxreact/JSBundleType.cpp b/ReactCommon/cxxreact/JSBundleType.cpp index c676c738e0aec1..05bacb4107c7b7 100644 --- a/ReactCommon/cxxreact/JSBundleType.cpp +++ b/ReactCommon/cxxreact/JSBundleType.cpp @@ -13,14 +13,11 @@ namespace facebook { namespace react { static uint32_t constexpr RAMBundleMagicNumber = 0xFB0BD1E5; -static uint32_t constexpr BCBundleMagicNumber = 0x6D657300; ScriptTag parseTypeFromHeader(const BundleHeader &header) { switch (folly::Endian::little(header.magic)) { case RAMBundleMagicNumber: return ScriptTag::RAMBundle; - case BCBundleMagicNumber: - return ScriptTag::BCBundle; default: return ScriptTag::String; } @@ -32,8 +29,6 @@ const char *stringForScriptTag(const ScriptTag &tag) { return "String"; case ScriptTag::RAMBundle: return "RAM Bundle"; - case ScriptTag::BCBundle: - return "BC Bundle"; } return ""; } diff --git a/ReactCommon/cxxreact/JSBundleType.h b/ReactCommon/cxxreact/JSBundleType.h index 0a5bad0894c293..45a9045818897c 100644 --- a/ReactCommon/cxxreact/JSBundleType.h +++ b/ReactCommon/cxxreact/JSBundleType.h @@ -27,7 +27,6 @@ namespace react { enum struct ScriptTag { String = 0, RAMBundle, - BCBundle, }; /** diff --git a/ReactCommon/cxxreact/NativeToJsBridge.cpp b/ReactCommon/cxxreact/NativeToJsBridge.cpp index 58e99c9922152e..d0c13554f28a13 100644 --- a/ReactCommon/cxxreact/NativeToJsBridge.cpp +++ b/ReactCommon/cxxreact/NativeToJsBridge.cpp @@ -14,6 +14,7 @@ #include #include +#include "ErrorUtils.h" #include "Instance.h" #include "JSBigString.h" #include "MessageQueueThread.h" @@ -26,6 +27,8 @@ #ifdef WITH_FBSYSTRACE #include +#include + using fbsystrace::FbSystraceAsyncFlow; #endif @@ -338,37 +341,23 @@ std::shared_ptr NativeToJsBridge::getDecoratedNativeCallInvoker( } RuntimeExecutor NativeToJsBridge::getRuntimeExecutor() { - auto runtimeExecutor = [this, isDestroyed = m_destroyed]( - std::function - &&callback) { - if (*isDestroyed) { - return; - } - runOnExecutorQueue([callback = std::move(callback)](JSExecutor *executor) { - jsi::Runtime *runtime = (jsi::Runtime *)executor->getJavaScriptContext(); - try { - callback(*runtime); - } catch (jsi::JSError &originalError) { - auto errorUtils = runtime->global().getProperty(*runtime, "ErrorUtils"); - if (errorUtils.isUndefined() || !errorUtils.isObject() || - !errorUtils.getObject(*runtime).hasProperty( - *runtime, "reportFatalError")) { - // ErrorUtils was not set up. This probably means the bundle didn't - // load properly. - throw jsi::JSError( - *runtime, - "ErrorUtils is not set up properly. Something probably went wrong trying to load the JS bundle. Trying to report error " + - originalError.getMessage(), - originalError.getStack()); + auto runtimeExecutor = + [this, isDestroyed = m_destroyed]( + std::function &&callback) { + if (*isDestroyed) { + return; } - // TODO(janzer): Rewrite this function to return the processed error - // instead of just reporting it through the native module - auto func = errorUtils.asObject(*runtime).getPropertyAsFunction( - *runtime, "reportFatalError"); - func.call(*runtime, originalError.value(), jsi::Value(true)); - } - }); - }; + runOnExecutorQueue( + [callback = std::move(callback)](JSExecutor *executor) { + jsi::Runtime *runtime = + (jsi::Runtime *)executor->getJavaScriptContext(); + try { + callback(*runtime); + } catch (jsi::JSError &originalError) { + handleJSError(*runtime, originalError, true); + } + }); + }; return runtimeExecutor; } diff --git a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp index 91b73f5bdaea05..9d97fb1d533e41 100644 --- a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp +++ b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.cpp @@ -50,6 +50,7 @@ static better::optional parseAnimationType(std::string param) { return better::optional(AnimationType::Keyboard); } + LOG(ERROR) << "Error parsing animation type: " << param; return {}; } @@ -68,6 +69,7 @@ static better::optional parseAnimationProperty( return better::optional(AnimationProperty::ScaleXY); } + LOG(ERROR) << "Error parsing animation property: " << param; return {}; } @@ -87,14 +89,19 @@ static better::optional parseAnimationConfig( auto const typeIt = config.find("type"); if (typeIt == config.items().end()) { + LOG(ERROR) << "Error parsing animation config: could not find field `type`"; return {}; } auto const animationTypeParam = typeIt->second; if (animationTypeParam.empty() || !animationTypeParam.isString()) { + LOG(ERROR) + << "Error parsing animation config: could not unwrap field `type`"; return {}; } const auto animationType = parseAnimationType(animationTypeParam.asString()); if (!animationType) { + LOG(ERROR) + << "Error parsing animation config: could not parse field `type`"; return {}; } @@ -102,15 +109,21 @@ static better::optional parseAnimationConfig( if (parsePropertyType) { auto const propertyIt = config.find("property"); if (propertyIt == config.items().end()) { + LOG(ERROR) + << "Error parsing animation config: could not find field `property`"; return {}; } auto const animationPropertyParam = propertyIt->second; if (animationPropertyParam.empty() || !animationPropertyParam.isString()) { + LOG(ERROR) + << "Error parsing animation config: could not unwrap field `property`"; return {}; } const auto animationPropertyParsed = parseAnimationProperty(animationPropertyParam.asString()); if (!animationPropertyParsed) { + LOG(ERROR) + << "Error parsing animation config: could not parse field `property`"; return {}; } animationProperty = *animationPropertyParsed; @@ -126,6 +139,8 @@ static better::optional parseAnimationConfig( if (durationIt->second.isDouble()) { duration = durationIt->second.asDouble(); } else { + LOG(ERROR) + << "Error parsing animation config: field `duration` must be a number"; return {}; } } @@ -135,6 +150,8 @@ static better::optional parseAnimationConfig( if (delayIt->second.isDouble()) { delay = delayIt->second.asDouble(); } else { + LOG(ERROR) + << "Error parsing animation config: field `delay` must be a number"; return {}; } } @@ -145,6 +162,8 @@ static better::optional parseAnimationConfig( if (springDampingIt->second.isDouble()) { springDamping = springDampingIt->second.asDouble(); } else { + LOG(ERROR) + << "Error parsing animation config: field `springDamping` must be a number"; return {}; } } @@ -154,6 +173,8 @@ static better::optional parseAnimationConfig( if (initialVelocityIt->second.isDouble()) { initialVelocity = initialVelocityIt->second.asDouble(); } else { + LOG(ERROR) + << "Error parsing animation config: field `initialVelocity` must be a number"; return {}; } } @@ -173,27 +194,28 @@ static better::optional parseLayoutAnimationConfig( return {}; } - auto const durationIt = config.find("duration"); + const auto durationIt = config.find("duration"); if (durationIt == config.items().end() || !durationIt->second.isDouble()) { return {}; } const double duration = durationIt->second.asDouble(); - const auto createConfig = - parseAnimationConfig(config["create"], duration, true); - if (!createConfig) { - return {}; - } + const auto createConfigIt = config.find("create"); + const auto createConfig = createConfigIt == config.items().end() + ? better::optional(AnimationConfig{}) + : parseAnimationConfig(createConfigIt->second, duration, true); - const auto updateConfig = - parseAnimationConfig(config["update"], duration, false); - if (!updateConfig) { - return {}; - } + const auto updateConfigIt = config.find("update"); + const auto updateConfig = updateConfigIt == config.items().end() + ? better::optional(AnimationConfig{}) + : parseAnimationConfig(updateConfigIt->second, duration, false); - const auto deleteConfig = - parseAnimationConfig(config["delete"], duration, true); - if (!deleteConfig) { + const auto deleteConfigIt = config.find("delete"); + const auto deleteConfig = deleteConfigIt == config.items().end() + ? better::optional(AnimationConfig{}) + : parseAnimationConfig(deleteConfigIt->second, duration, true); + + if (!createConfig || !updateConfig || !deleteConfig) { return {}; } @@ -255,6 +277,10 @@ LayoutAnimationKeyFrameManager::calculateAnimationProgress( uint64_t now, const LayoutAnimation &animation, const AnimationConfig &mutationConfig) const { + if (mutationConfig.animationType == AnimationType::None) { + return {1, 1}; + } + uint64_t startTime = animation.startTime; uint64_t delay = mutationConfig.delay; uint64_t endTime = startTime + delay + mutationConfig.duration; @@ -345,11 +371,14 @@ void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( } // Do we need to adjust the index of this operation? - if (isRemoveMutation && mutation.index <= finalAnimationMutation.index) { - finalAnimationMutation.index--; - } else if ( - isInsertMutation && mutation.index <= finalAnimationMutation.index) { - finalAnimationMutation.index++; + if (isRemoveMutation) { + if (mutation.index <= finalAnimationMutation.index) { + finalAnimationMutation.index--; + } + } else if (isInsertMutation) { + if (mutation.index <= finalAnimationMutation.index) { + finalAnimationMutation.index++; + } } } } @@ -492,8 +521,8 @@ LayoutAnimationKeyFrameManager::pullTransaction( mutation.type == ShadowViewMutation::Type::Remove ? mutation.oldChildShadowView : mutation.newChildShadowView); - auto const &componentDescriptor = - getComponentDescriptorForShadowView(baselineShadowView); + bool haveComponentDescriptor = + hasComponentDescriptorForShadowView(baselineShadowView); auto mutationConfig = (mutation.type == ShadowViewMutation::Type::Delete @@ -569,6 +598,38 @@ LayoutAnimationKeyFrameManager::pullTransaction( if (isRemoveReinserted || !haveConfiguration || isReparented || mutation.type == ShadowViewMutation::Type::Create || mutation.type == ShadowViewMutation::Type::Insert) { + // Indices for immediate INSERT mutations must be adjusted to insert + // at higher indices if previous animations have deferred removals + // before the insertion indect + // TODO: refactor to reduce code duplication + if (mutation.type == ShadowViewMutation::Type::Insert) { + int adjustedIndex = mutation.index; + for (const auto &inflightAnimation : inflightAnimations_) { + if (inflightAnimation.surfaceId != surfaceId) { + continue; + } + for (auto it = inflightAnimation.keyFrames.begin(); + it != inflightAnimation.keyFrames.end(); + it++) { + const auto &animatedKeyFrame = *it; + if (!animatedKeyFrame.finalMutationForKeyFrame.has_value() || + animatedKeyFrame.parentView.tag != + mutation.parentShadowView.tag || + animatedKeyFrame.type != AnimationConfigurationType::Noop) { + continue; + } + const auto &delayedFinalMutation = + *animatedKeyFrame.finalMutationForKeyFrame; + if (delayedFinalMutation.type == + ShadowViewMutation::Type::Remove && + delayedFinalMutation.index <= adjustedIndex) { + adjustedIndex++; + } + } + } + mutation.index = adjustedIndex; + } + immediateMutations.push_back(mutation); // Adjust indices for any non-directly-conflicting animations that @@ -599,8 +660,11 @@ LayoutAnimationKeyFrameManager::pullTransaction( AnimationKeyFrame keyFrame{}; if (mutation.type == ShadowViewMutation::Type::Insert) { if (mutationConfig->animationProperty == - AnimationProperty::Opacity) { - auto props = componentDescriptor.cloneProps(viewStart.props, {}); + AnimationProperty::Opacity && + haveComponentDescriptor) { + auto props = + getComponentDescriptorForShadowView(baselineShadowView) + .cloneProps(viewStart.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { @@ -614,8 +678,10 @@ LayoutAnimationKeyFrameManager::pullTransaction( bool isScaleY = mutationConfig->animationProperty == AnimationProperty::ScaleY || mutationConfig->animationProperty == AnimationProperty::ScaleXY; - if (isScaleX || isScaleY) { - auto props = componentDescriptor.cloneProps(viewStart.props, {}); + if ((isScaleX || isScaleY) && haveComponentDescriptor) { + auto props = + getComponentDescriptorForShadowView(baselineShadowView) + .cloneProps(viewStart.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { @@ -634,8 +700,11 @@ LayoutAnimationKeyFrameManager::pullTransaction( 0}; } else if (mutation.type == ShadowViewMutation::Type::Delete) { if (mutationConfig->animationProperty == - AnimationProperty::Opacity) { - auto props = componentDescriptor.cloneProps(viewFinal.props, {}); + AnimationProperty::Opacity && + haveComponentDescriptor) { + auto props = + getComponentDescriptorForShadowView(baselineShadowView) + .cloneProps(viewFinal.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { @@ -649,8 +718,10 @@ LayoutAnimationKeyFrameManager::pullTransaction( bool isScaleY = mutationConfig->animationProperty == AnimationProperty::ScaleY || mutationConfig->animationProperty == AnimationProperty::ScaleXY; - if (isScaleX || isScaleY) { - auto props = componentDescriptor.cloneProps(viewFinal.props, {}); + if ((isScaleX || isScaleY) && haveComponentDescriptor) { + auto props = + getComponentDescriptorForShadowView(baselineShadowView) + .cloneProps(viewFinal.props, {}); const auto viewProps = dynamic_cast(props.get()); if (viewProps != nullptr) { @@ -690,14 +761,51 @@ LayoutAnimationKeyFrameManager::pullTransaction( // instruction will be delayed and therefore may execute outside of // otherwise-expected order, other views may be inserted before the // Remove is executed, requiring index adjustment. + // To be clear: when executed synchronously, REMOVE operations + // always come before INSERT operations (at the same level of the + // tree hierarchy). { int adjustedIndex = mutation.index; - for (const auto &otherMutation : mutations) { + for (auto &otherMutation : mutations) { if (otherMutation.type == ShadowViewMutation::Type::Insert && otherMutation.parentShadowView.tag == parentTag) { if (otherMutation.index <= adjustedIndex && !mutatedViewIsVirtual(otherMutation)) { adjustedIndex++; + } else { + // If we are delaying this remove instruction, conversely, + // we must adjust upward the insertion index of any INSERT + // instructions if the View is insert *after* this view in + // the hierarchy. + otherMutation.index++; + } + } + } + + // We also need to account for delayed mutations that have already + // been queued, such that their ShadowNodes are not accounted for + // in mutation instructions, but they are still in the platform's + // View hierarchy. + for (const auto &inflightAnimation : inflightAnimations_) { + if (inflightAnimation.surfaceId != surfaceId) { + continue; + } + for (auto it = inflightAnimation.keyFrames.begin(); + it != inflightAnimation.keyFrames.end(); + it++) { + const auto &animatedKeyFrame = *it; + if (!animatedKeyFrame.finalMutationForKeyFrame.has_value() || + animatedKeyFrame.parentView.tag != parentTag || + animatedKeyFrame.type != + AnimationConfigurationType::Noop) { + continue; + } + const auto &delayedFinalMutation = + *animatedKeyFrame.finalMutationForKeyFrame; + if (delayedFinalMutation.type == + ShadowViewMutation::Type::Remove && + delayedFinalMutation.index <= adjustedIndex) { + adjustedIndex++; } } } @@ -776,12 +884,17 @@ LayoutAnimationKeyFrameManager::pullTransaction( } { - std::stringstream ss(getDebugDescription(mutationsToAnimate, {})); - std::string to; - while (std::getline(ss, to, '\n')) { - LOG(ERROR) - << "LayoutAnimationKeyFrameManager.cpp: got FINAL list: Line: " - << to; + for (const auto &keyframe : keyFramesToAnimate) { + if (keyframe.finalMutationForKeyFrame) { + std::stringstream ss( + getDebugDescription(*keyframe.finalMutationForKeyFrame, {})); + std::string to; + while (std::getline(ss, to, '\n')) { + LOG(ERROR) + << "LayoutAnimationKeyFrameManager.cpp: got FINAL list: Line: " + << to; + } + } } } #endif @@ -835,8 +948,7 @@ LayoutAnimationKeyFrameManager::pullTransaction( // another that has not yet been executed because it is a part of an ongoing // animation, its index may need to be adjusted. for (auto const &animatedMutation : mutationsForAnimation) { - if (animatedMutation.type == ShadowViewMutation::Type::Insert || - animatedMutation.type == ShadowViewMutation::Type::Remove) { + if (animatedMutation.type == ShadowViewMutation::Type::Remove) { adjustDelayedMutationIndicesForMutation(surfaceId, animatedMutation); } } @@ -861,9 +973,8 @@ LayoutAnimationKeyFrameManager::pullTransaction( } } - // TODO: fill in telemetry return MountingTransaction{ - surfaceId, transactionNumber, std::move(mutations), {}}; + surfaceId, transactionNumber, std::move(mutations), telemetry}; } bool LayoutAnimationKeyFrameManager::mutatedViewIsVirtual( @@ -882,6 +993,12 @@ bool LayoutAnimationKeyFrameManager::mutatedViewIsVirtual( return viewIsVirtual; } +bool LayoutAnimationKeyFrameManager::hasComponentDescriptorForShadowView( + ShadowView const &shadowView) const { + return componentDescriptorRegistry_->hasComponentDescriptorAt( + shadowView.componentHandle); +} + ComponentDescriptor const & LayoutAnimationKeyFrameManager::getComponentDescriptorForShadowView( ShadowView const &shadowView) const { @@ -907,6 +1024,9 @@ ShadowView LayoutAnimationKeyFrameManager::createInterpolatedShadowView( AnimationConfig const &animationConfig, ShadowView startingView, ShadowView finalView) const { + if (!hasComponentDescriptorForShadowView(startingView)) { + return finalView; + } ComponentDescriptor const &componentDescriptor = getComponentDescriptorForShadowView(startingView); auto mutatedShadowView = ShadowView(startingView); diff --git a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h index 444e310c454675..73906fb65ef1e2 100644 --- a/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h +++ b/ReactCommon/fabric/animations/LayoutAnimationKeyFrameManager.h @@ -22,6 +22,7 @@ namespace react { // This corresponds exactly with JS. enum class AnimationType { + None, Spring, Linear, EaseInEaseOut, @@ -46,13 +47,14 @@ enum class AnimationConfigurationType { // This corresponds exactly with JS. struct AnimationConfig { - AnimationType animationType; - AnimationProperty animationProperty; - double duration; // these are perhaps better represented as uint64_t, but they + AnimationType animationType = AnimationType::None; + AnimationProperty animationProperty = AnimationProperty::NotApplicable; + double duration = + 0; // these are perhaps better represented as uint64_t, but they // come from JS as doubles - double delay; - double springDamping; - double initialVelocity; + double delay = 0; + double springDamping = 0; + double initialVelocity = 0; }; // This corresponds exactly with JS. @@ -146,6 +148,7 @@ class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate, protected: bool mutatedViewIsVirtual(ShadowViewMutation const &mutation) const; + bool hasComponentDescriptorForShadowView(ShadowView const &shadowView) const; ComponentDescriptor const &getComponentDescriptorForShadowView( ShadowView const &shadowView) const; std::pair calculateAnimationProgress( diff --git a/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.cpp b/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.cpp index b85cf63661a532..2081886c660a9e 100644 --- a/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.cpp +++ b/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.cpp @@ -168,6 +168,18 @@ ComponentDescriptor const &ComponentDescriptorRegistry::at( return *_registryByHandle.at(componentHandle); } +bool ComponentDescriptorRegistry::hasComponentDescriptorAt( + ComponentHandle componentHandle) const { + std::shared_lock lock(mutex_); + + auto iterator = _registryByHandle.find(componentHandle); + if (iterator == _registryByHandle.end()) { + return false; + } + + return true; +} + SharedShadowNode ComponentDescriptorRegistry::createNode( Tag tag, std::string const &viewName, diff --git a/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.h b/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.h index 3f25839bf5c06d..07e211d7d0f0a4 100644 --- a/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.h +++ b/ReactCommon/fabric/componentregistry/ComponentDescriptorRegistry.h @@ -51,6 +51,8 @@ class ComponentDescriptorRegistry { ComponentDescriptor const &at(std::string const &componentName) const; ComponentDescriptor const &at(ComponentHandle componentHandle) const; + bool hasComponentDescriptorAt(ComponentHandle componentHandle) const; + ShadowNode::Shared createNode( Tag tag, std::string const &viewName, diff --git a/ReactCommon/fabric/components/art/group/ARTGroupProps.cpp b/ReactCommon/fabric/components/art/group/ARTGroupProps.cpp index 04e9c60237656d..2d735569efe33a 100644 --- a/ReactCommon/fabric/components/art/group/ARTGroupProps.cpp +++ b/ReactCommon/fabric/components/art/group/ARTGroupProps.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace facebook { namespace react { @@ -24,8 +25,9 @@ ARTGroupProps::ARTGroupProps( #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE -SharedDebugStringConvertibleList RawTextProps::getDebugProps() const { - return {debugStringConvertibleItem("opacity", opacity)}; +SharedDebugStringConvertibleList ARTGroupProps::getDebugProps() const { + return SharedDebugStringConvertibleList{ + debugStringConvertibleItem("opacity", opacity)}; } #endif diff --git a/ReactCommon/fabric/components/art/group/ARTGroupProps.h b/ReactCommon/fabric/components/art/group/ARTGroupProps.h index 2827288613fe38..234e530d3a01ed 100644 --- a/ReactCommon/fabric/components/art/group/ARTGroupProps.h +++ b/ReactCommon/fabric/components/art/group/ARTGroupProps.h @@ -31,6 +31,10 @@ class ARTGroupProps : public Props { std::vector clipping{}; #pragma mark - DebugStringConvertible + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif }; } // namespace react diff --git a/ReactCommon/fabric/components/art/shape/ARTShapeProps.cpp b/ReactCommon/fabric/components/art/shape/ARTShapeProps.cpp index 9b1c79c00aedce..9a170c9203e6ca 100644 --- a/ReactCommon/fabric/components/art/shape/ARTShapeProps.cpp +++ b/ReactCommon/fabric/components/art/shape/ARTShapeProps.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace facebook { namespace react { @@ -40,8 +41,9 @@ ARTShapeProps::ARTShapeProps( #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE -SharedDebugStringConvertibleList RawTextProps::getDebugProps() const { - return {debugStringConvertibleItem("opacity", opacity)}; +SharedDebugStringConvertibleList ARTShapeProps::getDebugProps() const { + return SharedDebugStringConvertibleList{ + debugStringConvertibleItem("opacity", opacity)}; } #endif diff --git a/ReactCommon/fabric/components/art/text/ARTTextProps.cpp b/ReactCommon/fabric/components/art/text/ARTTextProps.cpp index b7948e9a8feaec..c83c6fad859cf5 100644 --- a/ReactCommon/fabric/components/art/text/ARTTextProps.cpp +++ b/ReactCommon/fabric/components/art/text/ARTTextProps.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace facebook { namespace react { @@ -40,8 +41,9 @@ ARTTextProps::ARTTextProps( #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE -SharedDebugStringConvertibleList RawTextProps::getDebugProps() const { - return {debugStringConvertibleItem("opacity", opacity)}; +SharedDebugStringConvertibleList ARTTextProps::getDebugProps() const { + return SharedDebugStringConvertibleList{ + debugStringConvertibleItem("opacity", opacity)}; } #endif diff --git a/ReactCommon/fabric/components/inputaccessory/BUCK b/ReactCommon/fabric/components/inputaccessory/BUCK new file mode 100644 index 00000000000000..7db6c8ae3e02cc --- /dev/null +++ b/ReactCommon/fabric/components/inputaccessory/BUCK @@ -0,0 +1,47 @@ +load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "get_preprocessor_flags_for_build_mode") +load( + "//tools/build_defs/oss:rn_defs.bzl", + "APPLE", + "get_apple_compiler_flags", + "get_apple_inspector_flags", + "react_native_xplat_target", + "rn_xplat_cxx_library", + "subdir_glob", +) + +APPLE_COMPILER_FLAGS = get_apple_compiler_flags() + +rn_xplat_cxx_library( + name = "inputaccessory", + srcs = glob( + ["**/*.cpp"], + ), + headers = [], + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "*.h"), + ], + prefix = "react/components/inputaccessory", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, + fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), + force_static = True, + labels = ["supermodule:xplat/default/public.react_native.infra"], + platforms = (APPLE), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + visibility = ["PUBLIC"], + deps = [ + react_native_xplat_target("fabric/core:core"), + "//xplat/js/react-native-github:generated_components-rncore", + ], +) diff --git a/ReactCommon/fabric/components/inputaccessory/InputAccessoryComponentDescriptor.h b/ReactCommon/fabric/components/inputaccessory/InputAccessoryComponentDescriptor.h new file mode 100644 index 00000000000000..a850b1639aba0a --- /dev/null +++ b/ReactCommon/fabric/components/inputaccessory/InputAccessoryComponentDescriptor.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +/* + * Descriptor for component. + */ +class InputAccessoryComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + + void adopt(UnsharedShadowNode shadowNode) const override { + assert(std::dynamic_pointer_cast(shadowNode)); + auto concreteShadowNode = + std::static_pointer_cast(shadowNode); + + assert(std::dynamic_pointer_cast( + concreteShadowNode)); + auto layoutableShadowNode = + std::static_pointer_cast(concreteShadowNode); + + auto state = + std::static_pointer_cast( + shadowNode->getState()); + auto stateData = state->getData(); + + layoutableShadowNode->setSize( + Size{stateData.screenSize.width, stateData.screenSize.height}); + layoutableShadowNode->setPositionType(YGPositionTypeAbsolute); + + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.cpp b/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.cpp new file mode 100644 index 00000000000000..dcc59dcb0205e7 --- /dev/null +++ b/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.cpp @@ -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. + */ + +#include "InputAccessoryShadowNode.h" + +namespace facebook { +namespace react { + +extern const char InputAccessoryComponentName[] = "InputAccessoryView"; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.h b/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.h new file mode 100644 index 00000000000000..5a7f6679c79901 --- /dev/null +++ b/ReactCommon/fabric/components/inputaccessory/InputAccessoryShadowNode.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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +extern const char InputAccessoryComponentName[]; + +/* + * `ShadowNode` for component. + */ +class InputAccessoryShadowNode final : public ConcreteViewShadowNode< + InputAccessoryComponentName, + InputAccessoryProps, + InputAccessoryEventEmitter, + InputAccessoryState> { + public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + + static ShadowNodeTraits BaseTraits() { + auto traits = ConcreteViewShadowNode::BaseTraits(); + traits.set(ShadowNodeTraits::Trait::RootNodeKind); + return traits; + } +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/inputaccessory/InputAccessoryState.h b/ReactCommon/fabric/components/inputaccessory/InputAccessoryState.h new file mode 100644 index 00000000000000..5d1ae67a63ef21 --- /dev/null +++ b/ReactCommon/fabric/components/inputaccessory/InputAccessoryState.h @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/* + * State for component. + */ +class InputAccessoryState final { + public: + InputAccessoryState(){}; + InputAccessoryState(Size screenSize_) : screenSize(screenSize_){}; + + const Size screenSize{}; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/legacyviewmanagerinterop/LegacyViewManagerInteropComponentDescriptor.mm b/ReactCommon/fabric/components/legacyviewmanagerinterop/LegacyViewManagerInteropComponentDescriptor.mm index a5921741440945..06f79b844afbe0 100644 --- a/ReactCommon/fabric/components/legacyviewmanagerinterop/LegacyViewManagerInteropComponentDescriptor.mm +++ b/ReactCommon/fabric/components/legacyviewmanagerinterop/LegacyViewManagerInteropComponentDescriptor.mm @@ -25,6 +25,10 @@ return "FBStickerInputViewManager"; } + if (componentName == "FDSTooltipView") { + return "FBReactFDSTooltipViewManager"; + } + if (componentName == "FBRotatablePhotoPlayer") { return "FBRotatablePhotoPlayerViewManager"; } diff --git a/ReactCommon/fabric/components/modal/BUCK b/ReactCommon/fabric/components/modal/BUCK index 5913667729910f..b79d40c04b55ef 100644 --- a/ReactCommon/fabric/components/modal/BUCK +++ b/ReactCommon/fabric/components/modal/BUCK @@ -5,7 +5,6 @@ load( "APPLE", "CXX", "YOGA_CXX_TARGET", - "fb_xplat_cxx_test", "get_apple_compiler_flags", "get_apple_inspector_flags", "react_native_xplat_target", @@ -68,7 +67,6 @@ rn_xplat_cxx_library( "-DLOG_TAG=\"ReactNative\"", "-DWITH_FBSYSTRACE=1", ], - tests = [":tests"], visibility = ["PUBLIC"], deps = [ "//third-party/glog:glog", @@ -88,22 +86,3 @@ rn_xplat_cxx_library( "//xplat/js/react-native-github:generated_components-rncore", ], ) - -fb_xplat_cxx_test( - name = "tests", - srcs = glob(["tests/**/*.cpp"]), - headers = glob(["tests/**/*.h"]), - compiler_flags = [ - "-fexceptions", - "-frtti", - "-std=c++14", - "-Wall", - ], - contacts = ["oncall+react_native@xmail.facebook.com"], - platforms = (ANDROID, APPLE, CXX), - deps = [ - ":modal", - "//xplat/folly:molly", - "//xplat/third-party/gmock:gtest", - ], -) diff --git a/ReactCommon/fabric/components/root/RootProps.h b/ReactCommon/fabric/components/root/RootProps.h index 11bda3e4c6cdd5..3cffe6256a057e 100644 --- a/ReactCommon/fabric/components/root/RootProps.h +++ b/ReactCommon/fabric/components/root/RootProps.h @@ -27,8 +27,8 @@ class RootProps final : public ViewProps { #pragma mark - Props - LayoutConstraints const layoutConstraints{}; - LayoutContext const layoutContext{}; + LayoutConstraints layoutConstraints{}; + LayoutContext layoutContext{}; }; } // namespace react diff --git a/ReactCommon/fabric/components/scrollview/ScrollViewProps.h b/ReactCommon/fabric/components/scrollview/ScrollViewProps.h index e520c1736347bc..a6c08781f918c0 100644 --- a/ReactCommon/fabric/components/scrollview/ScrollViewProps.h +++ b/ReactCommon/fabric/components/scrollview/ScrollViewProps.h @@ -21,32 +21,32 @@ class ScrollViewProps final : public ViewProps { #pragma mark - Props - bool const alwaysBounceHorizontal{}; - bool const alwaysBounceVertical{}; - bool const bounces{true}; - bool const bouncesZoom{true}; - bool const canCancelContentTouches{true}; - bool const centerContent{}; - bool const automaticallyAdjustContentInsets{}; - Float const decelerationRate{0.998}; - bool const directionalLockEnabled{}; - ScrollViewIndicatorStyle const indicatorStyle{}; - ScrollViewKeyboardDismissMode const keyboardDismissMode{}; - Float const maximumZoomScale{1.0}; - Float const minimumZoomScale{1.0}; - bool const scrollEnabled{true}; - bool const pagingEnabled{}; - bool const pinchGestureEnabled{true}; - bool const scrollsToTop{true}; - bool const showsHorizontalScrollIndicator{true}; - bool const showsVerticalScrollIndicator{true}; - Float const scrollEventThrottle{}; - Float const zoomScale{1.0}; - EdgeInsets const contentInset{}; - Point const contentOffset{}; - EdgeInsets const scrollIndicatorInsets{}; - Float const snapToInterval{}; - ScrollViewSnapToAlignment const snapToAlignment{}; + bool alwaysBounceHorizontal{}; + bool alwaysBounceVertical{}; + bool bounces{true}; + bool bouncesZoom{true}; + bool canCancelContentTouches{true}; + bool centerContent{}; + bool automaticallyAdjustContentInsets{}; + Float decelerationRate{0.998}; + bool directionalLockEnabled{}; + ScrollViewIndicatorStyle indicatorStyle{}; + ScrollViewKeyboardDismissMode keyboardDismissMode{}; + Float maximumZoomScale{1.0}; + Float minimumZoomScale{1.0}; + bool scrollEnabled{true}; + bool pagingEnabled{}; + bool pinchGestureEnabled{true}; + bool scrollsToTop{true}; + bool showsHorizontalScrollIndicator{true}; + bool showsVerticalScrollIndicator{true}; + Float scrollEventThrottle{}; + Float zoomScale{1.0}; + EdgeInsets contentInset{}; + Point contentOffset{}; + EdgeInsets scrollIndicatorInsets{}; + Float snapToInterval{}; + ScrollViewSnapToAlignment snapToAlignment{}; #pragma mark - DebugStringConvertible diff --git a/ReactCommon/fabric/components/slider/SliderShadowNode.cpp b/ReactCommon/fabric/components/slider/SliderShadowNode.cpp index 488fe566783197..38929b89cfce43 100644 --- a/ReactCommon/fabric/components/slider/SliderShadowNode.cpp +++ b/ReactCommon/fabric/components/slider/SliderShadowNode.cpp @@ -83,7 +83,9 @@ ImageSource SliderShadowNode::getThumbImageSource() const { #pragma mark - LayoutableShadowNode -Size SliderShadowNode::measure(LayoutConstraints layoutConstraints) const { +Size SliderShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { if (SliderMeasurementsManager::shouldMeasureSlider()) { return measurementsManager_->measure(getSurfaceId(), layoutConstraints); } diff --git a/ReactCommon/fabric/components/slider/SliderShadowNode.h b/ReactCommon/fabric/components/slider/SliderShadowNode.h index 958f3b2aafb7b0..53ed30d82d0282 100644 --- a/ReactCommon/fabric/components/slider/SliderShadowNode.h +++ b/ReactCommon/fabric/components/slider/SliderShadowNode.h @@ -55,7 +55,9 @@ class SliderShadowNode final : public ConcreteViewShadowNode< #pragma mark - LayoutableShadowNode - Size measure(LayoutConstraints layoutConstraints) const override; + Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const override; void layout(LayoutContext layoutContext) override; private: diff --git a/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.cpp b/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.cpp index b717418afd6d01..53667c313a3bf8 100644 --- a/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.cpp +++ b/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.cpp @@ -21,8 +21,9 @@ void AndroidSwitchShadowNode::setAndroidSwitchMeasurementsManager( #pragma mark - LayoutableShadowNode -Size AndroidSwitchShadowNode::measure( - LayoutConstraints layoutConstraints) const { +Size AndroidSwitchShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { return measurementsManager_->measure(getSurfaceId(), layoutConstraints); } diff --git a/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.h b/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.h index 66df81a2a49955..e65ce7caabf417 100644 --- a/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.h +++ b/ReactCommon/fabric/components/switch/androidswitch/AndroidSwitchShadowNode.h @@ -35,7 +35,9 @@ class AndroidSwitchShadowNode final : public ConcreteViewShadowNode< #pragma mark - LayoutableShadowNode - Size measure(LayoutConstraints layoutConstraints) const override; + Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const override; private: std::shared_ptr measurementsManager_; diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp index 3ead9c9a4bf897..c81e1c1b4f0e93 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp @@ -23,7 +23,8 @@ using Content = ParagraphShadowNode::Content; char const ParagraphComponentName[] = "Paragraph"; -Content const &ParagraphShadowNode::getContent() const { +Content const &ParagraphShadowNode::getContent( + LayoutContext const &layoutContext) const { if (content_.has_value()) { return content_.value(); } @@ -31,6 +32,7 @@ Content const &ParagraphShadowNode::getContent() const { ensureUnsealed(); auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); textAttributes.layoutDirection = YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL @@ -49,7 +51,7 @@ Content const &ParagraphShadowNode::getContent() const { Content ParagraphShadowNode::getContentWithMeasuredAttachments( LayoutContext const &layoutContext, LayoutConstraints const &layoutConstraints) const { - auto content = getContent(); + auto content = getContent(layoutContext); if (content.attachments.empty()) { // Base case: No attachments, nothing to do. @@ -116,9 +118,11 @@ void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { #pragma mark - LayoutableShadowNode -Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { +Size ParagraphShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { auto content = - getContentWithMeasuredAttachments(LayoutContext{}, layoutConstraints); + getContentWithMeasuredAttachments(layoutContext, layoutConstraints); auto attributedString = content.attributedString; if (attributedString.isEmpty()) { @@ -128,6 +132,7 @@ Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { // of T67606511 auto string = BaseTextShadowNode::getEmptyPlaceholder(); auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); attributedString.appendFragment({string, textAttributes, {}}); } @@ -201,7 +206,6 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { auto attachmentOrigin = roundToPixel<&round>( attachmentFrame.origin, layoutMetrics.pointScaleFactor); auto attachmentLayoutContext = layoutContext; - attachmentLayoutContext.absolutePosition += attachmentOrigin; auto attachmentLayoutConstrains = LayoutConstraints{ attachmentSize, attachmentSize, layoutConstraints.layoutDirection}; diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h index cd7591149d7740..3e4441b0075805 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h @@ -62,7 +62,9 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< #pragma mark - LayoutableShadowNode void layout(LayoutContext layoutContext) override; - Size measure(LayoutConstraints layoutConstraints) const override; + Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const override; /* * Internal representation of the nested content of the node in a format @@ -79,7 +81,7 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< /* * Builds (if needed) and returns a reference to a `Content` object. */ - Content const &getContent() const; + Content const &getContent(LayoutContext const &layoutContext) const; /* * Builds and returns a `Content` object with given `layoutConstraints`. diff --git a/ReactCommon/fabric/components/text/rawtext/RawTextProps.cpp b/ReactCommon/fabric/components/text/rawtext/RawTextProps.cpp index 2823ca7dbd41c9..71cc7ded956e37 100644 --- a/ReactCommon/fabric/components/text/rawtext/RawTextProps.cpp +++ b/ReactCommon/fabric/components/text/rawtext/RawTextProps.cpp @@ -23,7 +23,8 @@ RawTextProps::RawTextProps( #if RN_DEBUG_STRING_CONVERTIBLE SharedDebugStringConvertibleList RawTextProps::getDebugProps() const { - return {debugStringConvertibleItem("text", text)}; + return SharedDebugStringConvertibleList{ + debugStringConvertibleItem("text", text)}; } #endif diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h index 0b083e5f619833..1644fc6b625261 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include "AndroidTextInputShadowNode.h" @@ -36,18 +37,33 @@ class AndroidTextInputComponentDescriptor final ShadowNodeFamily::Shared const &family) const override { int surfaceId = family->getSurfaceId(); - float defaultThemePaddingStart = NAN; - float defaultThemePaddingEnd = NAN; - float defaultThemePaddingTop = NAN; - float defaultThemePaddingBottom = NAN; - + YGStyle::Edges theme; + // TODO: figure out RTL/start/end/left/right stuff here if (surfaceIdToThemePaddingMap_.find(surfaceId) != surfaceIdToThemePaddingMap_.end()) { - YGStyle::Edges theme = surfaceIdToThemePaddingMap_[surfaceId]; - defaultThemePaddingStart = ((YGValue)theme[YGEdgeStart]).value; - defaultThemePaddingEnd = ((YGValue)theme[YGEdgeEnd]).value; - defaultThemePaddingTop = ((YGValue)theme[YGEdgeTop]).value; - defaultThemePaddingBottom = ((YGValue)theme[YGEdgeBottom]).value; + theme = surfaceIdToThemePaddingMap_[surfaceId]; + } else { + const jni::global_ref &fabricUIManager = + contextContainer_->at>("FabricUIManager"); + + auto env = jni::Environment::current(); + auto defaultTextInputPaddingArray = env->NewFloatArray(4); + static auto getThemeData = + jni::findClassStatic(UIManagerJavaDescriptor) + ->getMethod("getThemeData"); + + if (getThemeData( + fabricUIManager, surfaceId, defaultTextInputPaddingArray)) { + jfloat *defaultTextInputPadding = + env->GetFloatArrayElements(defaultTextInputPaddingArray, 0); + theme[YGEdgeStart] = (YGValue){defaultTextInputPadding[0], YGUnitPoint}; + theme[YGEdgeEnd] = (YGValue){defaultTextInputPadding[1], YGUnitPoint}; + theme[YGEdgeTop] = (YGValue){defaultTextInputPadding[2], YGUnitPoint}; + theme[YGEdgeBottom] = + (YGValue){defaultTextInputPadding[3], YGUnitPoint}; + surfaceIdToThemePaddingMap_.emplace(std::make_pair(surfaceId, theme)); + } + env->DeleteLocalRef(defaultTextInputPaddingArray); } return std::make_shared( @@ -59,10 +75,10 @@ class AndroidTextInputComponentDescriptor final {}, {}, textLayoutManager_, - defaultThemePaddingStart, - defaultThemePaddingEnd, - defaultThemePaddingTop, - defaultThemePaddingBottom)), + ((YGValue)theme[YGEdgeStart]).value, + ((YGValue)theme[YGEdgeEnd]).value, + ((YGValue)theme[YGEdgeTop]).value, + ((YGValue)theme[YGEdgeBottom]).value)), family); } @@ -79,27 +95,7 @@ class AndroidTextInputComponentDescriptor final textInputShadowNode->setContextContainer( const_cast(getContextContainer().get())); - // Get theme padding from cache, or set it from State. - // In theory, the Java ViewManager for TextInput should need to set state - // *exactly once* per surface to communicate the correct default padding, - // which will be cached here in C++. - // TODO T63008435: can this feature be removed entirely? - // TODO: figure out RTL/start/end/left/right stuff here int surfaceId = textInputShadowNode->getSurfaceId(); - const AndroidTextInputState &state = textInputShadowNode->getStateData(); - if (surfaceIdToThemePaddingMap_.find(surfaceId) == - surfaceIdToThemePaddingMap_.end() && - !isnan(state.defaultThemePaddingStart)) { - YGStyle::Edges result; - result[YGEdgeStart] = - (YGValue){state.defaultThemePaddingStart, YGUnitPoint}; - result[YGEdgeEnd] = (YGValue){state.defaultThemePaddingEnd, YGUnitPoint}; - result[YGEdgeTop] = (YGValue){state.defaultThemePaddingTop, YGUnitPoint}; - result[YGEdgeBottom] = - (YGValue){state.defaultThemePaddingBottom, YGUnitPoint}; - surfaceIdToThemePaddingMap_.emplace(std::make_pair(surfaceId, result)); - } - if (surfaceIdToThemePaddingMap_.find(surfaceId) != surfaceIdToThemePaddingMap_.end()) { YGStyle::Edges theme = surfaceIdToThemePaddingMap_[surfaceId]; @@ -177,6 +173,10 @@ class AndroidTextInputComponentDescriptor final } private: + // TODO T68526882: Unify with Binding::UIManagerJavaDescriptor + constexpr static auto UIManagerJavaDescriptor = + "com/facebook/react/fabric/FabricUIManager"; + SharedTextLayoutManager textLayoutManager_; mutable better::map surfaceIdToThemePaddingMap_; }; diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index 1349f3f4e88654..dadeb8977b2d6b 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -165,8 +165,9 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { #pragma mark - LayoutableShadowNode -Size AndroidTextInputShadowNode::measure( - LayoutConstraints layoutConstraints) const { +Size AndroidTextInputShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { // Layout is called right after measure. // Measure is marked as `const`, and `layout` is not; so State can be updated // during layout, but not during `measure`. If State is out-of-date in layout, diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h index be480cc8a13aba..45714febee6d2f 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h @@ -56,7 +56,9 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode< #pragma mark - LayoutableShadowNode - Size measure(LayoutConstraints layoutConstraints) const override; + Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const override; void layout(LayoutContext layoutContext) override; private: diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp index 0128c17f00ef09..4e37be0aa6fc74 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp @@ -21,7 +21,6 @@ folly::dynamic AndroidTextInputState::getDynamic() const { newState["attributedString"] = toDynamic(attributedString); newState["paragraphAttributes"] = toDynamic(paragraphAttributes); newState["hash"] = newState["attributedString"]["hash"]; - newState["hasThemeData"] = !isnan(defaultThemePaddingStart); return newState; } #endif diff --git a/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.cpp b/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.cpp index d98ac9d966d7fd..972c3768e0d134 100644 --- a/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.cpp +++ b/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.cpp @@ -54,10 +54,19 @@ TextInputProps::TextInputProps( rawProps, "mostRecentEventCount", sourceProps.mostRecentEventCount, + {})), + autoFocus( + convertRawProp(rawProps, "autoFocus", sourceProps.autoFocus, {})), + inputAccessoryViewID(convertRawProp( + rawProps, + "inputAccessoryViewID", + sourceProps.inputAccessoryViewID, {})){}; -TextAttributes TextInputProps::getEffectiveTextAttributes() const { +TextAttributes TextInputProps::getEffectiveTextAttributes( + Float fontSizeMultiplier) const { auto result = TextAttributes::defaultTextAttributes(); + result.fontSizeMultiplier = fontSizeMultiplier; result.apply(textAttributes); /* diff --git a/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.h b/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.h index fa8d1365c42e50..9109a2ae49d450 100644 --- a/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.h +++ b/ReactCommon/fabric/components/textinput/iostextinput/TextInputProps.h @@ -53,10 +53,14 @@ class TextInputProps final : public ViewProps, public BaseTextProps { std::string const text{}; int const mostRecentEventCount{0}; + bool autoFocus{false}; + + std::string const inputAccessoryViewID{}; + /* * Accessors */ - TextAttributes getEffectiveTextAttributes() const; + TextAttributes getEffectiveTextAttributes(Float fontSizeMultiplier) const; ParagraphAttributes getEffectiveParagraphAttributes() const; #ifdef ANDROID diff --git a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp index fd70e306d73135..8cce95c0af4095 100644 --- a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.cpp @@ -18,7 +18,8 @@ namespace react { extern char const TextInputComponentName[] = "TextInput"; -AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure() const { +AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure( + LayoutContext const &layoutContext) const { bool hasMeaningfulState = getState() && getState()->getRevision() != State::initialRevisionValue; @@ -31,8 +32,9 @@ AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure() const { } } - auto attributedString = - hasMeaningfulState ? AttributedString{} : getAttributedString(); + auto attributedString = hasMeaningfulState + ? AttributedString{} + : getAttributedString(layoutContext); if (attributedString.isEmpty()) { auto placeholder = getConcreteProps().placeholder; @@ -43,15 +45,18 @@ AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure() const { auto string = !placeholder.empty() ? placeholder : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = getConcreteProps().getEffectiveTextAttributes(); + auto textAttributes = getConcreteProps().getEffectiveTextAttributes( + layoutContext.fontSizeMultiplier); attributedString.appendFragment({string, textAttributes, {}}); } return AttributedStringBox{attributedString}; } -AttributedString TextInputShadowNode::getAttributedString() const { - auto textAttributes = getConcreteProps().getEffectiveTextAttributes(); +AttributedString TextInputShadowNode::getAttributedString( + LayoutContext const &layoutContext) const { + auto textAttributes = getConcreteProps().getEffectiveTextAttributes( + layoutContext.fontSizeMultiplier); auto attributedString = AttributedString{}; attributedString.appendFragment( @@ -70,10 +75,11 @@ void TextInputShadowNode::setTextLayoutManager( textLayoutManager_ = textLayoutManager; } -void TextInputShadowNode::updateStateIfNeeded() { +void TextInputShadowNode::updateStateIfNeeded( + LayoutContext const &layoutContext) { ensureUnsealed(); - auto reactTreeAttributedString = getAttributedString(); + auto reactTreeAttributedString = getAttributedString(layoutContext); auto const &state = getStateData(); assert(textLayoutManager_); @@ -97,17 +103,19 @@ void TextInputShadowNode::updateStateIfNeeded() { #pragma mark - LayoutableShadowNode -Size TextInputShadowNode::measure(LayoutConstraints layoutConstraints) const { +Size TextInputShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { return textLayoutManager_ ->measure( - attributedStringBoxToMeasure(), + attributedStringBoxToMeasure(layoutContext), getConcreteProps().getEffectiveParagraphAttributes(), layoutConstraints) .size; } void TextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(); + updateStateIfNeeded(layoutContext); ConcreteViewShadowNode::layout(layoutContext); } diff --git a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.h b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.h index cfa7849ac7bd0d..035d692379ae70 100644 --- a/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.h +++ b/ReactCommon/fabric/components/textinput/iostextinput/TextInputShadowNode.h @@ -40,11 +40,6 @@ class TextInputShadowNode : public ConcreteViewShadowNode< return traits; } - /* - * Returns a `AttributedString` which represents text content of the node. - */ - AttributedString getAttributedString() const; - /* * Associates a shared `TextLayoutManager` with the node. * `TextInputShadowNode` uses the manager to measure text content @@ -54,21 +49,30 @@ class TextInputShadowNode : public ConcreteViewShadowNode< #pragma mark - LayoutableShadowNode - Size measure(LayoutConstraints layoutConstraints) const override; + Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const override; void layout(LayoutContext layoutContext) override; private: /* * Creates a `State` object if needed. */ - void updateStateIfNeeded(); + void updateStateIfNeeded(LayoutContext const &layoutContext); + + /* + * Returns a `AttributedString` which represents text content of the node. + */ + AttributedString getAttributedString( + LayoutContext const &layoutContext) const; /* * Returns an `AttributedStringBox` which represents text content that should * be used for measuring purposes. It might contain actual text value, * placeholder value or some character that represents the size of the font. */ - AttributedStringBox attributedStringBoxToMeasure() const; + AttributedStringBox attributedStringBoxToMeasure( + LayoutContext const &layoutContext) const; TextLayoutManager::Shared textLayoutManager_; }; diff --git a/ReactCommon/fabric/components/view/ViewProps.h b/ReactCommon/fabric/components/view/ViewProps.h index f1db61f0fffbdf..6d004213414569 100644 --- a/ReactCommon/fabric/components/view/ViewProps.h +++ b/ReactCommon/fabric/components/view/ViewProps.h @@ -42,9 +42,9 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps { // Shadow SharedColor shadowColor{}; - Size shadowOffset{}; + Size shadowOffset{0, -3}; Float shadowOpacity{}; - Float shadowRadius{}; + Float shadowRadius{3}; // Transform Transform transform{}; @@ -59,7 +59,7 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps { bool collapsable{true}; - int elevation{}; + Float elevation{}; /* Android-only */ #pragma mark - Convenience Methods diff --git a/ReactCommon/fabric/components/view/ViewShadowNode.cpp b/ReactCommon/fabric/components/view/ViewShadowNode.cpp index 024318662f3a97..43b6b7229d4ee5 100644 --- a/ReactCommon/fabric/components/view/ViewShadowNode.cpp +++ b/ReactCommon/fabric/components/view/ViewShadowNode.cpp @@ -46,6 +46,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.elevation != 0 || (viewProps.zIndex != 0 && viewProps.yogaStyle.positionType() == YGPositionTypeAbsolute) || + viewProps.yogaStyle.display() == YGDisplayNone || viewProps.getClipsContentToBounds() || isColorMeaningful(viewProps.shadowColor); diff --git a/ReactCommon/fabric/components/view/accessibility/AccessibilityPrimitives.h b/ReactCommon/fabric/components/view/accessibility/AccessibilityPrimitives.h index a87cd35c53df81..a17b9aa44f7a0a 100644 --- a/ReactCommon/fabric/components/view/accessibility/AccessibilityPrimitives.h +++ b/ReactCommon/fabric/components/view/accessibility/AccessibilityPrimitives.h @@ -44,5 +44,22 @@ constexpr enum AccessibilityTraits operator&( return (enum AccessibilityTraits)((uint32_t)lhs & (uint32_t)rhs); } +struct AccessibilityState { + bool disabled{false}; + bool selected{false}; +}; + +constexpr bool operator==( + AccessibilityState const &lhs, + AccessibilityState const &rhs) { + return lhs.disabled == rhs.disabled && lhs.selected == rhs.selected; +} + +constexpr bool operator!=( + AccessibilityState const &lhs, + AccessibilityState const &rhs) { + return !(rhs == lhs); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.cpp b/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.cpp index b7f9bd7bfde62d..417eccd656524b 100644 --- a/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.cpp +++ b/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.cpp @@ -28,6 +28,11 @@ AccessibilityProps::AccessibilityProps( "accessibilityRole", sourceProps.accessibilityTraits, AccessibilityTraits::None)), + accessibilityState(convertRawProp( + rawProps, + "accessibilityState", + sourceProps.accessibilityState, + {})), accessibilityLabel(convertRawProp( rawProps, "accessibilityLabel", diff --git a/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.h b/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.h index b3994c99f8eed4..fc6088dbf0a149 100644 --- a/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.h +++ b/ReactCommon/fabric/components/view/accessibility/AccessibilityProps.h @@ -26,6 +26,7 @@ class AccessibilityProps { bool accessible{false}; AccessibilityTraits accessibilityTraits{AccessibilityTraits::None}; + AccessibilityState accessibilityState; std::string accessibilityLabel{""}; std::string accessibilityHint{""}; std::vector accessibilityActions{}; diff --git a/ReactCommon/fabric/components/view/accessibility/accessibilityPropsConversions.h b/ReactCommon/fabric/components/view/accessibility/accessibilityPropsConversions.h index a6b6b4e919d5e5..c0239600a8646d 100644 --- a/ReactCommon/fabric/components/view/accessibility/accessibilityPropsConversions.h +++ b/ReactCommon/fabric/components/view/accessibility/accessibilityPropsConversions.h @@ -9,6 +9,7 @@ #include #include +#include namespace facebook { namespace react { @@ -109,5 +110,17 @@ inline void fromRawValue(const RawValue &value, AccessibilityTraits &result) { abort(); } +inline void fromRawValue(const RawValue &value, AccessibilityState &result) { + auto map = (better::map)value; + auto selected = map.find("selected"); + if (selected != map.end()) { + fromRawValue(selected->second, result.selected); + } + auto disabled = map.find("disabled"); + if (disabled != map.end()) { + fromRawValue(disabled->second, result.disabled); + } +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/fabric/components/view/conversions.h b/ReactCommon/fabric/components/view/conversions.h index 0ac41614dd0dfc..5a3cb1a8cc22e6 100644 --- a/ReactCommon/fabric/components/view/conversions.h +++ b/ReactCommon/fabric/components/view/conversions.h @@ -422,6 +422,8 @@ inline void fromRawValue(const RawValue &value, Transform &result) { for (auto number : numbers) { transformMatrix.matrix[i++] = number; } + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}); } else if (operation == "perspective") { transformMatrix = transformMatrix * Transform::Perspective((Float)parameters); diff --git a/ReactCommon/fabric/components/view/tests/LayoutTest.cpp b/ReactCommon/fabric/components/view/tests/LayoutTest.cpp new file mode 100644 index 00000000000000..f309557e5d19c6 --- /dev/null +++ b/ReactCommon/fabric/components/view/tests/LayoutTest.cpp @@ -0,0 +1,186 @@ +/* + * 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. + */ + +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +// *******************************************************┌─ABCD:────┐**** +// *******************************************************│ {70,-50} │**** +// *******************************************************│ {30,60} │**** +// *******************************************************│ │**** +// *******************************************************│ │**** +// *******************┌─A: {0,0}{50,50}──┐****************│ │**** +// *******************│ │****************│ │**** +// *******************│ ┌─AB:──────┐ │****************│ │**** +// *******************│ │ {10,10}{30,90}****************│ │**** +// *******************│ │ ┌─ABC: {10,10}{110,20}──────┤ ├───┐ +// *******************│ │ │ │ │ │ +// *******************│ │ │ └──────────┘ │ +// *******************│ │ └──────┬───┬───────────────────────────────┘ +// *******************│ │ │ │******************************** +// *******************└───┤ ├───┘******************************** +// ***********************│ │************************************ +// ***********************│ │************************************ +// ┌─ABE: {-60,50}{70,20}─┴───┐ │************************************ +// │ │ │************************************ +// │ │ │************************************ +// │ │ │************************************ +// │ │ │************************************ +// └──────────────────────┬───┘ │************************************ +// ***********************│ │************************************ +// ***********************└──────────┘************************************ + +class LayoutTest : public ::testing::Test { + protected: + ComponentBuilder builder_; + std::shared_ptr rootShadowNode_; + std::shared_ptr viewShadowNodeA_; + std::shared_ptr viewShadowNodeAB_; + std::shared_ptr viewShadowNodeABC_; + std::shared_ptr viewShadowNodeABCD_; + std::shared_ptr viewShadowNodeABE_; + + LayoutTest() : builder_(simpleComponentBuilder()) {} + + void initialize(bool enforceClippingForABC) { + // clang-format off + auto element = + Element() + .reference(rootShadowNode_) + .tag(1) + .props([] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + props.layoutConstraints = LayoutConstraints{{0,0}, {500, 500}}; + auto &yogaStyle = props.yogaStyle; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{200, YGUnitPoint}; + return sharedProps; + }) + .children({ + Element() + .reference(viewShadowNodeA_) + .props([] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + auto &yogaStyle = props.yogaStyle; + yogaStyle.positionType() = YGPositionTypeAbsolute; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{50, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{50, YGUnitPoint}; + return sharedProps; + }) + .children({ + Element() + .reference(viewShadowNodeAB_) + .props([] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + auto &yogaStyle = props.yogaStyle; + yogaStyle.positionType() = YGPositionTypeAbsolute; + yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint}; + yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{90, YGUnitPoint}; + return sharedProps; + }) + .children({ + Element() + .reference(viewShadowNodeABC_) + .props([=] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + auto &yogaStyle = props.yogaStyle; + + if (enforceClippingForABC) { + yogaStyle.overflow() = YGOverflowHidden; + } + + yogaStyle.positionType() = YGPositionTypeAbsolute; + yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint}; + yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{110, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; + return sharedProps; + }) + .children({ + Element() + .reference(viewShadowNodeABCD_) + .props([] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + auto &yogaStyle = props.yogaStyle; + yogaStyle.positionType() = YGPositionTypeAbsolute; + yogaStyle.position()[YGEdgeLeft] = YGValue{70, YGUnitPoint}; + yogaStyle.position()[YGEdgeTop] = YGValue{-50, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{60, YGUnitPoint}; + return sharedProps; + }) + }), + Element() + .reference(viewShadowNodeABE_) + .props([] { + auto sharedProps = std::make_shared(); + auto &props = *sharedProps; + auto &yogaStyle = props.yogaStyle; + yogaStyle.positionType() = YGPositionTypeAbsolute; + yogaStyle.position()[YGEdgeLeft] = YGValue{-60, YGUnitPoint}; + yogaStyle.position()[YGEdgeTop] = YGValue{50, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionWidth] = YGValue{70, YGUnitPoint}; + yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; + return sharedProps; + }) + }) + }) + }); + // clang-format on + + builder_.build(element); + + rootShadowNode_->layoutIfNeeded(); + } +}; + +TEST_F(LayoutTest, overflowInsetTest) { + initialize(false); + + auto layoutMetrics = viewShadowNodeA_->getLayoutMetrics(); + + EXPECT_EQ(layoutMetrics.frame.size.width, 50); + EXPECT_EQ(layoutMetrics.frame.size.height, 50); + + EXPECT_EQ(layoutMetrics.overflowInset.left, -50); + EXPECT_EQ(layoutMetrics.overflowInset.top, -30); + EXPECT_EQ(layoutMetrics.overflowInset.right, -80); + EXPECT_EQ(layoutMetrics.overflowInset.bottom, -50); +} + +TEST_F(LayoutTest, overflowInsetWithClippingTest) { + initialize(true); + + auto layoutMetrics = viewShadowNodeA_->getLayoutMetrics(); + + EXPECT_EQ(layoutMetrics.frame.size.width, 50); + EXPECT_EQ(layoutMetrics.frame.size.height, 50); + + EXPECT_EQ(layoutMetrics.overflowInset.left, -50); + EXPECT_EQ(layoutMetrics.overflowInset.top, 0); + EXPECT_EQ(layoutMetrics.overflowInset.right, -80); + EXPECT_EQ(layoutMetrics.overflowInset.bottom, -50); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.cpp b/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.cpp index 1a9982b2cf50e1..f17a8e41bed25a 100644 --- a/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.cpp +++ b/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -103,14 +104,6 @@ bool YogaLayoutableShadowNode::getIsLayoutClean() const { return !yogaNode_.isDirty(); } -bool YogaLayoutableShadowNode::getHasNewLayout() const { - return yogaNode_.getHasNewLayout(); -} - -void YogaLayoutableShadowNode::setHasNewLayout(bool hasNewLayout) { - yogaNode_.setHasNewLayout(hasNewLayout); -} - #pragma mark - Mutating Methods void YogaLayoutableShadowNode::enableMeasurement() { @@ -328,6 +321,8 @@ void YogaLayoutableShadowNode::layoutTree( applyLayoutConstraints(yogaNode_.getStyle(), layoutConstraints); + ThreadStorage::getInstance().set(layoutContext); + if (layoutContext.swapLeftAndRightInRTL) { swapLeftAndRightInTree(*this); } @@ -339,47 +334,86 @@ void YogaLayoutableShadowNode::layoutTree( &yogaNode_, YGUndefined, YGUndefined, YGDirectionInherit); } - if (getHasNewLayout()) { + if (yogaNode_.getHasNewLayout()) { auto layoutMetrics = layoutMetricsFromYogaNode(yogaNode_); layoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; setLayoutMetrics(layoutMetrics); - setHasNewLayout(false); + yogaNode_.setHasNewLayout(false); } layout(layoutContext); } -void YogaLayoutableShadowNode::layoutChildren(LayoutContext layoutContext) { - assert(!yogaNode_.isDirty()); +static EdgeInsets calculateOverflowInset( + Rect containerFrame, + Rect contentFrame) { + auto size = containerFrame.size; + auto overflowInset = EdgeInsets{}; + overflowInset.left = std::min(contentFrame.getMinX(), Float{0.0}); + overflowInset.top = std::min(contentFrame.getMinY(), Float{0.0}); + overflowInset.right = + -std::max(contentFrame.getMaxX() - size.width, Float{0.0}); + overflowInset.bottom = + -std::max(contentFrame.getMaxY() - size.height, Float{0.0}); + return overflowInset; +} - for (const auto &childYogaNode : yogaNode_.getChildren()) { - if (!childYogaNode->getHasNewLayout()) { - continue; - } +void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { + // Reading data from a dirtied node does not make sense. + assert(!yogaNode_.isDirty()); - assert(!childYogaNode->isDirty()); + auto contentFrame = Rect{}; - auto childNode = - static_cast(childYogaNode->getContext()); + for (auto childYogaNode : yogaNode_.getChildren()) { + auto &childNode = + *static_cast(childYogaNode->getContext()); // Verifying that the Yoga node belongs to the ShadowNode. - assert(&childNode->yogaNode_ == childYogaNode); + assert(&childNode.yogaNode_ == childYogaNode); + + if (childYogaNode->getHasNewLayout()) { + childYogaNode->setHasNewLayout(false); - LayoutMetrics childLayoutMetrics = - layoutMetricsFromYogaNode(childNode->yogaNode_); - childLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + // Reading data from a dirtied node does not make sense. + assert(!childYogaNode->isDirty()); - // We must copy layout metrics from Yoga node only once (when the parent - // node exclusively ownes the child node). - assert(childYogaNode->getOwner() == &yogaNode_); + // We must copy layout metrics from Yoga node only once (when the parent + // node exclusively ownes the child node). + assert(childYogaNode->getOwner() == &yogaNode_); - childNode->ensureUnsealed(); - auto affected = childNode->setLayoutMetrics(childLayoutMetrics); + // We are about to mutate layout metrics of the node. + childNode.ensureUnsealed(); - if (affected && layoutContext.affectedNodes) { - layoutContext.affectedNodes->push_back(childNode); + auto newLayoutMetrics = layoutMetricsFromYogaNode(*childYogaNode); + newLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + + // Adding the node to `affectedNodes` if the node's `frame` was changed. + if (layoutContext.affectedNodes && + newLayoutMetrics.frame != childNode.getLayoutMetrics().frame) { + layoutContext.affectedNodes->push_back(&childNode); + } + + childNode.setLayoutMetrics(newLayoutMetrics); + + if (newLayoutMetrics.displayType != DisplayType::None) { + childNode.layout(layoutContext); + } + } + + auto layoutMetricsWithOverflowInset = childNode.getLayoutMetrics(); + if (layoutMetricsWithOverflowInset.displayType != DisplayType::None) { + contentFrame.unionInPlace(insetBy( + layoutMetricsWithOverflowInset.frame, + layoutMetricsWithOverflowInset.overflowInset)); } } + + if (yogaNode_.getStyle().overflow() == YGOverflowVisible) { + layoutMetrics_.overflowInset = + calculateOverflowInset(layoutMetrics_.frame, contentFrame); + } else { + layoutMetrics_.overflowInset = {}; + } } #pragma mark - Yoga Connectors @@ -442,7 +476,10 @@ YGSize YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector( break; } - auto size = shadowNodeRawPtr->measure({minimumSize, maximumSize}); + auto layoutContext = ThreadStorage::getInstance().get(); + + auto size = shadowNodeRawPtr->measureContent( + layoutContext.value_or(LayoutContext{}), {minimumSize, maximumSize}); return YGSize{yogaFloatFromFloat(size.width), yogaFloatFromFloat(size.height)}; diff --git a/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.h b/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.h index 59148e333b80f3..5566e10590f0d5 100644 --- a/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.h +++ b/ReactCommon/fabric/components/view/yoga/YogaLayoutableShadowNode.h @@ -73,9 +73,6 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { void dirtyLayout() override; bool getIsLayoutClean() const override; - void setHasNewLayout(bool hasNewLayout) override; - bool getHasNewLayout() const override; - /* * Computes layout using Yoga layout engine. * See `LayoutableShadowNode` for more details. @@ -84,7 +81,7 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { LayoutContext layoutContext, LayoutConstraints layoutConstraints) override; - void layoutChildren(LayoutContext layoutContext) override; + void layout(LayoutContext layoutContext) override; protected: /* diff --git a/ReactCommon/fabric/core/events/BatchedEventQueue.cpp b/ReactCommon/fabric/core/events/BatchedEventQueue.cpp index baf25201294dd4..d8acdd6112b4c1 100644 --- a/ReactCommon/fabric/core/events/BatchedEventQueue.cpp +++ b/ReactCommon/fabric/core/events/BatchedEventQueue.cpp @@ -20,13 +20,12 @@ void BatchedEventQueue::onEnqueue() const { void BatchedEventQueue::enqueueUniqueEvent(const RawEvent &rawEvent) const { { std::lock_guard lock(queueMutex_); - auto const position = std::find_if( - eventQueue_.begin(), eventQueue_.end(), [&rawEvent](auto const &event) { - return event.type == rawEvent.type && - event.eventTarget == rawEvent.eventTarget; - }); - if (position != eventQueue_.end()) { - eventQueue_.erase(position); + if (!eventQueue_.empty()) { + auto const position = eventQueue_.back(); + if (position.type == rawEvent.type && + position.eventTarget == rawEvent.eventTarget) { + eventQueue_.pop_back(); + } } eventQueue_.push_back(rawEvent); diff --git a/ReactCommon/fabric/core/events/BatchedEventQueue.h b/ReactCommon/fabric/core/events/BatchedEventQueue.h index 84bcb0ff39b8ab..af8348c0d66abf 100644 --- a/ReactCommon/fabric/core/events/BatchedEventQueue.h +++ b/ReactCommon/fabric/core/events/BatchedEventQueue.h @@ -24,8 +24,7 @@ class BatchedEventQueue final : public EventQueue { /* * Enqueues and (probably later) dispatch a given event. - * Deletes previous RawEvent of the same type and with same target - * from the queue. + * Deletes last RawEvent from the queu if it has the same type and target. * Can be called on any thread. */ void enqueueUniqueEvent(const RawEvent &rawEvent) const; diff --git a/ReactCommon/fabric/core/layout/LayoutContext.h b/ReactCommon/fabric/core/layout/LayoutContext.h index 31a926b50418b3..42b011fca7fcf3 100644 --- a/ReactCommon/fabric/core/layout/LayoutContext.h +++ b/ReactCommon/fabric/core/layout/LayoutContext.h @@ -20,11 +20,6 @@ namespace react { * layout approaches. */ struct LayoutContext { - /* - * Compound absolute position of the node relative to the root node. - */ - Point absolutePosition{0, 0}; - /* * Reflects the scale factor needed to convert from the logical coordinate * space into the device coordinate space of the physical screen. @@ -55,6 +50,11 @@ struct LayoutContext { * - border(Left|Right)Color → border(Start|End)Color */ bool swapLeftAndRightInRTL{false}; + + /* + * Multiplier used to change size of the font in surface. + */ + Float fontSizeMultiplier{1.0}; }; } // namespace react diff --git a/ReactCommon/fabric/core/layout/LayoutMetrics.h b/ReactCommon/fabric/core/layout/LayoutMetrics.h index 014ce93c6c1889..d4708a899fbcbd 100644 --- a/ReactCommon/fabric/core/layout/LayoutMetrics.h +++ b/ReactCommon/fabric/core/layout/LayoutMetrics.h @@ -23,6 +23,7 @@ struct LayoutMetrics { DisplayType displayType{DisplayType::Flex}; LayoutDirection layoutDirection{LayoutDirection::Undefined}; Float pointScaleFactor{1.0}; + EdgeInsets overflowInset{}; Rect getContentFrame() const { return Rect{ diff --git a/ReactCommon/fabric/core/layout/LayoutableShadowNode.cpp b/ReactCommon/fabric/core/layout/LayoutableShadowNode.cpp index c6f391eccef261..ba26ba40e32376 100644 --- a/ReactCommon/fabric/core/layout/LayoutableShadowNode.cpp +++ b/ReactCommon/fabric/core/layout/LayoutableShadowNode.cpp @@ -168,7 +168,9 @@ LayoutableShadowNode::getLayoutableChildNodes() const { return layoutableChildren; } -Size LayoutableShadowNode::measure(LayoutConstraints layoutConstraints) const { +Size LayoutableShadowNode::measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { return Size(); } @@ -202,26 +204,7 @@ void LayoutableShadowNode::layoutTree( } void LayoutableShadowNode::layout(LayoutContext layoutContext) { - layoutChildren(layoutContext); - - for (auto child : getLayoutableChildNodes()) { - if (!child->getHasNewLayout()) { - continue; - } - - child->ensureUnsealed(); - child->setHasNewLayout(false); - - auto childLayoutMetrics = child->getLayoutMetrics(); - if (childLayoutMetrics.displayType == DisplayType::None) { - continue; - } - - auto childLayoutContext = LayoutContext(layoutContext); - childLayoutContext.absolutePosition += childLayoutMetrics.frame.origin; - - child->layout(layoutContext); - } + // Default implementation does nothing. } ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( @@ -251,19 +234,10 @@ ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( return isPointInside ? node : nullptr; } -void LayoutableShadowNode::layoutChildren(LayoutContext layoutContext) { - // Default implementation does nothing. -} - #if RN_DEBUG_STRING_CONVERTIBLE SharedDebugStringConvertibleList LayoutableShadowNode::getDebugProps() const { auto list = SharedDebugStringConvertibleList{}; - if (getHasNewLayout()) { - list.push_back( - std::make_shared("hasNewLayout")); - } - if (!getIsLayoutClean()) { list.push_back(std::make_shared("dirty")); } diff --git a/ReactCommon/fabric/core/layout/LayoutableShadowNode.h b/ReactCommon/fabric/core/layout/LayoutableShadowNode.h index d94566390c443b..1bcd163ead7e1f 100644 --- a/ReactCommon/fabric/core/layout/LayoutableShadowNode.h +++ b/ReactCommon/fabric/core/layout/LayoutableShadowNode.h @@ -76,7 +76,9 @@ class LayoutableShadowNode : public ShadowNode { * given constrains and relying on possible layout. * Default implementation returns zero size. */ - virtual Size measure(LayoutConstraints layoutConstraints) const; + virtual Size measureContent( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const; /* * Measures the node with given `layoutContext` and `layoutConstraints`. @@ -91,9 +93,12 @@ class LayoutableShadowNode : public ShadowNode { * Computes layout recursively. * Additional environmental constraints might be provided via `layoutContext` * argument. - * Default implementation basically calls `layoutChildren()` and then - * `layout()` (recursively), and provides some obvious performance - * optimization. + * + * The typical concrete-layout-specific implementation of this method should: + * - Measure children with `LayoutConstraints` calculated from its size using + * a particular layout approach; + * - Calculate and assign `LayoutMetrics` for the children; + * - Call itself recursively on every child if needed. */ virtual void layout(LayoutContext layoutContext); @@ -139,7 +144,6 @@ class LayoutableShadowNode : public ShadowNode { ShadowNode::Shared node, Point point); - protected: /* * Clean or Dirty layout state: * Indicates whether all nodes (and possibly their subtrees) along the path @@ -149,19 +153,6 @@ class LayoutableShadowNode : public ShadowNode { virtual void dirtyLayout() = 0; virtual bool getIsLayoutClean() const = 0; - /* - * Indicates does the shadow node (or any descendand node of the node) - * get a new layout metrics during a previous layout pass. - */ - virtual void setHasNewLayout(bool hasNewLayout) = 0; - virtual bool getHasNewLayout() const = 0; - - /* - * Applies layout for all children; - * does not call anything in recursive manner *by desing*. - */ - virtual void layoutChildren(LayoutContext layoutContext); - /* * Unifed methods to access text layout metrics. */ @@ -179,7 +170,6 @@ class LayoutableShadowNode : public ShadowNode { SharedDebugStringConvertibleList getDebugProps() const; #endif - private: LayoutMetrics layoutMetrics_; }; diff --git a/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.cpp b/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.cpp index 47cee004354ec6..5ef1494d0060d4 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.cpp +++ b/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.cpp @@ -24,12 +24,12 @@ ShadowNodeFamilyFragment ShadowNodeFamilyFragment::build( using Value = ShadowNodeFamilyFragment::Value; Value::Value(ShadowNodeFamilyFragment const &fragment) - : tag_(fragment.tag), - surfaceId_(fragment.surfaceId), - eventEmitter_(fragment.eventEmitter) {} + : tag(fragment.tag), + surfaceId(fragment.surfaceId), + eventEmitter(fragment.eventEmitter) {} Value::operator ShadowNodeFamilyFragment() const { - return ShadowNodeFamilyFragment{tag_, surfaceId_, eventEmitter_}; + return ShadowNodeFamilyFragment{tag, surfaceId, eventEmitter}; } } // namespace react diff --git a/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.h b/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.h index 9b97a6d340748d..3c9c667d0b5c70 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.h +++ b/ReactCommon/fabric/core/shadownode/ShadowNodeFamilyFragment.h @@ -45,10 +45,9 @@ class ShadowNodeFamilyFragment final { */ explicit operator ShadowNodeFamilyFragment() const; - private: - Tag const tag_; - SurfaceId const surfaceId_; - EventEmitter::Shared const eventEmitter_; + Tag tag; + SurfaceId surfaceId; + EventEmitter::Shared eventEmitter; }; }; diff --git a/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.cpp b/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.cpp index e70e79b6be3052..37b11150c13677 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.cpp +++ b/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.cpp @@ -29,12 +29,12 @@ State::Shared const &ShadowNodeFragment::statePlaceholder() { using Value = ShadowNodeFragment::Value; Value::Value(ShadowNodeFragment const &fragment) - : props_(fragment.props), - children_(fragment.children), - state_(fragment.state) {} + : props(fragment.props), + children(fragment.children), + state(fragment.state) {} Value::operator ShadowNodeFragment() const { - return ShadowNodeFragment{props_, children_, state_}; + return ShadowNodeFragment{props, children, state}; } } // namespace react diff --git a/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.h b/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.h index 5cec5dde8d0c90..afa54425b950d0 100644 --- a/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.h +++ b/ReactCommon/fabric/core/shadownode/ShadowNodeFragment.h @@ -56,10 +56,9 @@ struct ShadowNodeFragment { */ explicit operator ShadowNodeFragment() const; - private: - Props::Shared const props_; - ShadowNode::SharedListOfShared const children_; - State::Shared const state_; + Props::Shared props; + ShadowNode::SharedListOfShared children; + State::Shared state; }; }; diff --git a/ReactCommon/fabric/graphics/Geometry.h b/ReactCommon/fabric/graphics/Geometry.h index e1701ca311d43b..36f71e8321562f 100644 --- a/ReactCommon/fabric/graphics/Geometry.h +++ b/ReactCommon/fabric/graphics/Geometry.h @@ -259,6 +259,15 @@ using EdgeInsets = RectangleEdges; */ using CornerInsets = RectangleCorners; +/* + * Adjusts a rectangle by the given edge insets. + */ +inline Rect insetBy(Rect rect, EdgeInsets insets) { + return Rect{{rect.origin.x + insets.left, rect.origin.y + insets.top}, + {rect.size.width - insets.left - insets.right, + rect.size.height - insets.top - insets.bottom}}; +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/fabric/graphics/Quaternion.h b/ReactCommon/fabric/graphics/Quaternion.h deleted file mode 100644 index 4233a2c6cb4409..00000000000000 --- a/ReactCommon/fabric/graphics/Quaternion.h +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Portions 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. - */ - -#pragma once - -#include -#include -#include - -// The following is a modified, stripped-down version of the Quaternion class -// by Frank Astier. Copyright notice below. -// The original has many, many more features, and has been stripped down -// to support the exact data-structures and use-cases we need for React Native. - -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Frank Astier - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace facebook { -namespace react { - -template -class Quaternion { - public: - /** - * Copy constructor. - */ - Quaternion(const Quaternion &y) : a_(y.a_), b_(y.b_), c_(y.c_), d_(y.d_) {} - - Quaternion(T a, T b, T c, T d) : a_(a), b_(b), c_(c), d_(d) {} - - static Quaternion fromRotationMatrix(std::array const &rm) { - T t = rm[0 * 4 + 0] + rm[1 * 4 + 1] + rm[2 * 4 + 2]; - if (t > 0) { - T s = (T)0.5 / std::sqrt(t + 1); - return {(T)0.25 / s, - (rm[2 * 4 + 1] - rm[1 * 4 + 2]) * s, - (rm[0 * 4 + 2] - rm[2 * 4 + 0]) * s, - (rm[1 * 4 + 0] - rm[0 * 4 + 1]) * s}; - } else if (rm[0 * 4 + 0] > rm[1 * 4 + 1] && rm[0 * 4 + 0] > rm[2 * 4 + 2]) { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[0 * 4 + 0] - rm[1 * 4 + 1] - rm[2 * 4 + 2]); // S=4*qx - return {(rm[2 * 4 + 1] - rm[1 * 4 + 2]) / s, - (T)0.25 * s, - (rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s, - (rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s}; - } else if (rm[1 * 4 + 1] > rm[2 * 4 + 2]) { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[1 * 4 + 1] - rm[0 * 4 + 0] - rm[2 * 4 + 2]); // S=4*qy - return {(rm[0 * 4 + 2] - rm[2 * 4 + 0]) / s, - (rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s, - (T)0.25 * s, - (rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s}; - } else { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[2 * 4 + 2] - rm[0 * 4 + 0] - rm[1 * 4 + 1]); // S=4*qz - return {(rm[1 * 4 + 0] - rm[0 * 4 + 1]) / s, - (rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s, - (rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s, - (T)0.25 * s}; - } - } - - /** - * Returns a 3D, 4x4 rotation matrix. - * This is the "homogeneous" expression to convert to a rotation matrix, - * which works even when the Quaternion is not a unit Quaternion. - */ - inline std::array toRotationMatrix4x4() { - T a2 = a_ * a_, b2 = b_ * b_, c2 = c_ * c_, d2 = d_ * d_; - T ab = a_ * b_, ac = a_ * c_, ad = a_ * d_; - T bc = b_ * c_, bd = b_ * d_; - T cd = c_ * d_; - return {a2 + b2 - c2 - d2, - 2 * (bc - ad), - 2 * (bd + ac), - 0, - 2 * (bc + ad), - a2 - b2 + c2 - d2, - 2 * (cd - ab), - 0, - 2 * (bd - ac), - 2 * (cd + ab), - a2 - b2 - c2 + d2, - 0, - 0, - 0, - 0, - 1}; - } - - inline Quaternion normalize() const { - assert(abs() > 0); // or this is not normalizable - T factor = abs(); - return *this / (factor != 0 ? factor : 1); - } - - inline T dot(const Quaternion &other) { - return a_ * other.a_ + b_ * other.b_ + c_ * other.c_ + d_ * other.d_; - } - - /** - * The square of the norm of the Quaternion. - * (The square is sometimes useful, and it avoids paying for a sqrt). - */ - inline T norm_squared() const { - return a_ * a_ + b_ * b_ + c_ * c_ + d_ * d_; - } - - /** - * The norm of the Quaternion (the l2 norm). - */ - inline T abs() const { - return std::sqrt(norm_squared()); - } - - inline Quaternion operator/=(T y) { - a_ /= y; - b_ /= y; - c_ /= y; - d_ /= y; - return *this; - } - - inline Quaternion operator*=(T y) { - a_ *= y; - b_ *= y; - c_ *= y; - d_ *= y; - return *this; - } - - inline Quaternion operator+=(Quaternion const &other) { - a_ += other.a_; - b_ += other.b_; - c_ += other.c_; - d_ += other.d_; - return *this; - } - - inline Quaternion operator-=(Quaternion const &other) { - a_ -= other.a_; - b_ -= other.b_; - c_ -= other.c_; - d_ -= other.d_; - return *this; - } - - private: - T a_; // AKA w, qw - T b_; // AKA x, qx - T c_; // AKA y, qy - T d_; // AKA z, qz -}; - -template -inline Quaternion operator/(Quaternion const &lhs, T rhs) { - return Quaternion(lhs) /= rhs; -} - -template -inline Quaternion operator*(Quaternion const &lhs, T rhs) { - return Quaternion(lhs) *= rhs; -} - -template -inline Quaternion operator+( - Quaternion const &lhs, - Quaternion const &rhs) { - return Quaternion(lhs) += rhs; -} - -template -inline Quaternion operator-( - Quaternion const &lhs, - Quaternion const &rhs) { - return Quaternion(lhs) -= rhs; -} - -} // namespace react -} // namespace facebook diff --git a/ReactCommon/fabric/graphics/React-graphics.podspec b/ReactCommon/fabric/graphics/React-graphics.podspec index ae881fde53edff..5a2643f61f98fd 100644 --- a/ReactCommon/fabric/graphics/React-graphics.podspec +++ b/ReactCommon/fabric/graphics/React-graphics.podspec @@ -18,6 +18,7 @@ end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' folly_version = '2020.01.13.00' +boost_compiler_flags = '-Wno-documentation' Pod::Spec.new do |s| s.name = "React-graphics" @@ -29,13 +30,13 @@ Pod::Spec.new do |s| s.platforms = { :ios => "9.0", :tvos => "10.0" } s.source = source s.library = "stdc++" - s.compiler_flags = folly_compiler_flags + s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags s.source_files = "**/*.{m,mm,cpp,h}" s.exclude_files = "**/tests/*", "**/android/*", "**/cxx/*" s.header_dir = "react/graphics" - s.pod_target_xcconfig = { "USE_HEADERMAP" => "NO", "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } + s.pod_target_xcconfig = { "USE_HEADERMAP" => "NO", "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } s.dependency "RCT-Folly/Fabric", folly_version end diff --git a/ReactCommon/fabric/graphics/Transform.cpp b/ReactCommon/fabric/graphics/Transform.cpp index f55721c3fc6be8..6caea8e9749869 100644 --- a/ReactCommon/fabric/graphics/Transform.cpp +++ b/ReactCommon/fabric/graphics/Transform.cpp @@ -7,7 +7,6 @@ #include "Transform.h" -#include #include #include @@ -34,198 +33,211 @@ Transform Transform::Identity() { Transform Transform::Perspective(Float perspective) { auto transform = Transform{}; + transform.operations.push_back(TransformOperation{ + TransformOperationType::Perspective, perspective, 0, 0}); transform.matrix[11] = -1 / perspective; return transform; } -Transform Transform::Scale(Float factorX, Float factorY, Float factorZ) { +Transform Transform::Scale(Float x, Float y, Float z) { auto transform = Transform{}; - transform.matrix[0] = factorX; - transform.matrix[5] = factorY; - transform.matrix[10] = factorZ; + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + Float zprime = isZero(z) ? 0 : z; + if (xprime != 1 || yprime != 1 || zprime != 1) { + transform.operations.push_back(TransformOperation{ + TransformOperationType::Scale, xprime, yprime, zprime}); + transform.matrix[0] = xprime; + transform.matrix[5] = yprime; + transform.matrix[10] = zprime; + } return transform; } Transform Transform::Translate(Float x, Float y, Float z) { auto transform = Transform{}; - transform.matrix[12] = x; - transform.matrix[13] = y; - transform.matrix[14] = z; + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + Float zprime = isZero(z) ? 0 : z; + if (xprime != 0 || yprime != 0 || zprime != 0) { + transform.operations.push_back(TransformOperation{ + TransformOperationType::Translate, xprime, yprime, zprime}); + transform.matrix[12] = xprime; + transform.matrix[13] = yprime; + transform.matrix[14] = zprime; + } return transform; } Transform Transform::Skew(Float x, Float y) { auto transform = Transform{}; - transform.matrix[4] = std::tan(x); - transform.matrix[1] = std::tan(y); + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + transform.operations.push_back( + TransformOperation{TransformOperationType::Skew, xprime, yprime, 0}); + transform.matrix[4] = std::tan(xprime); + transform.matrix[1] = std::tan(yprime); return transform; } Transform Transform::RotateX(Float radians) { auto transform = Transform{}; - transform.matrix[5] = std::cos(radians); - transform.matrix[6] = std::sin(radians); - transform.matrix[9] = -std::sin(radians); - transform.matrix[10] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, radians, 0, 0}); + transform.matrix[5] = std::cos(radians); + transform.matrix[6] = std::sin(radians); + transform.matrix[9] = -std::sin(radians); + transform.matrix[10] = std::cos(radians); + } return transform; } Transform Transform::RotateY(Float radians) { auto transform = Transform{}; - transform.matrix[0] = std::cos(radians); - transform.matrix[2] = -std::sin(radians); - transform.matrix[8] = std::sin(radians); - transform.matrix[10] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, 0, radians, 0}); + transform.matrix[0] = std::cos(radians); + transform.matrix[2] = -std::sin(radians); + transform.matrix[8] = std::sin(radians); + transform.matrix[10] = std::cos(radians); + } return transform; } Transform Transform::RotateZ(Float radians) { auto transform = Transform{}; - transform.matrix[0] = std::cos(radians); - transform.matrix[1] = std::sin(radians); - transform.matrix[4] = -std::sin(radians); - transform.matrix[5] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, 0, 0, radians}); + transform.matrix[0] = std::cos(radians); + transform.matrix[1] = std::sin(radians); + transform.matrix[4] = -std::sin(radians); + transform.matrix[5] = std::cos(radians); + } return transform; } Transform Transform::Rotate(Float x, Float y, Float z) { auto transform = Transform{}; - if (x != 0) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, x, y, z}); + if (!isZero(x)) { transform = transform * Transform::RotateX(x); } - if (y != 0) { + if (!isZero(y)) { transform = transform * Transform::RotateY(y); } - if (z != 0) { + if (!isZero(z)) { transform = transform * Transform::RotateZ(z); } return transform; } -Transform::SRT Transform::ExtractSRT(Transform const &t) { - // First we need to extract translation, rotation, and scale from both - // matrices, in that order. Matrices must be in this form: [a b c d] [e f g h] - // [i j k l] - // [0 0 0 1] - // We also assume that all scale factors are non-negative, because in - assert( - t.matrix[12] == 0 && t.matrix[13] == 0 && t.matrix[14] == 0 && - t.matrix[15] == 1 && "Last row of matrix must be [0,0,0,1]"); - - // lhs: - // Translation: extract the values from the rightmost column - Float translationX = t.matrix[3]; - Float translationY = t.matrix[7]; - Float translationZ = t.matrix[11]; - - // Scale: the length of the first three column vectors - // TODO: do we need to do anything special for negative scale factors? - // the last element is a uniform scale factor - Float scaleX = t.matrix[15] * - sqrt(pow(t.matrix[0], 2) + pow(t.matrix[4], 2) + - pow(t.matrix[8], 2)); // sqrt(a^2 + e^2 + i^2) - Float scaleY = t.matrix[15] * - sqrt(pow(t.matrix[1], 2) + pow(t.matrix[5], 2) + - pow(t.matrix[9], 2)); // sqrt(b^2 + f^2 + j^2) - Float scaleZ = t.matrix[15] * - sqrt(pow(t.matrix[2], 2) + pow(t.matrix[6], 2) + - pow(t.matrix[10], 2)); // sqrt(c^2 + g^2 + k^2) - - Float rScaleFactorX = scaleX == 0 ? 1 : scaleX; - Float rScaleFactorY = scaleY == 0 ? 1 : scaleY; - Float rScaleFactorZ = scaleZ == 0 ? 1 : scaleZ; - - // Construct a rotation matrix and convert that to quaternions - auto rotationMatrix = std::array{t.matrix[0] / rScaleFactorX, - t.matrix[1] / rScaleFactorY, - t.matrix[2] / rScaleFactorZ, - 0, - t.matrix[4] / rScaleFactorX, - t.matrix[5] / rScaleFactorY, - t.matrix[6] / rScaleFactorZ, - 0, - t.matrix[8] / rScaleFactorX, - t.matrix[9] / rScaleFactorY, - t.matrix[10] / rScaleFactorZ, - 0, - 0, - 0, - 0, - 1}; - - Quaternion q = - Quaternion::fromRotationMatrix(rotationMatrix).normalize(); - - return Transform::SRT{ - translationX, translationY, translationZ, scaleX, scaleY, scaleZ, q}; +Transform Transform::FromTransformOperation( + TransformOperation transformOperation) { + if (transformOperation.type == TransformOperationType::Perspective) { + return Transform::Perspective(transformOperation.x); + } + if (transformOperation.type == TransformOperationType::Scale) { + return Transform::Scale( + transformOperation.x, transformOperation.y, transformOperation.z); + } + if (transformOperation.type == TransformOperationType::Translate) { + return Transform::Translate( + transformOperation.x, transformOperation.y, transformOperation.z); + } + if (transformOperation.type == TransformOperationType::Skew) { + return Transform::Skew(transformOperation.x, transformOperation.y); + } + if (transformOperation.type == TransformOperationType::Rotate) { + return Transform::Rotate( + transformOperation.x, transformOperation.y, transformOperation.z); + } + + // Identity or Arbitrary + return Transform::Identity(); +} + +TransformOperation Transform::DefaultTransformOperation( + TransformOperationType type) { + switch (type) { + case TransformOperationType::Arbitrary: + return TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}; + case TransformOperationType::Perspective: + return TransformOperation{TransformOperationType::Perspective, 0, 0, 0}; + case TransformOperationType::Scale: + return TransformOperation{TransformOperationType::Scale, 1, 1, 1}; + case TransformOperationType::Translate: + return TransformOperation{TransformOperationType::Translate, 0, 0, 0}; + case TransformOperationType::Rotate: + return TransformOperation{TransformOperationType::Rotate, 0, 0, 0}; + case TransformOperationType::Skew: + return TransformOperation{TransformOperationType::Skew, 0, 0, 0}; + default: + case TransformOperationType::Identity: + return TransformOperation{TransformOperationType::Identity, 0, 0, 0}; + } } Transform Transform::Interpolate( float animationProgress, Transform const &lhs, Transform const &rhs) { - // Extract SRT for both sides - // This is extracted in the form: X,Y,Z coordinates for translations; X,Y,Z - // coordinates for scale; and a quaternion for rotation. - auto lhsSRT = ExtractSRT(lhs); - auto rhsSRT = ExtractSRT(rhs); - - // Interpolate translation and scale terms linearly (LERP) - Float translateX = - (lhsSRT.translationX + - (rhsSRT.translationX - lhsSRT.translationX) * animationProgress); - Float translateY = - (lhsSRT.translationY + - (rhsSRT.translationY - lhsSRT.translationY) * animationProgress); - Float translateZ = - (lhsSRT.translationZ + - (rhsSRT.translationZ - lhsSRT.translationZ) * animationProgress); - Float scaleX = - (lhsSRT.scaleX + (rhsSRT.scaleX - lhsSRT.scaleX) * animationProgress); - Float scaleY = - (lhsSRT.scaleY + (rhsSRT.scaleY - lhsSRT.scaleY) * animationProgress); - Float scaleZ = - (lhsSRT.scaleZ + (rhsSRT.scaleZ - lhsSRT.scaleZ) * animationProgress); - - // Use the quaternion vectors to produce an interpolated rotation via SLERP - // dot: cos of the angle between the two quaternion vectors - Quaternion q1 = lhsSRT.rotation; - Quaternion q2 = rhsSRT.rotation; - Float dot = q1.dot(q2); - // Clamp dot between -1 and 1 - dot = (dot < -1 ? -1 : (dot > 1 ? 1 : dot)); - // There are two ways of performing an identical slerp: q1 and -q1. - // If the dot-product is negative, we can multiply q1 by -1 and our animation - // will take the "short way" around instead of the "long way". - if (dot < 0) { - q1 = q1 * (Float)-1; - dot = dot * -1; - } - // Interpolated angle - Float theta = acosf(dot) * animationProgress; - - Transform rotation = Transform::Identity(); - - // Compute orthonormal basis - Quaternion orthonormalBasis = (q2 - q1 * dot); - - if (orthonormalBasis.abs() > 0) { - Quaternion orthonormalBasisNormalized = orthonormalBasis.normalize(); - - // Compute orthonormal basis - // Final quaternion result - slerp! - Quaternion resultingRotationVec = - (q1 * (Float)cos(theta) + - orthonormalBasisNormalized * (Float)sin(theta)) - .normalize(); + // Iterate through operations and reconstruct an interpolated resulting + // transform If at any point we hit an "Arbitrary" Transform, return at that + // point + Transform result = Transform::Identity(); + for (int i = 0, j = 0; + i < lhs.operations.size() || j < rhs.operations.size();) { + bool haveLHS = i < lhs.operations.size(); + bool haveRHS = j < rhs.operations.size(); + + if ((haveLHS && + lhs.operations[i].type == TransformOperationType::Arbitrary) || + (haveRHS && + rhs.operations[i].type == TransformOperationType::Arbitrary)) { + return result; + } + if (haveLHS && lhs.operations[i].type == TransformOperationType::Identity) { + i++; + continue; + } + if (haveRHS && rhs.operations[j].type == TransformOperationType::Identity) { + j++; + continue; + } - // Convert quaternion to matrix - rotation.matrix = resultingRotationVec.toRotationMatrix4x4(); + // Here we either set: + // 1. lhs = next left op, rhs = next right op (when types are identical and + // both exist) + // 2. lhs = next left op, rhs = default of type (if types unequal, or rhs + // doesn't exist) + // 3. lhs = default of type, rhs = next right op (if types unequal, or rhs + // doesn't exist) This guarantees that the types of both sides are equal, + // and that one or both indices moves forward. + TransformOperationType type = + (haveLHS ? lhs.operations[i] : rhs.operations[j]).type; + TransformOperation lhsOp = + (haveLHS ? lhs.operations[i++] + : Transform::DefaultTransformOperation(type)); + TransformOperation rhsOp = + (haveRHS && rhs.operations[j].type == type + ? rhs.operations[j++] + : Transform::DefaultTransformOperation(type)); + assert(type == lhsOp.type); + assert(type == rhsOp.type); + + result = result * + Transform::FromTransformOperation(TransformOperation{ + type, + lhsOp.x + (rhsOp.x - lhsOp.x) * animationProgress, + lhsOp.y + (rhsOp.y - lhsOp.y) * animationProgress, + lhsOp.z + (rhsOp.z - lhsOp.z) * animationProgress}); } - // Compose matrices and return - return (Scale(scaleX, scaleY, scaleZ) * rotation) * - Translate(translateX, translateY, translateZ); + return result; } bool Transform::operator==(Transform const &rhs) const { @@ -248,6 +260,20 @@ Transform Transform::operator*(Transform const &rhs) const { const auto &lhs = *this; auto result = Transform{}; + for (const auto &op : this->operations) { + if (op.type == TransformOperationType::Identity && + result.operations.size() > 0) { + continue; + } + result.operations.push_back(op); + } + for (const auto &op : rhs.operations) { + if (op.type == TransformOperationType::Identity && + result.operations.size() > 0) { + continue; + } + result.operations.push_back(op); + } auto lhs00 = lhs.matrix[0], lhs01 = lhs.matrix[1], lhs02 = lhs.matrix[2], lhs03 = lhs.matrix[3], lhs10 = lhs.matrix[4], lhs11 = lhs.matrix[5], diff --git a/ReactCommon/fabric/graphics/Transform.h b/ReactCommon/fabric/graphics/Transform.h index 357e2863d7df93..7d14a635afbdfd 100644 --- a/ReactCommon/fabric/graphics/Transform.h +++ b/ReactCommon/fabric/graphics/Transform.h @@ -8,11 +8,11 @@ #pragma once #include +#include #include #include #include -#include #ifdef ANDROID #include @@ -21,21 +21,38 @@ namespace facebook { namespace react { -struct ScaleRotationTranslation { - Float translationX; - Float translationY; - Float translationZ; - Float scaleX; - Float scaleY; - Float scaleZ; - Quaternion rotation; +inline bool isZero(Float n) { + // We use this ternary expression instead of abs, fabsf, etc, because + // Float can be double or float depending on compilation target. + return (n < 0 ? n * (-1) : n) < 0.00001; +} + +/** + * Defines operations used to construct a transform matrix. + * An "Arbitrary" operation means that the transform was seeded with some + * arbitrary initial result. + */ +enum class TransformOperationType { + Arbitrary, + Identity, + Perspective, + Scale, + Translate, + Rotate, + Skew +}; +struct TransformOperation { + TransformOperationType type; + Float x; + Float y; + Float z; }; /* * Defines transform matrix to apply affine transformations. */ struct Transform { - using SRT = ScaleRotationTranslation; + std::vector operations{}; std::array matrix{ {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}}; @@ -47,6 +64,14 @@ struct Transform { static void print(Transform const &t, std::string prefix); #endif + /* + * Given a TransformOperation, return the proper transform. + */ + static Transform FromTransformOperation( + TransformOperation transformOperation); + static TransformOperation DefaultTransformOperation( + TransformOperationType type); + /* * Returns the identity transform (`[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]`). */ @@ -80,25 +105,6 @@ struct Transform { static Transform RotateZ(Float angle); static Transform Rotate(Float angleX, Float angleY, Float angleZ); - /** - * Extract SRT (scale, rotation, transformation) from a Transform matrix. - * - * CAVEATS: - * 1. The input matrix must not have Skew applied. - * 2. Scaling factors must be non-negative. Scaling by a negative factor is - * equivalent to a rotation, and though it is possible to detect if 1 or - * 3 of the scale signs are flipped (but not two), it is not possible - * to detect WHICH of the scales are flipped. Thus, any animation - * that involves a negative scale factor will not crash but will - * interpolate over nonsensical values. - * 3. Another caveat is that if the animation interpolates TO a 90º - * rotation in the X, Y, or Z axis, the View will appear to suddenly - * explode in size. Interpolating THROUGH 90º is fine as long as you don't end - * up at 90º or close to it (89.99). The same is true for 0±90 and 360n+90, - * etc. - */ - static SRT ExtractSRT(Transform const &transform); - /** * Perform an interpolation between lhs and rhs, given progress. * This first decomposes the matrices into translation, scale, and rotation, diff --git a/ReactCommon/fabric/graphics/platform/ios/Color.cpp b/ReactCommon/fabric/graphics/platform/ios/Color.cpp index dfde827886e203..8cbdeee7fdc60b 100644 --- a/ReactCommon/fabric/graphics/platform/ios/Color.cpp +++ b/ReactCommon/fabric/graphics/platform/ios/Color.cpp @@ -17,7 +17,7 @@ SharedColor colorFromComponents(ColorComponents components) { auto color = CGColorCreate(CGColorSpaceCreateDeviceRGB(), componentsArray); - return SharedColor(color, CFRelease); + return SharedColor(color, CGColorRelease); } ColorComponents colorComponentsFromColor(SharedColor color) { diff --git a/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm index 49a371bc412981..9691162f65de56 100644 --- a/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm +++ b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm @@ -88,7 +88,7 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa [self->_imageLoader loadImageWithURLRequest:request size:CGSizeMake(imageSource.size.width, imageSource.size.height) scale:imageSource.scale - clipped:YES + clipped:NO resizeMode:RCTResizeModeStretch priority:RCTImageLoaderPriorityImmediate attribution:{ diff --git a/ReactCommon/fabric/mounting/MountingCoordinator.cpp b/ReactCommon/fabric/mounting/MountingCoordinator.cpp index ee6b7440fe8745..d977f35d36bdec 100644 --- a/ReactCommon/fabric/mounting/MountingCoordinator.cpp +++ b/ReactCommon/fabric/mounting/MountingCoordinator.cpp @@ -130,14 +130,18 @@ better::optional MountingCoordinator::pullTransaction() auto telemetry = (lastRevision_.hasValue() ? lastRevision_->getTelemetry() : MountingTelemetry{}); + if (!lastRevision_.hasValue()) { + telemetry.willLayout(); + telemetry.didLayout(); + telemetry.willCommit(); + telemetry.didCommit(); + } + telemetry.willDiff(); if (lastRevision_.hasValue()) { - telemetry.willDiff(); - diffMutations = calculateShadowViewMutations( baseRevision_.getRootShadowNode(), lastRevision_->getRootShadowNode()); - - telemetry.didDiff(); } + telemetry.didDiff(); better::optional transaction{}; diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTFontUtils.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTFontUtils.mm index 34ed2f0cdb5baa..a869b66505e53d 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTFontUtils.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTFontUtils.mm @@ -71,7 +71,8 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) static NSCache *fontCache; static std::mutex fontCacheMutex; - NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", fontProperties.size, fontProperties.weight]; + CGFloat effectiveFontSize = fontProperties.sizeMultiplier * fontProperties.size; + NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", effectiveFontSize, fontProperties.weight]; UIFont *font; { @@ -83,7 +84,7 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) } if (!font) { - font = [UIFont systemFontOfSize:fontProperties.size weight:fontProperties.weight]; + font = [UIFont systemFontOfSize:effectiveFontSize weight:fontProperties.weight]; if (fontProperties.variant == RCTFontStyleItalic) { UIFontDescriptor *fontDescriptor = [font fontDescriptor]; @@ -92,7 +93,7 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) symbolicTraits |= UIFontDescriptorTraitItalic; fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; - font = [UIFont fontWithDescriptor:fontDescriptor size:fontProperties.size]; + font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize]; } { diff --git a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm index 1029a87ad4a71c..7e0c4a9231a22c 100644 --- a/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm +++ b/ReactCommon/fabric/textlayoutmanager/platform/ios/RCTTextLayoutManager.mm @@ -10,12 +10,14 @@ #import "NSTextStorage+FontScaling.h" #import "RCTAttributedTextUtils.h" +#import +#import #import using namespace facebook::react; @implementation RCTTextLayoutManager { - SimpleThreadSafeCache, 256> _cache; + SimpleThreadSafeCache, 256> _cache; } static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode) @@ -55,6 +57,8 @@ - (TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedStr CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; + size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)}; + __block auto attachments = TextMeasurement::Attachments{}; [textStorage @@ -171,12 +175,11 @@ - (SharedEventEmitter)getEventEmitterWithAttributeString:(AttributedString)attri - (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString { - auto sharedNSAttributedString = _cache.get(attributedString, [](const AttributedString attributedString) { - return std::shared_ptr( - (__bridge_retained void *)RCTNSAttributedStringFromAttributedString(attributedString), CFRelease); + auto sharedNSAttributedString = _cache.get(attributedString, [](AttributedString attributedString) { + return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString)); }); - return (__bridge NSAttributedString *)sharedNSAttributedString.get(); + return unwrapManagedObject(sharedNSAttributedString); } @end diff --git a/ReactCommon/hermes/inspector/Inspector.cpp b/ReactCommon/hermes/inspector/Inspector.cpp index c2b58712119d55..14cb76cad3f943 100644 --- a/ReactCommon/hermes/inspector/Inspector.cpp +++ b/ReactCommon/hermes/inspector/Inspector.cpp @@ -136,6 +136,29 @@ Inspector::~Inspector() { debugger_.setEventObserver(nullptr); } +static bool toBoolean(jsi::Runtime &runtime, const jsi::Value &val) { + // Based on Operations.cpp:toBoolean in the Hermes VM. + if (val.isUndefined() || val.isNull()) { + return false; + } + if (val.isBool()) { + return val.getBool(); + } + if (val.isNumber()) { + double m = val.getNumber(); + return m != 0 && !std::isnan(m); + } + if (val.isSymbol() || val.isObject()) { + return true; + } + if (val.isString()) { + std::string s = val.getString(runtime).utf8(runtime); + return !s.empty(); + } + assert(false && "All cases should be covered"); + return false; +} + void Inspector::installConsoleFunction( jsi::Object &console, std::shared_ptr &originalConsole, @@ -169,11 +192,30 @@ void Inspector::installConsoleFunction( } if (auto inspector = weakInspector.lock()) { - jsi::Array argsArray(runtime, count); - for (size_t index = 0; index < count; ++index) - argsArray.setValueAtIndex(runtime, index, args[index]); - inspector->logMessage( - ConsoleMessageInfo{chromeType, std::move(argsArray)}); + if (name != "assert") { + // All cases other than assert just log a simple message. + jsi::Array argsArray(runtime, count); + for (size_t index = 0; index < count; ++index) + argsArray.setValueAtIndex(runtime, index, args[index]); + inspector->logMessage( + ConsoleMessageInfo{chromeType, std::move(argsArray)}); + return jsi::Value::undefined(); + } + // console.assert needs to check the first parameter before + // logging. + if (count == 0) { + // No parameters, throw a blank assertion failed message. + inspector->logMessage( + ConsoleMessageInfo{chromeType, jsi::Array(runtime, 0)}); + } else if (!toBoolean(runtime, args[0])) { + // Shift the message array down by one to not include the + // condition. + jsi::Array argsArray(runtime, count - 1); + for (size_t index = 1; index < count; ++index) + argsArray.setValueAtIndex(runtime, index, args[index]); + inspector->logMessage( + ConsoleMessageInfo{chromeType, std::move(argsArray)}); + } } return jsi::Value::undefined(); diff --git a/ReactAndroid/src/main/libraries/fbcore/src/test/java/com/facebook/powermock/BUCK b/ReactCommon/libraries/fbcore/src/test/java/com/facebook/powermock/BUCK similarity index 100% rename from ReactAndroid/src/main/libraries/fbcore/src/test/java/com/facebook/powermock/BUCK rename to ReactCommon/libraries/fbcore/src/test/java/com/facebook/powermock/BUCK diff --git a/ReactCommon/utils/ManagedObjectWrapper.h b/ReactCommon/utils/ManagedObjectWrapper.h index 0f44838c39f322..623158015740d5 100644 --- a/ReactCommon/utils/ManagedObjectWrapper.h +++ b/ReactCommon/utils/ManagedObjectWrapper.h @@ -21,6 +21,16 @@ namespace facebook { namespace react { +namespace detail { + +/* + * A custom deleter used for the deallocation of Objective-C managed objects. + * To be used only by `wrapManagedObject`. + */ +void wrappedManagedObjectDeleter(void *cfPointer) noexcept; + +} + /* * `wrapManagedObject` and `unwrapManagedObject` are wrapper functions that * convert ARC-managed objects into `std::shared_ptr` and vice-versa. It's @@ -35,24 +45,24 @@ namespace react { * represented as multiple bumps of C++ counter, so we can have multiple * counters for the same object that form some kind of counters tree. */ -inline std::shared_ptr wrapManagedObject(id object) +inline std::shared_ptr wrapManagedObject(id object) noexcept { - return std::shared_ptr((__bridge_retained void *)object, CFRelease); + return std::shared_ptr((__bridge_retained void *)object, detail::wrappedManagedObjectDeleter); } -inline id unwrapManagedObject(std::shared_ptr const &object) +inline id unwrapManagedObject(std::shared_ptr const &object) noexcept { return (__bridge id)object.get(); } -inline std::shared_ptr wrapManagedObjectWeakly(id object) +inline std::shared_ptr wrapManagedObjectWeakly(id object) noexcept { RCTInternalGenericWeakWrapper *weakWrapper = [RCTInternalGenericWeakWrapper new]; weakWrapper.object = object; return wrapManagedObject(weakWrapper); } -inline id unwrapManagedObjectWeakly(std::shared_ptr const &object) +inline id unwrapManagedObjectWeakly(std::shared_ptr const &object) noexcept { RCTInternalGenericWeakWrapper *weakWrapper = (RCTInternalGenericWeakWrapper *)unwrapManagedObject(object); assert(weakWrapper && "`RCTInternalGenericWeakWrapper` instance must not be `nil`."); diff --git a/ReactCommon/utils/ManagedObjectWrapper.mm b/ReactCommon/utils/ManagedObjectWrapper.mm index 1a6822eca6db3c..ede1d30aad840c 100644 --- a/ReactCommon/utils/ManagedObjectWrapper.mm +++ b/ReactCommon/utils/ManagedObjectWrapper.mm @@ -10,6 +10,27 @@ #if defined(__OBJC__) && defined(__cplusplus) #if TARGET_OS_MAC && TARGET_OS_IPHONE +namespace facebook { +namespace react { +namespace detail { + +void wrappedManagedObjectDeleter(void *cfPointer) noexcept +{ + // A shared pointer does call custom deleter on `nullptr`s. + // This is somewhat counter-intuitively but makes sense considering the type-erasured nature of shared pointer and an + // aliasing constructor feature. `CFRelease` crashes on null pointer though. Therefore we must check for this case + // explicitly. + if (cfPointer == NULL) { + return; + } + + CFRelease(cfPointer); +} + +} // namespace detail +} // namespace react +} // namespace facebook + @implementation RCTInternalGenericWeakWrapper @end diff --git a/ReactCommon/utils/ThreadStorage.h b/ReactCommon/utils/ThreadStorage.h new file mode 100644 index 00000000000000..6b391549b9c908 --- /dev/null +++ b/ReactCommon/utils/ThreadStorage.h @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +/* + * ThreadStorage is a class designed to store data for specific thread. + * When data is inserted from thread 1, it can only be retrieved from thread 1. + */ +template +class ThreadStorage { + /* + * Private default constructor. This class has to be used as a singleton. + */ + ThreadStorage() = default; + + public: + static ThreadStorage &getInstance() { + static ThreadStorage threadStorage; + return threadStorage; + } + + better::optional get() const { + std::lock_guard lock(mutex_); + auto iterator = storage_.find(std::this_thread::get_id()); + + if (iterator != storage_.end()) { + return iterator->second; + } else { + return {}; + } + } + + void set(DataT data) { + std::lock_guard lock(mutex_); + storage_[std::this_thread::get_id()] = data; + } + + private: + mutable std::mutex mutex_; + better::map storage_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/yoga/yoga/Utils.cpp b/ReactCommon/yoga/yoga/Utils.cpp index f6e55d0d8fcc5c..c4281b60fc6e16 100644 --- a/ReactCommon/yoga/yoga/Utils.cpp +++ b/ReactCommon/yoga/yoga/Utils.cpp @@ -52,6 +52,13 @@ bool YGFloatsEqual(const float a, const float b) { return yoga::isUndefined(a) && yoga::isUndefined(b); } +bool YGDoubleEqual(const double a, const double b) { + if (!yoga::isUndefined(a) && !yoga::isUndefined(b)) { + return fabs(a - b) < 0.0001f; + } + return yoga::isUndefined(a) && yoga::isUndefined(b); +} + float YGFloatSanitize(const float val) { return yoga::isUndefined(val) ? 0 : val; } diff --git a/ReactCommon/yoga/yoga/Utils.h b/ReactCommon/yoga/yoga/Utils.h index e9edf2f96268f2..57e1d45d992be7 100644 --- a/ReactCommon/yoga/yoga/Utils.h +++ b/ReactCommon/yoga/yoga/Utils.h @@ -64,6 +64,8 @@ inline bool YGValueEqual( // difference between two floats is less than 0.0001f or both are undefined. bool YGFloatsEqual(const float a, const float b); +bool YGDoubleEqual(const double a, const double b); + float YGFloatMax(const float a, const float b); YGFloatOptional YGFloatOptionalMax( diff --git a/ReactCommon/yoga/yoga/Yoga.cpp b/ReactCommon/yoga/yoga/Yoga.cpp index 91e09c15dac2bc..cb06c10ea3aeb2 100644 --- a/ReactCommon/yoga/yoga/Yoga.cpp +++ b/ReactCommon/yoga/yoga/Yoga.cpp @@ -3668,7 +3668,7 @@ YOGA_EXPORT float YGRoundValueToPixelGrid( double scaledValue = ((double) value) * pointScaleFactor; // We want to calculate `fractial` such that `floor(scaledValue) = scaledValue // - fractial`. - float fractial = fmodf(scaledValue, 1.0f); + double fractial = fmod(scaledValue, 1.0f); if (fractial < 0) { // This branch is for handling negative numbers for `value`. // @@ -3687,10 +3687,10 @@ YOGA_EXPORT float YGRoundValueToPixelGrid( // - Finding the `floor`: -2.2 - fractial2 = -2.2 - 0.8 = -3 ++fractial; } - if (YGFloatsEqual(fractial, 0)) { + if (YGDoubleEqual(fractial, 0)) { // First we check if the value is already rounded scaledValue = scaledValue - fractial; - } else if (YGFloatsEqual(fractial, 1.0f)) { + } else if (YGDoubleEqual(fractial, 1.0f)) { scaledValue = scaledValue - fractial + 1.0f; } else if (forceCeil) { // Next we check if we need to use forced rounding @@ -3701,7 +3701,7 @@ YOGA_EXPORT float YGRoundValueToPixelGrid( // Finally we just round the value scaledValue = scaledValue - fractial + (!YGFloatIsUndefined(fractial) && - (fractial > 0.5f || YGFloatsEqual(fractial, 0.5f)) + (fractial > 0.5f || YGDoubleEqual(fractial, 0.5f)) ? 1.0f : 0.0f); } @@ -4113,11 +4113,11 @@ static void YGRoundToPixelGrid( // whole number, we don't have any fraction To verify if the result is close // to whole number we want to check both floor and ceil numbers const bool hasFractionalWidth = - !YGFloatsEqual(fmodf(nodeWidth * pointScaleFactor, 1.0), 0) && - !YGFloatsEqual(fmodf(nodeWidth * pointScaleFactor, 1.0), 1.0); + !YGDoubleEqual(fmod(nodeWidth * pointScaleFactor, 1.0), 0) && + !YGDoubleEqual(fmod(nodeWidth * pointScaleFactor, 1.0), 1.0); const bool hasFractionalHeight = - !YGFloatsEqual(fmodf(nodeHeight * pointScaleFactor, 1.0), 0) && - !YGFloatsEqual(fmodf(nodeHeight * pointScaleFactor, 1.0), 1.0); + !YGDoubleEqual(fmod(nodeHeight * pointScaleFactor, 1.0), 0) && + !YGDoubleEqual(fmod(nodeHeight * pointScaleFactor, 1.0), 1.0); node->setLayoutDimension( YGRoundValueToPixelGrid( diff --git a/android-patches/patches/Focus/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js b/android-patches/patches/Focus/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js index 7b29470ba68ca3..650827975a4e20 100644 --- a/android-patches/patches/Focus/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js +++ b/android-patches/patches/Focus/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js @@ -1,15 +1,17 @@ ---- "E:\\github\\rnm-63-fresh\\Libraries\\Components\\View\\ReactNativeViewViewConfigAndroid.js" 2020-10-27 20:26:16.000189500 -0700 -+++ "E:\\github\\rnm-63\\Libraries\\Components\\View\\ReactNativeViewViewConfigAndroid.js" 2020-10-13 21:21:38.700969000 -0700 -@@ -19,6 +19,12 @@ +diff --git a/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js b/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js +index ad2542dfa..b6d868022 100644 +--- a/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js ++++ b/Libraries/Components/View/ReactNativeViewViewConfigAndroid.js +@@ -19,6 +19,12 @@ const ReactNativeViewViewConfigAndroid = { captured: 'onSelectCapture', }, }, + topOnFocusChange: { + phasedRegistrationNames: { -+ bubbled: 'onFocusChange', -+ captured: 'onFocusChangeCapture', ++ bubbled: 'onFocusChange', ++ captured: 'onFocusChangeCapture', + }, + }, - }, - directEventTypes: { - topClick: { + topAssetDidLoad: { + phasedRegistrationNames: { + bubbled: 'onAssetDidLoad', diff --git a/android-patches/patches/OfficeRNHost/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/android-patches/patches/OfficeRNHost/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index e7954d9a833342..13636884f481f9 100644 --- a/android-patches/patches/OfficeRNHost/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/android-patches/patches/OfficeRNHost/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -1,6 +1,8 @@ ---- "E:\\github\\rnm-63-fresh\\ReactAndroid\\src\\main\\java\\com\\facebook\\react\\bridge\\CatalystInstanceImpl.java" 2020-10-27 20:26:16.742190400 -0700 -+++ "E:\\github\\rnm-63\\ReactAndroid\\src\\main\\java\\com\\facebook\\react\\bridge\\CatalystInstanceImpl.java" 2020-10-13 22:13:10.906813300 -0700 -@@ -121,7 +121,8 @@ +diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +index dae969346..4b60fd1a7 100644 +--- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java ++++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +@@ -121,7 +121,8 @@ public class CatalystInstanceImpl implements CatalystInstance { final JavaScriptExecutor jsExecutor, final NativeModuleRegistry nativeModuleRegistry, final JSBundleLoader jsBundleLoader, @@ -10,16 +12,14 @@ FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge."); Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstanceImpl"); -@@ -139,15 +140,23 @@ +@@ -139,15 +140,21 @@ public class CatalystInstanceImpl implements CatalystInstance { mTraceListener = new JSProfilerTraceListener(this); Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + FLog.d(ReactConstants.TAG, "Create module registry"); -+ + createModuleRegistry(mNativeModulesQueueThread, + mNativeModuleRegistry.getJavaModules(this), + mNativeModuleRegistry.getCxxModules()); -+ + if (catalystInstanceEventListener != null) { + FLog.d(ReactConstants.TAG, "Invoking callback onModuleRegistryCreated"); + catalystInstanceEventListener.onModuleRegistryCreated(this); @@ -38,7 +38,7 @@ FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge after initializeBridge"); Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); -@@ -208,13 +217,15 @@ +@@ -208,13 +215,15 @@ public class CatalystInstanceImpl implements CatalystInstance { private native void jniExtendNativeModules( Collection javaModules, Collection cxxModules); @@ -58,26 +58,25 @@ private native void initializeBridge( @Override public void setSourceURLs(String deviceURL, String remoteURL) { -@@ -403,7 +414,8 @@ +@@ -398,7 +407,8 @@ public class CatalystInstanceImpl implements CatalystInstance { mJavaScriptContextHolder.clear(); mHybridData.resetNative(); - getReactQueueConfiguration().destroy(); -+ // TODO :: Office patch :: Not sure why is this needed ? ++ // TODO :: Office patch :: Not sure why is this needed ? + // getReactQueueConfiguration().destroy(); FLog.d( ReactConstants.TAG, "CatalystInstanceImpl.destroy() end"); -@@ -679,6 +691,8 @@ +@@ -568,6 +578,7 @@ public class CatalystInstanceImpl implements CatalystInstance { + } private native long getJavaScriptContext(); - + public native long getPointerOfInstancePointer(); -+ + private void incrementPendingJSCalls() { int oldPendingCalls = mPendingJSCalls.getAndIncrement(); - boolean wasIdle = oldPendingCalls == 0; -@@ -784,6 +798,7 @@ +@@ -671,6 +682,7 @@ public class CatalystInstanceImpl implements CatalystInstance { private @Nullable NativeModuleRegistry mRegistry; private @Nullable JavaScriptExecutor mJSExecutor; private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; @@ -85,14 +84,14 @@ private void incrementPendingJSCalls() { public Builder setReactQueueConfigurationSpec( ReactQueueConfigurationSpec ReactQueueConfigurationSpec) { -@@ -811,13 +826,20 @@ +@@ -698,13 +710,20 @@ public class CatalystInstanceImpl implements CatalystInstance { return this; } + public Builder setCatalystInstanceEventListener( + CatalystInstanceEventListener catalystInstanceEventListener) { -+ mCatalystInstanceEventListener = catalystInstanceEventListener; -+ return this; ++ mCatalystInstanceEventListener = catalystInstanceEventListener; ++ return this; + } + public CatalystInstanceImpl build() { diff --git a/jest/setup.js b/jest/setup.js index 5f85d8602f5a91..7e9eb680489992 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -211,6 +211,10 @@ jest }; }, }, + DevSettings: { + addMenuItem: jest.fn(), + reload: jest.fn(), + }, ImageLoader: { getSize: jest.fn(url => Promise.resolve({width: 320, height: 240})), prefetchImage: jest.fn(), diff --git a/package.json b/package.json index 368655dfb433b5..d078180d18b02f 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "eslint-plugin-react-hooks": "^3.0.0", "eslint-plugin-react-native": "3.8.1", "eslint-plugin-relay": "1.7.1", - "flow-bin": "^0.126.1", + "flow-bin": "^0.127.0", "flow-remove-types": "1.2.3", "hermes-engine-darwin": "~0.5.0", "jest": "^26.0.1", diff --git a/packages/eslint-config-react-native-community/package.json b/packages/eslint-config-react-native-community/package.json index 1e599a5952d39c..5a8a3dd6df253c 100644 --- a/packages/eslint-config-react-native-community/package.json +++ b/packages/eslint-config-react-native-community/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-community/eslint-config", - "version": "1.1.0", + "version": "2.0.0", "description": "ESLint config for React Native", "main": "index.js", "license": "MIT", @@ -11,17 +11,17 @@ "homepage": "https://github.com/facebook/react-native/tree/master/packages/eslint-config-react-native-community#readme", "dependencies": { "@react-native-community/eslint-plugin": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^2.25.0", - "@typescript-eslint/parser": "^2.25.0", + "@typescript-eslint/eslint-plugin": "^3.1.0", + "@typescript-eslint/parser": "^3.1.0", "babel-eslint": "^10.1.0", "eslint-config-prettier": "^6.10.1", "eslint-plugin-eslint-comments": "^3.1.2", "eslint-plugin-flowtype": "2.50.3", "eslint-plugin-jest": "22.4.1", "eslint-plugin-prettier": "3.1.2", - "eslint-plugin-react": "7.19.0", - "eslint-plugin-react-hooks": "^3.0.0", - "eslint-plugin-react-native": "3.8.1", + "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react-hooks": "^4.0.4", + "eslint-plugin-react-native": "^3.8.1", "prettier": "^2.0.2" }, "peerDependencies": { diff --git a/packages/eslint-config-react-native-community/yarn.lock b/packages/eslint-config-react-native-community/yarn.lock index 2bc0acb2eee40f..ab6ed874d7fb01 100644 --- a/packages/eslint-config-react-native-community/yarn.lock +++ b/packages/eslint-config-react-native-community/yarn.lock @@ -138,47 +138,48 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== -"@typescript-eslint/eslint-plugin@^2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.25.0.tgz#0b60917332f20dcff54d0eb9be2a9e9f4c9fbd02" - integrity sha512-W2YyMtjmlrOjtXc+FtTelVs9OhuR6OlYc4XKIslJ8PUJOqgYYAPRJhAqkYRQo3G4sjvG8jSodsNycEn4W2gHUw== +"@typescript-eslint/eslint-plugin@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" + integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== dependencies: - "@typescript-eslint/experimental-utils" "2.25.0" + "@typescript-eslint/experimental-utils" "3.1.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" + semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz#13691c4fe368bd377b1e5b1e4ad660b220bf7714" - integrity sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw== +"@typescript-eslint/experimental-utils@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" + integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.25.0" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.25.0.tgz#abfb3d999084824d9a756d9b9c0f36fba03adb76" - integrity sha512-mccBLaBSpNVgp191CP5W+8U1crTyXsRziWliCqzj02kpxdjKMvFHGJbK33NroquH3zB/gZ8H511HEsJBa2fNEg== +"@typescript-eslint/parser@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.1.0.tgz#9c02ba5d88ad2355672f39e6cd4176f172dd47f8" + integrity sha512-NcDSJK8qTA2tPfyGiPes9HtVKLbksmuYjlgGAUs7Ld2K0swdWibnCq9IJx9kJN8JJdgUJSorFiGaPHBgH81F/Q== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.25.0" - "@typescript-eslint/typescript-estree" "2.25.0" + "@typescript-eslint/experimental-utils" "3.1.0" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz#b790497556734b7476fa7dd3fa539955a5c79e2c" - integrity sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg== +"@typescript-eslint/typescript-estree@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" + integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" glob "^7.1.6" is-glob "^4.0.1" lodash "^4.17.15" - semver "^6.3.0" + semver "^7.3.2" tsutils "^3.17.1" acorn-jsx@^5.2.0: @@ -510,27 +511,27 @@ eslint-plugin-prettier@3.1.2: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-3.0.0.tgz#9e80c71846eb68dd29c3b21d832728aa66e5bd35" - integrity sha512-EjxTHxjLKIBWFgDJdhKKzLh5q+vjTFrqNZX36uIxWS4OfyXe5DawqPj3U5qeJ1ngLwatjzQnmR0Lz0J0YH3kxw== +eslint-plugin-react-hooks@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz#aed33b4254a41b045818cacb047b81e6df27fa58" + integrity sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA== eslint-plugin-react-native-globals@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2" integrity sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g== -eslint-plugin-react-native@3.8.1: +eslint-plugin-react-native@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react-native/-/eslint-plugin-react-native-3.8.1.tgz#92811e37191ecb0d29c0f0a0c9e5c943ee573821" integrity sha512-6Z4s4nvgFRdda/1s1+uu4a6EMZwEjjJ9Bk/1yBImv0fd9U2CsGu2cUakAtV83cZKhizbWhSouXoaK4JtlScdFg== dependencies: eslint-plugin-react-native-globals "^0.1.1" -eslint-plugin-react@7.19.0: - version "7.19.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666" - integrity sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ== +eslint-plugin-react@^7.20.0: + version "7.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz#f98712f0a5e57dfd3e5542ef0604b8739cd47be3" + integrity sha512-rqe1abd0vxMjmbPngo4NaYxTcR3Y4Hrmc/jg4T+sYz63yqlmJRknpEQfmWY+eDWPuMmix6iUIK+mv0zExjeLgA== dependencies: array-includes "^3.1.1" doctrine "^2.1.0" @@ -541,7 +542,6 @@ eslint-plugin-react@7.19.0: object.values "^1.1.1" prop-types "^15.7.2" resolve "^1.15.1" - semver "^6.3.0" string.prototype.matchall "^4.0.2" xregexp "^4.3.0" @@ -1294,11 +1294,16 @@ semver@^5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== -semver@^6.1.2, semver@^6.3.0: +semver@^6.1.2: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" diff --git a/packages/react-native-codegen/DEFS.bzl b/packages/react-native-codegen/DEFS.bzl index ce5d7225cd5bec..51f85ed9b27934 100644 --- a/packages/react-native-codegen/DEFS.bzl +++ b/packages/react-native-codegen/DEFS.bzl @@ -166,6 +166,7 @@ def rn_codegen_components( srcs = [":{}".format(copy_generated_java_files)], out = "{}.src.zip".format(zip_generated_java_files), visibility = ["PUBLIC"], + labels = ["codegen_rule"], ) fb_native.genrule( diff --git a/packages/react-native-codegen/package.json b/packages/react-native-codegen/package.json index 59168f8f95a1a9..f0a19aa194a297 100644 --- a/packages/react-native-codegen/package.json +++ b/packages/react-native-codegen/package.json @@ -1,6 +1,6 @@ { "name": "react-native-codegen", - "version": "0.0.2", + "version": "0.0.3", "description": "⚛️ Code generation tools for React Native", "homepage": "https://github.com/facebook/react-native/tree/master/packages/react-native-codegen", "repository": { diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 17fb0d7df9c09d..5ae6d4a24732fb 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -55,6 +55,11 @@ export type StringTypeAnnotation = $ReadOnly<{| type: 'StringTypeAnnotation', |}>; +export type TypeAliasTypeAnnotation = $ReadOnly<{| + type: 'TypeAliasTypeAnnotation', + name: string, +|}>; + export type EventObjectPropertyType = | $ReadOnly<{| type: 'BooleanTypeAnnotation', @@ -223,19 +228,25 @@ export type FunctionTypeAnnotationParamTypeAnnotation = |}> | $ReadOnly<{| type: 'ArrayTypeAnnotation', - elementType: ?FunctionTypeAnnotationParamTypeAnnotation, + elementType: + | ?FunctionTypeAnnotationParamTypeAnnotation + | ?TypeAliasTypeAnnotation, |}> | $ReadOnly<{| type: 'ObjectTypeAnnotation', properties: ?$ReadOnlyArray, |}>; -export type FunctionTypeAnnotationReturnArrayElementType = FunctionTypeAnnotationParamTypeAnnotation; +export type FunctionTypeAnnotationReturnArrayElementType = + | FunctionTypeAnnotationParamTypeAnnotation + | TypeAliasTypeAnnotation; export type ObjectParamTypeAnnotation = $ReadOnly<{| optional: boolean, name: string, - typeAnnotation: FunctionTypeAnnotationParamTypeAnnotation, + typeAnnotation?: + | FunctionTypeAnnotationParamTypeAnnotation + | TypeAliasTypeAnnotation, // TODO (T67898313): Workaround for NativeLinking's use of union type, typeAnnotations should not be optional |}>; export type FunctionTypeAnnotationReturn = @@ -265,7 +276,9 @@ export type FunctionTypeAnnotationReturn = export type FunctionTypeAnnotationParam = $ReadOnly<{| nullable: boolean, name: string, - typeAnnotation: FunctionTypeAnnotationParamTypeAnnotation, + typeAnnotation: + | FunctionTypeAnnotationParamTypeAnnotation + | TypeAliasTypeAnnotation, |}>; export type FunctionTypeAnnotation = $ReadOnly<{| @@ -280,7 +293,13 @@ export type NativeModuleMethodTypeShape = $ReadOnly<{| typeAnnotation: FunctionTypeAnnotation, |}>; +export type ObjectTypeAliasTypeShape = $ReadOnly<{| + type: 'ObjectTypeAnnotation', + properties: $ReadOnlyArray, +|}>; + export type NativeModuleShape = $ReadOnly<{| + aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>, properties: $ReadOnlyArray, |}>; diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js index 5acd433a04c64f..c87add8b58c880 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js @@ -75,7 +75,7 @@ function traverseArg(arg, index): string { default: (typeAnnotation.name: empty); throw new Error( - `Unknown prop type for "${arg.name}, found: ${typeAnnotation.name}"`, + `Unknown prop type for "${arg.name}", found: "${typeAnnotation.name}"`, ); } case 'StringTypeAnnotation': @@ -91,6 +91,7 @@ function traverseArg(arg, index): string { case 'FunctionTypeAnnotation': return `std::move(${wrap('.getObject(rt).getFunction(rt)')})`; case 'GenericObjectTypeAnnotation': + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'ObjectTypeAnnotation': return wrap('.getObject(rt)'); case 'AnyTypeAnnotation': @@ -99,7 +100,7 @@ function traverseArg(arg, index): string { // TODO (T65847278): Figure out why this does not work. // (typeAnnotation.type: empty); throw new Error( - `Unknown prop type for "${arg.name}, found: ${typeAnnotation.type}"`, + `Unknown prop type for "${arg.name}", found: "${typeAnnotation.type}"`, ); } } diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index a423127ee49f75..5de8f8ee3c6518 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -74,6 +74,7 @@ function translatePrimitiveJSTypeToCpp( return 'int'; case 'BooleanTypeAnnotation': return 'bool'; + // case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': case 'ObjectTypeAnnotation': return 'jsi::Object'; diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleHObjCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleHObjCpp.js index 474927f779940e..a2fd47e7c34fc5 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleHObjCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleHObjCpp.js @@ -124,6 +124,7 @@ function translatePrimitiveJSTypeToObjCType( return nullable ? 'NSNumber *' : 'double'; case 'BooleanTypeAnnotation': return nullable ? 'NSNumber * _Nullable' : 'BOOL'; + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': return wrapIntoNullableIfNeeded('NSDictionary *'); case 'ArrayTypeAnnotation': @@ -181,6 +182,31 @@ function translatePrimitiveJSTypeToObjCTypeForReturn( } } +function handleArrayOfObjects( + objectForGeneratingStructs: Array, + param: FunctionTypeAnnotationParam, + name: string, +) { + if ( + param.typeAnnotation.type === 'ArrayTypeAnnotation' && + param.typeAnnotation.elementType && + param.typeAnnotation.elementType.type === 'ObjectTypeAnnotation' && + param.typeAnnotation.elementType.properties + ) { + const {elementType} = param.typeAnnotation; + const {properties} = elementType; + if (properties && properties.length > 0) { + objectForGeneratingStructs.push({ + name, + object: { + type: 'ObjectTypeAnnotation', + properties, + }, + }); + } + } +} + const methodImplementationTemplate = '- (::_RETURN_VALUE_::) ::_PROPERTY_NAME_::::_ARGS_::;'; @@ -232,12 +258,31 @@ module.exports = { }, }); paramObjCType = `JS::Native::_MODULE_NAME_::::Spec${variableName}&`; + + param.typeAnnotation.properties.map(aProp => + handleArrayOfObjects( + objectForGeneratingStructs, + aProp, + capitalizeFirstLetter(prop.name) + + capitalizeFirstLetter(param.name) + + capitalizeFirstLetter(aProp.name) + + 'Element', + ), + ); } else { paramObjCType = translatePrimitiveJSTypeToObjCType( param, typeName => `Unsupported type for param "${param.name}" in ${prop.name}. Found: ${typeName}`, ); + + handleArrayOfObjects( + objectForGeneratingStructs, + param, + capitalizeFirstLetter(prop.name) + + capitalizeFirstLetter(param.name) + + 'Element', + ); } return `${i === 0 ? '' : param.name}:(${paramObjCType})${ param.name @@ -285,7 +330,7 @@ module.exports = { return protocolTemplate .replace( /::_STRUCTS_::/g, - translateObjectsForStructs(objectForGeneratingStructs), + translateObjectsForStructs(objectForGeneratingStructs, name), ) .replace(/::_MODULE_PROPERTIES_::/g, implementations) .replace(/::_MODULE_NAME_::/g, name) diff --git a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructs.js b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructs.js index f6ff56e544cdca..9be5bcbd53729c 100644 --- a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructs.js +++ b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructs.js @@ -11,7 +11,11 @@ 'use strict'; import type {ObjectParamTypeAnnotation} from '../../../CodegenSchema'; -const {flatObjects, capitalizeFirstLetter} = require('./Utils'); +const { + flatObjects, + capitalizeFirstLetter, + getSafePropertyName, +} = require('./Utils'); const {generateStructsForConstants} = require('./GenerateStructsForConstants'); const template = ` @@ -44,24 +48,29 @@ inline ::_RETURN_TYPE_::JS::Native::_MODULE_NAME_::::Spec::_STRUCT_NAME_::::::_P } `; -function getSafePropertyName(name: string) { - if (name === 'id') { - return `${name}_`; - } - return name; -} - -function getNamespacedStructName(structName: string, propertyName: string) { +function getNamespacedStructName( + structName: string, + property: ObjectParamTypeAnnotation, +) { return `JS::Native::_MODULE_NAME_::::Spec${structName}${capitalizeFirstLetter( - getSafePropertyName(propertyName), + getSafePropertyName(property), )}`; } function getElementTypeForArray( property: ObjectParamTypeAnnotation, name: string, + moduleName: string, ): string { const {typeAnnotation} = property; + + // TODO(T67898313): Workaround for NativeLinking's use of union type. This check may be removed once typeAnnotation is non-optional. + if (!typeAnnotation) { + throw new Error( + `Cannot get array element type, property ${property.name} does not contain a type annotation`, + ); + } + if (typeAnnotation.type !== 'ArrayTypeAnnotation') { throw new Error( `Cannot get array element type for non-array type ${typeAnnotation.type}`, @@ -82,9 +91,13 @@ function getElementTypeForArray( case 'Int32TypeAnnotation': return 'double'; case 'ObjectTypeAnnotation': - return getNamespacedStructName(name, property.name); + return getNamespacedStructName(name, property) + 'Element'; + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': - // TODO T67565166: Generic objects are not type safe and should be disallowed in the schema. + // TODO(T67565166): Generic objects are not type safe and should be disallowed in the schema. This case should throw an error once it is disallowed in schema. + console.error( + `Warning: Generic objects are not type safe and should be avoided whenever possible (see '${property.name}' in ${moduleName}'s ${name})`, + ); return 'id'; case 'BooleanTypeAnnotation': case 'AnyObjectTypeAnnotation': @@ -104,6 +117,7 @@ function getElementTypeForArray( function getInlineMethodSignature( property: ObjectParamTypeAnnotation, name: string, + moduleName: string, ): string { const {typeAnnotation} = property; function markOptionalTypeIfNecessary(type: string) { @@ -112,45 +126,54 @@ function getInlineMethodSignature( } return type; } + + // TODO(T67672788): Workaround for values key in NativeLinking which lacks a typeAnnotation. id is not type safe! + if (!typeAnnotation) { + console.error( + `Warning: Unsafe type found (see '${property.name}' in ${moduleName}'s ${name})`, + ); + return `id ${getSafePropertyName(property)}() const;`; + } + switch (typeAnnotation.type) { case 'ReservedFunctionValueTypeAnnotation': switch (typeAnnotation.name) { case 'RootTag': - return `double ${getSafePropertyName(property.name)}() const;`; + return `double ${getSafePropertyName(property)}() const;`; default: (typeAnnotation.name: empty); throw new Error(`Unknown prop type, found: ${typeAnnotation.name}"`); } case 'StringTypeAnnotation': - return `NSString *${getSafePropertyName(property.name)}() const;`; + return `NSString *${getSafePropertyName(property)}() const;`; case 'NumberTypeAnnotation': case 'FloatTypeAnnotation': case 'Int32TypeAnnotation': return `${markOptionalTypeIfNecessary('double')} ${getSafePropertyName( - property.name, + property, )}() const;`; case 'BooleanTypeAnnotation': return `${markOptionalTypeIfNecessary('bool')} ${getSafePropertyName( - property.name, + property, )}() const;`; case 'ObjectTypeAnnotation': return ( - markOptionalTypeIfNecessary( - getNamespacedStructName(name, property.name), - ) + ` ${getSafePropertyName(property.name)}() const;` + markOptionalTypeIfNecessary(getNamespacedStructName(name, property)) + + ` ${getSafePropertyName(property)}() const;` ); case 'GenericObjectTypeAnnotation': case 'AnyTypeAnnotation': return `id ${ property.optional ? '_Nullable ' : ' ' - }${getSafePropertyName(property.name)}() const;`; + }${getSafePropertyName(property)}() const;`; case 'ArrayTypeAnnotation': return `${markOptionalTypeIfNecessary( `facebook::react::LazyVector<${getElementTypeForArray( property, name, + moduleName, )}>`, - )} ${getSafePropertyName(property.name)}() const;`; + )} ${getSafePropertyName(property)}() const;`; case 'FunctionTypeAnnotation': default: throw new Error(`Unknown prop type, found: ${typeAnnotation.type}"`); @@ -160,6 +183,7 @@ function getInlineMethodSignature( function getInlineMethodImplementation( property: ObjectParamTypeAnnotation, name: string, + moduleName: string, ): string { const {typeAnnotation} = property; function markOptionalTypeIfNecessary(type: string): string { @@ -175,6 +199,13 @@ function getInlineMethodImplementation( return `RCTBridgingTo${capitalizeFirstLetter(value)}`; } function bridgeArrayElementValueIfNecessary(element: string): string { + // TODO(T67898313): Workaround for NativeLinking's use of union type + if (!typeAnnotation) { + throw new Error( + `Cannot get array element type, property ${property.name} does not contain a type annotation`, + ); + } + if (typeAnnotation.type !== 'ArrayTypeAnnotation') { throw new Error( `Cannot get array element type for non-array type ${typeAnnotation.type}`, @@ -197,7 +228,8 @@ function getInlineMethodImplementation( case 'BooleanTypeAnnotation': return `RCTBridgingToBool(${element})`; case 'ObjectTypeAnnotation': - return `${getNamespacedStructName(name, property.name)}(${element})`; + return `${getNamespacedStructName(name, property)}Element(${element})`; + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': return element; case 'AnyObjectTypeAnnotation': @@ -215,6 +247,19 @@ function getInlineMethodImplementation( } } + // TODO(T67672788): Workaround for values key in NativeLinking which lacks a typeAnnotation. id is not type safe! + if (!typeAnnotation) { + console.error( + `Warning: Unsafe type found (see '${property.name}' in ${moduleName}'s ${name})`, + ); + return inlineTemplate + .replace( + /::_RETURN_TYPE_::/, + property.optional ? 'id _Nullable ' : 'id ', + ) + .replace(/::_RETURN_VALUE_::/, 'p'); + } + switch (typeAnnotation.type) { case 'ReservedFunctionValueTypeAnnotation': switch (typeAnnotation.name) { @@ -258,18 +303,16 @@ function getInlineMethodImplementation( return inlineTemplate .replace( /::_RETURN_TYPE_::/, - markOptionalTypeIfNecessary( - getNamespacedStructName(name, property.name), - ), + markOptionalTypeIfNecessary(getNamespacedStructName(name, property)), ) .replace( /::_RETURN_VALUE_::/, property.optional ? `(p == nil ? folly::none : folly::make_optional(${getNamespacedStructName( name, - property.name, + property, )}(p)))` - : `${getNamespacedStructName(name, property.name)}(p)`, + : `${getNamespacedStructName(name, property)}(p)`, ); case 'ArrayTypeAnnotation': return inlineTemplate @@ -279,6 +322,7 @@ function getInlineMethodImplementation( `facebook::react::LazyVector<${getElementTypeForArray( property, name, + moduleName, )}>`, ), ) @@ -287,6 +331,7 @@ function getInlineMethodImplementation( `${markOptionalValueIfNecessary('vec')}(p, ^${getElementTypeForArray( property, name, + moduleName, )}(id itemValue_0) { return ${bridgeArrayElementValueIfNecessary( 'itemValue_0', )}; })`, @@ -307,6 +352,7 @@ function translateObjectsForStructs( |}>, |}>, >, + moduleName: string, ): string { const flattenObjects = flatObjects(annotations); @@ -315,11 +361,8 @@ function translateObjectsForStructs( (acc, object) => acc.concat( object.properties.map(property => - getInlineMethodImplementation(property, object.name) - .replace( - /::_PROPERTY_NAME_::/g, - getSafePropertyName(property.name), - ) + getInlineMethodImplementation(property, object.name, moduleName) + .replace(/::_PROPERTY_NAME_::/g, getSafePropertyName(property)) .replace(/::_STRUCT_NAME_::/g, object.name), ), ), @@ -333,7 +376,9 @@ function translateObjectsForStructs( .replace( /::_STRUCT_PROPERTIES_::/g, object.properties - .map(property => getInlineMethodSignature(property, object.name)) + .map(property => + getInlineMethodSignature(property, object.name, moduleName), + ) .join('\n '), ) .replace(/::_STRUCT_NAME_::/g, object.name), diff --git a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructsForConstants.js b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructsForConstants.js index adf71dff20d49f..5d8dc29af72748 100644 --- a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructsForConstants.js +++ b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/GenerateStructsForConstants.js @@ -62,6 +62,14 @@ function getBuilderInputFieldDeclaration( return 'folly::Optional<' + annotation + '> ' + property.name + ';'; } const {typeAnnotation} = property; + + // TODO(T67898313): Workaround for NativeLinking's use of union type. This check may be removed once typeAnnotation is non-optional. + if (!typeAnnotation) { + throw new Error( + `Cannot get array element type, property ${property.name} does not contain a type annotation`, + ); + } + switch (typeAnnotation.type) { case 'ReservedFunctionValueTypeAnnotation': switch (typeAnnotation.name) { @@ -88,6 +96,7 @@ function getBuilderInputFieldDeclaration( property.name, )}::Builder`, ); + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': case 'AnyTypeAnnotation': if (property.optional) { @@ -153,6 +162,14 @@ function unsafeGetter(name: string, optional: boolean) { function getObjectProperty(property: ObjectParamTypeAnnotation): string { const {typeAnnotation} = property; + + // TODO(T67898313): Workaround for NativeLinking's use of union type. This check may be removed once typeAnnotation is non-optional. + if (!typeAnnotation) { + throw new Error( + `Cannot get array element type, property ${property.name} does not contain a type annotation`, + ); + } + switch (typeAnnotation.type) { case 'ReservedFunctionValueTypeAnnotation': switch (typeAnnotation.name) { @@ -169,6 +186,7 @@ function getObjectProperty(property: ObjectParamTypeAnnotation): string { case 'BooleanTypeAnnotation': return boolGetter(property.name, property.optional); case 'StringTypeAnnotation': + case 'TypeAliasTypeAnnotation': // TODO: Handle aliases case 'GenericObjectTypeAnnotation': case 'AnyTypeAnnotation': return safeGetter(property.name, property.optional); diff --git a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/Utils.js b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/Utils.js index ee0f928690895a..eb7d73974e8781 100644 --- a/packages/react-native-codegen/src/generators/modules/ObjCppUtils/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/ObjCppUtils/Utils.js @@ -66,6 +66,7 @@ function flatObjects( objectTypesToFlatten = objectTypesToFlatten.concat( properties.reduce((acc, curr) => { if ( + curr.typeAnnotation && curr.typeAnnotation.type === 'ObjectTypeAnnotation' && curr.typeAnnotation.properties ) { @@ -85,7 +86,16 @@ function flatObjects( return flattenObjects; } + +function getSafePropertyName(property: ObjectParamTypeAnnotation): string { + if (property.name === 'id') { + return `${property.name}_`; + } + return property.name; +} + module.exports = { flatObjects, capitalizeFirstLetter, + getSafePropertyName, }; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 110476acc5f7f1..6feda946974e16 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -17,6 +17,7 @@ const EMPTY_NATIVE_MODULES: SchemaType = { SampleTurboModule: { nativeModules: { SampleTurboModule: { + aliases: {}, properties: [], }, }, @@ -29,6 +30,7 @@ const SIMPLE_NATIVE_MODULES: SchemaType = { SampleTurboModule: { nativeModules: { SampleTurboModule: { + aliases: {}, properties: [ { name: 'getConstants', @@ -291,6 +293,7 @@ const TWO_MODULES_SAME_FILE: SchemaType = { NativeSampleTurboModule: { nativeModules: { SampleTurboModule: { + aliases: {}, properties: [ { name: 'voidFunc', @@ -307,6 +310,7 @@ const TWO_MODULES_SAME_FILE: SchemaType = { ], }, Sample2TurboModule: { + aliases: {}, properties: [ { name: 'voidFunc', @@ -332,6 +336,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { NativeSampleTurboModule: { nativeModules: { SampleTurboModule: { + aliases: {}, properties: [ { name: 'voidFunc', @@ -352,6 +357,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { NativeSampleTurboModule2: { nativeModules: { Sample2TurboModule: { + aliases: {}, properties: [ { name: 'getConstants', @@ -390,6 +396,7 @@ const COMPLEX_OBJECTS: SchemaType = { NativeSampleTurboModule: { nativeModules: { SampleTurboModule: { + aliases: {}, properties: [ { name: 'difficult', @@ -601,6 +608,10 @@ const COMPLEX_OBJECTS: SchemaType = { type: 'StringTypeAnnotation', }, }, + { + optional: false, + name: 'value', + }, ], }, }, diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateStructs-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateStructs-test.js index ef865d1ef80c8c..4791308f586b89 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateStructs-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateStructs-test.js @@ -23,7 +23,7 @@ describe('GenerateStructs', () => { it(`can generate fixture ${fixtureName}`, () => { expect( generator - .translateObjectsForStructs(fixture) + .translateObjectsForStructs(fixture, fixtureName) .replace(/::_MODULE_NAME_::/g, 'SampleTurboModule'), ).toMatchSnapshot(); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap index 83c96819594ef0..0d92adac32c8aa 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap @@ -72,6 +72,23 @@ namespace JS { @end +namespace JS { + namespace NativeSampleTurboModule { + struct SpecGetArraysOptionsArrayOfObjectsElement { + double numberProperty() const; + + SpecGetArraysOptionsArrayOfObjectsElement(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeSampleTurboModule_SpecGetArraysOptionsArrayOfObjectsElement) ++ (RCTManagedPointer *)JS_NativeSampleTurboModule_SpecGetArraysOptionsArrayOfObjectsElement:(id)json; +@end + + namespace JS { namespace NativeSampleTurboModule { struct SpecGetArraysOptions { @@ -79,7 +96,7 @@ namespace JS { folly::Optional> optionalArrayOfNumbers() const; facebook::react::LazyVector arrayOfStrings() const; folly::Optional> optionalArrayOfStrings() const; - facebook::react::LazyVector arrayOfObjects() const; + facebook::react::LazyVector arrayOfObjects() const; SpecGetArraysOptions(NSDictionary *const v) : _v(v) {} private: @@ -93,6 +110,24 @@ namespace JS { @end +namespace JS { + namespace NativeSampleTurboModule { + struct SpecOptionalMethodExtrasElement { + NSString *key() const; + id value() const; + + SpecOptionalMethodExtrasElement(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeSampleTurboModule_SpecOptionalMethodExtrasElement) ++ (RCTManagedPointer *)JS_NativeSampleTurboModule_SpecOptionalMethodExtrasElement:(id)json; +@end + + namespace JS { namespace NativeSampleTurboModule { struct SpecOptionalsA { @@ -228,6 +263,20 @@ inline folly::Optional JS::NativeSampleTurboModule::SpecOptionalsA::option } +inline NSString *JS::NativeSampleTurboModule::SpecOptionalMethodExtrasElement::key() const +{ + id const p = _v[@\\"key\\"]; + return RCTBridgingToString(p); +} + + +inline id JS::NativeSampleTurboModule::SpecOptionalMethodExtrasElement::value() const +{ + id const p = _v[@\\"value\\"]; + return p; +} + + inline facebook::react::LazyVector JS::NativeSampleTurboModule::SpecGetArraysOptions::arrayOfNumbers() const { id const p = _v[@\\"arrayOfNumbers\\"]; @@ -256,10 +305,17 @@ inline folly::Optional> JS::NativeSample } -inline facebook::react::LazyVector JS::NativeSampleTurboModule::SpecGetArraysOptions::arrayOfObjects() const +inline facebook::react::LazyVector JS::NativeSampleTurboModule::SpecGetArraysOptions::arrayOfObjects() const { id const p = _v[@\\"arrayOfObjects\\"]; - return RCTBridgingToVec(p, ^JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjects(id itemValue_0) { return JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjects(itemValue_0); }); + return RCTBridgingToVec(p, ^JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjectsElement(id itemValue_0) { return JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjectsElement(itemValue_0); }); +} + + +inline double JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjectsElement::numberProperty() const +{ + id const p = _v[@\\"numberProperty\\"]; + return RCTBridgingToDouble(p); } diff --git a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js index 851257da1d052f..ef77f69d312742 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js @@ -66,6 +66,22 @@ const EVENT_DEFINITION = ` int32_optional_both?: ?Int32, } }, + + object_readonly_required: $ReadOnly<{ + boolean_required: boolean, + }>, + + object_readonly_optional_key?: $ReadOnly<{ + string_optional_key?: string, + }>, + + object_readonly_optional_value: ?$ReadOnly<{ + float_optional_value: ?Float, + }>, + + object_readonly_optional_both?: ?$ReadOnly<{ + int32_optional_both?: ?Int32, + }>, `; const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS = ` diff --git a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap index 6437404dd14201..5ae118d770dd63 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap @@ -1550,6 +1550,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -1803,6 +1851,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -2055,6 +2151,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -2308,6 +2452,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -2865,6 +3057,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -3117,34 +3357,82 @@ Object { ], "type": "ObjectTypeAnnotation", }, - ], - "type": "ObjectTypeAnnotation", - }, - "type": "EventTypeAnnotation", - }, - }, - Object { - "bubblingType": "direct", - "name": "onDirectEventDefinedInlineOptionalValue", - "optional": true, - "typeAnnotation": Object { - "argument": Object { - "properties": Array [ Object { - "name": "boolean_required", + "name": "object_readonly_required", "optional": false, - "type": "BooleanTypeAnnotation", + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", }, Object { - "name": "boolean_optional_key", + "name": "object_readonly_optional_key", "optional": true, - "type": "BooleanTypeAnnotation", + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", }, Object { - "name": "boolean_optional_value", + "name": "object_readonly_optional_value", "optional": true, - "type": "BooleanTypeAnnotation", - }, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + "type": "EventTypeAnnotation", + }, + }, + Object { + "bubblingType": "direct", + "name": "onDirectEventDefinedInlineOptionalValue", + "optional": true, + "typeAnnotation": Object { + "argument": Object { + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + Object { + "name": "boolean_optional_key", + "optional": true, + "type": "BooleanTypeAnnotation", + }, + Object { + "name": "boolean_optional_value", + "optional": true, + "type": "BooleanTypeAnnotation", + }, Object { "name": "boolean_optional_both", "optional": true, @@ -3369,6 +3657,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -3621,6 +3957,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -3874,6 +4258,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -4126,6 +4558,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -4378,6 +4858,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -4630,6 +5158,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -4838,46 +5414,94 @@ Object { Object { "name": "int32_optional_both", "optional": true, - "type": "Int32TypeAnnotation", + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_required_nested_2_layers", + "optional": false, + "properties": Array [ + Object { + "name": "object_optional_nested_1_layer", + "optional": true, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "Int32TypeAnnotation", + }, + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + Object { + "name": "double_optional_value", + "optional": true, + "type": "DoubleTypeAnnotation", + }, + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", }, ], "type": "ObjectTypeAnnotation", }, Object { - "name": "object_required_nested_2_layers", - "optional": false, + "name": "object_readonly_optional_both", + "optional": true, "properties": Array [ Object { - "name": "object_optional_nested_1_layer", + "name": "int32_optional_both", "optional": true, - "properties": Array [ - Object { - "name": "boolean_required", - "optional": false, - "type": "Int32TypeAnnotation", - }, - Object { - "name": "string_optional_key", - "optional": true, - "type": "StringTypeAnnotation", - }, - Object { - "name": "double_optional_value", - "optional": true, - "type": "DoubleTypeAnnotation", - }, - Object { - "name": "float_optional_value", - "optional": true, - "type": "FloatTypeAnnotation", - }, - Object { - "name": "int32_optional_both", - "optional": true, - "type": "Int32TypeAnnotation", - }, - ], - "type": "ObjectTypeAnnotation", + "type": "Int32TypeAnnotation", }, ], "type": "ObjectTypeAnnotation", @@ -5135,6 +5759,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -6333,6 +7005,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -6586,6 +7306,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -6838,6 +7606,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, @@ -7091,6 +7907,54 @@ Object { ], "type": "ObjectTypeAnnotation", }, + Object { + "name": "object_readonly_required", + "optional": false, + "properties": Array [ + Object { + "name": "boolean_required", + "optional": false, + "type": "BooleanTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_key", + "optional": true, + "properties": Array [ + Object { + "name": "string_optional_key", + "optional": true, + "type": "StringTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_value", + "optional": true, + "properties": Array [ + Object { + "name": "float_optional_value", + "optional": true, + "type": "FloatTypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, + Object { + "name": "object_readonly_optional_both", + "optional": true, + "properties": Array [ + Object { + "name": "int32_optional_both", + "optional": true, + "type": "Int32TypeAnnotation", + }, + ], + "type": "ObjectTypeAnnotation", + }, ], "type": "ObjectTypeAnnotation", }, diff --git a/packages/react-native-codegen/src/parsers/flow/components/events.js b/packages/react-native-codegen/src/parsers/flow/components/events.js index 8767cf88a9a802..81295411a08c06 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/events.js +++ b/packages/react-native-codegen/src/parsers/flow/components/events.js @@ -52,6 +52,12 @@ function getPropertyType(name, optional, typeAnnotation) { name, optional, }; + case '$ReadOnly': + return getPropertyType( + name, + optional, + typeAnnotation.typeParameters.params[0], + ); case 'ObjectTypeAnnotation': return { type: 'ObjectTypeAnnotation', diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js index e15a2f823f8f0e..a326be96accd23 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -154,12 +154,54 @@ type Num2 = Num; export type Void = void; export type A = number; export type B = number; +export type ObjectAlias = {| + x: number, + y: number, + label: string, + truthy: boolean, +|} export interface Spec extends TurboModule { // Exported methods. +getNumber: Num2; +getVoid: () => Void; +getArray: (a: Array) => {| a: B |}; + +getStringFromAlias: (a: ObjectAlias) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ALIASES = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = {| + z: number +|}; + +type Foo = {| + bar1: Bar, + bar2: Bar, +|}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => void; + foo2: (x: Foo) => void; } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); @@ -459,6 +501,7 @@ module.exports = { NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, NATIVE_MODULE_WITH_FLOAT_AND_INT32, NATIVE_MODULE_WITH_ALIASES, + NATIVE_MODULE_WITH_NESTED_ALIASES, NATIVE_MODULE_WITH_PROMISE, NATIVE_MODULE_WITH_COMPLEX_OBJECTS, NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-test.js.snap index ebfd9e4637b22d..0aeb307d1b2201 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-test.js.snap @@ -25,6 +25,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [], }, }, @@ -39,6 +40,41 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object { + "ObjectAlias": Object { + "properties": Array [ + Object { + "name": "x", + "optional": false, + "typeAnnotation": Object { + "type": "NumberTypeAnnotation", + }, + }, + Object { + "name": "y", + "optional": false, + "typeAnnotation": Object { + "type": "NumberTypeAnnotation", + }, + }, + Object { + "name": "label", + "optional": false, + "typeAnnotation": Object { + "type": "StringTypeAnnotation", + }, + }, + Object { + "name": "truthy", + "optional": false, + "typeAnnotation": Object { + "type": "BooleanTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, "properties": Array [ Object { "name": "getNumber", @@ -104,6 +140,27 @@ Object { "type": "FunctionTypeAnnotation", }, }, + Object { + "name": "getStringFromAlias", + "typeAnnotation": Object { + "optional": false, + "params": Array [ + Object { + "name": "a", + "nullable": false, + "typeAnnotation": Object { + "name": "ObjectAlias", + "type": "TypeAliasTypeAnnotation", + }, + }, + ], + "returnTypeAnnotation": Object { + "nullable": false, + "type": "StringTypeAnnotation", + }, + "type": "FunctionTypeAnnotation", + }, + }, ], }, }, @@ -118,6 +175,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getArray", @@ -159,6 +217,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getArray", @@ -196,6 +255,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getArray", @@ -263,6 +323,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "passBool", @@ -358,6 +419,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getValueWithCallback", @@ -393,6 +455,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getArray", @@ -452,6 +515,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getObject", @@ -667,6 +731,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getConstants", @@ -768,6 +833,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getInt", @@ -817,12 +883,105 @@ Object { } `; +exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NESTED_ALIASES 1`] = ` +Object { + "modules": Object { + "NativeSampleTurboModule": Object { + "nativeModules": Object { + "SampleTurboModule": Object { + "aliases": Object { + "Bar": Object { + "properties": Array [ + Object { + "name": "z", + "optional": false, + "typeAnnotation": Object { + "type": "NumberTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + "Foo": Object { + "properties": Array [ + Object { + "name": "bar1", + "optional": false, + "typeAnnotation": Object { + "name": "Bar", + "type": "TypeAliasTypeAnnotation", + }, + }, + Object { + "name": "bar2", + "optional": false, + "typeAnnotation": Object { + "name": "Bar", + "type": "TypeAliasTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, + "properties": Array [ + Object { + "name": "foo1", + "typeAnnotation": Object { + "optional": false, + "params": Array [ + Object { + "name": "x", + "nullable": false, + "typeAnnotation": Object { + "name": "Foo", + "type": "TypeAliasTypeAnnotation", + }, + }, + ], + "returnTypeAnnotation": Object { + "nullable": false, + "type": "VoidTypeAnnotation", + }, + "type": "FunctionTypeAnnotation", + }, + }, + Object { + "name": "foo2", + "typeAnnotation": Object { + "optional": false, + "params": Array [ + Object { + "name": "x", + "nullable": false, + "typeAnnotation": Object { + "name": "Foo", + "type": "TypeAliasTypeAnnotation", + }, + }, + ], + "returnTypeAnnotation": Object { + "nullable": false, + "type": "VoidTypeAnnotation", + }, + "type": "FunctionTypeAnnotation", + }, + }, + ], + }, + }, + }, + }, +} +`; + exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NULLABLE_PARAM 1`] = ` Object { "modules": Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "voidFunc", @@ -858,6 +1017,20 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object { + "DisplayMetricsAndroid": Object { + "properties": Array [ + Object { + "name": "width", + "optional": false, + "typeAnnotation": Object { + "type": "NumberTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, "properties": Array [ Object { "name": "getConstants", @@ -876,16 +1049,8 @@ Object { "name": "windowPhysicalPixels", "optional": false, "typeAnnotation": Object { - "properties": Array [ - Object { - "name": "width", - "optional": false, - "typeAnnotation": Object { - "type": "NumberTypeAnnotation", - }, - }, - ], - "type": "ObjectTypeAnnotation", + "name": "DisplayMetricsAndroid", + "type": "TypeAliasTypeAnnotation", }, }, ], @@ -915,16 +1080,8 @@ Object { "name": "windowPhysicalPixels", "optional": false, "typeAnnotation": Object { - "properties": Array [ - Object { - "name": "width", - "optional": false, - "typeAnnotation": Object { - "type": "NumberTypeAnnotation", - }, - }, - ], - "type": "ObjectTypeAnnotation", + "name": "DisplayMetricsAndroid", + "type": "TypeAliasTypeAnnotation", }, }, ], @@ -951,6 +1108,20 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object { + "SomeObj": Object { + "properties": Array [ + Object { + "name": "a", + "optional": false, + "typeAnnotation": Object { + "type": "StringTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, "properties": Array [ Object { "name": "getValueWithPromise", @@ -1002,6 +1173,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getRootTag", @@ -1039,6 +1211,7 @@ Object { "NativeSampleTurboModule": Object { "nativeModules": Object { "SampleTurboModule": Object { + "aliases": Object {}, "properties": Array [ Object { "name": "getObject", diff --git a/packages/react-native-codegen/src/parsers/flow/modules/aliases.js b/packages/react-native-codegen/src/parsers/flow/modules/aliases.js new file mode 100644 index 00000000000000..7c55e4d9ff61a6 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/flow/modules/aliases.js @@ -0,0 +1,70 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {ObjectTypeAliasTypeShape} from '../../../CodegenSchema.js'; + +import type {TypeMap} from '../utils.js'; + +const {getObjectProperties} = require('./properties'); + +// $FlowFixMe there's no flowtype for ASTs +type MethodAST = Object; + +function getAliases( + typeDefinition: $ReadOnlyArray, + types: TypeMap, +): $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}> { + const aliases = {}; + typeDefinition.map(moduleAlias => { + const aliasName = Object.keys(moduleAlias)[0]; + const typeAnnotation = moduleAlias[Object.keys(moduleAlias)[0]]; + + switch (typeAnnotation.type) { + case 'ObjectTypeAnnotation': + aliases[aliasName] = { + type: 'ObjectTypeAnnotation', + ...(typeAnnotation.properties && { + properties: getObjectProperties( + aliasName, + {properties: typeAnnotation.properties}, + aliasName, + types, + ), + }), + }; + return; + case 'GenericTypeAnnotation': + if (typeAnnotation.id.name && typeAnnotation.id.name !== '') { + aliases[aliasName] = { + type: 'TypeAliasTypeAnnotation', + name: typeAnnotation.id.name, + }; + return; + } else { + throw new Error( + `Cannot use "${typeAnnotation.type}" type annotation for "${aliasName}": must specify a type alias name`, + ); + } + default: + // TODO (T65847278): Figure out why this does not work. + // (typeAnnotation.type: empty); + throw new Error( + `Unknown prop type, found "${typeAnnotation.type}" in "${aliasName}"`, + ); + } + }); + return aliases; +} + +module.exports = { + getAliases, +}; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index b610dc30e4bb85..c09c66acf47c51 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -11,6 +11,7 @@ 'use strict'; import type {NativeModuleSchemaBuilderConfig} from './schema.js'; +const {getAliases} = require('./aliases'); const {getMethods} = require('./methods'); function getModuleProperties(types, interfaceName) { @@ -33,13 +34,41 @@ function findInterfaceName(types) { )[0].id.name; } +function findAliasNames(types) { + return Object.keys(types) + .map(typeName => types[typeName]) + .filter( + type => + type.type && + type.type === 'TypeAlias' && + type.right && + type.right.type === 'ObjectTypeAnnotation', + ) + .map(type => type.id.name); +} + +function getModuleAliases(types, aliasNames) { + return aliasNames.map(aliasName => { + if (types[aliasName] && types[aliasName].right) { + return {[aliasName]: types[aliasName].right}; + } + throw new Error( + `Interface properties for "${aliasName}" has been specified incorrectly.`, + ); + }); +} + // $FlowFixMe there's no flowtype for AST function processModule(types): NativeModuleSchemaBuilderConfig { const interfaceName = findInterfaceName(types); - const moduleProperties = getModuleProperties(types, interfaceName); const properties = getMethods(moduleProperties, types); - return {properties}; + + const aliasNames = findAliasNames(types); + const moduleAliases = getModuleAliases(types, aliasNames); + const aliases = getAliases(moduleAliases, types); + + return {aliases, properties}; } module.exports = { diff --git a/packages/react-native-codegen/src/parsers/flow/modules/methods.js b/packages/react-native-codegen/src/parsers/flow/modules/methods.js index e6e34b6af2480b..c63fc903692d56 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/methods.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/methods.js @@ -14,144 +14,21 @@ import type { NativeModuleMethodTypeShape, FunctionTypeAnnotationParam, FunctionTypeAnnotationReturn, - FunctionTypeAnnotationParamTypeAnnotation, - ObjectParamTypeAnnotation, } from '../../../CodegenSchema.js'; -import type {TypeMap} from '../utils.js'; +import type {ASTNode, TypeMap} from '../utils.js'; const {getValueFromTypes} = require('../utils.js'); +const { + getElementTypeForArrayOrObject, + getObjectProperties, +} = require('./properties'); // $FlowFixMe there's no flowtype for ASTs type MethodAST = Object; -function getObjectProperties( - name: string, - objectParam, - paramName: string, - types: TypeMap, -): $ReadOnlyArray { - return objectParam.properties.map(objectTypeProperty => { - let optional = objectTypeProperty.optional; - let value = objectTypeProperty.value; - if (value.type === 'NullableTypeAnnotation') { - if ( - objectTypeProperty.value.typeAnnotation.type !== 'StringTypeAnnotation' - ) { - optional = true; - } - value = objectTypeProperty.value.typeAnnotation; - } - return { - optional, - name: objectTypeProperty.key.name, - typeAnnotation: getElementTypeForArrayOrObject( - name, - value, - paramName, - types, - ), - }; - }); -} - -function getElementTypeForArrayOrObject( - name, - arrayParam, - paramName, - types: TypeMap, -): FunctionTypeAnnotationParamTypeAnnotation | typeof undefined { - const typeAnnotation = getValueFromTypes(arrayParam, types); - const type = - typeAnnotation.type === 'GenericTypeAnnotation' - ? typeAnnotation.id.name - : typeAnnotation.type; - - switch (type) { - case 'RootTag': - return { - type: 'ReservedFunctionValueTypeAnnotation', - name: 'RootTag', - }; - case 'Array': - case '$ReadOnlyArray': - if ( - typeAnnotation.typeParameters && - typeAnnotation.typeParameters.params[0] - ) { - return { - type: 'ArrayTypeAnnotation', - elementType: getElementTypeForArrayOrObject( - name, - typeAnnotation.typeParameters.params[0], - 'returning value', - types, - ), - }; - } else { - throw new Error( - `Unsupported type for ${name}, param: "${paramName}": expected to find annotation for type of nested array contents`, - ); - } - case 'ObjectTypeAnnotation': - return { - type: 'ObjectTypeAnnotation', - properties: getObjectProperties(name, typeAnnotation, paramName, types), - }; - case '$ReadOnly': - if ( - typeAnnotation.typeParameters.params && - typeAnnotation.typeParameters.params[0] - ) { - return { - type: 'ObjectTypeAnnotation', - properties: getObjectProperties( - name, - typeAnnotation.typeParameters.params[0], - paramName, - types, - ), - }; - } else { - throw new Error( - `Unsupported param for method "${name}", param "${paramName}". No type specified for $ReadOnly`, - ); - } - case 'AnyTypeAnnotation': - return { - type, - }; - case 'NumberTypeAnnotation': - case 'BooleanTypeAnnotation': - return { - type, - }; - case 'StringTypeAnnotation': - case 'Stringish': - return { - type: 'StringTypeAnnotation', - }; - case 'Int32': - return { - type: 'Int32TypeAnnotation', - }; - case 'Float': - return { - type: 'FloatTypeAnnotation', - }; - case 'TupleTypeAnnotation': - case 'UnionTypeAnnotation': - return undefined; - default: - // TODO T67565166: Generic objects are not type safe and should be disallowed in the schema. - return { - type: 'GenericObjectTypeAnnotation', - }; - } -} - function getTypeAnnotationForParam( name: string, - paramAnnotation, + paramAnnotation: ASTNode, types: TypeMap, ): FunctionTypeAnnotationParam { let param = paramAnnotation; @@ -208,6 +85,16 @@ function getTypeAnnotationForParam( ); } case 'ObjectTypeAnnotation': + if (param.typeAnnotation.type === 'GenericTypeAnnotation') { + return { + nullable, + name: paramName, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: param.typeAnnotation.id.name, + }, + }; + } return { nullable, name: paramName, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/properties.js b/packages/react-native-codegen/src/parsers/flow/modules/properties.js new file mode 100644 index 00000000000000..f1e8c51e7fe443 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/flow/modules/properties.js @@ -0,0 +1,159 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type { + FunctionTypeAnnotationParamTypeAnnotation, + ObjectParamTypeAnnotation, + TypeAliasTypeAnnotation, +} from '../../../CodegenSchema.js'; + +import type {ASTNode, TypeMap} from '../utils.js'; +const {getValueFromTypes} = require('../utils.js'); + +function getObjectProperties( + name: string, + objectParam: ASTNode, + paramName: string, + types: TypeMap, +): $ReadOnlyArray { + return objectParam.properties.map(objectTypeProperty => { + let optional = objectTypeProperty.optional; + let value = objectTypeProperty.value; + if (value.type === 'NullableTypeAnnotation') { + if ( + objectTypeProperty.value.typeAnnotation.type !== 'StringTypeAnnotation' + ) { + optional = true; + } + value = objectTypeProperty.value.typeAnnotation; + } + return { + optional, + name: objectTypeProperty.key.name, + typeAnnotation: getElementTypeForArrayOrObject( + name, + value, + paramName, + types, + ), + }; + }); +} + +function getElementTypeForArrayOrObject( + name: string, + arrayParam: ASTNode, + paramName: string, + types: TypeMap, +): + | FunctionTypeAnnotationParamTypeAnnotation + | TypeAliasTypeAnnotation + | typeof undefined { + const typeAnnotation = getValueFromTypes(arrayParam, types); + const type = + typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + + switch (type) { + case 'RootTag': + return { + type: 'ReservedFunctionValueTypeAnnotation', + name: 'RootTag', + }; + case 'Array': + case '$ReadOnlyArray': + if ( + typeAnnotation.typeParameters && + typeAnnotation.typeParameters.params[0] + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getElementTypeForArrayOrObject( + name, + typeAnnotation.typeParameters.params[0], + 'returning value', + types, + ), + }; + } else { + throw new Error( + `Unsupported type for "${name}", param: "${paramName}": expected to find annotation for type of nested array contents`, + ); + } + case 'ObjectTypeAnnotation': + if (arrayParam.id) { + return { + type: 'TypeAliasTypeAnnotation', + name: arrayParam.id.name, + }; + } + return { + type: 'ObjectTypeAnnotation', + properties: getObjectProperties(name, typeAnnotation, paramName, types), + }; + case '$ReadOnly': + if ( + typeAnnotation.typeParameters.params && + typeAnnotation.typeParameters.params[0] + ) { + return { + type: 'ObjectTypeAnnotation', + properties: getObjectProperties( + name, + typeAnnotation.typeParameters.params[0], + paramName, + types, + ), + }; + } else { + throw new Error( + `Unsupported param for method "${name}", param "${paramName}". No type specified for $ReadOnly`, + ); + } + case 'AnyTypeAnnotation': + return { + type, + }; + case 'NumberTypeAnnotation': + case 'BooleanTypeAnnotation': + return { + type, + }; + case 'StringTypeAnnotation': + case 'Stringish': + return { + type: 'StringTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'TupleTypeAnnotation': + case 'UnionTypeAnnotation': + return undefined; + default: + // TODO T67565166: Generic objects are not type safe and should be disallowed in the schema. + return { + type: 'GenericObjectTypeAnnotation', + }; + } +} + +module.exports = { + getElementTypeForArrayOrObject, + getObjectProperties, +}; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/schema.js b/packages/react-native-codegen/src/parsers/flow/modules/schema.js index b2da0d43dd99c9..68b1d61c98b97d 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/schema.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/schema.js @@ -12,15 +12,17 @@ import type { SchemaType, + ObjectTypeAliasTypeShape, NativeModuleMethodTypeShape, } from '../../../CodegenSchema.js'; export type NativeModuleSchemaBuilderConfig = $ReadOnly<{| + aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>, properties: $ReadOnlyArray, |}>; function buildModuleSchema( - {properties}: NativeModuleSchemaBuilderConfig, + {aliases, properties}: NativeModuleSchemaBuilderConfig, moduleName: string, ): SchemaType { return { @@ -28,6 +30,7 @@ function buildModuleSchema( [`Native${moduleName}`]: { nativeModules: { [moduleName]: { + aliases, properties, }, }, diff --git a/scripts/generate-native-modules-specs.sh b/scripts/generate-native-modules-specs.sh index 6f77c36c503603..fc567f9f388158 100755 --- a/scripts/generate-native-modules-specs.sh +++ b/scripts/generate-native-modules-specs.sh @@ -20,28 +20,37 @@ set -e -describe () { - printf "\\n\\n>>>>> %s\\n\\n\\n" "$1" -} - THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd) RN_DIR=$(cd "$THIS_DIR/.." && pwd) - -SRCS_DIR=$(cd "$RN_DIR/Libraries" && pwd) +CODEGEN_DIR=$(cd "$RN_DIR/packages/react-native-codegen" && pwd) +OUTPUT_DIR="${1:-$RN_DIR/Libraries/FBReactNativeSpec/FBReactNativeSpec}" SCHEMA_FILE="$RN_DIR/schema-native-modules.json" +YARN_BINARY="${YARN_BINARY:-yarn}" + +describe () { + printf "\\n\\n>>>>> %s\\n\\n\\n" "$1" +} -if [ -n "$1" ]; then - OUTPUT_DIR="$1" -fi +step_build_codegen () { + describe "Building react-native-codegen package" + pushd "$CODEGEN_DIR" >/dev/null || exit + "$YARN_BINARY" + "$YARN_BINARY" build + popd >/dev/null || exit +} -pushd "$RN_DIR/packages/react-native-codegen" >/dev/null - yarn - yarn build -popd >/dev/null +step_gen_schema () { + describe "Generating schema from flow types" + SRCS_DIR=$(cd "$RN_DIR/Libraries" && pwd) + grep --exclude NativeSampleTurboModule.js --exclude NativeUIManager.js --include=Native\*.js -rnwl "$SRCS_DIR" -e 'export interface Spec extends TurboModule' -e "export default \(TurboModuleRegistry.get(Enforcing)?\('.*\): Spec\);/" \ + | xargs "$YARN_BINARY" node "$CODEGEN_DIR/lib/cli/combine/combine-js-to-schema-cli.js" "$SCHEMA_FILE" +} -describe "Generating schema from flow types" -grep --exclude NativeUIManager.js --include=Native\*.js -rnwl "$SRCS_DIR" -e 'export interface Spec extends TurboModule' -e "export default \(TurboModuleRegistry.get(Enforcing)?\('.*\): Spec\);/" \ - | xargs yarn node packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js "$SCHEMA_FILE" +step_gen_specs () { + describe "Generating native code from schema" + "$YARN_BINARY" --silent node scripts/generate-native-modules-specs-cli.js "$SCHEMA_FILE" "$OUTPUT_DIR" +} -describe "Generating native code from schema" -yarn node scripts/generate-native-modules-specs-cli.js "$SCHEMA_FILE" "$OUTPUT_DIR" +step_build_codegen +step_gen_schema +step_gen_specs diff --git a/scripts/react-native-xcode.sh b/scripts/react-native-xcode.sh index a87c68fec607fe..7c7ad2aa61902e 100755 --- a/scripts/react-native-xcode.sh +++ b/scripts/react-native-xcode.sh @@ -79,6 +79,11 @@ else ENTRY_FILE=${1:-index.js} fi +if [[ $DEV != true && ! -f "$ENTRY_FILE" ]]; then + echo "error: Entry file $ENTRY_FILE does not exist. If you use another file as your entry point, pass ENTRY_FILE=myindex.js" >&2 + exit 2 +fi + if [[ -s "$HOME/.nvm/nvm.sh" ]]; then . "$HOME/.nvm/nvm.sh" elif [[ -x "$(command -v brew)" && -s "$(brew --prefix nvm)/nvm.sh" ]]; then diff --git a/template/_flowconfig b/template/_flowconfig index b3d4c2fbf0ccec..b94086e0b04702 100644 --- a/template/_flowconfig +++ b/template/_flowconfig @@ -69,4 +69,4 @@ untyped-import untyped-type-import [version] -^0.126.1 +^0.127.0 diff --git a/tools/build_defs/oss/rn_defs.bzl b/tools/build_defs/oss/rn_defs.bzl index 887e25d3651577..5553eb884af16d 100644 --- a/tools/build_defs/oss/rn_defs.bzl +++ b/tools/build_defs/oss/rn_defs.bzl @@ -188,7 +188,7 @@ def rn_robolectric_test(name, srcs, vm_args = None, *args, **kwargs): kwargs["deps"] = kwargs.pop("deps", []) + [ react_native_android_toplevel_dep("third-party/java/mockito2:mockito2"), - react_native_dep("libraries/fbcore/src/test/java/com/facebook/powermock:powermock2"), + react_native_xplat_dep("libraries/fbcore/src/test/java/com/facebook/powermock:powermock2"), react_native_dep("third-party/java/robolectric/4.3.1:robolectric"), ] diff --git a/yarn.lock b/yarn.lock index 7dbddb40618474..c8383b9a0dc562 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3319,10 +3319,10 @@ flat-cache@^1.2.1: rimraf "~2.6.2" write "^0.2.1" -flow-bin@^0.126.1: - version "0.126.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.126.1.tgz#2726595e1891dc35b379b5994627432df4ead52c" - integrity sha512-RI05x7rVzruRVJQN3M4vLEjZMwUHJKhGz9FmL8HN7WiSo66/131EyJS6Vo8PkKyM2pgT9GRWfGP/tXlqS54XUg== +flow-bin@^0.127.0: + version "0.127.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.127.0.tgz#0614cff4c1b783beef1feeb7108d536e09d77632" + integrity sha512-ywvCCdV4NJWzrqjFrMU5tAiVGyBiXjsJQ1+/kj8thXyX15V17x8BFvNwoAH97NrUU8T1HzmFBjLzWc0l2319qg== flow-parser@0.*: version "0.89.0"