diff --git a/.flowconfig b/.flowconfig index 4f9e8498e5c341..054b0d01d80f30 100644 --- a/.flowconfig +++ b/.flowconfig @@ -29,3 +29,6 @@ Examples/UIExplorer/ImageMocks.js [options] module.system=haste + +[version] +0.10.0 diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index d9c2acf9a654dc..1790dc49164d46 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -57,6 +57,17 @@ var styles = StyleSheet.create({ borderLeftWidth: 40, borderLeftColor: 'blue', }, + border5: { + borderRadius: 50, + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, }); exports.title = 'Border'; @@ -71,7 +82,7 @@ exports.examples = [ }, { title: 'Equal-Width / Same-Color', - description: 'borderWidth & borderColor', + description: 'borderWidth & borderColor & borderRadius', render() { return ; } @@ -97,4 +108,11 @@ exports.examples = [ return ; } }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + render() { + return ; + } + }, ]; diff --git a/Examples/UIExplorer/GeolocationExample.js b/Examples/UIExplorer/GeolocationExample.js index c55bd351b09ce4..9bd744678df243 100644 --- a/Examples/UIExplorer/GeolocationExample.js +++ b/Examples/UIExplorer/GeolocationExample.js @@ -50,7 +50,8 @@ var GeolocationExample = React.createClass({ componentDidMount: function() { navigator.geolocation.getCurrentPosition( (initialPosition) => this.setState({initialPosition}), - (error) => console.error(error) + (error) => console.error(error), + {enableHighAccuracy: true, timeout: 100, maximumAge: 1000} ); this.watchID = navigator.geolocation.watchPosition((lastPosition) => { this.setState({lastPosition}); diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js new file mode 100644 index 00000000000000..119196d8f8a97d --- /dev/null +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -0,0 +1,169 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * 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 NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK 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. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + SegmentedControlIOS, + Text, + View, + StyleSheet +} = React; + +var BasicSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + } +}); + +var PreSelectedSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var MomentarySegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var DisabledSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + }, +}); + +var ColorSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + }, +}); + +var EventSegmentedControlExample = React.createClass({ + getInitialState() { + return { + values: ['One', 'Two', 'Three'], + value: 'Not selected', + selectedIndex: undefined + }; + }, + + render() { + return ( + + + Value: {this.state.value} + + + Index: {this.state.selectedIndex} + + + + ); + }, + + _onChange(event) { + this.setState({ + selectedIndex: event.nativeEvent.selectedIndex, + }); + }, + + _onValueChange(value) { + this.setState({ + value: value, + }); + } +}); + +var styles = StyleSheet.create({ + text: { + fontSize: 14, + textAlign: 'center', + fontWeight: '500', + margin: 10, + }, +}); + +exports.title = ''; +exports.displayName = 'SegmentedControlExample'; +exports.description = 'Native segmented control'; +exports.examples = [ + { + title: 'Segmented controls can have values', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can have a pre-selected value', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be momentary', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be disabled', + render(): ReactElement { return ; } + }, + { + title: 'Custom colors can be provided', + render(): ReactElement { return ; } + }, + { + title: 'Change events can be detected', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index e0ae1b46517070..922dd9607d4b88 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -88,9 +88,9 @@ var styles = StyleSheet.create({ height: 26, borderWidth: 0.5, borderColor: '#0f0f0f', - padding: 4, flex: 1, fontSize: 13, + padding: 4, }, multiline: { borderWidth: 0.5, @@ -98,6 +98,22 @@ var styles = StyleSheet.create({ flex: 1, fontSize: 13, height: 50, + padding: 4, + marginBottom: 4, + }, + multilineWithFontStyles: { + color: 'blue', + fontWeight: 'bold', + fontSize: 18, + fontFamily: 'Cochin', + height: 60, + }, + multilineChild: { + width: 50, + height: 40, + position: 'absolute', + right: 5, + backgroundColor: 'red', }, eventLabel: { margin: 3, @@ -118,7 +134,7 @@ var styles = StyleSheet.create({ }); exports.title = ''; -exports.description = 'Single-line text inputs.'; +exports.description = 'Single and multi-line text inputs.'; exports.examples = [ { title: 'Auto-focus', @@ -313,7 +329,7 @@ exports.examples = [ }, { title: 'Clear and select', - render: function () { + render: function() { return ( @@ -336,4 +352,42 @@ exports.examples = [ ); } }, + { + title: 'Multiline', + render: function() { + return ( + + + + + + + + + ) + } + } ]; diff --git a/Examples/UIExplorer/UIExplorerBlock.js b/Examples/UIExplorer/UIExplorerBlock.js index 924415e013e211..e7c2a2a8ea9d16 100644 --- a/Examples/UIExplorer/UIExplorerBlock.js +++ b/Examples/UIExplorer/UIExplorerBlock.js @@ -70,6 +70,7 @@ var styles = StyleSheet.create({ }, titleContainer: { borderWidth: 0.5, + borderRadius: 2.5, borderColor: '#d6d7da', backgroundColor: '#f6f7f8', paddingHorizontal: 10, @@ -78,8 +79,10 @@ var styles = StyleSheet.create({ titleRow: { flexDirection: 'row', justifyContent: 'space-between', + backgroundColor: 'transparent', }, titleText: { + backgroundColor: 'transparent', fontSize: 14, fontWeight: '500', }, @@ -97,6 +100,7 @@ var styles = StyleSheet.create({ height: 8, }, children: { + backgroundColor: 'transparent', padding: 10, } }); diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 73010617732abd..a10d291bdb54d2 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -43,6 +43,7 @@ var COMPONENTS = [ require('./NavigatorIOSExample'), require('./PickerIOSExample'), require('./ScrollViewExample'), + require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), require('./SwitchIOSExample'), require('./TabBarIOSExample'), diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png index f90bf2c6a3afdd..a1789c7914b5cd 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSliderExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSliderExampleSnapshot_1@2x.png index ee049f1941df84..bf6fa1f9f76b7a 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSliderExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSliderExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png index a40558413378c6..3ff3cef9e45888 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png index 5b71135b978b71..d697ae5b5e3e17 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png index 4d16edfb23cf56..74e03581c464bc 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 8813a8afdb1a01..d1e990cb4b4995 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -94,6 +94,7 @@ var WebViewExample = React.createClass({ automaticallyAdjustContentInsets={false} style={styles.webView} url={this.state.url} + javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} startInLoadingState={true} /> diff --git a/Libraries/ART/ART.xcodeproj/project.pbxproj b/Libraries/ART/ART.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..c3255c9656a9da --- /dev/null +++ b/Libraries/ART/ART.xcodeproj/project.pbxproj @@ -0,0 +1,371 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0CF68B051AF0549300FF9E5C /* ARTGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */; }; + 0CF68B061AF0549300FF9E5C /* ARTNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE01AF0549300FF9E5C /* ARTNode.m */; }; + 0CF68B071AF0549300FF9E5C /* ARTRenderable.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */; }; + 0CF68B081AF0549300FF9E5C /* ARTShape.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE41AF0549300FF9E5C /* ARTShape.m */; }; + 0CF68B091AF0549300FF9E5C /* ARTSurfaceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */; }; + 0CF68B0A1AF0549300FF9E5C /* ARTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE81AF0549300FF9E5C /* ARTText.m */; }; + 0CF68B0B1AF0549300FF9E5C /* ARTBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */; }; + 0CF68B0C1AF0549300FF9E5C /* ARTLinearGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */; }; + 0CF68B0D1AF0549300FF9E5C /* ARTPattern.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */; }; + 0CF68B0E1AF0549300FF9E5C /* ARTRadialGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */; }; + 0CF68B0F1AF0549300FF9E5C /* ARTSolidColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */; }; + 0CF68B101AF0549300FF9E5C /* RCTConvert+ART.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */; }; + 0CF68B111AF0549300FF9E5C /* ARTGroupManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */; }; + 0CF68B121AF0549300FF9E5C /* ARTNodeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */; }; + 0CF68B131AF0549300FF9E5C /* ARTRenderableManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */; }; + 0CF68B141AF0549300FF9E5C /* ARTShapeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */; }; + 0CF68B151AF0549300FF9E5C /* ARTSurfaceViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */; }; + 0CF68B161AF0549300FF9E5C /* ARTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0CF68ABF1AF0540F00FF9E5C /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0CF68AC11AF0540F00FF9E5C /* libART.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libART.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTCGFloatArray.h; sourceTree = ""; }; + 0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTContainer.h; sourceTree = ""; }; + 0CF68ADD1AF0549300FF9E5C /* ARTGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTGroup.h; sourceTree = ""; }; + 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTGroup.m; sourceTree = ""; }; + 0CF68ADF1AF0549300FF9E5C /* ARTNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTNode.h; sourceTree = ""; }; + 0CF68AE01AF0549300FF9E5C /* ARTNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTNode.m; sourceTree = ""; }; + 0CF68AE11AF0549300FF9E5C /* ARTRenderable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRenderable.h; sourceTree = ""; }; + 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRenderable.m; sourceTree = ""; }; + 0CF68AE31AF0549300FF9E5C /* ARTShape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTShape.h; sourceTree = ""; }; + 0CF68AE41AF0549300FF9E5C /* ARTShape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTShape.m; sourceTree = ""; }; + 0CF68AE51AF0549300FF9E5C /* ARTSurfaceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSurfaceView.h; sourceTree = ""; }; + 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSurfaceView.m; sourceTree = ""; }; + 0CF68AE71AF0549300FF9E5C /* ARTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTText.h; sourceTree = ""; }; + 0CF68AE81AF0549300FF9E5C /* ARTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTText.m; sourceTree = ""; }; + 0CF68AE91AF0549300FF9E5C /* ARTTextFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTTextFrame.h; sourceTree = ""; }; + 0CF68AEB1AF0549300FF9E5C /* ARTBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTBrush.h; sourceTree = ""; }; + 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTBrush.m; sourceTree = ""; }; + 0CF68AED1AF0549300FF9E5C /* ARTLinearGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTLinearGradient.h; sourceTree = ""; }; + 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTLinearGradient.m; sourceTree = ""; }; + 0CF68AEF1AF0549300FF9E5C /* ARTPattern.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPattern.h; sourceTree = ""; }; + 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTPattern.m; sourceTree = ""; }; + 0CF68AF11AF0549300FF9E5C /* ARTRadialGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRadialGradient.h; sourceTree = ""; }; + 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRadialGradient.m; sourceTree = ""; }; + 0CF68AF31AF0549300FF9E5C /* ARTSolidColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSolidColor.h; sourceTree = ""; }; + 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSolidColor.m; sourceTree = ""; }; + 0CF68AF61AF0549300FF9E5C /* RCTConvert+ART.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+ART.h"; sourceTree = ""; }; + 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+ART.m"; sourceTree = ""; }; + 0CF68AF91AF0549300FF9E5C /* ARTGroupManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTGroupManager.h; sourceTree = ""; }; + 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTGroupManager.m; sourceTree = ""; }; + 0CF68AFB1AF0549300FF9E5C /* ARTNodeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTNodeManager.h; sourceTree = ""; }; + 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTNodeManager.m; sourceTree = ""; }; + 0CF68AFD1AF0549300FF9E5C /* ARTRenderableManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRenderableManager.h; sourceTree = ""; }; + 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRenderableManager.m; sourceTree = ""; }; + 0CF68AFF1AF0549300FF9E5C /* ARTShapeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTShapeManager.h; sourceTree = ""; }; + 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTShapeManager.m; sourceTree = ""; }; + 0CF68B011AF0549300FF9E5C /* ARTSurfaceViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSurfaceViewManager.h; sourceTree = ""; }; + 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSurfaceViewManager.m; sourceTree = ""; }; + 0CF68B031AF0549300FF9E5C /* ARTTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTTextManager.h; sourceTree = ""; }; + 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTTextManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0CF68ABE1AF0540F00FF9E5C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0CF68AB81AF0540F00FF9E5C = { + isa = PBXGroup; + children = ( + 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */, + 0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */, + 0CF68ADD1AF0549300FF9E5C /* ARTGroup.h */, + 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */, + 0CF68ADF1AF0549300FF9E5C /* ARTNode.h */, + 0CF68AE01AF0549300FF9E5C /* ARTNode.m */, + 0CF68AE11AF0549300FF9E5C /* ARTRenderable.h */, + 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */, + 0CF68AE31AF0549300FF9E5C /* ARTShape.h */, + 0CF68AE41AF0549300FF9E5C /* ARTShape.m */, + 0CF68AE51AF0549300FF9E5C /* ARTSurfaceView.h */, + 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */, + 0CF68AE71AF0549300FF9E5C /* ARTText.h */, + 0CF68AE81AF0549300FF9E5C /* ARTText.m */, + 0CF68AE91AF0549300FF9E5C /* ARTTextFrame.h */, + 0CF68AEA1AF0549300FF9E5C /* Brushes */, + 0CF68AF61AF0549300FF9E5C /* RCTConvert+ART.h */, + 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */, + 0CF68AF81AF0549300FF9E5C /* ViewManagers */, + 0CF68AC21AF0540F00FF9E5C /* Products */, + ); + sourceTree = ""; + }; + 0CF68AC21AF0540F00FF9E5C /* Products */ = { + isa = PBXGroup; + children = ( + 0CF68AC11AF0540F00FF9E5C /* libART.a */, + ); + name = Products; + sourceTree = ""; + }; + 0CF68AEA1AF0549300FF9E5C /* Brushes */ = { + isa = PBXGroup; + children = ( + 0CF68AEB1AF0549300FF9E5C /* ARTBrush.h */, + 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */, + 0CF68AED1AF0549300FF9E5C /* ARTLinearGradient.h */, + 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */, + 0CF68AEF1AF0549300FF9E5C /* ARTPattern.h */, + 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */, + 0CF68AF11AF0549300FF9E5C /* ARTRadialGradient.h */, + 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */, + 0CF68AF31AF0549300FF9E5C /* ARTSolidColor.h */, + 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */, + ); + path = Brushes; + sourceTree = ""; + }; + 0CF68AF81AF0549300FF9E5C /* ViewManagers */ = { + isa = PBXGroup; + children = ( + 0CF68AF91AF0549300FF9E5C /* ARTGroupManager.h */, + 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */, + 0CF68AFB1AF0549300FF9E5C /* ARTNodeManager.h */, + 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */, + 0CF68AFD1AF0549300FF9E5C /* ARTRenderableManager.h */, + 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */, + 0CF68AFF1AF0549300FF9E5C /* ARTShapeManager.h */, + 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */, + 0CF68B011AF0549300FF9E5C /* ARTSurfaceViewManager.h */, + 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */, + 0CF68B031AF0549300FF9E5C /* ARTTextManager.h */, + 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */, + ); + path = ViewManagers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0CF68AC01AF0540F00FF9E5C /* ART */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0CF68AD51AF0540F00FF9E5C /* Build configuration list for PBXNativeTarget "ART" */; + buildPhases = ( + 0CF68ABD1AF0540F00FF9E5C /* Sources */, + 0CF68ABE1AF0540F00FF9E5C /* Frameworks */, + 0CF68ABF1AF0540F00FF9E5C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ART; + productName = ART; + productReference = 0CF68AC11AF0540F00FF9E5C /* libART.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0CF68AB91AF0540F00FF9E5C /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0620; + TargetAttributes = { + 0CF68AC01AF0540F00FF9E5C = { + CreatedOnToolsVersion = 6.2; + }; + }; + }; + buildConfigurationList = 0CF68ABC1AF0540F00FF9E5C /* Build configuration list for PBXProject "ART" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 0CF68AB81AF0540F00FF9E5C; + productRefGroup = 0CF68AC21AF0540F00FF9E5C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0CF68AC01AF0540F00FF9E5C /* ART */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 0CF68ABD1AF0540F00FF9E5C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CF68B161AF0549300FF9E5C /* ARTTextManager.m in Sources */, + 0CF68B111AF0549300FF9E5C /* ARTGroupManager.m in Sources */, + 0CF68B0D1AF0549300FF9E5C /* ARTPattern.m in Sources */, + 0CF68B0A1AF0549300FF9E5C /* ARTText.m in Sources */, + 0CF68B121AF0549300FF9E5C /* ARTNodeManager.m in Sources */, + 0CF68B051AF0549300FF9E5C /* ARTGroup.m in Sources */, + 0CF68B131AF0549300FF9E5C /* ARTRenderableManager.m in Sources */, + 0CF68B091AF0549300FF9E5C /* ARTSurfaceView.m in Sources */, + 0CF68B0E1AF0549300FF9E5C /* ARTRadialGradient.m in Sources */, + 0CF68B151AF0549300FF9E5C /* ARTSurfaceViewManager.m in Sources */, + 0CF68B081AF0549300FF9E5C /* ARTShape.m in Sources */, + 0CF68B071AF0549300FF9E5C /* ARTRenderable.m in Sources */, + 0CF68B101AF0549300FF9E5C /* RCTConvert+ART.m in Sources */, + 0CF68B061AF0549300FF9E5C /* ARTNode.m in Sources */, + 0CF68B0F1AF0549300FF9E5C /* ARTSolidColor.m in Sources */, + 0CF68B0C1AF0549300FF9E5C /* ARTLinearGradient.m in Sources */, + 0CF68B0B1AF0549300FF9E5C /* ARTBrush.m in Sources */, + 0CF68B141AF0549300FF9E5C /* ARTShapeManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 0CF68AD31AF0540F00FF9E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 0CF68AD41AF0540F00FF9E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0CF68AD61AF0540F00FF9E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 0CF68AD71AF0540F00FF9E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0CF68ABC1AF0540F00FF9E5C /* Build configuration list for PBXProject "ART" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CF68AD31AF0540F00FF9E5C /* Debug */, + 0CF68AD41AF0540F00FF9E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0CF68AD51AF0540F00FF9E5C /* Build configuration list for PBXNativeTarget "ART" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CF68AD61AF0540F00FF9E5C /* Debug */, + 0CF68AD71AF0540F00FF9E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0CF68AB91AF0540F00FF9E5C /* Project object */; +} diff --git a/Libraries/ART/ARTCGFloatArray.h b/Libraries/ART/ARTCGFloatArray.h new file mode 100644 index 00000000000000..9d748549973d30 --- /dev/null +++ b/Libraries/ART/ARTCGFloatArray.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// A little helper to make sure we have the right memory allocation ready for use. +// We assume that we will only this in one place so no reference counting is necessary. +// Needs to be freed when dealloced. + +// This is fragile since this relies on these values not getting reused. Consider +// wrapping these in an Obj-C class or some ARC hackery to get refcounting. + +typedef struct { + size_t count; + CGFloat *array; +} ARTCGFloatArray; diff --git a/Libraries/ART/ARTContainer.h b/Libraries/ART/ARTContainer.h new file mode 100644 index 00000000000000..d83f7ae1a559f8 --- /dev/null +++ b/Libraries/ART/ARTContainer.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol ARTContainer + +// This is used as a hook for child to mark it's parent as dirty. +// This bubbles up to the root which gets marked as dirty. +- (void)invalidate; + +@end diff --git a/Libraries/ART/ARTGroup.h b/Libraries/ART/ARTGroup.h new file mode 100644 index 00000000000000..15a8b643bb86fb --- /dev/null +++ b/Libraries/ART/ARTGroup.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTContainer.h" +#import "ARTNode.h" + +@interface ARTGroup : ARTNode + +@end diff --git a/Libraries/ART/ARTGroup.m b/Libraries/ART/ARTGroup.m new file mode 100644 index 00000000000000..9ecbf8ee821b84 --- /dev/null +++ b/Libraries/ART/ARTGroup.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTGroup.h" + +@implementation ARTGroup + +- (void)renderLayerTo:(CGContextRef)context +{ +// TO-DO: Clipping rectangle + + for (ARTNode *node in self.subviews) { + [node renderTo:context]; + } +} + +@end diff --git a/Libraries/ART/ARTNode.h b/Libraries/ART/ARTNode.h new file mode 100644 index 00000000000000..511c09a5a6aae2 --- /dev/null +++ b/Libraries/ART/ARTNode.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +/** + * ART nodes are implemented as empty UIViews but this is just an implementation detail to fit + * into the existing view management. They should also be shadow views and painted on a background + * thread. + */ + +@interface ARTNode : UIView + +@property (nonatomic, assign) CGFloat opacity; + +- (void)invalidate; +- (void)renderTo:(CGContextRef)context; + +/** + * renderTo will take opacity into account and draw renderLayerTo off-screen if there is opacity + * specified, then composite that onto the context. renderLayerTo always draws at opacity=1. + * @abstract + */ +- (void)renderLayerTo:(CGContextRef)context; + +@end diff --git a/Libraries/ART/ARTNode.m b/Libraries/ART/ARTNode.m new file mode 100644 index 00000000000000..d23d5880a95677 --- /dev/null +++ b/Libraries/ART/ARTNode.m @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTNode.h" + +#import "ARTContainer.h" + +@implementation ARTNode + +- (void)insertSubview:(UIView *)subview atIndex:(NSInteger)index +{ + [self invalidate]; + [super insertSubview:subview atIndex:index]; +} + +- (void)removeFromSuperview +{ + [self invalidate]; + [super removeFromSuperview]; +} + +- (void)setOpacity:(CGFloat)opacity +{ + [self invalidate]; + _opacity = opacity; +} + +- (void)setTransform:(CGAffineTransform)transform +{ + [self invalidate]; + super.transform = transform; +} + +- (void)invalidate +{ + id container = (id)self.superview; + [container invalidate]; +} + +- (void)renderTo:(CGContextRef)context +{ + if (self.opacity <= 0) { + // Nothing to paint + return; + } + if (self.opacity >= 1) { + // Just paint at full opacity + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, 1); + [self renderLayerTo:context]; + CGContextRestoreGState(context); + return; + } + // This needs to be painted on a layer before being composited. + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, self.opacity); + CGContextBeginTransparencyLayer(context, NULL); + [self renderLayerTo:context]; + CGContextEndTransparencyLayer(context); + CGContextRestoreGState(context); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/ARTRenderable.h b/Libraries/ART/ARTRenderable.h new file mode 100644 index 00000000000000..8eae9c25ae4a70 --- /dev/null +++ b/Libraries/ART/ARTRenderable.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTBrush.h" +#import "ARTCGFloatArray.h" +#import "ARTNode.h" + +@interface ARTRenderable : ARTNode + +@property (nonatomic, strong) ARTBrush *fill; +@property (nonatomic, assign) CGColorRef stroke; +@property (nonatomic, assign) CGFloat strokeWidth; +@property (nonatomic, assign) CGLineCap strokeCap; +@property (nonatomic, assign) CGLineJoin strokeJoin; +@property (nonatomic, assign) ARTCGFloatArray strokeDash; + +@end diff --git a/Libraries/ART/ARTRenderable.m b/Libraries/ART/ARTRenderable.m new file mode 100644 index 00000000000000..7ba9a9a6073915 --- /dev/null +++ b/Libraries/ART/ARTRenderable.m @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTRenderable.h" + +@implementation ARTRenderable + +- (void)setFill:(ARTBrush *)fill +{ + [self invalidate]; + _fill = fill; +} + +- (void)setStroke:(CGColorRef)stroke +{ + if (stroke == _stroke) { + return; + } + [self invalidate]; + CGColorRelease(_stroke); + _stroke = CGColorRetain(stroke); +} + +- (void)setStrokeWidth:(CGFloat)strokeWidth +{ + [self invalidate]; + _strokeWidth = strokeWidth; +} + +- (void)setStrokeCap:(CGLineCap)strokeCap +{ + [self invalidate]; + _strokeCap = strokeCap; +} + +- (void)setStrokeJoin:(CGLineJoin)strokeJoin +{ + [self invalidate]; + _strokeJoin = strokeJoin; +} + +- (void)setStrokeDash:(ARTCGFloatArray)strokeDash +{ + if (strokeDash.array == _strokeDash.array) { + return; + } + if (_strokeDash.array) { + free(_strokeDash.array); + } + [self invalidate]; + _strokeDash = strokeDash; +} + +- (void)dealloc +{ + CGColorRelease(_stroke); + if (_strokeDash.array) { + free(_strokeDash.array); + } +} + +- (void)renderTo:(CGContextRef)context +{ + if (self.opacity <= 0 || self.opacity >= 1 || (self.fill && self.stroke)) { + // If we have both fill and stroke, we will need to paint this using normal compositing + [super renderTo: context]; + return; + } + // This is a terminal with only one painting. Therefore we don't need to paint this + // off-screen. We can just composite it straight onto the buffer. + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, self.opacity); + [self renderLayerTo:context]; + CGContextRestoreGState(context); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/ARTSerializablePath.js b/Libraries/ART/ARTSerializablePath.js new file mode 100644 index 00000000000000..2df8ff6bb83bcd --- /dev/null +++ b/Libraries/ART/ARTSerializablePath.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ARTSerializablePath + */ + +"use strict"; + +// TODO: Move this into an ART mode called "serialized" or something + +var Class = require('art/core/class.js'); +var Path = require('art/core/path.js'); + +var MOVE_TO = 0; +var CLOSE = 1; +var LINE_TO = 2; +var CURVE_TO = 3; +var ARC = 4; + +var SerializablePath = Class(Path, { + + initialize: function(path) { + this.reset(); + if (path instanceof SerializablePath) { + this.path = path.path.slice(0); + } else if (path) { + if (path.applyToPath) { + path.applyToPath(this); + } else { + this.push(path); + } + } + }, + + onReset: function() { + this.path = []; + }, + + onMove: function(sx, sy, x, y) { + this.path.push(MOVE_TO, x, y); + }, + + onLine: function(sx, sy, x, y) { + this.path.push(LINE_TO, x, y); + }, + + onBezierCurve: function(sx, sy, p1x, p1y, p2x, p2y, x, y) { + this.path.push(CURVE_TO, p1x, p1y, p2x, p2y, x, y); + }, + + _arcToBezier: Path.prototype.onArc, + + onArc: function(sx, sy, ex, ey, cx, cy, rx, ry, sa, ea, ccw, rotation) { + if (rx !== ry || rotation) { + return this._arcToBezier( + sx, sy, ex, ey, cx, cy, rx, ry, sa, ea, ccw, rotation + ); + } + this.path.push(ARC, cx, cy, rx, sa, ea, ccw ? 0 : 1); + }, + + onClose: function() { + this.path.push(CLOSE); + }, + + toJSON: function() { + return this.path; + } + +}); + +module.exports = SerializablePath; diff --git a/Libraries/ART/ARTShape.h b/Libraries/ART/ARTShape.h new file mode 100644 index 00000000000000..7d13c268f6e802 --- /dev/null +++ b/Libraries/ART/ARTShape.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTRenderable.h" + +@interface ARTShape : ARTRenderable + +@property (nonatomic, assign) CGPathRef d; + +@end diff --git a/Libraries/ART/ARTShape.m b/Libraries/ART/ARTShape.m new file mode 100644 index 00000000000000..c07d68e62aa4a3 --- /dev/null +++ b/Libraries/ART/ARTShape.m @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTShape.h" + +@implementation ARTShape + +- (void)setD:(CGPathRef)d +{ + if (d == _d) { + return; + } + [self invalidate]; + CGPathRelease(_d); + _d = CGPathRetain(d); +} + +- (void)dealloc +{ + CGPathRelease(_d); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + if ((!self.fill && !self.stroke) || !self.d) { + return; + } + + CGPathDrawingMode mode = kCGPathStroke; + if (self.fill) { + if ([self.fill applyFillColor:context]) { + mode = kCGPathFill; + } else { + CGContextSaveGState(context); + CGContextAddPath(context, self.d); + CGContextClip(context); + [self.fill paint:context]; + CGContextRestoreGState(context); + if (!self.stroke) { + return; + } + } + } + if (self.stroke) { + CGContextSetStrokeColorWithColor(context, self.stroke); + CGContextSetLineWidth(context, self.strokeWidth); + CGContextSetLineCap(context, self.strokeCap); + CGContextSetLineJoin(context, self.strokeJoin); + ARTCGFloatArray dash = self.strokeDash; + if (dash.count) { + CGContextSetLineDash(context, 0, dash.array, dash.count); + } + if (mode == kCGPathFill) { + mode = kCGPathFillStroke; + } + } + + CGContextAddPath(context, self.d); + CGContextDrawPath(context, mode); +} + +@end diff --git a/Libraries/ART/ARTSurfaceView.h b/Libraries/ART/ARTSurfaceView.h new file mode 100644 index 00000000000000..8be8d95040c146 --- /dev/null +++ b/Libraries/ART/ARTSurfaceView.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTContainer.h" + +@interface ARTSurfaceView : UIView + +@end diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m new file mode 100644 index 00000000000000..8949e43c349d05 --- /dev/null +++ b/Libraries/ART/ARTSurfaceView.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTSurfaceView.h" + +#import "ARTNode.h" +#import "RCTLog.h" + +@implementation ARTSurfaceView + +- (void)invalidate +{ + [self setNeedsDisplay]; +} + +- (void)drawRect:(CGRect)rect +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + for (ARTNode *node in self.subviews) { + [node renderTo:context]; + } +} + +@end diff --git a/Libraries/ART/ARTText.h b/Libraries/ART/ARTText.h new file mode 100644 index 00000000000000..ee976e329a46e8 --- /dev/null +++ b/Libraries/ART/ARTText.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTRenderable.h" +#import "ARTTextFrame.h" + +@interface ARTText : ARTRenderable + +@property (nonatomic, assign) CTTextAlignment alignment; +@property (nonatomic, assign) ARTTextFrame textFrame; + +@end diff --git a/Libraries/ART/ARTText.m b/Libraries/ART/ARTText.m new file mode 100644 index 00000000000000..7c8a570270b7ff --- /dev/null +++ b/Libraries/ART/ARTText.m @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTText.h" + +#import + +@implementation ARTText + +- (void)setAlignment:(CTTextAlignment)alignment +{ + [self invalidate]; + _alignment = alignment; +} + +- (void)setTextFrame:(ARTTextFrame)frame +{ + if (frame.lines != _textFrame.lines && _textFrame.count) { + // We must release each line before overriding the old one + for (int i = 0; i < _textFrame.count; i++) { + CFRelease(_textFrame.lines[0]); + } + free(_textFrame.lines); + free(_textFrame.widths); + } + [self invalidate]; + _textFrame = frame; +} + +- (void)dealloc +{ + if (_textFrame.count) { + // We must release each line before freeing up this struct + for (int i = 0; i < _textFrame.count; i++) { + CFRelease(_textFrame.lines[0]); + } + free(_textFrame.lines); + free(_textFrame.widths); + } +} + +- (void)renderLayerTo:(CGContextRef)context +{ + ARTTextFrame frame = self.textFrame; + + if ((!self.fill && !self.stroke) || !frame.count) { + return; + } + + // to-do: draw along a path + + CGTextDrawingMode mode = kCGTextStroke; + if (self.fill) { + if ([self.fill applyFillColor:context]) { + mode = kCGTextFill; + } else { + + for (int i = 0; i < frame.count; i++) { + CGContextSaveGState(context); + // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space + CGContextScaleCTM(context, 1.0, -1.0); + CGContextSetTextDrawingMode(context, kCGTextClip); + [self renderLineTo:context atIndex:i]; + // Inverse the coordinate space back to the original before filling + CGContextScaleCTM(context, 1.0, -1.0); + [self.fill paint:context]; + // Restore the state so that the next line can be clipped separately + CGContextRestoreGState(context); + } + + if (!self.stroke) { + return; + } + } + } + if (self.stroke) { + CGContextSetStrokeColorWithColor(context, self.stroke); + CGContextSetLineWidth(context, self.strokeWidth); + CGContextSetLineCap(context, self.strokeCap); + CGContextSetLineJoin(context, self.strokeJoin); + ARTCGFloatArray dash = self.strokeDash; + if (dash.count) { + CGContextSetLineDash(context, 0, dash.array, dash.count); + } + if (mode == kCGTextFill) { + mode = kCGTextFillStroke; + } + } + + CGContextSetTextDrawingMode(context, mode); + + // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space + CGContextScaleCTM(context, 1.0, -1.0); + for (int i = 0; i < frame.count; i++) { + [self renderLineTo:context atIndex:i]; + } +} + +- (void)renderLineTo:(CGContextRef)context atIndex:(int)index +{ + ARTTextFrame frame = self.textFrame; + CGFloat shift; + switch (self.alignment) { + case kCTTextAlignmentRight: + shift = frame.widths[index]; + break; + case kCTTextAlignmentCenter: + shift = (frame.widths[index] / 2); + break; + default: + shift = 0; + break; + } + // We should consider snapping this shift to device pixels to improve rendering quality + // when a line has subpixel width. + CGContextSetTextPosition(context, -shift, -frame.baseLine - frame.lineHeight * index); + CTLineRef line = frame.lines[index]; + CTLineDraw(line, context); +} + +@end diff --git a/Libraries/ART/ARTTextFrame.h b/Libraries/ART/ARTTextFrame.h new file mode 100644 index 00000000000000..1f6b557bfc77af --- /dev/null +++ b/Libraries/ART/ARTTextFrame.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +// A little helper to make sure we have a set of lines including width ready for use. +// We assume that we will only this in one place so no reference counting is necessary. +// Needs to be freed when dealloced. + +// This is fragile since this relies on these values not getting reused. Consider +// wrapping these in an Obj-C class or some ARC hackery to get refcounting. + +typedef struct { + size_t count; + CGFloat baseLine; // Distance from the origin to the base line of the first line + CGFloat lineHeight; // Distance between lines + CTLineRef *lines; + CGFloat *widths; // Width of each line +} ARTTextFrame; diff --git a/Libraries/ART/Brushes/ARTBrush.h b/Libraries/ART/Brushes/ARTBrush.h new file mode 100644 index 00000000000000..05020dd7bafa26 --- /dev/null +++ b/Libraries/ART/Brushes/ARTBrush.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@interface ARTBrush : NSObject + +/* @abstract */ +- (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER; + +/** + * For certain brushes we can fast path a combined fill and stroke. + * For those brushes we override applyFillColor which sets the fill + * color to be used by those batch paints. Those return YES. + * We can't batch gradient painting in CoreGraphics, so those will + * return NO and paint gets called instead. + * @abstract + */ +- (BOOL)applyFillColor:(CGContextRef)context; + +/** + * paint fills the context with a brush. The context is assumed to + * be clipped. + * @abstract + */ +- (void)paint:(CGContextRef)context; + +@end diff --git a/Libraries/ART/Brushes/ARTBrush.m b/Libraries/ART/Brushes/ARTBrush.m new file mode 100644 index 00000000000000..efc82dea35bb25 --- /dev/null +++ b/Libraries/ART/Brushes/ARTBrush.m @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTBrush.h" + +@implementation ARTBrush + +- (instancetype)initWithArray:(NSArray *)data +{ + return [super init]; +} + +- (BOOL)applyFillColor:(CGContextRef)context +{ + return NO; +} + +- (void)paint:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.h b/Libraries/ART/Brushes/ARTLinearGradient.h new file mode 100644 index 00000000000000..d7ff2e5684458f --- /dev/null +++ b/Libraries/ART/Brushes/ARTLinearGradient.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTBrush.h" + +@interface ARTLinearGradient : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.m b/Libraries/ART/Brushes/ARTLinearGradient.m new file mode 100644 index 00000000000000..8793ff07bf71ff --- /dev/null +++ b/Libraries/ART/Brushes/ARTLinearGradient.m @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTLinearGradient.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTLinearGradient +{ + CGGradientRef _gradient; + CGPoint _startPoint; + CGPoint _endPoint; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 5) { + RCTLogError(@"-[%@ %@] expects 5 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _startPoint = [RCTConvert CGPoint:array offset:1]; + _endPoint = [RCTConvert CGPoint:array offset:3]; + _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]); + } + return self; +} + +- (void)dealloc +{ + CGGradientRelease(_gradient); +} + +- (void)paint:(CGContextRef)context +{ + CGGradientDrawingOptions extendOptions = + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawLinearGradient(context, _gradient, _startPoint, _endPoint, extendOptions); +} + +@end diff --git a/Libraries/ART/Brushes/ARTPattern.h b/Libraries/ART/Brushes/ARTPattern.h new file mode 100644 index 00000000000000..5f513ec60e2ae6 --- /dev/null +++ b/Libraries/ART/Brushes/ARTPattern.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTBrush.h" + +@interface ARTPattern : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTPattern.m b/Libraries/ART/Brushes/ARTPattern.m new file mode 100644 index 00000000000000..07dd867001880f --- /dev/null +++ b/Libraries/ART/Brushes/ARTPattern.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTPattern.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTPattern +{ + CGImageRef _image; + CGRect _rect; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 6) { + RCTLogError(@"-[%@ %@] expects 6 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _image = CGImageRetain([RCTConvert CGImage:array[1]]); + _rect = [RCTConvert CGRect:array offset:2]; + } + return self; +} + +- (void)dealloc +{ + CGImageRelease(_image); +} + +// Note: This could use applyFillColor with a pattern. This could be more efficient but +// to do that, we need to calculate our own user space CTM. + +- (void)paint:(CGContextRef)context +{ + CGContextDrawTiledImage(context, _rect, _image); +} + + + +@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.h b/Libraries/ART/Brushes/ARTRadialGradient.h new file mode 100644 index 00000000000000..7f86d93058ce80 --- /dev/null +++ b/Libraries/ART/Brushes/ARTRadialGradient.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTBrush.h" + +@interface ARTRadialGradient : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.m b/Libraries/ART/Brushes/ARTRadialGradient.m new file mode 100644 index 00000000000000..b59b1736937a37 --- /dev/null +++ b/Libraries/ART/Brushes/ARTRadialGradient.m @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTRadialGradient.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTRadialGradient +{ + CGGradientRef _gradient; + CGPoint _focusPoint; + CGPoint _centerPoint; + CGFloat _radius; + CGFloat _radiusRatio; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 7) { + RCTLogError(@"-[%@ %@] expects 7 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _radius = [RCTConvert CGFloat:array[3]]; + _radiusRatio = [RCTConvert CGFloat:array[4]] / _radius; + _focusPoint.x = [RCTConvert CGFloat:array[1]]; + _focusPoint.y = [RCTConvert CGFloat:array[2]] / _radiusRatio; + _centerPoint.x = [RCTConvert CGFloat:array[5]]; + _centerPoint.y = [RCTConvert CGFloat:array[6]] / _radiusRatio; + _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:7]); + } + return self; +} + +- (void)dealloc +{ + CGGradientRelease(_gradient); +} + +- (void)paint:(CGContextRef)context +{ + CGAffineTransform transform = CGAffineTransformMakeScale(1, _radiusRatio); + CGContextConcatCTM(context, transform); + CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawRadialGradient(context, _gradient, _focusPoint, 0, _centerPoint, _radius, extendOptions); +} + +@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.h b/Libraries/ART/Brushes/ARTSolidColor.h new file mode 100644 index 00000000000000..f212c735680f60 --- /dev/null +++ b/Libraries/ART/Brushes/ARTSolidColor.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTBrush.h" + +@interface ARTSolidColor : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.m b/Libraries/ART/Brushes/ARTSolidColor.m new file mode 100644 index 00000000000000..229942ddec6490 --- /dev/null +++ b/Libraries/ART/Brushes/ARTSolidColor.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTSolidColor.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTSolidColor +{ + CGColorRef _color; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + _color = CGColorRetain([RCTConvert CGColor:array offset:1]); + } + return self; +} + +- (void)dealloc +{ + CGColorRelease(_color); +} + +- (BOOL)applyFillColor:(CGContextRef)context +{ + CGContextSetFillColorWithColor(context, _color); + return YES; +} + +@end diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h new file mode 100644 index 00000000000000..24944fb1298b42 --- /dev/null +++ b/Libraries/ART/RCTConvert+ART.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ARTBrush.h" +#import "ARTCGFloatArray.h" +#import "ARTTextFrame.h" +#import "RCTConvert.h" + +@interface RCTConvert (ART) + ++ (CGPathRef)CGPath:(id)json; ++ (CTFontRef)CTFont:(id)json; ++ (CTTextAlignment)CTTextAlignment:(id)json; ++ (ARTTextFrame)ARTTextFrame:(id)json; ++ (ARTCGFloatArray)ARTCGFloatArray:(id)json; ++ (ARTBrush *)ARTBrush:(id)json; + ++ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; ++ (CGRect)CGRect:(id)json offset:(NSUInteger)offset; ++ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset; ++ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset; + +@end diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m new file mode 100644 index 00000000000000..4cd11bd3fbba9d --- /dev/null +++ b/Libraries/ART/RCTConvert+ART.m @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTConvert+ART.h" + +#import "ARTLinearGradient.h" +#import "ARTPattern.h" +#import "ARTRadialGradient.h" +#import "ARTSolidColor.h" +#import "RCTLog.h" + +@implementation RCTConvert (ART) + ++ (CGPathRef)CGPath:(id)json +{ + NSArray *arr = [self NSNumberArray:json]; + + NSUInteger count = [arr count]; + +#define NEXT_VALUE [self double:arr[i++]] + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathMoveToPoint(path, NULL, 0, 0); + + @try { + NSUInteger i = 0; + while (i < count) { + NSUInteger type = [arr[i++] unsignedIntegerValue]; + switch (type) { + case 0: + CGPathMoveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); + break; + case 1: + CGPathCloseSubpath(path); + break; + case 2: + CGPathAddLineToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); + break; + case 3: + CGPathAddCurveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE); + break; + case 4: + CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0); + break; + default: + RCTLogError(@"Invalid CGPath type %zd at element %zd of %@", type, i, arr); + CGPathRelease(path); + return NULL; + } + } + } + @catch (NSException *exception) { + RCTLogError(@"Invalid CGPath format: %@", arr); + CGPathRelease(path); + return NULL; + } + + return (CGPathRef)CFAutorelease(path); +} + ++ (CTFontRef)CTFont:(id)json +{ + NSDictionary *dict = [self NSDictionary:json]; + if (!dict) { + return nil; + } + CTFontDescriptorRef fontDescriptor = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)dict); + CTFontRef font = CTFontCreateWithFontDescriptor(fontDescriptor, 0.0, NULL); + CFRelease(fontDescriptor); + return (CTFontRef)CFAutorelease(font); +} + +RCT_ENUM_CONVERTER(CTTextAlignment, (@{ + @"auto": @(kCTTextAlignmentNatural), + @"left": @(kCTTextAlignmentLeft), + @"center": @(kCTTextAlignmentCenter), + @"right": @(kCTTextAlignmentRight), + @"justify": @(kCTTextAlignmentJustified), +}), kCTTextAlignmentNatural, integerValue) + +// This takes a tuple of text lines and a font to generate a CTLine for each text line. +// This prepares everything for rendering a frame of text in ARTText. ++ (ARTTextFrame)ARTTextFrame:(id)json +{ + NSDictionary *dict = [self NSDictionary:json]; + ARTTextFrame frame; + frame.count = 0; + + NSArray *lines = [self NSArray:dict[@"lines"]]; + NSUInteger lineCount = [lines count]; + if (lineCount == 0) { + return frame; + } + + CTFontRef font = [self CTFont:dict[@"font"]]; + if (!font) { + return frame; + } + + // Create a dictionary for this font + CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{ + (NSString *)kCTFontAttributeName: (__bridge id)font, + (NSString *)kCTForegroundColorFromContextAttributeName: @YES + }; + + // Set up text frame with font metrics + CGFloat size = CTFontGetSize(font); + frame.count = lineCount; + frame.baseLine = size; // estimate base line + frame.lineHeight = size * 1.1; // Base on ART canvas line height estimate + frame.lines = malloc(sizeof(CTLineRef) * lineCount); + frame.widths = malloc(sizeof(CGFloat) * lineCount); + + [lines enumerateObjectsUsingBlock:^(NSString *text, NSUInteger i, BOOL *stop) { + + CFStringRef string = (__bridge CFStringRef)text; + CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); + CTLineRef line = CTLineCreateWithAttributedString(attrString); + CFRelease(attrString); + + frame.lines[i] = line; + frame.widths[i] = CTLineGetTypographicBounds(line, NULL, NULL, NULL); + }]; + + return frame; +} + ++ (ARTCGFloatArray)ARTCGFloatArray:(id)json +{ + NSArray *arr = [self NSNumberArray:json]; + NSUInteger count = arr.count; + + ARTCGFloatArray array; + array.count = count; + array.array = NULL; + + if (count) { + // Ideally, these arrays should already use the same memory layout. + // In that case we shouldn't need this new malloc. + array.array = malloc(sizeof(CGFloat) * count); + for (NSUInteger i = 0; i < count; i++) { + array.array[i] = [arr[i] doubleValue]; + } + } + + return array; +} + ++ (ARTBrush *)ARTBrush:(id)json +{ + NSArray *arr = [self NSArray:json]; + NSUInteger type = [self NSUInteger:arr[0]]; + switch (type) { + case 0: // solid color + // These are probably expensive allocations since it's often the same value. + // We should memoize colors but look ups may be just as expensive. + return [[ARTSolidColor alloc] initWithArray:arr]; + case 1: // linear gradient + return [[ARTLinearGradient alloc] initWithArray:arr]; + case 2: // radial gradient + return [[ARTRadialGradient alloc] initWithArray:arr]; + case 3: // pattern + return [[ARTPattern alloc] initWithArray:arr]; + default: + RCTLogError(@"Unknown brush type: %zd", type); + return nil; + } +} + ++ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 2) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 2 + offset, arr); + return CGPointZero; + } + return (CGPoint){ + [self CGFloat:arr[offset]], + [self CGFloat:arr[offset + 1]], + }; +} + ++ (CGRect)CGRect:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 4) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); + return CGRectZero; + } + return (CGRect){ + {[self CGFloat:arr[offset]], [self CGFloat:arr[offset + 1]]}, + {[self CGFloat:arr[offset + 2]], [self CGFloat:arr[offset + 3]]}, + }; +} + ++ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 4) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); + return NULL; + } + return [self CGColor:[arr subarrayWithRange:(NSRange){offset, 4}]]; +} + ++ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", offset, arr); + return NULL; + } + arr = [arr subarrayWithRange:(NSRange){offset, arr.count - offset}]; + ARTCGFloatArray colorsAndOffsets = [self ARTCGFloatArray:arr]; + size_t stops = colorsAndOffsets.count / 5; + CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents( + rgb, + colorsAndOffsets.array, + colorsAndOffsets.array + stops * 4, + stops + ); + CGColorSpaceRelease(rgb); + free(colorsAndOffsets.array); + return (CGGradientRef)CFAutorelease(gradient); +} + +@end diff --git a/Libraries/ART/ReactIOSART.js b/Libraries/ART/ReactIOSART.js new file mode 100644 index 00000000000000..9ef2f8843bf7e1 --- /dev/null +++ b/Libraries/ART/ReactIOSART.js @@ -0,0 +1,611 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactIOSART + */ + +"use strict"; + +var Color = require('art/core/color'); +var Path = require('ARTSerializablePath'); +var Transform = require('art/core/transform'); + +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +// Diff Helpers + +function arrayDiffer(a, b) { + if (a == null) { + return true; + } + if (a.length !== b.length) { + return true; + } + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return true; + } + } + return false; +} + +function fontAndLinesDiffer(a, b) { + if (a === b) { + return false; + } + if (a.font !== b.font) { + if (a.font === null) { + return true; + } + if (b.font === null) { + return true; + } + + var aTraits = a.font.NSCTFontTraitsAttribute; + var bTraits = b.font.NSCTFontTraitsAttribute; + + if ( + a.font.fontFamily !== b.font.fontFamily || + a.font.fontSize !== b.font.fontSize || + a.font.fontWeight !== b.font.fontWeight || + a.font.fontStyle !== b.font.fontStyle || + // TODO(6364240): remove iOS-specific attrs + a.font.NSFontFamilyAttribute !== b.font.NSFontFamilyAttribute || + a.font.NSFontSizeAttribute !== b.font.NSFontSizeAttribute || + aTraits.NSCTFontSymbolicTrait !== bTraits.NSCTFontSymbolicTrait + ) { + return true; + } + } + return arrayDiffer(a.lines, b.lines); +} + +// Native Attributes + +var SurfaceViewAttributes = merge(ReactIOSViewAttributes.UIView, { + // This should contain pixel information such as width, height and + // resolution to know what kind of buffer needs to be allocated. + // Currently we rely on UIViews and style to figure that out. +}); + +var NodeAttributes = { + transform: { diff: arrayDiffer }, + opacity: true, +}; + +var GroupAttributes = merge(NodeAttributes, { + clipping: { diff: arrayDiffer } +}); + +var RenderableAttributes = merge(NodeAttributes, { + fill: { diff: arrayDiffer }, + stroke: { diff: arrayDiffer }, + strokeWidth: true, + strokeCap: true, + strokeJoin: true, + strokeDash: { diff: arrayDiffer }, +}); + +var ShapeAttributes = merge(RenderableAttributes, { + d: { diff: arrayDiffer }, +}); + +var TextAttributes = merge(RenderableAttributes, { + alignment: true, + frame: { diff: fontAndLinesDiffer }, + path: { diff: arrayDiffer } +}); + +// Native Components + +var NativeSurfaceView = createReactIOSNativeComponentClass({ + validAttributes: SurfaceViewAttributes, + uiViewClassName: 'ARTSurfaceView', +}); + +var NativeGroup = createReactIOSNativeComponentClass({ + validAttributes: GroupAttributes, + uiViewClassName: 'ARTGroup', +}); + +var NativeShape = createReactIOSNativeComponentClass({ + validAttributes: ShapeAttributes, + uiViewClassName: 'ARTShape', +}); + +var NativeText = createReactIOSNativeComponentClass({ + validAttributes: TextAttributes, + uiViewClassName: 'ARTText', +}); + +// Utilities + +function childrenAsString(children) { + if (!children) { + return ''; + } + if (typeof children === 'string') { + return children; + } + if (children.length) { + return children.join('\n'); + } + return ''; +} + +// Surface - Root node of all ART + +var Surface = React.createClass({ + + render: function() { + var props = this.props; + var w = extractNumber(props.width, 0); + var h = extractNumber(props.height, 0); + return ( + + {this.props.children} + + ); + } + +}); + +// Node Props + +// TODO: The desktop version of ART has title and cursor. We should have +// accessibility support here too even though hovering doesn't work. + +function extractNumber(value, defaultValue) { + if (value == null) { + return defaultValue; + } + return +value; +} + +var pooledTransform = new Transform(); + +function extractTransform(props) { + var scaleX = props.scaleX != null ? props.scaleX : + props.scale != null ? props.scale : 1; + var scaleY = props.scaleY != null ? props.scaleY : + props.scale != null ? props.scale : 1; + + pooledTransform + .transformTo(1, 0, 0, 1, 0, 0) + .move(props.x || 0, props.y || 0) + .rotate(props.rotation || 0, props.originX, props.originY) + .scale(scaleX, scaleY, props.originX, props.originY); + + if (props.transform != null) { + pooledTransform.transform(props.transform); + } + + return [ + pooledTransform.xx, pooledTransform.yx, + pooledTransform.xy, pooledTransform.yy, + pooledTransform.x, pooledTransform.y, + ]; +} + +function extractOpacity(props) { + // TODO: visible === false should also have no hit detection + if (props.visible === false) { + return 0; + } + if (props.opacity == null) { + return 1; + } + return +props.opacity; +} + +// Groups + +// Note: ART has a notion of width and height on Group but AFAIK it's a noop in +// ReactART. + +var Group = React.createClass({ + + render: function() { + var props = this.props; + return ( + + {this.props.children} + + ); + } + +}); + +var ClippingRectangle = React.createClass({ + + render: function() { + var props = this.props; + var x = extractNumber(props.x, 0); + var y = extractNumber(props.y, 0); + var w = extractNumber(props.width, 0); + var h = extractNumber(props.height, 0); + var clipping = new Path() + .moveTo(x, y) + .line(w, 0) + .line(0, h) + .line(w, 0) + .close() + .toJSON(); + // The current clipping API requires x and y to be ignored in the transform + var propsExcludingXAndY = merge(props); + delete propsExcludingXAndY.x; + delete propsExcludingXAndY.y; + return ( + + {this.props.children} + + ); + } + +}); + +// Renderables + +var SOLID_COLOR = 0; +var LINEAR_GRADIENT = 1; +var RADIAL_GRADIENT = 2; +var PATTERN = 3; + +function insertColorIntoArray(color, targetArray, atIndex) { + var c = new Color(color); + targetArray[atIndex + 0] = c.red / 255; + targetArray[atIndex + 1] = c.green / 255; + targetArray[atIndex + 2] = c.blue / 255; + targetArray[atIndex + 3] = c.alpha; +} + +function insertColorsIntoArray(stops, targetArray, atIndex) { + var i = 0; + if ('length' in stops) { + while (i < stops.length) { + insertColorIntoArray(stops[i], targetArray, atIndex + i * 4); + i++; + } + } else { + for (var offset in stops) { + insertColorIntoArray(stops[offset], targetArray, atIndex + i * 4); + i++; + } + } + return atIndex + i * 4; +} + +function insertOffsetsIntoArray(stops, targetArray, atIndex, multi, reverse) { + var offsetNumber; + var i = 0; + if ('length' in stops) { + while (i < stops.length) { + offsetNumber = i / (stops.length - 1) * multi; + targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; + i++; + } + } else { + for (var offsetString in stops) { + offsetNumber = (+offsetString) * multi; + targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; + i++; + } + } + return atIndex + i; +} + +function insertColorStopsIntoArray(stops, targetArray, atIndex) { + var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); + insertOffsetsIntoArray(stops, targetArray, lastIndex, 1, false); +} + +function insertDoubleColorStopsIntoArray(stops, targetArray, atIndex) { + var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); + lastIndex = insertColorsIntoArray(stops, targetArray, lastIndex); + lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false); + insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true); +} + +function applyBoundingBoxToBrushData(brushData, props) { + var type = brushData[0]; + var width = +props.width; + var height = +props.height; + if (type === LINEAR_GRADIENT) { + brushData[1] *= width; + brushData[2] *= height; + brushData[3] *= width; + brushData[4] *= height; + } else if (type === RADIAL_GRADIENT) { + brushData[1] *= width; + brushData[2] *= height; + brushData[3] *= width; + brushData[4] *= height; + brushData[5] *= width; + brushData[6] *= height; + } else if (type === PATTERN) { + // todo + } +} + +function extractBrush(colorOrBrush, props) { + if (colorOrBrush == null) { + return null; + } + if (colorOrBrush._brush) { + if (colorOrBrush._bb) { + // The legacy API for Gradients allow for the bounding box to be used + // as a convenience for specifying gradient positions. This should be + // deprecated. It's not properly implemented in canvas mode. ReactART + // doesn't handle update to the bounding box correctly. That's why we + // mutate this so that if it's reused, we reuse the same resolved box. + applyBoundingBoxToBrushData(colorOrBrush._brush, props); + colorOrBrush._bb = false; + } + return colorOrBrush._brush; + } + var c = new Color(colorOrBrush); + return [SOLID_COLOR, c.red / 255, c.green / 255, c.blue / 255, c.alpha]; +} + +function extractColor(color) { + if (color == null) { + return null; + } + var c = new Color(color); + return [c.red / 255, c.green / 255, c.blue / 255, c.alpha]; +} + +function extractStrokeCap(strokeCap) { + switch (strokeCap) { + case 'butt': return 0; + case 'square': return 2; + default: return 1; // round + } +} + +function extractStrokeJoin(strokeJoin) { + switch (strokeJoin) { + case 'miter': return 0; + case 'bevel': return 2; + default: return 1; // round + } +} + +// Shape + +// Note: ART has a notion of width and height on Shape but AFAIK it's a noop in +// ReactART. + +var Shape = React.createClass({ + + render: function() { + var props = this.props; + var path = props.d || childrenAsString(props.children); + var d = new Path(path).toJSON(); + return ( + + ); + } + +}); + +// Text + +var cachedFontObjectsFromString = {}; + +function extractFontTraits(isBold, isItalic) { + var italic = isItalic ? 1 : 0; + var bold = isBold ? 2 : 0; + return { + NSCTFontSymbolicTrait: italic | bold + }; +} + +var fontFamilyPrefix = /^[\s"']*/; +var fontFamilySuffix = /[\s"']*$/; + +function extractSingleFontFamily(fontFamilyString) { + // ART on the web allows for multiple font-families to be specified. + // For compatibility, we extract the first font-family, hoping + // we'll get a match. + return fontFamilyString.split(',')[0] + .replace(fontFamilyPrefix, '') + .replace(fontFamilySuffix, ''); +} + +function parseFontString(font) { + if (cachedFontObjectsFromString.hasOwnProperty(font)) { + return cachedFontObjectsFromString[font]; + } + var regexp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm\%]*(?:\s*\/.*?)?\s+)?\s*\"?([^\"]*)/i; + var match = regexp.exec(font); + if (!match) { + return null; + } + var fontFamily = extractSingleFontFamily(match[3]); + var fontSize = +match[2] || 12; + var isBold = /bold/.exec(match[1]); + var isItalic = /italic/.exec(match[1]); + cachedFontObjectsFromString[font] = { + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: isBold ? 'bold' : 'normal', + fontStyle: isItalic ? 'italic' : 'normal', + // TODO(6364240): remove iOS-specific attrs + NSFontFamilyAttribute: fontFamily, + NSFontSizeAttribute: fontSize, + NSCTFontTraitsAttribute: extractFontTraits(isBold, isItalic) + }; + return cachedFontObjectsFromString[font]; +} + +function extractFont(font) { + if (font == null) { + return null; + } + if (typeof font === 'string') { + return parseFontString(font); + } + var fontFamily = extractSingleFontFamily(font.fontFamily); + var fontSize = +font.fontSize || 12; + return { + // Normalize + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: font.fontWeight, + fontStyle: font.fontStyle, + // TODO(6364240): remove iOS-specific attrs + NSFontFamilyAttribute: fontFamily, + NSFontSizeAttribute: fontSize, + NSCTFontTraitsAttribute: extractFontTraits( + font.fontWeight === 'bold', + font.fontStyle === 'italic' + ) + }; +} + +var newLine = /\n/g; +function extractFontAndLines(font, text) { + return { font: extractFont(font), lines: text.split(newLine) }; +} + +function extractAlignment(alignment) { + switch (alignment) { + case 'right': + return 1; + case 'center': + return 2; + default: + return 0; + } +} + +var Text = React.createClass({ + + render: function() { + var props = this.props; + var textPath = props.path ? new Path(props.path).toJSON() : null; + var textFrame = extractFontAndLines( + props.font, + childrenAsString(props.children) + ); + return ( + + ); + } + +}); + +// Declarative fill type objects - API design not finalized + +function LinearGradient(stops, x1, y1, x2, y2) { + var type = LINEAR_GRADIENT; + + if (arguments.length < 5) { + var angle = ((x1 == null) ? 270 : x1) * Math.PI / 180; + + var x = Math.cos(angle); + var y = -Math.sin(angle); + var l = (Math.abs(x) + Math.abs(y)) / 2; + + x *= l; y *= l; + + x1 = 0.5 - x; + x2 = 0.5 + x; + y1 = 0.5 - y; + y2 = 0.5 + y; + this._bb = true; + } else { + this._bb = false; + } + + var brushData = [type, +x1, +y1, +x2, +y2]; + insertColorStopsIntoArray(stops, brushData, 5); + this._brush = brushData; +} + +function RadialGradient(stops, fx, fy, rx, ry, cx, cy) { + if (ry == null) { + ry = rx; + } + if (cx == null) { + cx = fx; + } + if (cy == null) { + cy = fy; + } + if (fx == null) { + // As a convenience we allow the whole radial gradient to cover the + // bounding box. We should consider dropping this API. + fx = fy = rx = ry = cx = cy = 0.5; + this._bb = true; + } else { + this._bb = false; + } + // The ART API expects the radial gradient to be repeated at the edges. + // To simulate this we render the gradient twice as large and add double + // color stops. Ideally this API would become more restrictive so that this + // extra work isn't needed. + var brushData = [RADIAL_GRADIENT, +fx, +fy, +rx * 2, +ry * 2, +cx, +cy]; + insertDoubleColorStopsIntoArray(stops, brushData, 7); + this._brush = brushData; +} + +function Pattern(url, width, height, left, top) { + this._brush = [PATTERN, url, +left || 0, +top || 0, +width, +height]; +} + +var ReactART = { + + LinearGradient: LinearGradient, + RadialGradient: RadialGradient, + Pattern: Pattern, + Transform: Transform, + Path: Path, + Surface: Surface, + Group: Group, + ClippingRectangle: ClippingRectangle, + Shape: Shape, + Text: Text, + +}; + +module.exports = ReactART; diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.h b/Libraries/ART/ViewManagers/ARTGroupManager.h new file mode 100644 index 00000000000000..0a90eb3d6c18c9 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTGroupManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTNodeManager.h" + +@interface ARTGroupManager : ARTNodeManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.m b/Libraries/ART/ViewManagers/ARTGroupManager.m new file mode 100644 index 00000000000000..15f55d4df12d11 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTGroupManager.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTGroupManager.h" + +#import "ARTGroup.h" + +@implementation ARTGroupManager + +RCT_EXPORT_MODULE() + +- (ARTNode *)node +{ + return [[ARTGroup alloc] init]; +} + +@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.h b/Libraries/ART/ViewManagers/ARTNodeManager.h new file mode 100644 index 00000000000000..1097eefde91246 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTNodeManager.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTNode.h" +#import "RCTViewManager.h" + +@interface ARTNodeManager : RCTViewManager + +- (ARTNode *)node; + +@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.m b/Libraries/ART/ViewManagers/ARTNodeManager.m new file mode 100644 index 00000000000000..c2f0dba35ad065 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTNodeManager.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTNodeManager.h" + +#import "ARTNode.h" + +@implementation ARTNodeManager + +RCT_EXPORT_MODULE() + +- (ARTNode *)node +{ + return [[ARTNode alloc] init]; +} + +- (UIView *)view +{ + return [self node]; +} + +- (RCTShadowView *)shadowView +{ + return nil; +} + +RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(transform, CGAffineTransform) + +@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.h b/Libraries/ART/ViewManagers/ARTRenderableManager.h new file mode 100644 index 00000000000000..376fcf518b3293 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTNodeManager.h" +#import "ARTRenderable.h" + +@interface ARTRenderableManager : ARTNodeManager + +- (ARTRenderable *)node; + +@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.m b/Libraries/ART/ViewManagers/ARTRenderableManager.m new file mode 100644 index 00000000000000..01b579dca4c940 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTRenderableManager.h" + +#import "RCTConvert+ART.h" + +@implementation ARTRenderableManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTRenderable alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(strokeCap, CGLineCap) +RCT_EXPORT_VIEW_PROPERTY(strokeJoin, CGLineJoin) +RCT_EXPORT_VIEW_PROPERTY(fill, ARTBrush) +RCT_EXPORT_VIEW_PROPERTY(stroke, CGColor) +RCT_EXPORT_VIEW_PROPERTY(strokeDash, ARTCGFloatArray) + +@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.h b/Libraries/ART/ViewManagers/ARTShapeManager.h new file mode 100644 index 00000000000000..d6bc76baa09fdb --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTShapeManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTRenderableManager.h" + +@interface ARTShapeManager : ARTRenderableManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.m b/Libraries/ART/ViewManagers/ARTShapeManager.m new file mode 100644 index 00000000000000..426237fa75cfc8 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTShapeManager.m @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTShapeManager.h" + +#import "ARTShape.h" +#import "RCTConvert+ART.h" + +@implementation ARTShapeManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTShape alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(d, CGPath) + +@end diff --git a/React/Views/RCTUIActivityIndicatorViewManager.h b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h similarity index 84% rename from React/Views/RCTUIActivityIndicatorViewManager.h rename to Libraries/ART/ViewManagers/ARTSurfaceViewManager.h index e5a10fdd75c746..6d8e140049e8fb 100644 --- a/React/Views/RCTUIActivityIndicatorViewManager.h +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h @@ -9,6 +9,6 @@ #import "RCTViewManager.h" -@interface RCTUIActivityIndicatorViewManager : RCTViewManager +@interface ARTSurfaceViewManager : RCTViewManager @end diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m new file mode 100644 index 00000000000000..ddfba6697be52c --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTSurfaceViewManager.h" + +#import "ARTSurfaceView.h" + +@implementation ARTSurfaceViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[ARTSurfaceView alloc] init]; +} + +@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.h b/Libraries/ART/ViewManagers/ARTTextManager.h new file mode 100644 index 00000000000000..48da9c891f4757 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTTextManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTRenderableManager.h" + +@interface ARTTextManager : ARTRenderableManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.m b/Libraries/ART/ViewManagers/ARTTextManager.m new file mode 100644 index 00000000000000..473d0cf4fa4a88 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTTextManager.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ARTTextManager.h" + +#import "ARTText.h" +#import "RCTConvert+ART.h" + +@implementation ARTTextManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTText alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(alignment, CTTextAlignment) +RCT_REMAP_VIEW_PROPERTY(frame, textFrame, ARTTextFrame) + +@end diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js index 28da1bc321fe53..4d5c7b34c3b9c8 100644 --- a/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js +++ b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js @@ -5,9 +5,9 @@ var NativeModules = { I18n: { - translationsDictionary: { + translationsDictionary: JSON.stringify({ 'Good bye, {name}!|Bye message': '¡Adiós {name}!', - }, + }), }, Timing: { createTimer: jest.genMockFunction(), @@ -29,6 +29,7 @@ var NativeModules = { UIManager: { customBubblingEventTypes: {}, customDirectEventTypes: {}, + Dimensions: {}, }, AsyncLocalStorage: { getItem: jest.genMockFunction(), diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index 3a44020a663590..a3f1fe6be8c3d5 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -18,21 +18,16 @@ var React = require('React'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var keyMirror = require('keyMirror'); var requireNativeComponent = require('requireNativeComponent'); var verifyPropTypes = require('verifyPropTypes'); -var SpinnerSize = keyMirror({ - large: null, - small: null, -}); - var GRAY = '#999999'; type DefaultProps = { animating: boolean; - size: 'small' | 'large'; color: string; + hidesWhenStopped: boolean; + size: 'small' | 'large'; }; var ActivityIndicatorIOS = React.createClass({ @@ -47,7 +42,10 @@ var ActivityIndicatorIOS = React.createClass({ * The foreground color of the spinner (default is gray). */ color: PropTypes.string, - + /** + * Whether the indicator should hide when not animating (true by default). + */ + hidesWhenStopped: PropTypes.bool, /** * Size of the indicator. Small has a height of 20, large has a height of 36. */ @@ -60,27 +58,18 @@ var ActivityIndicatorIOS = React.createClass({ getDefaultProps: function(): DefaultProps { return { animating: true, - size: SpinnerSize.small, color: GRAY, + hidesWhenStopped: true, + size: 'small', }; }, render: function() { - var style = styles.sizeSmall; - var NativeConstants = NativeModules.UIManager.UIActivityIndicatorView.Constants; - var activityIndicatorViewStyle = NativeConstants.StyleWhite; - if (this.props.size === 'large') { - style = styles.sizeLarge; - activityIndicatorViewStyle = NativeConstants.StyleWhiteLarge; - } + var {style, ...props} = this.props; + var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall; return ( - - + + ); } @@ -99,15 +88,15 @@ var styles = StyleSheet.create({ } }); -var UIActivityIndicatorView = requireNativeComponent( - 'UIActivityIndicatorView', +var RCTActivityIndicatorView = requireNativeComponent( + 'RCTActivityIndicatorView', null ); if (__DEV__) { var nativeOnlyProps = {activityIndicatorViewStyle: true}; verifyPropTypes( ActivityIndicatorIOS, - UIActivityIndicatorView.viewConfig, + RCTActivityIndicatorView.viewConfig, nativeOnlyProps ); } diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js new file mode 100644 index 00000000000000..28fbea0271dc11 --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js @@ -0,0 +1,49 @@ + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SegmentedControlIOS + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +var Dummy = React.createClass({ + render: function() { + return ( + + + SegmentedControlIOS is not supported on this platform! + + + ); + }, +}); + +var styles = StyleSheet.create({ + dummy: { + width: 120, + height: 50, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: '#333333', + margin: 5, + fontSize: 10, + } +}); + +module.exports = Dummy; diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js new file mode 100644 index 00000000000000..23d952776bb96e --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SegmentedControlIOS + * @flow + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); + +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); + +type DefaultProps = { + values: Array; + enabled: boolean; +}; + +var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol'; + +type Event = Object; + +/** + * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. + */ +var SegmentedControlIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The labels for the control's segment buttons, in order. + */ + values: PropTypes.arrayOf(PropTypes.string), + + /** + * The index in `props.values` of the segment to be pre-selected + */ + selectedIndex: PropTypes.number, + + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange: PropTypes.func, + + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange: PropTypes.func, + + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled: PropTypes.bool, + + /** + * Accent color of the control. + */ + tintColor: PropTypes.string, + + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary: PropTypes.bool + }, + + getDefaultProps: function(): DefaultProps { + return { + values: [], + enabled: true + }; + }, + + _onChange: function(event: Event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + segmentedControl: { + height: NativeModules.SegmentedControlManager.ComponentHeight + }, +}); + +var RCTSegmentedControl = requireNativeComponent( + 'RCTSegmentedControl', + null +); +if (__DEV__) { + verifyPropTypes( + RCTSegmentedControl, + RCTSegmentedControl.viewConfig + ); +} + +module.exports = SegmentedControlIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index 2baaee21b416c6..b27e22d4b1e92c 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -66,6 +66,9 @@ var TabBarItemIOS = React.createClass({ * blank content, you probably forgot to add a selected one. */ selected: React.PropTypes.bool, + /** + * React style object. + */ style: View.propTypes.style, /** * Text that appears under the icon. It is ignored when a system icon diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index dfd3ab1a128469..c21184b7da5130 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -38,6 +38,7 @@ var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType; var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { autoCorrect: true, autoCapitalize: true, + clearTextOnFocus: true, color: true, editable: true, fontFamily: true, @@ -48,6 +49,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { returnKeyType: true, enablesReturnKeyAutomatically: true, secureTextEntry: true, + selectTextOnFocus: true, mostRecentEventCounter: true, placeholder: true, placeholderTextColor: true, @@ -445,6 +447,7 @@ var TextInput = React.createClass({ onSubmitEditing={this.props.onSubmitEditing} onSelectionChangeShouldSetResponder={() => true} placeholder={this.props.placeholder} + placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} @@ -498,6 +501,8 @@ var TextInput = React.createClass({ autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} clearButtonMode={clearButtonMode} + selectTextOnFocus={this.props.selectTextOnFocus} + clearTextOnFocus={this.props.clearTextOnFocus} />; } diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 0da57f554257e5..c7ca2ee261ab91 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -29,7 +29,7 @@ var stylePropType = StyleSheetPropType(ViewStylePropTypes); * container that supports layout with flexbox, style, some touch handling, and * accessibility controls, and is designed to be nested inside other views and * to have 0 to many children of any type. `View` maps directly to the native - * view equivalent on whatever platform react is running on, whether that is a + * view equivalent on whatever platform React is running on, whether that is a * `UIView`, `
`, `android.view`, etc. This example creates a `View` that * wraps two colored boxes and custom component in a row with padding. * diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index bfc823f9f85928..959422bbc9888a 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -42,6 +42,7 @@ var WebView = React.createClass({ onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, + javaScriptEnabledAndroid: PropTypes.bool, /** * Used to locate this view in end-to-end tests. */ @@ -90,6 +91,7 @@ var WebView = React.createClass({ key="webViewKey" style={webViewStyles} url={this.props.url} + javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid} contentInset={this.props.contentInset} automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets} onLoadingStart={this.onLoadingStart} @@ -157,6 +159,7 @@ var WebView = React.createClass({ var RCTWebView = createReactIOSNativeComponentClass({ validAttributes: merge(ReactIOSViewAttributes.UIView, { url: true, + javaScriptEnabledAndroid: true, }), uiViewClassName: 'RCTWebView', }); diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index c4e4fbcd3299c8..ed2c98fae02f5a 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -92,6 +92,10 @@ var WebView = React.createClass({ onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, + /** + * Used for android only, JS is enabled by default for WebView on iOS + */ + javaScriptEnabledAndroid: PropTypes.bool, }, getInitialState: function() { diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index da4d2ab10f52ca..6d3d55c316f097 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -267,7 +267,8 @@ var Navigator = React.createClass({ }, contextTypes: { - navigator: PropTypes.object, + // TODO (t6707746) Re-enable this when owner context switches to parent context + // navigator: PropTypes.object, }, statics: { @@ -314,13 +315,11 @@ var Navigator = React.createClass({ // On first render, we will render every scene in the initialRouteStack updatingRangeStart: 0, updatingRangeLength: routeStack.length, - // Either animating or gesturing. - isAnimating: false, - jumpToIndex: routeStack.length - 1, presentedIndex: initialRouteIndex, - isResponderOnlyToBlockTouches: false, - fromIndex: initialRouteIndex, - toIndex: initialRouteIndex, + transitionFromIndex: null, + activeGesture: null, + pendingGestureProgress: null, + transitionQueue: [], }; }, @@ -355,9 +354,24 @@ var Navigator = React.createClass({ popToTop: this.popToTop, }; this._handlers = {}; - + this.springSystem = new rebound.SpringSystem(); + this.spring = this.springSystem.createSpring(); + this.spring.setRestSpeedThreshold(0.05); + this.spring.setCurrentValue(0).setAtRest(); + this.spring.addListener({ + onSpringEndStateChange: () => { + if (!this._interactionHandle) { + this._interactionHandle = this.createInteractionHandle(); + } + }, + onSpringUpdate: () => { + this._handleSpringUpdate(); + }, + onSpringAtRest: () => { + this._completeTransition(); + }, + }); this.panGesture = PanResponder.create({ - onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderRelease: this._handlePanResponderRelease, @@ -426,20 +440,8 @@ var Navigator = React.createClass({ this._handlers[this.state.routeStack.indexOf(route)] = handler; }, - _configureSpring: function(animationConfig) { - var config = this.spring.getSpringConfig(); - config.friction = animationConfig.springFriction; - config.tension = animationConfig.springTension; - }, - componentDidMount: function() { - this.springSystem = new rebound.SpringSystem(); - this.spring = this.springSystem.createSpring(); - this.spring.setRestSpeedThreshold(0.05); - var animationConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - animationConfig && this._configureSpring(animationConfig); - this.spring.addListener(this); - this.onSpringUpdate(); + this._handleSpringUpdate(); this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]); if (this.parentNavigator) { this.parentNavigator.setHandler(this._handleRequest); @@ -483,98 +485,113 @@ var Navigator = React.createClass({ updatingRangeStart: 0, updatingRangeLength: nextRouteStack.length, presentedIndex: destIndex, - jumpToIndex: destIndex, - toIndex: destIndex, - fromIndex: destIndex, + activeGesture: null, + transitionFromIndex: null, + transitionQueue: [], }, () => { - this.onSpringUpdate(); + this._handleSpringUpdate(); }); }, + _transitionTo: function(destIndex, velocity, jumpSpringTo, cb) { + if (destIndex === this.state.presentedIndex) { + return; + } + if (this.state.transitionFromIndex !== null) { + this.state.transitionQueue.push({ + destIndex, + velocity, + cb, + }); + return; + } + this.state.transitionFromIndex = this.state.presentedIndex; + this.state.presentedIndex = destIndex; + this.state.transitionCb = cb; + this._onAnimationStart(); + if (AnimationsDebugModule) { + AnimationsDebugModule.startRecordingFps(); + } + var sceneConfig = this.state.sceneConfigStack[this.state.transitionFromIndex] || + this.state.sceneConfigStack[this.state.presentedIndex]; + invariant( + sceneConfig, + 'Cannot configure scene at index ' + this.state.transitionFromIndex + ); + if (jumpSpringTo != null) { + this.spring.setCurrentValue(jumpSpringTo); + } + this.spring.setOvershootClampingEnabled(true); + this.spring.getSpringConfig().friction = sceneConfig.springFriction; + this.spring.getSpringConfig().tension = sceneConfig.springTension; + this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity); + this.spring.setEndValue(1); + var willFocusRoute = this._subRouteFocus[this.state.presentedIndex] || this.state.routeStack[this.state.presentedIndex]; + this._emitWillFocus(willFocusRoute); + }, + /** - * TODO: Accept callback for spring completion. + * This happens for each frame of either a gesture or a transition. If both are + * happening, we only set values for the transition and the gesture will catch up later */ - _requestTransitionTo: function(topOfStack) { - if (topOfStack !== this.state.presentedIndex) { - invariant(!this.state.isAnimating, 'Cannot navigate while transitioning'); - this.state.fromIndex = this.state.presentedIndex; - this.state.toIndex = topOfStack; - this.spring.setOvershootClampingEnabled(false); - if (AnimationsDebugModule) { - AnimationsDebugModule.startRecordingFps(); - } - this._transitionToToIndexWithVelocity( - this.state.sceneConfigStack[this.state.fromIndex].defaultTransitionVelocity + _handleSpringUpdate: function() { + // Prioritize handling transition in progress over a gesture: + if (this.state.transitionFromIndex != null) { + this._transitionBetween( + this.state.transitionFromIndex, + this.state.presentedIndex, + this.spring.getCurrentValue() + ); + } else if (this.state.activeGesture != null) { + this._transitionBetween( + this.state.presentedIndex, + this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture), + this.spring.getCurrentValue() ); } }, /** - * `onSpring*` spring delegate. Wired up via `spring.addListener(this)` + * This happens at the end of a transition started by transitionTo */ - onSpringEndStateChange: function() { - if (!this._interactionHandle) { - this._interactionHandle = this.createInteractionHandle(); + _completeTransition: function() { + if (this.spring.getCurrentValue() !== 1) { + if (this.state.pendingGestureProgress) { + this.state.pendingGestureProgress = null; + } + return; } - }, - - onSpringUpdate: function() { - this._transitionBetween( - this.state.fromIndex, - this.state.toIndex, - this.spring.getCurrentValue() - ); - }, - - onSpringAtRest: function() { - this.state.isAnimating = false; - this._completeTransition(); + this._onAnimationEnd(); + var presentedIndex = this.state.presentedIndex; + var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex]; + this._emitDidFocus(didFocusRoute); + if (AnimationsDebugModule) { + AnimationsDebugModule.stopRecordingFps(Date.now()); + } + this.state.transitionFromIndex = null; this.spring.setCurrentValue(0).setAtRest(); + this._hideScenes(); + if (this.state.transitionCb) { + this.state.transitionCb(); + this.state.transitionCb = null; + } if (this._interactionHandle) { this.clearInteractionHandle(this._interactionHandle); this._interactionHandle = null; } - }, - - _completeTransition: function() { - if (this.spring.getCurrentValue() === 1) { - this._onAnimationEnd(); - var presentedIndex = this.state.toIndex; - this.state.presentedIndex = presentedIndex; - this.state.fromIndex = presentedIndex; - var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex]; - this._emitDidFocus(didFocusRoute); - this._removePoppedRoutes(); - if (AnimationsDebugModule) { - AnimationsDebugModule.stopRecordingFps(Date.now()); - } - } else { - this.state.fromIndex = this.state.presentedIndex; - this.state.toIndex = this.state.presentedIndex; + if (this.state.pendingGestureProgress) { + this.spring.setEndValue(this.state.pendingGestureProgress); + return; + } + if (this.state.transitionQueue.length) { + var queuedTransition = this.state.transitionQueue.shift(); + this._transitionTo( + queuedTransition.destIndex, + queuedTransition.velocity, + null, + queuedTransition.cb + ); } - this._hideOtherScenes(this.state.presentedIndex); - }, - - _transitionToToIndexWithVelocity: function(v) { - this._configureSpring( - // For visual consistency, the from index is always used to configure the spring - this.state.sceneConfigStack[this.state.fromIndex] - ); - this._onAnimationStart(); - this.state.isAnimating = true; - this.spring.setVelocity(v); - this.spring.setEndValue(1); - var willFocusRoute = this._subRouteFocus[this.state.toIndex] || this.state.routeStack[this.state.toIndex]; - this._emitWillFocus(willFocusRoute); - }, - - _transitionToFromIndexWithVelocity: function(v) { - this._configureSpring( - this.state.sceneConfigStack[this.state.fromIndex] - ); - this.state.isAnimating = true; - this.spring.setVelocity(v); - this.spring.setEndValue(0); }, _emitDidFocus: function(route) { @@ -608,9 +625,11 @@ var Navigator = React.createClass({ /** * Does not delete the scenes - merely hides them. */ - _hideOtherScenes: function(activeIndex) { + _hideScenes: function() { for (var i = 0; i < this.state.routeStack.length; i++) { - if (i === activeIndex) { + // This gets called when we detach a gesture, so there will not be a + // current gesture, but there might be a transition in progress + if (i === this.state.presentedIndex || i === this.state.transitionFromIndex) { continue; } var sceneRef = 'scene_' + i; @@ -620,22 +639,31 @@ var Navigator = React.createClass({ }, _onAnimationStart: function() { - this._setRenderSceneToHarwareTextureAndroid(this.state.fromIndex, true); - this._setRenderSceneToHarwareTextureAndroid(this.state.toIndex, true); + var fromIndex = this.state.presentedIndex; + var toIndex = this.state.presentedIndex; + if (this.state.transitionFromIndex != null) { + fromIndex = this.state.transitionFromIndex; + } else if (this.state.activeGesture) { + toIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + } + this._setRenderSceneToHarwareTextureAndroid(fromIndex, true); + this._setRenderSceneToHarwareTextureAndroid(toIndex, true); var navBar = this._navBar; if (navBar && navBar.onAnimationStart) { - navBar.onAnimationStart(this.state.fromIndex, this.state.toIndex); + navBar.onAnimationStart(fromIndex, toIndex); } }, _onAnimationEnd: function() { - this._setRenderSceneToHarwareTextureAndroid(this.state.fromIndex, false); - this._setRenderSceneToHarwareTextureAndroid(this.state.toIndex, false); + var max = this.state.routeStack.length - 1; + for (var index = 0; index <= max; index++) { + this._setRenderSceneToHarwareTextureAndroid(index, false); + } var navBar = this._navBar; if (navBar && navBar.onAnimationEnd) { - navBar.onAnimationEnd(this.state.fromIndex, this.state.toIndex); + navBar.onAnimationEnd(); } }, @@ -647,16 +675,6 @@ var Navigator = React.createClass({ viewAtIndex.setNativeProps({renderToHardwareTextureAndroid: shouldRenderToHardwareTexture}); }, - /** - * Becomes the responder on touch start (capture) while animating so that it - * blocks all touch interactions inside of it. However, this responder lock - * means nothing more than that. We record if the sole reason for being - * responder is to block interactions (`isResponderOnlyToBlockTouches`). - */ - _handleStartShouldSetPanResponderCapture: function(e, gestureState) { - return this.state.isAnimating; - }, - _handleMoveShouldSetPanResponder: function(e, gestureState) { var currentRoute = this.state.routeStack[this.state.presentedIndex]; var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; @@ -674,18 +692,12 @@ var Navigator = React.createClass({ _handlePanResponderGrant: function(e, gestureState) { invariant( - this._expectingGestureGrant || this.state.isAnimating, + this._expectingGestureGrant, 'Responder granted unexpectedly.' ); - this._activeGestureAction = this._expectingGestureGrant; + this._attachGesture(this._expectingGestureGrant); + this._onAnimationStart(); this._expectingGestureGrant = null; - this.state.isResponderOnlyToBlockTouches = this.state.isAnimating; - if (!this.state.isAnimating) { - this.state.fromIndex = this.state.presentedIndex; - var gestureSceneDelta = this._deltaForGestureAction(this._activeGestureAction); - this.state.toIndex = this.state.presentedIndex + gestureSceneDelta; - this._onAnimationStart(); - } }, _deltaForGestureAction: function(gestureAction) { @@ -703,13 +715,13 @@ var Navigator = React.createClass({ _handlePanResponderRelease: function(e, gestureState) { var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - var releaseGestureAction = this._activeGestureAction; - this._activeGestureAction = null; - if (this.state.isResponderOnlyToBlockTouches) { - this.state.isResponderOnlyToBlockTouches = false; + var releaseGestureAction = this.state.activeGesture; + if (!releaseGestureAction) { + // The gesture may have been detached while responder, so there is no action here return; } var releaseGesture = sceneConfig.gestures[releaseGestureAction]; + var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); if (this.spring.getCurrentValue() === 0) { // The spring is at zero, so the gesture is already complete this.spring.setCurrentValue(0).setAtRest(); @@ -732,38 +744,86 @@ var Navigator = React.createClass({ var hasGesturedEnoughToComplete = gestureDistance > releaseGesture.fullDistance * releaseGesture.stillCompletionRatio; transitionVelocity = hasGesturedEnoughToComplete ? releaseGesture.snapVelocity : -releaseGesture.snapVelocity; } - this.spring.setOvershootClampingEnabled(true); if (transitionVelocity < 0 || this._doesGestureOverswipe(releaseGestureAction)) { - this._transitionToFromIndexWithVelocity(transitionVelocity); + // This gesture is to an overswiped region or does not have enough velocity to complete + // If we are currently mid-transition, then this gesture was a pending gesture. Because this gesture takes no action, we can stop here + if (this.state.transitionFromIndex == null) { + // There is no current transition, so we need to transition back to the presented index + var transitionBackToPresentedIndex = this.state.presentedIndex; + // slight hack: change the presented index for a moment in order to transitionTo correctly + this.state.presentedIndex = destIndex; + this._transitionTo( + transitionBackToPresentedIndex, + - transitionVelocity, + 1 - this.spring.getCurrentValue() + ); + } } else { - this._transitionToToIndexWithVelocity(transitionVelocity); + // The gesture has enough velocity to complete, so we transition to the gesture's destination + this._transitionTo(destIndex, transitionVelocity); } + this._detachGesture(); }, _handlePanResponderTerminate: function(e, gestureState) { - this._activeGestureAction = null; - this.state.isResponderOnlyToBlockTouches = false; - this._transitionToFromIndexWithVelocity(0); + var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._detachGesture(); + var transitionBackToPresentedIndex = this.state.presentedIndex; + // slight hack: change the presented index for a moment in order to transitionTo correctly + this.state.presentedIndex = destIndex; + this._transitionTo( + transitionBackToPresentedIndex, + null, + 1 - this.spring.getCurrentValue() + ); + }, + + _attachGesture: function(gestureId) { + this.state.activeGesture = gestureId; + }, + + _detachGesture: function() { + this.state.activeGesture = null; + this.state.pendingGestureProgress = null; + this._hideScenes(); }, _handlePanResponderMove: function(e, gestureState) { - if (!this.state.isResponderOnlyToBlockTouches) { - var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - var gesture = sceneConfig.gestures[this._activeGestureAction]; - var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top'; - var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top'; - var distance = isTravelVertical ? gestureState.dy : gestureState.dx; - distance = isTravelInverted ? - distance : distance; - var gestureDetectMovement = gesture.gestureDetectMovement; - var nextProgress = (distance - gestureDetectMovement) / - (gesture.fullDistance - gestureDetectMovement); - if (this._doesGestureOverswipe(this._activeGestureAction)) { - var frictionConstant = gesture.overswipe.frictionConstant; - var frictionByDistance = gesture.overswipe.frictionByDistance; - var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance)); - nextProgress *= frictionRatio; - } - this.spring.setCurrentValue(clamp(0, nextProgress, 1)); + var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; + if (this.state.activeGesture) { + var gesture = sceneConfig.gestures[this.state.activeGesture]; + return this._moveAttachedGesture(gesture, gestureState); + } + var matchedGesture = this._matchGestureAction(sceneConfig.gestures, gestureState); + if (matchedGesture) { + this._attachGesture(matchedGesture); + } + }, + + _moveAttachedGesture: function(gesture, gestureState) { + var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top'; + var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top'; + var distance = isTravelVertical ? gestureState.dy : gestureState.dx; + distance = isTravelInverted ? - distance : distance; + var gestureDetectMovement = gesture.gestureDetectMovement; + var nextProgress = (distance - gestureDetectMovement) / + (gesture.fullDistance - gestureDetectMovement); + if (nextProgress < 0 && gesture.isDetachable) { + this._detachGesture(); + } + if (this._doesGestureOverswipe(this.state.activeGesture)) { + var frictionConstant = gesture.overswipe.frictionConstant; + var frictionByDistance = gesture.overswipe.frictionByDistance; + var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance)); + nextProgress *= frictionRatio; + } + nextProgress = clamp(0, nextProgress, 1); + if (this.state.transitionFromIndex != null) { + this.state.pendingGestureProgress = nextProgress; + } else if (this.state.pendingGestureProgress) { + this.spring.setEndValue(nextProgress); + } else { + this.spring.setCurrentValue(nextProgress); } }, @@ -771,9 +831,6 @@ var Navigator = React.createClass({ if (!gestures) { return null; } - if (this.state.isResponderOnlyToBlockTouches || this.state.isAnimating) { - return null; - } var matchedGesture = null; GESTURE_ACTIONS.some((gestureName) => { var gesture = gestures[gestureName]; @@ -814,7 +871,7 @@ var Navigator = React.createClass({ return; } // Use toIndex animation when we move forwards. Use fromIndex when we move back - var sceneConfigIndex = this.state.presentedIndex < toIndex ? toIndex : fromIndex; + var sceneConfigIndex = fromIndex < toIndex ? toIndex : fromIndex; var sceneConfig = this.state.sceneConfigStack[sceneConfigIndex]; // this happens for overswiping when there is no scene at toIndex if (!sceneConfig) { @@ -849,10 +906,6 @@ var Navigator = React.createClass({ this.state.updatingRangeLength = this.state.routeStack.length; }, - _canNavigate: function() { - return !this.state.isAnimating; - }, - _getDestIndexWithinBounds: function(n) { var currentIndex = this.state.presentedIndex; var destIndex = currentIndex + n; @@ -870,17 +923,13 @@ var Navigator = React.createClass({ _jumpN: function(n) { var destIndex = this._getDestIndexWithinBounds(n); - if (!this._canNavigate()) { - return; // It's busy animating or transitioning. - } var requestTransitionAndResetUpdatingRange = () => { - this._requestTransitionTo(destIndex); + this._transitionTo(destIndex); this._resetUpdatingRange(); }; this.setState({ updatingRangeStart: destIndex, updatingRangeLength: 1, - toIndex: destIndex, }, requestTransitionAndResetUpdatingRange); }, @@ -903,9 +952,6 @@ var Navigator = React.createClass({ push: function(route) { invariant(!!route, 'Must supply route to push'); - if (!this._canNavigate()) { - return; // It's busy animating or transitioning. - } var activeLength = this.state.presentedIndex + 1; var activeStack = this.state.routeStack.slice(0, activeLength); var activeIDStack = this.state.idStack.slice(0, activeLength); @@ -916,34 +962,34 @@ var Navigator = React.createClass({ this.props.configureScene(route), ]); var requestTransitionAndResetUpdatingRange = () => { - this._requestTransitionTo(nextStack.length - 1); + this._transitionTo(nextStack.length - 1); this._resetUpdatingRange(); }; - var navigationState = { - toRoute: route, - fromRoute: this.state.routeStack[this.state.routeStack.length - 1], - }; this.setState({ idStack: nextIDStack, routeStack: nextStack, sceneConfigStack: nextAnimationConfigStack, - jumpToIndex: nextStack.length - 1, updatingRangeStart: nextStack.length - 1, updatingRangeLength: 1, }, requestTransitionAndResetUpdatingRange); }, _popN: function(n) { - if (n === 0 || !this._canNavigate()) { + if (n === 0) { return; } invariant( this.state.presentedIndex - n >= 0, 'Cannot pop below zero' ); - this.state.jumpToIndex = this.state.presentedIndex - n; - this._requestTransitionTo( - this.state.presentedIndex - n + var popIndex = this.state.presentedIndex - n; + this._transitionTo( + popIndex, + null, // default velocity + null, // no spring jumping + () => { + this._cleanScenesPastIndex(popIndex); + } ); }, @@ -957,7 +1003,7 @@ var Navigator = React.createClass({ * `index` specifies the route in the stack that should be replaced. * If it's negative, it counts from the back. */ - replaceAtIndex: function(route, index) { + replaceAtIndex: function(route, index, cb) { invariant(!!route, 'Must supply route to replace'); if (index < 0) { index += this.state.routeStack.length; @@ -988,6 +1034,7 @@ var Navigator = React.createClass({ this._emitWillFocus(route); this._emitDidFocus(route); } + cb && cb(); }); }, @@ -1024,7 +1071,7 @@ var Navigator = React.createClass({ }, replacePreviousAndPop: function(route) { - if (this.state.routeStack.length < 2 || !this._canNavigate()) { + if (this.state.routeStack.length < 2) { return; } this.replacePrevious(route); @@ -1033,10 +1080,9 @@ var Navigator = React.createClass({ resetTo: function(route) { invariant(!!route, 'Must supply route to push'); - if (this._canNavigate()) { - this.replaceAtIndex(route, 0); + this.replaceAtIndex(route, 0, () => { this.popToRoute(route); - } + }); }, getCurrentRoutes: function() { @@ -1052,8 +1098,8 @@ var Navigator = React.createClass({ this.props.onItemRef && this.props.onItemRef(ref, itemIndex); }, - _removePoppedRoutes: function() { - var newStackLength = this.state.jumpToIndex + 1; + _cleanScenesPastIndex: function(index) { + var newStackLength = index + 1; // Remove any unneeded rendered routes. if (newStackLength < this.state.routeStack.length) { var updatingRangeStart = newStackLength; // One past the top diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js index fa62b3f455baf6..a43f2dafdebba9 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js +++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js @@ -33,6 +33,8 @@ var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); var View = require('View'); +var invariant = require('invariant'); + var Interpolators = NavigatorBreadcrumbNavigationBarStyles.Interpolators; var PropTypes = React.PropTypes; @@ -99,6 +101,10 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ var oldDistToCenter = index - fromIndex; var newDistToCenter = index - toIndex; var interpolate; + invariant( + Interpolators[index], + 'Cannot find breadcrumb interpolators for ' + index + ); if (oldDistToCenter > 0 && newDistToCenter === 0 || newDistToCenter > 0 && oldDistToCenter === 0) { interpolate = Interpolators[index].RightToCenter; @@ -146,10 +152,9 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ } }, - onAnimationEnd: function(fromIndex, toIndex) { - var max = Math.max(fromIndex, toIndex); - var min = Math.min(fromIndex, toIndex); - for (var index = min; index <= max; index++) { + onAnimationEnd: function() { + var max = this.props.navState.routeStack.length - 1; + for (var index = 0; index <= max; index++) { this._setRenderViewsToHardwareTextureAndroid(index, false); } }, diff --git a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js index ac16542adc2ea7..3b4666b9388ce5 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js +++ b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js @@ -101,6 +101,30 @@ var FadeToTheLeft = { }, }; +var FadeIn = { + opacity: { + from: 0, + to: 1, + min: 0.5, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, +}; + +var FadeOut = { + opacity: { + from: 1, + to: 0, + min: 0, + max: 0.5, + type: 'linear', + extrapolate: false, + round: 100, + }, +}; + var ToTheLeft = { transformTranslate: { from: {x: 0, y: 0, z: 0}, @@ -115,8 +139,17 @@ var ToTheLeft = { value: 1.0, type: 'constant', }, -}; + translateX: { + from: 0, + to: -Dimensions.get('window').width, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, +}; var FromTheRight = { opacity: { @@ -236,6 +269,43 @@ var FromTheFront = { }, }; +var ToTheBackAndroid = { + opacity: { + value: 1, + type: 'constant', + }, +}; + +var FromTheFrontAndroid = { + opacity: { + from: 0, + to: 1, + min: 0, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, + transformTranslate: { + from: {x: 0, y: 50, z: 0}, + to: {x: 0, y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + translateY: { + from: 50, + to: 0, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, +}; + var BaseOverswipeConfig = { frictionConstant: 1, frictionByDistance: 1.5, @@ -243,6 +313,9 @@ var BaseOverswipeConfig = { var BaseLeftToRightGesture = { + // If the gesture can end and restart during one continuous touch + isDetachable: false, + // How far the swipe must drag to start transitioning gestureDetectMovement: 2, @@ -316,6 +389,22 @@ var NavigatorSceneConfigs = { out: buildStyleInterpolator(ToTheBack), }, }, + FloatFromBottomAndroid: { + ...BaseConfig, + gestures: null, + animationInterpolators: { + into: buildStyleInterpolator(FromTheFrontAndroid), + out: buildStyleInterpolator(ToTheBackAndroid), + }, + }, + FadeAndroid: { + ...BaseConfig, + gestures: null, + animationInterpolators: { + into: buildStyleInterpolator(FadeIn), + out: buildStyleInterpolator(FadeOut), + }, + }, HorizontalSwipeJump: { ...BaseConfig, gestures: { @@ -323,11 +412,13 @@ var NavigatorSceneConfigs = { ...BaseLeftToRightGesture, overswipe: BaseOverswipeConfig, edgeHitWidth: null, + isDetachable: true, }, jumpForward: { ...BaseRightToLeftGesture, overswipe: BaseOverswipeConfig, edgeHitWidth: null, + isDetachable: true, }, }, animationInterpolators: { diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index 13fe40a2364447..fae309aef5fe68 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -22,6 +22,12 @@ var subscriptions = []; var updatesEnabled = false; +type GeoOptions = { + timeout: number; + maximumAge: number; + enableHighAccuracy: bool; +} + /** * You need to include the `NSLocationWhenInUseUsageDescription` key * in Info.plist to enable geolocation. Geolocation is enabled by default @@ -32,10 +38,14 @@ var updatesEnabled = false; */ var Geolocation = { + /* + * Invokes the success callback once with the latest location info. Supported + * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool) + */ getCurrentPosition: function( geo_success: Function, geo_error?: Function, - geo_options?: Object + geo_options?: GeoOptions ) { invariant( typeof geo_success === 'function', @@ -48,7 +58,11 @@ var Geolocation = { ); }, - watchPosition: function(success: Function, error?: Function, options?: Object): number { + /* + * Invokes the success callback whenever the location changes. Supported + * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool) + */ + watchPosition: function(success: Function, error?: Function, options?: GeoOptions): number { if (!updatesEnabled) { RCTLocationObserver.startObserving(options || {}); updatesEnabled = true; diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 5d56caccbe314b..3e864657b82c5d 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -163,12 +163,12 @@ - (void)timeout:(NSTimer *)timer #pragma mark - Public API -RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON) +RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options) { [self checkLocationConfig]; // Select best options - _observerOptions = [RCTConvert RCTLocationOptions:optionsJSON]; + _observerOptions = options; for (RCTLocationRequest *request in _pendingRequests) { _observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy); } @@ -189,7 +189,7 @@ - (void)timeout:(NSTimer *)timer } } -RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON +RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options withSuccessCallback:(RCTResponseSenderBlock)successBlock errorCallback:(RCTResponseSenderBlock)errorBlock) { @@ -219,7 +219,6 @@ - (void)timeout:(NSTimer *)timer } // Check if previous recorded location exists and is good enough - RCTLocationOptions options = [RCTConvert RCTLocationOptions:optionsJSON]; if (_lastLocationEvent && CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge && [_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) { diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index e917b6b637355b..a41352f44ce4b0 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -31,7 +31,7 @@ var verifyPropTypes = require('verifyPropTypes'); var warning = require('warning'); /** - * A react component for displaying different types of images, + * A React component for displaying different types of images, * including network images, static resources, temporary local images, and * images from local disk, such as the camera roll. * diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 26f3c9ea32e999..632ff96567ee1c 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -32,6 +32,12 @@ describe('resolveAssetSource', () => { expect(resolveAssetSource(source2)).toBe(source2); }); + it('ignores any weird data', () => { + expect(resolveAssetSource(null)).toBe(null); + expect(resolveAssetSource(42)).toBe(null); + expect(resolveAssetSource('nonsense')).toBe(null); + }); + describe('bundle was loaded from network', () => { beforeEach(() => { SourceCode.scriptURL = 'http://10.0.0.1:8081/main.bundle'; diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index da136e9a73d583..02189155877006 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -43,10 +43,11 @@ function pickScale(scales, deviceScale) { return scales[scales.length - 1] || 1; } -// TODO(frantic): -// * Pick best scale and append @Nx to file path -// * We are currently using httpServerLocation for both http and in-app bundle function resolveAssetSource(source) { + if (!source || typeof source !== 'object') { + return null; + } + if (!source.__packager_asset) { return source; } diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index c5476eaab5cfe3..b5868139c7c135 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -16,6 +16,7 @@ var RCTExceptionsManager = require('NativeModules').ExceptionsManager; var loadSourceMap = require('loadSourceMap'); var parseErrorStack = require('parseErrorStack'); +var stringifySafe = require('stringifySafe'); var sourceMapPromise; @@ -25,17 +26,11 @@ type Exception = { message: string; } -function handleException(e: Exception) { - var stack = parseErrorStack(e); - console.error( - 'Err0r: ' + - '\n stack: \n' + stackToString(stack) + - '\n URL: ' + e.sourceURL + - '\n line: ' + e.line + - '\n message: ' + e.message - ); - +function reportException(e: Exception, stack?: any) { if (RCTExceptionsManager) { + if (!stack) { + stack = parseErrorStack(e); + } RCTExceptionsManager.reportUnhandledException(e.message, stack); if (__DEV__) { (sourceMapPromise = sourceMapPromise || loadSourceMap()) @@ -50,6 +45,47 @@ function handleException(e: Exception) { } } +function handleException(e: Exception) { + var stack = parseErrorStack(e); + var msg = + 'Error: ' + e.message + + '\n stack: \n' + stackToString(stack) + + '\n URL: ' + e.sourceURL + + '\n line: ' + e.line + + '\n message: ' + e.message; + if (console.errorOriginal) { + console.errorOriginal(msg); + } else { + console.error(msg); + } + reportException(e, stack); +} + +/** + * Shows a redbox with stacktrace for all console.error messages. Disable by + * setting `console.reportErrorsAsExceptions = false;` in your app. + */ +function installConsoleErrorReporter() { + if (console.reportException) { + return; // already installed + } + console.reportException = reportException; + console.errorOriginal = console.error.bind(console); + console.error = function reactConsoleError() { + console.errorOriginal.apply(null, arguments); + if (!console.reportErrorsAsExceptions) { + return; + } + var str = Array.prototype.map.call(arguments, stringifySafe).join(', '); + var error: any = new Error('console.error: ' + str); + error.framesToPop = 1; + reportException(error); + }; + if (console.reportErrorsAsExceptions === undefined) { + console.reportErrorsAsExceptions = true; // Individual apps can disable this + } +} + function stackToString(stack) { var maxLength = Math.max.apply(null, stack.map(frame => frame.methodName.length)); return stack.map(frame => stackFrameToString(frame, maxLength)).join('\n'); @@ -71,4 +107,4 @@ function fillSpaces(n) { return new Array(n + 1).join(' '); } -module.exports = { handleException }; +module.exports = { handleException, installConsoleErrorReporter }; diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 634d325e913359..1d0a793de8e454 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -61,7 +61,7 @@ @implementation RCTDataManager responseJSON = @{ @"status": @0, @"responseHeaders": @{}, - @"responseText": [connectionError localizedDescription] + @"responseText": [connectionError localizedDescription] ?: [NSNull null] }; } diff --git a/Libraries/ReactIOS/ReactIOSComponentMixin.js b/Libraries/ReactIOS/ReactIOSComponentMixin.js index 94c708a433e863..03d7f5ad04fc3b 100644 --- a/Libraries/ReactIOS/ReactIOSComponentMixin.js +++ b/Libraries/ReactIOS/ReactIOSComponentMixin.js @@ -46,7 +46,7 @@ var ReactInstanceMap = require('ReactInstanceMap'); * * `mountImage`: A way to represent the potential to create lower level * resources whos `nodeHandle` can be discovered immediately by knowing the - * `rootNodeID`. Today's web react represents this with `innerHTML` annotated + * `rootNodeID`. Today's web React represents this with `innerHTML` annotated * with DOM ids that match the `rootNodeID`. * * Opaque name TodaysWebReact FutureWebWorkerReact ReactNative diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js index ab46d6fe9d3144..9b1428fdd6d27a 100644 --- a/Libraries/ReactIOS/ReactIOSMount.js +++ b/Libraries/ReactIOS/ReactIOSMount.js @@ -191,7 +191,7 @@ var ReactIOSMount = { * that has been rendered and unmounting it. There should just be one child * component at this time. */ - unmountComponentAtNode: function(containerTag: number): bool { + unmountComponentAtNode: function(containerTag: number): boolean { if (!ReactIOSTagHandles.reactTagIsNativeTopRootID(containerTag)) { console.error('You cannot render into anything but a top root'); return false; diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js new file mode 100644 index 00000000000000..37076ef5c299fe --- /dev/null +++ b/Libraries/ReactIOS/WarningBox.js @@ -0,0 +1,398 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule WarningBox + */ +'use strict'; + +var AsyncStorage = require('AsyncStorage'); +var EventEmitter = require('EventEmitter'); +var Map = require('Map'); +var PanResponder = require('PanResponder'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var TouchableOpacity = require('TouchableOpacity'); +var View = require('View'); + +var invariant = require('invariant'); +var rebound = require('rebound'); +var stringifySafe = require('stringifySafe'); + +var SCREEN_WIDTH = require('Dimensions').get('window').width; +var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED'; + +var consoleWarn = console.warn.bind(console); + +var warningCounts = new Map(); +var ignoredWarnings: Array = []; +var totalWarningCount = 0; +var warningCountEvents = new EventEmitter(); + +/** + * WarningBox renders warnings on top of the app being developed. Warnings help + * guard against subtle yet significant issues that can impact the quality of + * your application, such as accessibility and memory leaks. This "in your + * face" style of warning allows developers to notice and correct these issues + * as quickly as possible. + * + * The warning box is currently opt-in. Set the following flag to enable it: + * + * `console.yellowBoxEnabled = true;` + * + * If "ignore" is tapped on a warning, the WarningBox will record that warning + * and will not display it again. This is useful for hiding errors that already + * exist or have been introduced elsewhere. To re-enable all of the errors, set + * the following: + * + * `console.yellowBoxResetIgnored = true;` + * + * This can also be set permanently, and ignore will only silence the warnings + * until the next refresh. + */ + +if (__DEV__) { + console.warn = function() { + consoleWarn.apply(null, arguments); + if (!console.yellowBoxEnabled) { + return; + } + var warning = Array.prototype.map.call(arguments, stringifySafe).join(' '); + if (!console.yellowBoxResetIgnored && + ignoredWarnings.indexOf(warning) !== -1) { + return; + } + var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1; + warningCounts.set(warning, count); + totalWarningCount += 1; + warningCountEvents.emit('count', totalWarningCount); + }; +} + +function saveIgnoredWarnings() { + AsyncStorage.setItem( + IGNORED_WARNINGS_KEY, + JSON.stringify(ignoredWarnings), + function(err) { + if (err) { + console.warn('Could not save ignored warnings.', err); + } + } + ); +} + +AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) { + if (!err && data && !console.yellowBoxResetIgnored) { + ignoredWarnings = JSON.parse(data); + } +}); + +var WarningRow = React.createClass({ + componentWillMount: function() { + this.springSystem = new rebound.SpringSystem(); + this.dismissalSpring = this.springSystem.createSpring(); + this.dismissalSpring.setRestSpeedThreshold(0.05); + this.dismissalSpring.setCurrentValue(0); + this.dismissalSpring.addListener({ + onSpringUpdate: () => { + var val = this.dismissalSpring.getCurrentValue(); + this.text && this.text.setNativeProps({ + left: SCREEN_WIDTH * val, + }); + this.container && this.container.setNativeProps({ + opacity: 1 - val, + }); + this.closeButton && this.closeButton.setNativeProps({ + opacity: 1 - (val * 5), + }); + }, + onSpringAtRest: () => { + if (this.dismissalSpring.getCurrentValue()) { + this.collapseSpring.setEndValue(1); + } + }, + }); + this.collapseSpring = this.springSystem.createSpring(); + this.collapseSpring.setRestSpeedThreshold(0.05); + this.collapseSpring.setCurrentValue(0); + this.collapseSpring.getSpringConfig().friction = 20; + this.collapseSpring.getSpringConfig().tension = 200; + this.collapseSpring.addListener({ + onSpringUpdate: () => { + var val = this.collapseSpring.getCurrentValue(); + this.container && this.container.setNativeProps({ + height: Math.abs(46 - (val * 46)), + }); + }, + onSpringAtRest: () => { + this.props.onDismissed(); + }, + }); + this.panGesture = PanResponder.create({ + onStartShouldSetPanResponder: () => { + return !! this.dismissalSpring.getCurrentValue(); + }, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + this.isResponderOnlyToBlockTouches = + !! this.dismissalSpring.getCurrentValue(); + }, + onPanResponderMove: (e, gestureState) => { + if (this.isResponderOnlyToBlockTouches) { + return; + } + this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH); + }, + onPanResponderRelease: (e, gestureState) => { + if (this.isResponderOnlyToBlockTouches) { + return; + } + var gestureCompletion = gestureState.dx / SCREEN_WIDTH; + var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5; + this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0); + } + }); + }, + render: function() { + var countText; + if (warningCounts.get(this.props.warning) > 1) { + countText = ( + + ({warningCounts.get(this.props.warning)}){" "} + + ); + } + return ( + { this.container = container; }} + {...this.panGesture.panHandlers}> + + + { this.text = text; }}> + {countText} + {this.props.warning} + + + + { this.closeButton = closeButton; }} + style={styles.closeButton}> + { + this.dismissalSpring.setEndValue(1); + }}> + ✕ + + + + ); + } +}); + +var WarningBoxOpened = React.createClass({ + render: function() { + var countText; + if (warningCounts.get(this.props.warning) > 1) { + countText = ( + + ({warningCounts.get(this.props.warning)}){" "} + + ); + } + return ( + + + + {countText} + {this.props.warning} + + + + + + Dismiss + + + + + + + Ignore + + + + + + + ); + }, +}); + +var canMountWarningBox = true; + +var WarningBox = React.createClass({ + getInitialState: function() { + return { + totalWarningCount, + openWarning: null, + }; + }, + componentWillMount: function() { + if (console.yellowBoxResetIgnored) { + AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) { + if (err) { + console.warn('Could not reset ignored warnings.', err); + } + }); + ignoredWarnings = []; + } + }, + componentDidMount: function() { + invariant( + canMountWarningBox, + 'There can only be one WarningBox' + ); + canMountWarningBox = false; + warningCountEvents.addListener( + 'count', + this._onWarningCount + ); + }, + componentWillUnmount: function() { + warningCountEvents.removeAllListeners(); + canMountWarningBox = true; + }, + _onWarningCount: function(totalWarningCount) { + // Must use setImmediate because warnings often happen during render and + // state cannot be set while rendering + setImmediate(() => { + this.setState({ totalWarningCount, }); + }); + }, + _onDismiss: function(warning) { + warningCounts.delete(warning); + this.setState({ + openWarning: null, + }); + }, + render: function() { + if (warningCounts.size === 0) { + return ; + } + if (this.state.openWarning) { + return ( + { + this.setState({ openWarning: null }); + }} + onDismissed={this._onDismiss.bind(this, this.state.openWarning)} + onIgnored={() => { + ignoredWarnings.push(this.state.openWarning); + saveIgnoredWarnings(); + this._onDismiss(this.state.openWarning); + }} + /> + ); + } + var warningRows = []; + warningCounts.forEach((count, warning) => { + warningRows.push( + { + this.setState({ openWarning: warning }); + }} + onDismissed={this._onDismiss.bind(this, warning)} + warning={warning} + /> + ); + }); + return ( + + {warningRows} + + ); + }, +}); + +var styles = StyleSheet.create({ + bold: { + fontWeight: 'bold', + }, + closeButton: { + position: 'absolute', + right: 0, + height: 46, + width: 46, + }, + closeButtonText: { + color: 'white', + fontSize: 32, + position: 'relative', + left: 8, + }, + warningContainer: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0 + }, + warningBox: { + position: 'relative', + backgroundColor: 'rgba(171, 124, 36, 0.9)', + flex: 1, + height: 46, + }, + warningText: { + color: 'white', + position: 'absolute', + left: 0, + marginLeft: 15, + marginRight: 46, + top: 7, + }, + yellowBox: { + backgroundColor: 'rgba(171, 124, 36, 0.9)', + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + padding: 15, + paddingTop: 35, + }, + yellowBoxText: { + color: 'white', + fontSize: 20, + }, + yellowBoxButtons: { + flexDirection: 'row', + position: 'absolute', + bottom: 0, + }, + yellowBoxButton: { + flex: 1, + padding: 25, + }, + yellowBoxButtonText: { + color: 'white', + fontSize: 16, + } +}); + +module.exports = WarningBox; diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 084390ac50057d..39e5720f520eb9 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -11,7 +11,12 @@ */ 'use strict'; +require('ExceptionsManager').installConsoleErrorReporter(); + var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); +var WarningBox = require('WarningBox'); var invariant = require('invariant'); @@ -24,12 +29,27 @@ function renderApplication( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); + var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled; + var warningBox = shouldRenderWarningBox ? : null; React.render( - , + + + {warningBox} + , rootTag ); } +var styles = StyleSheet.create({ + appContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + module.exports = renderApplication; diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 3c4bcf5bae846f..224c7e6b97fa7e 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */; }; + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; }; 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; }; 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; }; 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; @@ -27,6 +29,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextView.h; sourceTree = ""; }; + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = ""; }; + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = ""; }; + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = ""; }; 58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; }; 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; }; @@ -64,6 +70,10 @@ 58B512141A9E6EFF00147676 /* RCTText.m */, 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */, 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */, + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */, + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */, + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */, + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */, 58B5119C1A9E6C1200147676 /* Products */, ); indentWidth = 2; @@ -135,8 +145,10 @@ buildActionMask = 2147483647; files = ( 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */, 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, ); diff --git a/Libraries/Text/RCTTextManager.h b/Libraries/Text/RCTTextManager.h index 13e8f854642da4..91ac87ba88fdbc 100644 --- a/Libraries/Text/RCTTextManager.h +++ b/Libraries/Text/RCTTextManager.h @@ -12,4 +12,3 @@ @interface RCTTextManager : RCTViewManager @end - diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index e5e9ad00a1ad23..ef518d20483d70 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -123,7 +123,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowVie UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTText *text = (RCTText *)viewRegistry[reactTag]; + RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; text.layoutManager = shadowView.layoutManager; text.textContainer = shadowView.textContainer; diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h new file mode 100644 index 00000000000000..19f2fea397b8c5 --- /dev/null +++ b/Libraries/Text/RCTTextView.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTView.h" +#import "UIView+React.h" + +@class RCTEventDispatcher; + +@interface RCTTextView : RCTView + +@property (nonatomic, assign) BOOL autoCorrect; +@property (nonatomic, assign) BOOL clearTextOnFocus; +@property (nonatomic, assign) BOOL selectTextOnFocus; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, strong) UIColor *placeholderTextColor; +@property (nonatomic, assign) UIFont *font; + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m new file mode 100644 index 00000000000000..c5947f317e0bd8 --- /dev/null +++ b/Libraries/Text/RCTTextView.m @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTTextView.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTUtils.h" +#import "UIView+React.h" + +@implementation RCTTextView +{ + RCTEventDispatcher *_eventDispatcher; + BOOL _jsRequestingFirstResponder; + NSString *_placeholder; + UITextView *_placeholderView; + UITextView *_textView; +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _contentInset = UIEdgeInsetsZero; + _eventDispatcher = eventDispatcher; + _placeholderTextColor = [self defaultPlaceholderTextColor]; + + _textView = [[UITextView alloc] initWithFrame:self.bounds]; + _textView.backgroundColor = [UIColor clearColor]; + _textView.delegate = self; + [self addSubview:_textView]; + } + + return self; +} + +- (void)updateFrames +{ + // Adjust the insets so that they are as close as possible to single-line + // RCTTextField defaults + UIEdgeInsets adjustedInset = (UIEdgeInsets){ + _contentInset.top - 5, _contentInset.left - 4, + _contentInset.bottom, _contentInset.right + }; + + [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; + [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; +} + +- (void)updatePlaceholder +{ + [_placeholderView removeFromSuperview]; + _placeholderView = nil; + + if (_placeholder) { + _placeholderView = [[UITextView alloc] initWithFrame:self.bounds]; + _placeholderView.backgroundColor = [UIColor clearColor]; + _placeholderView.scrollEnabled = false; + _placeholderView.attributedText = + [[NSAttributedString alloc] initWithString:_placeholder attributes:@{ + NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]), + NSForegroundColorAttributeName : _placeholderTextColor + }]; + + [self insertSubview:_placeholderView belowSubview:_textView]; + [self _setPlaceholderVisibility]; + } +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + _textView.font = _font; + [self updatePlaceholder]; +} + +- (void)setTextColor:(UIColor *)textColor +{ + _textView.textColor = textColor; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + _placeholder = placeholder; + [self updatePlaceholder]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + if (placeholderTextColor) { + _placeholderTextColor = placeholderTextColor; + } else { + _placeholderTextColor = [self defaultPlaceholderTextColor]; + } + [self updatePlaceholder]; +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _contentInset = contentInset; + [self updateFrames]; +} + +- (void)setText:(NSString *)text +{ + if (![text isEqualToString:_textView.text]) { + [_textView setText:text]; + [self _setPlaceholderVisibility]; + } +} + +- (void)_setPlaceholderVisibility +{ + if (_textView.text.length > 0) { + [_placeholderView setHidden:YES]; + } else { + [_placeholderView setHidden:NO]; + } +} + +- (void)setAutoCorrect:(BOOL)autoCorrect +{ + _textView.autocorrectionType = (autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo); +} + +- (BOOL)autoCorrect +{ + return _textView.autocorrectionType == UITextAutocorrectionTypeYes; +} + +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + if (_selectTextOnFocus) { + dispatch_async(dispatch_get_main_queue(), ^{ + [textView selectAll:nil]; + }); + } + return YES; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + if (_clearTextOnFocus) { + [_textView setText:@""]; + _textView.text = @""; + [self _setPlaceholderVisibility]; + } + + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:textView.text]; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + [self _setPlaceholderVisibility]; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange + reactTag:self.reactTag + text:textView.text]; + +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd + reactTag:self.reactTag + text:textView.text]; +} + +- (BOOL)becomeFirstResponder +{ + _jsRequestingFirstResponder = YES; + BOOL result = [_textView becomeFirstResponder]; + _jsRequestingFirstResponder = NO; + return result; +} + +- (BOOL)resignFirstResponder +{ + [super resignFirstResponder]; + BOOL result = [_textView resignFirstResponder]; + if (result) { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur + reactTag:self.reactTag + text:_textView.text]; + } + return result; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self updateFrames]; +} + +- (BOOL)canBecomeFirstResponder +{ + return _jsRequestingFirstResponder; +} + +- (UIFont *)defaultPlaceholderFont +{ + return [UIFont fontWithName:@"Helvetica" size:17]; +} + +- (UIColor *)defaultPlaceholderTextColor +{ + return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; +} + +@end diff --git a/Libraries/Text/RCTTextViewManager.h b/Libraries/Text/RCTTextViewManager.h new file mode 100644 index 00000000000000..fd2f2b44d3d334 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTTextViewManager : RCTViewManager + +@end diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m new file mode 100644 index 00000000000000..570a511157bee5 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.m @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTTextViewManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTShadowView.h" +#import "RCTSparseArray.h" +#import "RCTTextView.h" + +@implementation RCTTextViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) +RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL) +RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(text, NSString) +RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) +RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) +RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) +RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) +RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) +RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; +} +RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withWeight:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withStyle:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; +} + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +{ + NSNumber *reactTag = shadowView.reactTag; + UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + ((RCTTextView *)viewRegistry[reactTag]).contentInset = padding; + }; +} + +@end diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index ce7b4078e9acd4..a67abacb087ec5 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -33,7 +33,7 @@ var viewConfig = { }; /** - * A react component for displaying text which supports nesting, + * A React component for displaying text which supports nesting, * styling, and touch handling. In the following example, the nested title and * body text will inherit the `fontFamily` from `styles.baseText`, but the title * provides its own additional styles. The title and body will stack on top of diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index fe28a7684c9d46..b93000a33a8f50 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -20,7 +20,7 @@ var dimensions = NativeModules.UIManager.Dimensions; // We calculate the window dimensions in JS so that we don't encounter loss of // precision in transferring the dimensions (which could be non-integers) over // the bridge. -if (dimensions.windowPhysicalPixels) { +if (dimensions && dimensions.windowPhysicalPixels) { // parse/stringify => Clone hack dimensions = JSON.parse(JSON.stringify(dimensions)); diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 576aaa9917bba2..97658c2356b3d6 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -21,6 +21,9 @@ var JSTimersExecution = require('JSTimersExecution'); var INTERNAL_ERROR = 'Error in MessageQueue implementation'; +// Prints all bridge traffic to console.log +var DEBUG_SPY_MODE = false; + type ModulesConfig = { [key:string]: { moduleID: number; @@ -263,6 +266,9 @@ var MessageQueueMixin = { 'both the success callback and the error callback.', cbID ); + if (DEBUG_SPY_MODE) { + console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')'); + } cb.apply(scope, args); } catch(ie_requires_catch) { throw ie_requires_catch; @@ -292,6 +298,11 @@ var MessageQueueMixin = { var moduleName = this._localModuleIDToModuleName[moduleID]; var methodName = this._localModuleNameToMethodIDToName[moduleName][methodID]; + if (DEBUG_SPY_MODE) { + console.log( + 'N->JS: ' + moduleName + '.' + methodName + + '(' + JSON.stringify(params) + ')'); + } var ret = jsCall(this._requireFunc(moduleName), methodName, params); return ret; @@ -460,6 +471,17 @@ var MessageQueueMixin = { var ret = currentOutgoingItems[REQUEST_MODULE_IDS].length || currentOutgoingItems[RESPONSE_RETURN_VALUES].length ? currentOutgoingItems : null; + if (DEBUG_SPY_MODE && ret) { + for (var i = 0; i < currentOutgoingItems[0].length; i++) { + var moduleName = this._remoteModuleIDToModuleName[currentOutgoingItems[0][i]]; + var methodName = + this._remoteModuleNameToMethodIDToName[moduleName][currentOutgoingItems[1][i]]; + console.log( + 'JS->N: ' + moduleName + '.' + methodName + + '(' + JSON.stringify(currentOutgoingItems[2][i]) + ')'); + } + } + return ret; }, diff --git a/Libraries/Utilities/stringifySafe.js b/Libraries/Utilities/stringifySafe.js new file mode 100644 index 00000000000000..053ea69849e821 --- /dev/null +++ b/Libraries/Utilities/stringifySafe.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule stringifySafe + * @flow + */ +'use strict'; + +/** + * Tries to stringify with JSON.stringify and toString, but catches exceptions + * (e.g. from circular objects) and always returns a string and never throws. + */ +function stringifySafe(arg: any): string { + var ret; + if (arg === undefined) { + ret = 'undefined'; + } else if (arg === null) { + ret = 'null'; + } else if (typeof arg === 'string') { + ret = '"' + arg + '"'; + } else { + // Perform a try catch, just in case the object has a circular + // reference or stringify throws for some other reason. + try { + ret = JSON.stringify(arg); + } catch (e) { + if (typeof arg.toString === 'function') { + try { + ret = arg.toString(); + } catch (E) {} + } + } + } + return ret || '["' + typeof arg + '" failed to stringify]'; +} + +module.exports = stringifySafe; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 01bff7eae5dfb0..b94b172f37de61 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -27,6 +27,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), Navigator: require('Navigator'), + SegmentedControlIOS: require('SegmentedControlIOS'), ScrollView: require('ScrollView'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), diff --git a/Libraries/vendor/react/platform/NodeHandle.js b/Libraries/vendor/react/platform/NodeHandle.js index 19f74c6ece0b73..c7c93545bf0317 100644 --- a/Libraries/vendor/react/platform/NodeHandle.js +++ b/Libraries/vendor/react/platform/NodeHandle.js @@ -19,7 +19,7 @@ * worker thread. * * The only other requirement of a platform/environment is that it always be - * possible to extract the react rootNodeID in a blocking manner (see + * possible to extract the React rootNodeID in a blocking manner (see * `getRootNodeID`). * * +------------------+ +------------------+ +------------------+ diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 020731fde52cbe..37c42382712627 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -436,6 +436,7 @@ var TouchableMixin = { if (isTouchWithinActive) { this._receiveSignal(Signals.ENTER_PRESS_RECT, e); } else { + this._cancelLongPressDelayTimeout(); this._receiveSignal(Signals.LEAVE_PRESS_RECT, e); } }, diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 48fd672a3926d4..5b6a92682afbdf 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -31,6 +31,8 @@ NSString *const RCTReloadNotification = @"RCTReloadNotification"; NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +dispatch_queue_t const RCTJSThread = nil; + /** * Must be kept in sync with `MessageQueue.js`. */ @@ -189,6 +191,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { superclass = class_getSuperclass(superclass); } } + free(classes); } }); @@ -353,7 +356,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName #define RCT_CONVERT_CASE(_value, _type) \ case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ break; \ } @@ -375,12 +378,27 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName RCT_CONVERT_CASE('B', BOOL) RCT_CONVERT_CASE('@', id) RCT_CONVERT_CASE('^', void *) - case '{': - RCTAssert(NO, @"Argument %zd of %C[%@ %@] is defined as %@, however RCT_EXPORT_METHOD() " - "does not currently support struct-type arguments.", i - 2, - [reactMethodName characterAtIndex:0], _moduleClassName, - objCMethodName, argumentName); - break; + + case '{': { + [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { + NSUInteger size; + NSGetSizeAndAlignment(argumentType, &size, NULL); + void *returnValue = malloc(size); + NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [_invocation setTarget:[RCTConvert class]]; + [_invocation setSelector:selector]; + [_invocation setArgument:&json atIndex:2]; + [_invocation invoke]; + [_invocation getReturnValue:returnValue]; + + [invocation setArgument:returnValue atIndex:index]; + + free(returnValue); + }]; + break; + } + default: defaultCase(argumentType); } @@ -436,6 +454,10 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName RCT_SIMPLE_CASE('d', double, doubleValue) RCT_SIMPLE_CASE('B', BOOL, boolValue) + case '{': + RCTLogMustFix(@"Cannot convert JSON to struct %s", argumentType); + break; + default: defaultCase(argumentType); } @@ -650,6 +672,8 @@ - (NSString *)description */ static NSMutableDictionary *RCTLocalModuleIDs; static NSMutableDictionary *RCTLocalMethodIDs; +static NSMutableArray *RCTLocalModuleNames; +static NSMutableArray *RCTLocalMethodNames; static NSDictionary *RCTLocalModulesConfig() { static NSMutableDictionary *localModules; @@ -658,6 +682,8 @@ - (NSString *)description RCTLocalModuleIDs = [[NSMutableDictionary alloc] init]; RCTLocalMethodIDs = [[NSMutableDictionary alloc] init]; + RCTLocalModuleNames = [[NSMutableArray alloc] init]; + RCTLocalMethodNames = [[NSMutableArray alloc] init]; localModules = [[NSMutableDictionary alloc] init]; for (NSString *moduleDotMethod in RCTJSMethods()) { @@ -689,6 +715,8 @@ - (NSString *)description // Add module and method lookup RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"]; RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"]; + [RCTLocalModuleNames addObject:moduleName]; + [RCTLocalMethodNames addObject:methodName]; } }); @@ -795,6 +823,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; + [self setUp]; [self bindKeys]; } @@ -872,6 +901,8 @@ - (void)setUp dispatch_queue_t queue = [module methodQueue]; if (queue) { _queuesByID[moduleID] = queue; + } else { + _queuesByID[moduleID] = [NSNull null]; } } }]; @@ -1047,7 +1078,7 @@ - (void)invalidate #pragma mark - RCTBridge methods /** - * Like JS::call, for objective-c. + * Public. Can be invoked from any thread. */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { @@ -1058,12 +1089,10 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, args ?: @[]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - } + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, args ?: @[]] + context:RCTGetExecutorID(_javaScriptExecutor)]; } /** @@ -1081,10 +1110,17 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer if (!_loading) { #if BATCHED_BRIDGE - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + } #else @@ -1128,33 +1164,93 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: #pragma mark - Payload Generation +- (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID +{ + id queue = _queuesByID[moduleID]; + if (queue == [NSNull null]) { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + } else { + dispatch_async(queue ?: _methodQueue, block); + } +} + +/** + * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer, + * on the JS thread, but only in non-batched mode. + */ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { #if BATCHED_BRIDGE - RCTProfileBeginEvent(); - if ([module isEqualToString:@"RCTEventEmitter"]) { - for (NSDictionary *call in _scheduledCalls) { - if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) { - [_scheduledCalls removeObject:call]; + __weak NSMutableArray *weakScheduledCalls = _scheduledCalls; + __weak RCTSparseArray *weakScheduledCallbacks = _scheduledCallbacks; + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileBeginEvent(); + + NSMutableArray *scheduledCalls = weakScheduledCalls; + RCTSparseArray *scheduledCallbacks = weakScheduledCallbacks; + if (!scheduledCalls || !scheduledCallbacks) { + return; + } + + /** + * Event deduping + * + * Right now we make a lot of assumptions about the arguments structure + * so just iterate if it's a `callFunctionReturnFlushedQueue()` + */ + if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) { + NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]]; + /** + * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter + */ + if ([moduleName hasSuffix:@"EventEmitter"]) { + for (NSDictionary *call in [scheduledCalls copy]) { + NSArray *callArgs = call[@"args"]; + /** + * If it's the same module && method call on the bridge && + * the same EventEmitter module && method + */ + if ( + [call[@"module"] isEqualToString:module] && + [call[@"method"] isEqualToString:method] && + [callArgs[0] isEqual:args[0]] && + [callArgs[1] isEqual:args[1]] + ) { + /** + * args[2] contains the actual arguments for the event call, where + * args[2][0] is the target for RCTEventEmitter or the eventName + * for the other EventEmitters + * if RCTEventEmitter we need to compare args[2][1] that will be + * the eventName + */ + if ( + [args[2][0] isEqual:callArgs[2][0]] && + ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES) + ) { + [scheduledCalls removeObject:call]; + } + } + } } } - } - id call = @{ - @"module": module, - @"method": method, - @"args": args, - @"context": context ?: @0, - }; + id call = @{ + @"module": module, + @"method": method, + @"args": args, + @"context": context ?: @0, + }; - if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - _scheduledCallbacks[args[0]] = call; - } else { - [_scheduledCalls addObject:call]; - } + if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { + scheduledCallbacks[args[0]] = call; + } else { + [scheduledCalls addObject:call]; + } - RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + }]; } - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context @@ -1235,10 +1331,9 @@ - (void)_handleBuffer:(id)buffer context:(NSNumber *)context // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) { if ([module respondsToSelector:@selector(batchDidComplete)]) { - dispatch_queue_t queue = _queuesByID[moduleID]; - dispatch_async(queue ?: _methodQueue, ^{ + [self dispatchBlock:^{ [module batchDidComplete]; - }); + } forModule:moduleID]; } }]; } @@ -1273,8 +1368,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i } __weak RCTBridge *weakSelf = self; - dispatch_queue_t queue = _queuesByID[moduleID]; - dispatch_async(queue ?: _methodQueue, ^{ + [self dispatchBlock:^{ RCTProfileBeginEvent(); __strong RCTBridge *strongSelf = weakSelf; @@ -1303,7 +1397,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i @"method": method.JSMethodName, @"selector": NSStringFromSelector(method.selector), }); - }); + } forModule:@(moduleID)]; return YES; } diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index dd6f61e235e279..34b861ff3f8f10 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -17,6 +17,16 @@ */ typedef void (^RCTResponseSenderBlock)(NSArray *response); +/** + * This constant can be returned from +methodQueue to force module + * methods to be called on the JavaScript thread. This can have serious + * implications for performance, so only use this if you're sure it's what + * you need. + * + * NOTE: RCTJSThread is not a real libdispatch queue + */ +extern const dispatch_queue_t RCTJSThread; + /** * Provides the interface needed to register a bridge module. */ diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index f1ed77298dafdb..465d1f0bb65413 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -49,11 +49,11 @@ + (NSNumber *)NSNumber:(id)json }); NSNumber *number = [formatter numberFromString:json]; if (!number) { - RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); + RCTLogConvertError(json, "a number"); } return number; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]); + RCTLogConvertError(json, "a number"); } return nil; } @@ -66,30 +66,38 @@ + (NSData *)NSData:(id)json + (NSURL *)NSURL:(id)json { - if (!json || json == (id)kCFNull) { + NSString *path = [self NSString:json]; + if (!path.length) { return nil; } - if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); - return nil; - } + @try { // NSURL has a history of crashing with bad input, so let's be safe - NSString *path = json; - if ([path isAbsolutePath]) - { + NSURL *URL = [NSURL URLWithString:path]; + if (URL.scheme) { // Was a well-formed absolute URL + return URL; + } + + // Check if it has a scheme + if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) { + path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + URL = [NSURL URLWithString:path]; + if (URL) { + return URL; + } + } + + // Assume that it's a local path + path = [path stringByRemovingPercentEncoding]; + if (![path isAbsolutePath]) { + path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path]; + } return [NSURL fileURLWithPath:path]; } - else if ([path length]) - { - NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; - if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) { - RCTLogWarn(@"The file '%@' does not exist", URL); - return nil; - } - return URL; + @catch (__unused NSException *e) { + RCTLogConvertError(json, "a valid URL"); + return nil; } - return nil; } + (NSURLRequest *)NSURLRequest:(id)json @@ -112,11 +120,12 @@ + (NSDate *)NSDate:(id)json }); NSDate *date = [formatter dateFromString:json]; if (!date) { - RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); + RCTLogError(@"JSON String '%@' could not be interpreted as a date. " + "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); } return date; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]); + RCTLogConvertError(json, "a date"); } return nil; } @@ -698,35 +707,48 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family const RCTFontWeight RCTDefaultFontWeight = UIFontWeightRegular; const CGFloat RCTDefaultFontSize = 14; - // Get existing properties + // Initialize properties to defaults + CGFloat fontSize = RCTDefaultFontSize; + RCTFontWeight fontWeight = RCTDefaultFontWeight; + NSString *familyName = RCTDefaultFontFamily; BOOL isItalic = NO; BOOL isCondensed = NO; - RCTFontWeight fontWeight = RCTDefaultFontWeight; + if (font) { - family = font.familyName; + familyName = font.familyName ?: RCTDefaultFontFamily; + fontSize = font.pointSize ?: RCTDefaultFontSize; fontWeight = RCTWeightOfFont(font); isItalic = RCTFontIsItalic(font); isCondensed = RCTFontIsCondensed(font); } + // Get font size + fontSize = [self CGFloat:size] ?: fontSize; + + // Get font family + familyName = [self NSString:family] ?: familyName; + // Get font style if (style) { isItalic = [self RCTFontStyle:style]; } - // Get font size - CGFloat fontSize = [self CGFloat:size] ?: RCTDefaultFontSize; + // Get font weight + if (weight) { + fontWeight = [self RCTFontWeight:weight]; + } - // Get font family - NSString *familyName = [self NSString:family] ?: RCTDefaultFontFamily; + // Gracefully handle being given a font name rather than font family, for + // example: "Helvetica Light Oblique" rather than just "Helvetica". if ([UIFont fontNamesForFamilyName:familyName].count == 0) { font = [UIFont fontWithName:familyName size:fontSize]; if (font) { // It's actually a font name, not a font family name, // but we'll do what was meant, not what was said. familyName = font.familyName; - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - fontWeight = [traits[UIFontWeightTrait] doubleValue]; + fontWeight = RCTWeightOfFont(font); + isItalic = RCTFontIsItalic(font); + isCondensed = RCTFontIsCondensed(font); } else { // Not a valid font or family RCTLogError(@"Unrecognized font family '%@'", familyName); @@ -734,14 +756,16 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family } } - // Get font weight - if (weight) { - fontWeight = [self RCTFontWeight:weight]; + // Get the closest font that matches the given weight for the fontFamily + UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize]; + CGFloat closestWeight; + + if (font && [font.familyName isEqualToString: familyName]) { + closestWeight = RCTWeightOfFont(font); + } else { + closestWeight = INFINITY; } - // Get closest match - UIFont *bestMatch = font; - CGFloat closestWeight = font ? RCTWeightOfFont(font) : INFINITY; for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == RCTFontIsItalic(match) && diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index dd6bd8ed6f8aa2..15cb180210f6e6 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -50,7 +50,7 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { /** * Send a user input event. The body dictionary must contain a "target" - * parameter, representing the react tag of the view sending the event + * parameter, representing the React tag of the view sending the event */ - (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body; diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index fb4e02eae30f75..8487556e576a53 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -44,7 +44,7 @@ - (void)sendDeviceEventWithName:(NSString *)name body:(id)body - (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body { RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]], - @"Event body dictionary must include a 'target' property containing a react tag"); + @"Event body dictionary must include a 'target' property containing a React tag"); [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]]; diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index eb7fd7d31947cd..8f1eb8a98c076a 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -49,6 +49,15 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); */ - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block; +@optional + +/** + * Special case for Timers + ContextExecutor - instead of the default + * if jsthread then call else dispatch call on jsthread + * ensure the call is made async on the jsthread + */ +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block; + @end static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID"; diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index dd8fab461868fd..2e7d21b9442fe7 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -10,47 +10,15 @@ #import "RCTJavaScriptLoader.h" #import "RCTBridge.h" -#import "RCTInvalidating.h" -#import "RCTLog.h" -#import "RCTRedBox.h" +#import "RCTConvert.h" #import "RCTSourceCode.h" #import "RCTUtils.h" -#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading." -#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists." - -#define CACHE_DIR @"RCTJSBundleCache" - -#pragma mark - Application Engine - -/** - * TODO: - * - Add window resize rotation events matching the DOM API. - * - Device pixel ration hooks. - * - Source maps. - */ @implementation RCTJavaScriptLoader { __weak RCTBridge *_bridge; } -/** - * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore - * engine in its own dedicated thread. - * - * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one - * additional GCD dispatch per frame and likely makes it so that other UIThread - * operations don't delay the dispatch (so we can begin working in JS much - * faster.) Event handling must still be sent via a GCD dispatch, of course. - * - * We must add the display link to two runloops in order to get setTimeouts to - * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`) - * TODO: We can invent a `requestAnimationFrame` and - * `requestAvailableAnimationFrame` to control if callbacks can be fired during - * an animation. - * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink - * - */ - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { @@ -61,92 +29,86 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete { - if (scriptURL == nil) { + // Sanitize the script URL + scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; + + if (!scriptURL || + ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) { NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ - NSLocalizedDescriptionKey: @"No script URL provided" + NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; onComplete(error); return; } - if ([scriptURL isFileURL]) { - NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; - NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length]; - - if (![localPath hasPrefix:bundlePath]) { - NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath]; - scriptURL = [NSURL fileURLWithPath:absolutePath]; - } - } - NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { - // Handle general request errors - if (error) { - if ([[error domain] isEqualToString:NSURLErrorDomain]) { - NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: desc, - NSLocalizedFailureReasonErrorKey: [error localizedDescription], - NSUnderlyingErrorKey: error, - }; - error = [NSError errorWithDomain:@"JSServer" - code:error.code - userInfo:userInfo]; - } - onComplete(error); - return; - } - - // Parse response as text - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName != nil) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - if (cfEncoding != kCFStringEncodingInvalidId) { - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - } - NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; - - // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { - NSDictionary *userInfo; - NSDictionary *errorDetails = RCTJSONParse(rawText, nil); - if ([errorDetails isKindOfClass:[NSDictionary class]] && - [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { - NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; - for (NSDictionary *err in errorDetails[@"errors"]) { - [fakeStack addObject: @{ - @"methodName": err[@"description"] ?: @"", - @"file": err[@"filename"] ?: @"", - @"lineNumber": err[@"lineNumber"] ?: @0 - }]; - } - userInfo = @{ - NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", - @"stack": fakeStack, - }; - } else { - userInfo = @{NSLocalizedDescriptionKey: rawText}; - } - error = [NSError errorWithDomain:@"JSServer" - code:[(NSHTTPURLResponse *)response statusCode] - userInfo:userInfo]; - - onComplete(error); - return; - } - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = scriptURL; - sourceCodeModule.scriptText = rawText; + // Handle general request errors + if (error) { + if ([[error domain] isEqualToString:NSURLErrorDomain]) { + NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: desc, + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }; + error = [NSError errorWithDomain:@"JSServer" + code:error.code + userInfo:userInfo]; + } + onComplete(error); + return; + } - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(scriptError); - }); - }]; - }]; + // Parse response as text + NSStringEncoding encoding = NSUTF8StringEncoding; + if (response.textEncodingName != nil) { + CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (cfEncoding != kCFStringEncodingInvalidId) { + encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + } + NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; + + // Handle HTTP errors + if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { + NSDictionary *userInfo; + NSDictionary *errorDetails = RCTJSONParse(rawText, nil); + if ([errorDetails isKindOfClass:[NSDictionary class]] && + [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { + NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; + for (NSDictionary *err in errorDetails[@"errors"]) { + [fakeStack addObject: @{ + @"methodName": err[@"description"] ?: @"", + @"file": err[@"filename"] ?: @"", + @"lineNumber": err[@"lineNumber"] ?: @0 + }]; + } + userInfo = @{ + NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", + @"stack": fakeStack, + }; + } else { + userInfo = @{NSLocalizedDescriptionKey: rawText}; + } + error = [NSError errorWithDomain:@"JSServer" + code:[(NSHTTPURLResponse *)response statusCode] + userInfo:userInfo]; + + onComplete(error); + return; + } + RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = scriptURL; + sourceCodeModule.scriptText = rawText; + + [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { + dispatch_async(dispatch_get_main_queue(), ^{ + onComplete(scriptError); + }); + }]; + }]; [task resume]; } diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 45624efdcc1e8a..54556d41810ba4 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -23,16 +23,6 @@ #import "RCTWebViewExecutor.h" #import "UIView+React.h" -/** - * HACK(t6568049) This should be removed soon, hiding to prevent people from - * relying on it - */ -@interface RCTBridge (RCTRootView) - -- (void)setJavaScriptExecutor:(id)executor; - -@end - @interface RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag; @@ -120,11 +110,11 @@ - (void)bundleFinishedLoading dispatch_async(dispatch_get_main_queue(), ^{ /** - * Every root view that is created must have a unique react tag. + * Every root view that is created must have a unique React tag. * Numbering of these tags goes from 1, 11, 21, 31, etc * * NOTE: Since the bridge persists, the RootViews might be reused, so now - * the react tag is assigned every time we load new content. + * the React tag is assigned every time we load new content. */ [_contentView removeFromSuperview]; _contentView = [[UIView alloc] initWithFrame:self.bounds]; diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index bd731a99844715..7af26da74eab70 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -26,7 +26,7 @@ @implementation RCTTouchHandler /** * Arrays managed in parallel tracking native touch object along with the - * native view that was touched, and the react touch data dictionary. + * native view that was touched, and the React touch data dictionary. * This must be kept track of because `UIKit` destroys the touch targets * if touches are canceled and we have no other way to recover this information. */ diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 812a651222a85c..1c04125684fd11 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -46,3 +46,6 @@ RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); // TODO(#6472857): create NSErrors and automatically convert them over the bridge. RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); + +// Returns YES if React is running in a test environment +RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index cea45c324c63e7..3c644dbff4e788 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -35,8 +35,7 @@ id RCTJSONParse(NSString *jsonString, NSError **error) if (jsonData) { RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString); } else { - // If our backup conversion fails, log the issue so we can see what strings are causing this (t6452813) - RCTLogError(@"RCTJSONParse received the following string, which could not be converted to UTF8 data: '%@'", jsonString); + RCTLogError(@"RCTJSONParse received invalid UTF8 data"); return nil; } } @@ -201,3 +200,13 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector) RCTLogError(@"\nError: %@", error); return error; } + +BOOL RCTRunningInTestEnvironment(void) +{ + static BOOL _isTestEnvironment = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _isTestEnvironment = (NSClassFromString(@"SenTestCase") != nil || NSClassFromString(@"XCTest") != nil); + }); + return _isTestEnvironment; +} diff --git a/React/Executors/RCTContextExecutor.h b/React/Executors/RCTContextExecutor.h index 159965a2fbed01..a41fcf31419a43 100644 --- a/React/Executors/RCTContextExecutor.h +++ b/React/Executors/RCTContextExecutor.h @@ -23,6 +23,6 @@ * You probably don't want to use this; use -init instead. */ - (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread - globalContextRef:(JSGlobalContextRef)context; + globalContextRef:(JSGlobalContextRef)context NS_DESIGNATED_INITIALIZER; @end diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 86444dd2a7f903..412ffd25693c36 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -55,6 +55,11 @@ - (void)invalidate } } +- (void)dealloc +{ + CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]); +} + @end @implementation RCTContextExecutor @@ -74,12 +79,12 @@ @implementation RCTContextExecutor static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { if (argumentCount > 0) { - JSStringRef string = JSValueToStringCopy(context, arguments[0], exception); - if (!string) { + JSStringRef messageRef = JSValueToStringCopy(context, arguments[0], exception); + if (!messageRef) { return JSValueMakeUndefined(context); } - NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); - JSStringRelease(string); + NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef); + JSStringRelease(messageRef); NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)" options:NSRegularExpressionCaseInsensitive @@ -89,14 +94,11 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, range:(NSRange){0, message.length} withTemplate:@"[$4$5] \t$2"]; - // TODO: it would be good if log level was sent as a param, instead of this hack RCTLogLevel level = RCTLogLevelInfo; - if ([message rangeOfString:@"error" options:NSCaseInsensitiveSearch].length) { - level = RCTLogLevelError; - } else if ([message rangeOfString:@"warning" options:NSCaseInsensitiveSearch].length) { - level = RCTLogLevelWarning; + if (argumentCount > 1) { + level = MAX(level, JSValueToNumber(context, arguments[1], exception) - 1); } - _RCTLogFormat(level, NULL, -1, @"%@", message); + RCTGetLogFunction()(level, nil, nil, message); } return JSValueMakeUndefined(context); @@ -156,15 +158,12 @@ + (void)runRunLoopThread - (instancetype)init { - static NSThread *javaScriptThread; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // All JS is single threaded, so a serial queue is our only option. - javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil]; - [javaScriptThread setName:@"com.facebook.React.JavaScript"]; - [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]]; - [javaScriptThread start]; - }); + NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class] + selector:@selector(runRunLoopThread) + object:nil]; + [javaScriptThread setName:@"com.facebook.React.JavaScript"]; + [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]]; + [javaScriptThread start]; return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL]; } @@ -172,6 +171,9 @@ - (instancetype)init - (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread globalContextRef:(JSGlobalContextRef)context { + RCTAssert(javaScriptThread != nil, + @"Can't initialize RCTContextExecutor without a javaScriptThread"); + if ((self = [super init])) { _javaScriptThread = javaScriptThread; __weak RCTContextExecutor *weakSelf = self; @@ -305,17 +307,30 @@ - (void)executeApplicationScript:(NSString *)script } onComplete(error); } - }), @"js_call", (@{ @"url": sourceURL }))]; + }), @"js_call", (@{ @"url": sourceURL.absoluteString }))]; } - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - if ([NSThread currentThread] != _javaScriptThread) { - [self performSelector:@selector(executeBlockOnJavaScriptQueue:) - onThread:_javaScriptThread withObject:block waitUntilDone:NO]; - } else { - block(); - } + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } +} + +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block +{ + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread + withObject:block + waitUntilDone:NO]; +} + +- (void)_runBlock:(dispatch_block_t)block +{ + block(); } - (void)injectJSONText:(NSString *)script diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 1d99c1a2d42663..e2df5befca18eb 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -110,7 +110,7 @@ - (void)dealloc - (dispatch_queue_t)methodQueue { - return dispatch_get_main_queue(); + return RCTJSThread; } - (BOOL)isValid @@ -131,8 +131,6 @@ - (void)stopTimers - (void)startTimers { - RCTAssertMainThread(); - if (![self isValid] || _timers.count == 0) { return; } diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 451a343d04960d..496b9c3513a677 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -661,7 +661,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag { id container = registry[containerReactTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); - RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one react child to add"); + RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); // Removes (both permanent and temporary moves) are using "before" indices NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; @@ -918,7 +918,7 @@ - (void)flushUIBlocks } // TODO: this doesn't work because sometimes view is inside a modal window - // RCTAssert([rootView isReactRootView], @"React view is not inside a react root view"); + // RCTAssert([rootView isReactRootView], @"React view is not inside a React root view"); // By convention, all coordinates, whether they be touch coordinates, or // measurement coordinates are with respect to the root view. @@ -995,18 +995,17 @@ static void RCTMeasureLayout(RCTShadowView *view, } /** - * Returns an array of computed offset layouts in a dictionary form. The layouts are of any react subviews + * Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews * that are immediate descendants to the parent view found within a specified rect. The dictionary result * contains left, top, width, height and an index. The index specifies the position among the other subviews. * Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the * passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts. */ -RCT_EXPORT_METHOD(measureViewsInRect:(id)rectJSON +RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect parentView:(NSNumber *)reactTag errorCallback:(RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback) { - CGRect rect = [RCTConvert CGRect:rectJSON]; RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; if (!shadowView) { RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag); @@ -1102,9 +1101,8 @@ static void RCTMeasureLayout(RCTShadowView *view, } RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag - withRect:(id)rectJSON) + withRect:(CGRect)rect) { - CGRect rect = [RCTConvert CGRect:rectJSON]; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 3364cce76fbfe8..10867e9cbc5aa9 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */; }; + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; }; 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */; }; 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */; }; 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; @@ -33,7 +35,7 @@ 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; }; 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080151A69489C00A75B9A /* RCTTextField.m */; }; 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; }; - 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; }; + 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; }; 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; @@ -84,6 +86,10 @@ 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; 00C1A2B11AC0B7E000E89A1C /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevMenu.h; sourceTree = ""; }; 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = ""; }; + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; }; + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; }; 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; }; @@ -136,8 +142,8 @@ 13B080151A69489C00A75B9A /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = ""; }; 13B080161A69489C00A75B9A /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = ""; }; 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = ""; }; - 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIActivityIndicatorViewManager.h; sourceTree = ""; }; - 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; }; + 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorViewManager.h; sourceTree = ""; }; + 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = ""; }; 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; 13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; }; @@ -290,6 +296,10 @@ 58114A141AAE854800E7D092 /* RCTPickerManager.h */, 58114A151AAE854800E7D092 /* RCTPickerManager.m */, 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */, + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */, + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */, + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, @@ -317,8 +327,8 @@ 13B080151A69489C00A75B9A /* RCTTextField.m */, 13B080161A69489C00A75B9A /* RCTTextFieldManager.h */, 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */, - 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */, - 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */, + 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */, + 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E067501A70F44B002CDEE1 /* RCTView.m */, 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */, @@ -489,6 +499,7 @@ 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, @@ -499,7 +510,7 @@ 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, - 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */, + 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, @@ -529,6 +540,7 @@ 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, diff --git a/React/Views/RCTActivityIndicatorViewManager.h b/React/Views/RCTActivityIndicatorViewManager.h new file mode 100644 index 00000000000000..cbd6816ae4cddf --- /dev/null +++ b/React/Views/RCTActivityIndicatorViewManager.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTConvert (UIActivityIndicatorView) + ++ (UIActivityIndicatorViewStyle)UIActivityIndicatorViewStyle:(id)json; + +@end + +@interface RCTActivityIndicatorViewManager : RCTViewManager + +@end diff --git a/React/Views/RCTUIActivityIndicatorViewManager.m b/React/Views/RCTActivityIndicatorViewManager.m similarity index 52% rename from React/Views/RCTUIActivityIndicatorViewManager.m rename to React/Views/RCTActivityIndicatorViewManager.m index e2c9b3d353bf06..3876400dff3714 100644 --- a/React/Views/RCTUIActivityIndicatorViewManager.m +++ b/React/Views/RCTActivityIndicatorViewManager.m @@ -7,35 +7,37 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTUIActivityIndicatorViewManager.h" +#import "RCTActivityIndicatorViewManager.h" #import "RCTConvert.h" @implementation RCTConvert (UIActivityIndicatorView) +// NOTE: It's pointless to support UIActivityIndicatorViewStyleGray +// as we can set the color to any arbitrary value that we want to + RCT_ENUM_CONVERTER(UIActivityIndicatorViewStyle, (@{ - @"white-large": @(UIActivityIndicatorViewStyleWhiteLarge), - @"large-white": @(UIActivityIndicatorViewStyleWhiteLarge), - @"white": @(UIActivityIndicatorViewStyleWhite), - @"gray": @(UIActivityIndicatorViewStyleGray), + @"large": @(UIActivityIndicatorViewStyleWhiteLarge), + @"small": @(UIActivityIndicatorViewStyleWhite), }), UIActivityIndicatorViewStyleWhiteLarge, integerValue) @end -@implementation RCTUIActivityIndicatorViewManager +@implementation RCTActivityIndicatorViewManager -RCT_EXPORT_MODULE(UIActivityIndicatorViewManager) +RCT_EXPORT_MODULE() - (UIView *)view { return [[UIActivityIndicatorView alloc] init]; } -RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle, UIActivityIndicatorViewStyle) RCT_EXPORT_VIEW_PROPERTY(color, UIColor) +RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL) +RCT_REMAP_VIEW_PROPERTY(size, activityIndicatorViewStyle, UIActivityIndicatorViewStyle) RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView) { - BOOL animating = json ? [json boolValue] : [defaultView isAnimating]; + BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating]; if (animating != [view isAnimating]) { if (animating) { [view startAnimating]; @@ -45,14 +47,4 @@ - (UIView *)view } } -- (NSDictionary *)constantsToExport -{ - return - @{ - @"StyleWhite": @(UIActivityIndicatorViewStyleWhite), - @"StyleWhiteLarge": @(UIActivityIndicatorViewStyleWhiteLarge), - @"StyleGray": @(UIActivityIndicatorViewStyleGray), - }; -} - @end diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index f3ebb6554a2cb8..a4cb338fb3c88b 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -60,7 +60,7 @@ @interface RCTNavigationController : UINavigationController + +@class RCTEventDispatcher; + +@interface RCTSegmentedControl : UISegmentedControl + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, copy) NSArray *values; +@property (nonatomic, assign) NSInteger selectedIndex; + +@end diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m new file mode 100644 index 00000000000000..59e4cfb86b5fa4 --- /dev/null +++ b/React/Views/RCTSegmentedControl.m @@ -0,0 +1,57 @@ +// +// RCTSegmentedControl.m +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTSegmentedControl.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" + +@implementation RCTSegmentedControl +{ + RCTEventDispatcher *_eventDispatcher; +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + _selectedIndex = self.selectedSegmentIndex; + [self addTarget:self action:@selector(onChange:) + forControlEvents:UIControlEventValueChanged]; + } + return self; +} + +- (void)setValues:(NSArray *)values +{ + _values = [values copy]; + [self removeAllSegments]; + for (NSString *value in values) { + [self insertSegmentWithTitle:value atIndex:self.numberOfSegments animated:NO]; + } + super.selectedSegmentIndex = _selectedIndex; +} + +- (void)setSelectedIndex:(NSInteger)selectedIndex +{ + _selectedIndex = selectedIndex; + super.selectedSegmentIndex = selectedIndex; +} + +- (void)onChange:(UISegmentedControl *)sender +{ + NSDictionary *event = @{ + @"target": self.reactTag, + @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex], + @"selectedSegmentIndex": @(sender.selectedSegmentIndex) + }; + [_eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +@end diff --git a/React/Views/RCTSegmentedControlManager.h b/React/Views/RCTSegmentedControlManager.h new file mode 100644 index 00000000000000..03647c72edb9bf --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTSegmentedControlManager : RCTViewManager + +@end diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m new file mode 100644 index 00000000000000..d7e1156ff00725 --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTSegmentedControlManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTSegmentedControl.h" + +@implementation RCTSegmentedControlManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(values, NSStringArray) +RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL) +RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) + +- (NSDictionary *)constantsToExport +{ + RCTSegmentedControl *view = [[RCTSegmentedControl alloc] init]; + return @{ + @"ComponentHeight": @(view.intrinsicContentSize.height), + }; +} + +@end diff --git a/React/Views/RCTTextField.h b/React/Views/RCTTextField.h index bd1be9c187bd39..ef0a07887faabc 100644 --- a/React/Views/RCTTextField.h +++ b/React/Views/RCTTextField.h @@ -17,6 +17,7 @@ @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, strong) UIColor *placeholderTextColor; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index 35eb84d9653dde..12d52b1b8b7561 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -42,6 +42,30 @@ - (void)setText:(NSString *)text } } +static void RCTUpdatePlaceholder(RCTTextField *self) +{ + if (self.placeholder.length > 0 && self.placeholderTextColor) { + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder + attributes:@{ + NSForegroundColorAttributeName : self.placeholderTextColor + }]; + } else if (self.placeholder.length) { + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder]; + } +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + _placeholderTextColor = placeholderTextColor; + RCTUpdatePlaceholder(self); +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + super.placeholder = placeholder; + RCTUpdatePlaceholder(self); +} + - (NSArray *)reactSubviews { // TODO: do we support subviews of textfield in React? diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index 6e78d86a3b1c39..ff401a719c7c7a 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -10,7 +10,6 @@ #import "RCTTextFieldManager.h" #import "RCTBridge.h" -#import "RCTConvert.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTTextField.h" @@ -28,6 +27,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 73fe2c7cbb0338..1a4bcb40007e75 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -13,13 +13,6 @@ #import "RCTPointerEvents.h" -typedef NS_ENUM(NSInteger, RCTBorderSide) { - RCTBorderSideTop, - RCTBorderSideRight, - RCTBorderSideBottom, - RCTBorderSideLeft -}; - @protocol RCTAutoInsetsProtocol; @interface RCTView : UIView diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d40798302b2a94..c0786b5abfa863 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -12,9 +12,10 @@ #import "RCTAutoInsetsProtocol.h" #import "RCTConvert.h" #import "RCTLog.h" +#import "RCTUtils.h" #import "UIView+React.h" -static const RCTBorderSide RCTBorderSideCount = 4; +static void *RCTViewCornerRadiusKVOContext = &RCTViewCornerRadiusKVOContext; static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) { @@ -30,6 +31,10 @@ return nil; } +static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]); +static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform); +static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise); + @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -107,8 +112,39 @@ - (UIView *)react_findClipView @implementation RCTView { NSMutableArray *_reactSubviews; - CAShapeLayer *_borderLayers[RCTBorderSideCount]; - CGFloat _borderWidths[RCTBorderSideCount]; + UIColor *_backgroundColor; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _borderWidth = -1; + _borderTopWidth = -1; + _borderRightWidth = -1; + _borderBottomWidth = -1; + _borderLeftWidth = -1; + + _backgroundColor = [super backgroundColor]; + [super setBackgroundColor:[UIColor clearColor]]; + + [self.layer addObserver:self forKeyPath:@"cornerRadius" options:0 context:RCTViewCornerRadiusKVOContext]; + } + + return self; +} + +- (void)dealloc +{ + [self.layer removeObserver:self forKeyPath:@"cornerRadius" context:RCTViewCornerRadiusKVOContext]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == RCTViewCornerRadiusKVOContext) { + [self.layer setNeedsDisplay]; + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } } - (NSString *)accessibilityLabel @@ -381,189 +417,353 @@ - (void)layoutSubviews if (_reactSubviews) { [self updateClippedSubviews]; } - - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - if (_borderLayers[side]) [self updatePathForShapeLayerForSide:side]; - } } -- (void)layoutSublayersOfLayer:(CALayer *)layer +#pragma mark - Borders + +- (UIColor *)backgroundColor { - [super layoutSublayersOfLayer:layer]; + return _backgroundColor; +} - const CGRect bounds = layer.bounds; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderLayers[side].frame = bounds; +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + if ([_backgroundColor isEqual:backgroundColor]) { + return; } + _backgroundColor = backgroundColor; + [self.layer setNeedsDisplay]; } -- (BOOL)getTrapezoidPoints:(CGPoint[4])outPoints forSide:(RCTBorderSide)side +- (UIImage *)generateBorderImage:(out CGRect *)contentsCenter { - const CGRect bounds = self.layer.bounds; - const CGFloat minX = CGRectGetMinX(bounds); - const CGFloat maxX = CGRectGetMaxX(bounds); - const CGFloat minY = CGRectGetMinY(bounds); - const CGFloat maxY = CGRectGetMaxY(bounds); + const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width) / 2.0; + const CGFloat radius = MAX(0, MIN(self.layer.cornerRadius, maxRadius)); -#define BW(SIDE) [self borderWidthForSide:RCTBorderSide##SIDE] + const CGFloat borderWidth = MAX(0, _borderWidth); + const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth; + const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth; + const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; + const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; - switch (side) { - case RCTBorderSideRight: - outPoints[0] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); - outPoints[1] = CGPointMake(maxX - BW(Right), minY + BW(Top)); - outPoints[2] = CGPointMake(maxX, minY); - outPoints[3] = CGPointMake(maxX, maxY); - break; - case RCTBorderSideBottom: - outPoints[0] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); - outPoints[1] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); - outPoints[2] = CGPointMake(maxX, maxY); - outPoints[3] = CGPointMake(minX, maxY); - break; - case RCTBorderSideLeft: - outPoints[0] = CGPointMake(minX + BW(Left), minY + BW(Top)); - outPoints[1] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); - outPoints[2] = CGPointMake(minX, maxY); - outPoints[3] = CGPointMake(minX, minY); - break; - case RCTBorderSideTop: - outPoints[0] = CGPointMake(maxX - BW(Right), minY + BW(Top)); - outPoints[1] = CGPointMake(minX + BW(Left), minY + BW(Top)); - outPoints[2] = CGPointMake(minX, minY); - outPoints[3] = CGPointMake(maxX, minY); - break; - } + const CGFloat topRadius = MAX(0, radius - topWidth); + const CGFloat rightRadius = MAX(0, radius - rightWidth); + const CGFloat bottomRadius = MAX(0, radius - bottomWidth); + const CGFloat leftRadius = MAX(0, radius - leftWidth); - return YES; -} + const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + topRadius, leftWidth + leftRadius, bottomWidth + bottomRadius, rightWidth + rightRadius); + const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom); -- (CAShapeLayer *)createShapeLayerIfNotExistsForSide:(RCTBorderSide)side -{ - CAShapeLayer *borderLayer = _borderLayers[side]; - if (!borderLayer) { - borderLayer = [CAShapeLayer layer]; - borderLayer.fillColor = self.layer.borderColor; - [self.layer addSublayer:borderLayer]; - _borderLayers[side] = borderLayer; - } - return borderLayer; -} + UIScreen *screen = self.window.screen ?: [UIScreen mainScreen]; + UIGraphicsBeginImageContextWithOptions(size, NO, screen.scale * 2); -- (void)updatePathForShapeLayerForSide:(RCTBorderSide)side -{ - CAShapeLayer *borderLayer = [self createShapeLayerIfNotExistsForSide:side]; + CGContextRef ctx = UIGraphicsGetCurrentContext(); + const CGRect rect = {CGPointZero, size}; + CGPathRef path = CGPathCreateWithRoundedRect(rect, radius, radius, NULL); - CGPoint trapezoidPoints[4]; - [self getTrapezoidPoints:trapezoidPoints forSide:side]; + if (_backgroundColor) { + CGContextSaveGState(ctx); - CGMutablePathRef path = CGPathCreateMutable(); - CGPathAddLines(path, NULL, trapezoidPoints, 4); - CGPathCloseSubpath(path); - borderLayer.path = path; + CGContextAddPath(ctx, path); + CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + CGContextAddPath(ctx, path); CGPathRelease(path); -} -- (void)updateBorderLayers -{ - BOOL widthsAndColorsSame = YES; - CGFloat width = _borderWidths[0]; - CGColorRef color = _borderLayers[0].fillColor; - for (RCTBorderSide side = 1; side < RCTBorderSideCount; side++) { - CAShapeLayer *layer = _borderLayers[side]; - if (_borderWidths[side] != width || (layer && !CGColorEqualToColor(layer.fillColor, color))) { - widthsAndColorsSame = NO; - break; + if (radius > 0 && topWidth > 0 && rightWidth > 0 && bottomWidth > 0 && leftWidth > 0) { + const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth); + const CGRect insetRect = UIEdgeInsetsInsetRect(rect, insetEdgeInsets); + CGPathRef insetPath = RCTPathCreateWithRoundedRect(insetRect, leftRadius, topRadius, rightRadius, topRadius, leftRadius, bottomRadius, rightRadius, bottomRadius, NULL); + CGContextAddPath(ctx, insetPath); + CGPathRelease(insetPath); + } + + CGContextEOClip(ctx); + + BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor; + BOOL hasEqualBorder = _borderWidth >= 0 && _borderTopWidth < 0 && _borderRightWidth < 0 && _borderBottomWidth < 0 && _borderLeftWidth < 0; + if (radius <= 0 && hasEqualBorder && hasEqualColor) { + CGContextSetStrokeColorWithColor(ctx, _borderColor); + CGContextSetLineWidth(ctx, 2 * _borderWidth); + CGContextClipToRect(ctx, rect); + CGContextStrokeRect(ctx, rect); + } else if (radius <= 0 && hasEqualColor) { + CGContextSetFillColorWithColor(ctx, _borderColor); + CGContextAddRect(ctx, rect); + const CGRect insetRect = UIEdgeInsetsInsetRect(rect, edgeInsets); + CGContextAddRect(ctx, insetRect); + CGContextEOFillPath(ctx); + } else { + BOOL didSet = NO; + CGPoint topLeft; + if (topRadius > 0 && leftRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * leftRadius, 2 * topRadius), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + topLeft = points[1]; + didSet = YES; + } } - } - if (widthsAndColorsSame) { - // Set main layer border - if (width) { - _borderWidth = self.layer.borderWidth = width; + if (!didSet) { + topLeft = CGPointMake(leftWidth, topWidth); } - if (color) { - self.layer.borderColor = color; + + didSet = NO; + CGPoint bottomLeft; + if (bottomRadius > 0 && leftRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * bottomRadius, 2 * leftRadius, 2 * bottomRadius), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + bottomLeft = points[1]; + didSet = YES; + } } - // Remove border layers - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - [_borderLayers[side] removeFromSuperlayer]; - _borderLayers[side] = nil; + if (!didSet) { + bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth); } - } else { + didSet = NO; + CGPoint topRight; + if (topRadius > 0 && rightRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, topWidth, 2 * rightRadius, 2 * topRadius), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + topRight = points[0]; + didSet = YES; + } + } + + if (!didSet) { + topRight = CGPointMake(size.width - rightWidth, topWidth); + } + + didSet = NO; + CGPoint bottomRight; + if (bottomRadius > 0 && rightRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, (size.height - bottomWidth) - 2 * bottomRadius, 2 * rightRadius, 2 * bottomRadius), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + bottomRight = points[0]; + didSet = YES; + } + } - // Clear main layer border - self.layer.borderWidth = 0; + if (!didSet) { + bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth); + } + + // RIGHT + if (rightWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(size.width, 0), + topRight, + bottomRight, + CGPointMake(size.width, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } - // Set up border layers - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - [self updatePathForShapeLayerForSide:side]; + // BOTTOM + if (bottomWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, size.height), + bottomLeft, + bottomRight, + CGPointMake(size.width, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + // LEFT + if (leftWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, 0), + topLeft, + bottomLeft, + CGPointMake(0, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + // TOP + if (topWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, 0), + topLeft, + topRight, + CGPointMake(size.width, 0), + }; + + CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); } } -} -- (CGFloat)borderWidthForSide:(RCTBorderSide)side -{ - return _borderWidths[side] ?: _borderWidth; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height); + return [image resizableImageWithCapInsets:edgeInsets]; } -- (void)setBorderWidth:(CGFloat)width forSide:(RCTBorderSide)side +- (void)displayLayer:(CALayer *)layer { - _borderWidths[side] = width; - [self updateBorderLayers]; -} + CGRect contentsCenter; + UIImage *image = [self generateBorderImage:&contentsCenter]; -#define BORDER_WIDTH(SIDE) \ -- (CGFloat)border##SIDE##Width { return [self borderWidthForSide:RCTBorderSide##SIDE]; } \ -- (void)setBorder##SIDE##Width:(CGFloat)width { [self setBorderWidth:width forSide:RCTBorderSide##SIDE]; } + if (RCTRunningInTestEnvironment()) { + const CGSize size = self.bounds.size; + UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); + [image drawInRect:(CGRect){CGPointZero, size}]; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); -BORDER_WIDTH(Top) -BORDER_WIDTH(Right) -BORDER_WIDTH(Bottom) -BORDER_WIDTH(Left) + contentsCenter = CGRectMake(0, 0, 1, 1); + } -- (CGColorRef)borderColorForSide:(RCTBorderSide)side -{ - return _borderLayers[side].fillColor ?: self.layer.borderColor; + layer.contents = (id)image.CGImage; + layer.contentsCenter = contentsCenter; + layer.contentsScale = image.scale; + layer.magnificationFilter = kCAFilterNearest; } -- (void)setBorderColor:(CGColorRef)color forSide:(RCTBorderSide)side -{ - [self createShapeLayerIfNotExistsForSide:side].fillColor = color; - [self updateBorderLayers]; -} +#pragma mark Border Color -#define BORDER_COLOR(SIDE) \ -- (CGColorRef)border##SIDE##Color { return [self borderColorForSide:RCTBorderSide##SIDE]; } \ -- (void)setBorder##SIDE##Color:(CGColorRef)color { [self setBorderColor:color forSide:RCTBorderSide##SIDE]; } +#define setBorderColor(side) \ + - (void)setBorder##side##Color:(CGColorRef)border##side##Color \ + { \ + if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \ + return; \ + } \ + _border##side##Color = border##side##Color; \ + [self.layer setNeedsDisplay]; \ + } -BORDER_COLOR(Top) -BORDER_COLOR(Right) -BORDER_COLOR(Bottom) -BORDER_COLOR(Left) +setBorderColor() +setBorderColor(Top) +setBorderColor(Right) +setBorderColor(Bottom) +setBorderColor(Left) -- (void)setBorderWidth:(CGFloat)borderWidth -{ - _borderWidth = borderWidth; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderWidths[side] = borderWidth; +#pragma mark - Border Width + +#define setBorderWidth(side) \ + - (void)setBorder##side##Width:(CGFloat)border##side##Width \ + { \ + if (_border##side##Width == border##side##Width) { \ + return; \ + } \ + _border##side##Width = border##side##Width; \ + [self.layer setNeedsDisplay]; \ } - [self updateBorderLayers]; -} -- (void)setBorderColor:(CGColorRef)borderColor +setBorderWidth() +setBorderWidth(Top) +setBorderWidth(Right) +setBorderWidth(Bottom) +setBorderWidth(Left) + +@end + +static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise) { - self.layer.borderColor = borderColor; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderLayers[side].fillColor = borderColor; + CGFloat xScale = 1, yScale = 1, radius = 0; + if (xRadius != 0) { + xScale = 1; + yScale = yRadius / xRadius; + radius = xRadius; + } else if (yRadius != 0) { + xScale = xRadius / yRadius; + yScale = 1; + radius = yRadius; } - [self updateBorderLayers]; + + CGAffineTransform t = CGAffineTransformMakeTranslation(x, y); + t = CGAffineTransformScale(t, xScale, yScale); + if (m != NULL) { + t = CGAffineTransformConcat(t, *m); + } + + CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); } -- (CGColorRef)borderColor +static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform) { - return self.layer.borderColor; + const CGFloat minX = CGRectGetMinX(rect); + const CGFloat minY = CGRectGetMinY(rect); + const CGFloat maxX = CGRectGetMaxX(rect); + const CGFloat maxY = CGRectGetMaxY(rect); + + CGMutablePathRef path = CGPathCreateMutable(); + RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false); + RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false); + RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false); + RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false); + CGPathCloseSubpath(path); + return path; } -@end +static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]) +{ + const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect); + const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect); + + // ellipseBoundingRect.origin.x -= ellipseCenterX; + // ellipseBoundingRect.origin.y -= ellipseCenterY; + + p1.x -= ellipseCenterX; + p1.y -= ellipseCenterY; + + p2.x -= ellipseCenterX; + p2.y -= ellipseCenterY; + + const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x); + const CGFloat a = ellipseBoundingRect.size.width / 2; + const CGFloat b = ellipseBoundingRect.size.height / 2; + const CGFloat c = p1.y - m * p1.x; + const CGFloat A = (b * b + a * a * m * m); + const CGFloat B = 2 * a * a * c * m; + const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2)); + + const CGFloat x_ = -B / (2 * A); + const CGFloat x1 = x_ + D; + const CGFloat x2 = x_ - D; + const CGFloat y1 = m * x1 + c; + const CGFloat y2 = m * x2 + c; + + intersections[0] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY); + intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY); + return YES; +} diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 8388b83cfb489c..62fb29116f5540 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -164,7 +164,9 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) RCT_EXPORT_SHADOW_PROPERTY(borderRightWidth, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(borderBottomWidth, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(borderLeftWidth, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(borderWidth, CGFloat); +RCT_CUSTOM_SHADOW_PROPERTY(borderWidth, CGFloat, RCTShadowView) { + [view setBorderWidth:[RCTConvert CGFloat:json]]; +} RCT_EXPORT_SHADOW_PROPERTY(marginTop, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(marginRight, CGFloat); diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h index 691aaaba15af3a..e78cc2ce7b26fc 100644 --- a/React/Views/RCTViewNodeProtocol.h +++ b/React/Views/RCTViewNodeProtocol.h @@ -35,7 +35,7 @@ @end // TODO: this is kinda dumb - let's come up with a -// better way of identifying root react views please! +// better way of identifying root React views please! static inline BOOL RCTIsReactRootView(NSNumber *reactTag) { return reactTag.integerValue % 10 == 1; } diff --git a/package.json b/package.json index be9d9b943b67bb..fde82307eaefa9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "main": "Libraries/react-native/react-native.js", "files": [ "React", + "React.podspec", "Examples/SampleApp", "Libraries", "packager", @@ -54,7 +55,7 @@ "optimist": "0.6.1", "promise": "^7.0.0", "react-timer-mixin": "^0.13.1", - "react-tools": "0.13.1", + "react-tools": "0.13.2", "rebound": "^0.0.12", "sane": "1.0.3", "source-map": "0.1.31", diff --git a/packager/packager.js b/packager/packager.js index 212e17f715e547..48552d93edbc7d 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -213,7 +213,8 @@ function runServer( .use(openStackFrameInEditor) .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) - .use(getFlowTypeCheckMiddleware(options)) + // Temporarily disable flow check until it's more stable + //.use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options)); options.projectRoots.forEach(function(root) { diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js index 8df5bbcc05ab41..80e62f05dee332 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js @@ -3,53 +3,14 @@ * * @provides Array.prototype.es6 * @polyfill - * @requires __DEV__ */ /*eslint-disable */ /*jslint bitwise: true */ -(function (undefined) { - if (__DEV__) { - // Define DEV-only setter that blows up when someone incorrectly - // iterates over arrays. - try { - Object.defineProperty && Object.defineProperty( - Array.prototype, - '__ARRAY_ENUMERATION_GUARD__', - { - configurable: true, - enumerable: true, - get: function() { - console.error( - 'Your code is broken! Do not iterate over arrays with ' + - 'for...in.' - ); - } - } - ); - } catch (e) { - // Nothing - } - } - +(function(undefined) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex function findIndex(predicate, context) { - /** - * Why am I seeing this "findIndex" method as a value in my array!? - * - * We polyfill the "findIndex" method -- called like - * `[1, 2, 3].findIndex(1)` -- for older browsers. A side effect of the way - * we do that is that the method is enumerable. If you were incorrectly - * iterating over your array using the object property iterator syntax - * `for (key in obj)` you will see the method name "findIndex" as a key. - * - * To fix your code please do one of the following: - * - * - Use a regular for loop with index. - * - Use one of the array methods: a.forEach, a.map, etc. - * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - */ if (this == null) { throw new TypeError( 'Array.prototype.findIndex called on null or undefined' @@ -69,32 +30,29 @@ } if (!Array.prototype.findIndex) { - Array.prototype.findIndex = findIndex; + Object.defineProperty(Array.prototype, 'findIndex', { + enumerable: false, + writable: true, + configurable: true, + value: findIndex + }); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find if (!Array.prototype.find) { - Array.prototype.find = function(predicate, context) { - /** - * Why am I seeing this "find" method as a value in my array!? - * - * We polyfill the "find" method -- called like - * `[1, 2, 3].find(1)` -- for older browsers. A side effect of the way - * we do that is that the method is enumerable. If you were incorrectly - * iterating over your array using the object property iterator syntax - * `for (key in obj)` you will see the method name "find" as a key. - * - * To fix your code please do one of the following: - * - * - Use a regular for loop with index. - * - Use one of the array methods: a.forEach, a.map, etc. - * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - */ - if (this == null) { - throw new TypeError('Array.prototype.find called on null or undefined'); + Object.defineProperty(Array.prototype, 'find', { + enumerable: false, + writable: true, + configurable: true, + value: function(predicate, context) { + if (this == null) { + throw new TypeError( + 'Array.prototype.find called on null or undefined' + ); + } + var index = findIndex.call(this, predicate, context); + return index === -1 ? undefined : this[index]; } - var index = findIndex.call(this, predicate, context); - return index === -1 ? undefined : this[index]; - }; + }); } })(); diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index 38ad19bf952675..cd1a28e558cdbb 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -26,7 +26,7 @@ var detectingWatcherClass = new Promise(function(resolve) { module.exports = FileWatcher; -var MAX_WAIT_TIME = 10000; +var MAX_WAIT_TIME = 25000; // Singleton var fileWatcher = null; diff --git a/packager/webSocketProxy.js b/packager/webSocketProxy.js index 8223bbf24b0e78..f863621362e421 100644 --- a/packager/webSocketProxy.js +++ b/packager/webSocketProxy.js @@ -34,7 +34,12 @@ function attachToServer(server, path) { ws.on('message', function(message) { allClientsExcept(ws).forEach(function(cn) { - cn.send(message); + try { + // Sometimes this call throws 'not opened' + cn.send(message); + } catch(e) { + console.warn('WARN: ' + e.message); + } }); }); });