/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow */ 'use strict'; const Dimensions = require('../Utilities/Dimensions'); const InspectorOverlay = require('./InspectorOverlay'); const InspectorPanel = require('./InspectorPanel'); const Platform = require('../Utilities/Platform'); const React = require('react'); const ReactNative = require('../Renderer/shims/ReactNative'); const StyleSheet = require('../StyleSheet/StyleSheet'); const Touchable = require('../Components/Touchable/Touchable'); const View = require('../Components/View/View'); const invariant = require('invariant'); import type { HostComponent, TouchedViewDataAtPoint, } from '../Renderer/shims/ReactNativeTypes'; type HostRef = React.ElementRef>; export type ReactRenderer = { rendererConfig: { getInspectorDataForViewAtPoint: ( inspectedView: ?HostRef, locationX: number, locationY: number, callback: Function, ) => void, ... }, }; const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; const renderers = findRenderers(); // Required for React DevTools to view/edit React Native styles in Flipper. // Flipper doesn't inject these values when initializing DevTools. hook.resolveRNStyle = require('../StyleSheet/flattenStyle'); const viewConfig = require('../Components/View/ReactNativeViewViewConfig'); hook.nativeStyleEditorValidAttributes = Object.keys( viewConfig.validAttributes.style, ); function findRenderers(): $ReadOnlyArray { const allRenderers = Array.from(hook.renderers.values()); invariant( allRenderers.length >= 1, 'Expected to find at least one React Native renderer on DevTools hook.', ); return allRenderers; } function getInspectorDataForViewAtPoint( inspectedView: ?HostRef, locationX: number, locationY: number, callback: (viewData: TouchedViewDataAtPoint) => void, ) { // Check all renderers for inspector data. for (let i = 0; i < renderers.length; i++) { const renderer = renderers[i]; if (renderer?.rendererConfig?.getInspectorDataForViewAtPoint != null) { renderer.rendererConfig.getInspectorDataForViewAtPoint( inspectedView, locationX, locationY, viewData => { // Only return with non-empty view data since only one renderer will have this view. if (viewData && viewData.hierarchy.length > 0) { callback(viewData); } }, ); } } } class Inspector extends React.Component< { inspectedView: ?HostRef, onRequestRerenderApp: (callback: (instance: ?HostRef) => void) => void, ... }, { devtoolsAgent: ?Object, hierarchy: any, panelPos: string, inspecting: boolean, selection: ?number, perfing: boolean, inspected: any, inspectedView: ?HostRef, networking: boolean, ... }, > { _hideTimeoutID: TimeoutID | null = null; _subs: ?Array<() => void>; _setTouchedViewData: ?(TouchedViewDataAtPoint) => void; constructor(props: Object) { super(props); this.state = { devtoolsAgent: null, hierarchy: null, panelPos: 'bottom', inspecting: true, perfing: false, inspected: null, selection: null, inspectedView: this.props.inspectedView, networking: false, }; } componentDidMount() { hook.on('react-devtools', this._attachToDevtools); // if devtools is already started if (hook.reactDevtoolsAgent) { this._attachToDevtools(hook.reactDevtoolsAgent); } } componentWillUnmount() { if (this._subs) { this._subs.map(fn => fn()); } hook.off('react-devtools', this._attachToDevtools); this._setTouchedViewData = null; } UNSAFE_componentWillReceiveProps(newProps: Object) { this.setState({inspectedView: newProps.inspectedView}); } _attachToDevtools = (agent: Object) => { agent.addListener('hideNativeHighlight', this._onAgentHideNativeHighlight); agent.addListener('showNativeHighlight', this._onAgentShowNativeHighlight); agent.addListener('shutdown', this._onAgentShutdown); this.setState({ devtoolsAgent: agent, }); }; _onAgentHideNativeHighlight = () => { if (this.state.inspected === null) { return; } // we wait to actually hide in order to avoid flicker this._hideTimeoutID = setTimeout(() => { this.setState({ inspected: null, }); }, 100); }; _onAgentShowNativeHighlight = node => { clearTimeout(this._hideTimeoutID); node.measure((x, y, width, height, left, top) => { this.setState({ hierarchy: [], inspected: { frame: {left, top, width, height}, }, }); }); }; _onAgentShutdown = () => { const agent = this.state.devtoolsAgent; if (agent != null) { agent.removeListener( 'hideNativeHighlight', this._onAgentHideNativeHighlight, ); agent.removeListener( 'showNativeHighlight', this._onAgentShowNativeHighlight, ); agent.removeListener('shutdown', this._onAgentShutdown); this.setState({devtoolsAgent: null}); } }; setSelection(i: number) { const hierarchyItem = this.state.hierarchy[i]; // we pass in ReactNative.findNodeHandle as the method is injected const {measure, props, source} = hierarchyItem.getInspectorData( ReactNative.findNodeHandle, ); measure((x, y, width, height, left, top) => { this.setState({ inspected: { frame: {left, top, width, height}, style: props.style, source, }, selection: i, }); }); } onTouchPoint(locationX: number, locationY: number) { this._setTouchedViewData = viewData => { const { hierarchy, props, selectedIndex, source, frame, pointerY, touchedViewTag, } = viewData; // Sync the touched view with React DevTools. // Note: This is Paper only. To support Fabric, // DevTools needs to be updated to not rely on view tags. if (this.state.devtoolsAgent && touchedViewTag) { this.state.devtoolsAgent.selectNode( ReactNative.findNodeHandle(touchedViewTag), ); } this.setState({ panelPos: pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom', selection: selectedIndex, hierarchy, inspected: { style: props.style, frame, source, }, }); }; getInspectorDataForViewAtPoint( this.state.inspectedView, locationX, locationY, viewData => { if (this._setTouchedViewData != null) { this._setTouchedViewData(viewData); this._setTouchedViewData = null; } }, ); } setPerfing(val: boolean) { this.setState({ perfing: val, inspecting: false, inspected: null, networking: false, }); } setInspecting(val: boolean) { this.setState({ inspecting: val, inspected: null, }); } setTouchTargeting(val: boolean) { Touchable.TOUCH_TARGET_DEBUG = val; this.props.onRequestRerenderApp(inspectedView => { this.setState({inspectedView}); }); } setNetworking(val: boolean) { this.setState({ networking: val, perfing: false, inspecting: false, inspected: null, }); } render(): React.Node { const panelContainerStyle = this.state.panelPos === 'bottom' ? {bottom: 0} : {top: Platform.OS === 'ios' ? 20 : 0}; return ( {this.state.inspecting && ( )} ); } } const styles = StyleSheet.create({ container: { position: 'absolute', backgroundColor: 'transparent', top: 0, left: 0, right: 0, bottom: 0, }, panelContainer: { position: 'absolute', left: 0, right: 0, }, }); module.exports = Inspector;