diff --git a/Libraries/Inspector/DevtoolsHighlighter.js b/Libraries/Inspector/DevtoolsHighlighter.js deleted file mode 100644 index c3d6f1c3a3fbc3..00000000000000 --- a/Libraries/Inspector/DevtoolsHighlighter.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ - -import ElementBox from './ElementBox'; -import * as React from 'react'; -const {useEffect, useState} = React; - -const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - -export default function DevtoolsHighlighter(): React.Node { - const [inspected, setInspected] = useState(null); - useEffect(() => { - let devToolsAgent = null; - let hideTimeoutId = null; - - function onAgentHideNativeHighlight() { - // we wait to actually hide in order to avoid flicker - clearTimeout(hideTimeoutId); - hideTimeoutId = setTimeout(() => { - setInspected(null); - }, 100); - } - - function onAgentShowNativeHighlight(node: any) { - clearTimeout(hideTimeoutId); - // Shape of `node` is different in Fabric. - const component = node.canonical ?? node; - - component.measure((x, y, width, height, left, top) => { - setInspected({ - frame: {left, top, width, height}, - }); - }); - } - - function cleanup() { - const currentAgent = devToolsAgent; - if (currentAgent != null) { - currentAgent.removeListener( - 'hideNativeHighlight', - onAgentHideNativeHighlight, - ); - currentAgent.removeListener( - 'showNativeHighlight', - onAgentShowNativeHighlight, - ); - currentAgent.removeListener('shutdown', cleanup); - devToolsAgent = null; - } - } - - function _attachToDevtools(agent: Object) { - devToolsAgent = agent; - agent.addListener('hideNativeHighlight', onAgentHideNativeHighlight); - agent.addListener('showNativeHighlight', onAgentShowNativeHighlight); - agent.addListener('shutdown', cleanup); - } - - hook.on('react-devtools', _attachToDevtools); - if (hook.reactDevtoolsAgent) { - _attachToDevtools(hook.reactDevtoolsAgent); - } - return () => { - hook.off('react-devtools', _attachToDevtools); - cleanup(); - }; - }, []); - - if (inspected != null) { - return ; - } - return null; -} diff --git a/Libraries/Inspector/DevtoolsOverlay.js b/Libraries/Inspector/DevtoolsOverlay.js new file mode 100644 index 00000000000000..6df5593becb5f4 --- /dev/null +++ b/Libraries/Inspector/DevtoolsOverlay.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import ElementBox from './ElementBox'; +import * as React from 'react'; +import type {PressEvent} from '../Types/CoreEventTypes'; +import View from '../Components/View/View'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import Dimensions from '../Utilities/Dimensions'; +const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint'); +const ReactNative = require('../Renderer/shims/ReactNative'); + +import type {HostRef} from './getInspectorDataForViewAtPoint'; + +const {useEffect, useState, useCallback, useRef} = React; + +const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + +export default function DevtoolsOverlay({ + inspectedView, +}: { + inspectedView: ?HostRef, +}): React.Node { + const [inspected, setInspected] = useState(null); + const [isInspecting, setIsInspecting] = useState(false); + const devToolsAgentRef = useRef(null); + + useEffect(() => { + let devToolsAgent = null; + let hideTimeoutId = null; + + function onAgentHideNativeHighlight() { + // we wait to actually hide in order to avoid flicker + clearTimeout(hideTimeoutId); + hideTimeoutId = setTimeout(() => { + setInspected(null); + }, 100); + } + + function onAgentShowNativeHighlight(node: any) { + clearTimeout(hideTimeoutId); + // Shape of `node` is different in Fabric. + const component = node.canonical ?? node; + + component.measure((x, y, width, height, left, top) => { + setInspected({ + frame: {left, top, width, height}, + }); + }); + } + + function cleanup() { + const currentAgent = devToolsAgent; + if (currentAgent != null) { + currentAgent.removeListener( + 'hideNativeHighlight', + onAgentHideNativeHighlight, + ); + currentAgent.removeListener( + 'showNativeHighlight', + onAgentShowNativeHighlight, + ); + currentAgent.removeListener('shutdown', cleanup); + currentAgent.removeListener( + 'startInspectingNative', + onStartInspectingNative, + ); + currentAgent.removeListener( + 'stopInspectingNative', + onStopInspectingNative, + ); + devToolsAgent = null; + } + devToolsAgentRef.current = null; + } + + function onStartInspectingNative() { + setIsInspecting(true); + } + + function onStopInspectingNative() { + setIsInspecting(false); + } + + function _attachToDevtools(agent: Object) { + devToolsAgent = agent; + devToolsAgentRef.current = agent; + agent.addListener('hideNativeHighlight', onAgentHideNativeHighlight); + agent.addListener('showNativeHighlight', onAgentShowNativeHighlight); + agent.addListener('shutdown', cleanup); + agent.addListener('startInspectingNative', onStartInspectingNative); + agent.addListener('stopInspectingNative', onStopInspectingNative); + } + + hook.on('react-devtools', _attachToDevtools); + if (hook.reactDevtoolsAgent) { + _attachToDevtools(hook.reactDevtoolsAgent); + } + return () => { + hook.off('react-devtools', _attachToDevtools); + cleanup(); + }; + }, []); + + const findViewForTouchEvent = useCallback( + (e: PressEvent) => { + const agent = devToolsAgentRef.current; + if (agent == null) { + return; + } + const {locationX, locationY} = e.nativeEvent.touches[0]; + getInspectorDataForViewAtPoint( + inspectedView, + locationX, + locationY, + viewData => { + const {touchedViewTag} = viewData; + if (touchedViewTag != null) { + agent.selectNode(ReactNative.findNodeHandle(touchedViewTag)); + return true; + } + return false; + }, + ); + }, + [inspectedView], + ); + + const shouldSetResponser = useCallback( + (e: PressEvent): boolean => { + findViewForTouchEvent(e); + return true; + }, + [findViewForTouchEvent], + ); + + let highlight = inspected ? : null; + if (isInspecting) { + return ( + + {highlight} + + ); + } + return highlight; +} + +const styles = StyleSheet.create({ + inspector: { + backgroundColor: 'transparent', + position: 'absolute', + left: 0, + top: 0, + right: 0, + }, +}); diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 504c6963f5386a..ff2a2eda91c34d 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -20,70 +20,18 @@ const ReactNative = require('../Renderer/shims/ReactNative'); const StyleSheet = require('../StyleSheet/StyleSheet'); const View = require('../Components/View/View'); const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes'); +const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint'); -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, - ... - }, -}; +import type {TouchedViewDataAtPoint} from '../Renderer/shims/ReactNativeTypes'; +import type {HostRef} from './getInspectorDataForViewAtPoint'; 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'); hook.nativeStyleEditorValidAttributes = Object.keys(ReactNativeStyleAttributes); -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, @@ -221,6 +169,7 @@ class Inspector extends React.Component< this._setTouchedViewData(viewData); this._setTouchedViewData = null; } + return false; }, ); } diff --git a/Libraries/Inspector/getInspectorDataForViewAtPoint.js b/Libraries/Inspector/getInspectorDataForViewAtPoint.js new file mode 100644 index 00000000000000..94d2a915e9ebe4 --- /dev/null +++ b/Libraries/Inspector/getInspectorDataForViewAtPoint.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import type { + HostComponent, + TouchedViewDataAtPoint, +} from '../Renderer/shims/ReactNativeTypes'; + +export type HostRef = React.ElementRef>; +export type ReactRenderer = { + rendererConfig: { + getInspectorDataForViewAtPoint: ( + inspectedView: ?HostRef, + locationX: number, + locationY: number, + callback: Function, + ) => void, + ... + }, +}; + +const React = require('react'); +const invariant = require('invariant'); + +const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; +const renderers = findRenderers(); + +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; +} + +module.exports = function getInspectorDataForViewAtPoint( + inspectedView: ?HostRef, + locationX: number, + locationY: number, + callback: (viewData: TouchedViewDataAtPoint) => boolean, +) { + let shouldBreak = false; + // Check all renderers for inspector data. + for (let i = 0; i < renderers.length; i++) { + if (shouldBreak) { + break; + } + 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) { + shouldBreak = callback(viewData); + } + }, + ); + } + } +}; diff --git a/Libraries/ReactNative/AppContainer.js b/Libraries/ReactNative/AppContainer.js index ccf04260152b34..c49cf04ced8520 100644 --- a/Libraries/ReactNative/AppContainer.js +++ b/Libraries/ReactNative/AppContainer.js @@ -29,6 +29,7 @@ type Props = $ReadOnly<{| type State = {| inspector: ?React.Node, + devtoolsOverlay: ?React.Node, mainKey: number, hasError: boolean, |}; @@ -36,6 +37,7 @@ type State = {| class AppContainer extends React.Component { state: State = { inspector: null, + devtoolsOverlay: null, mainKey: 1, hasError: false, }; @@ -65,6 +67,14 @@ class AppContainer extends React.Component { this.setState({inspector}); }, ); + if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null) { + const DevtoolsOverlay = + require('../Inspector/DevtoolsOverlay').default; + const devtoolsOverlay = ( + + ); + this.setState({devtoolsOverlay}); + } } } } @@ -77,7 +87,6 @@ class AppContainer extends React.Component { render(): React.Node { let logBox = null; - let devtoolsHighlighter = null; if (__DEV__) { if (!global.__RCTProfileIsProfiling) { if (!this.props.internal_excludeLogBox) { @@ -85,17 +94,12 @@ class AppContainer extends React.Component { require('../LogBox/LogBoxNotificationContainer').default; logBox = ; } - if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null) { - const DevtoolsHighlighter = - require('../Inspector/DevtoolsHighlighter').default; - devtoolsHighlighter = ; - } } } let innerView: React.Node = ( { {!this.state.hasError && innerView} - {devtoolsHighlighter} + {this.state.devtoolsOverlay} {this.state.inspector} {logBox}