Files
react-native/Libraries/Inspector/Inspector.js
T
Rick Hanlon 21396bb380 Enable inspector for Fabric
Summary:
## Overview
This diff refactors the Inspector, moving logic to look up view data for a touched view inside the renderer as `getInspectorDataForViewAtPoint`. We then implement that same function for Fabric in order to support the inspector in that renderer.

Requires https://github.com/facebook/react/pull/18388

## Motivation

Reason one for this refactor is that, previously, the inspector held all of the logic to look up view data for a given x,y touch coordinate. To do this, it would take the React tag and coordinates, look up the native view tag, measure it, and then ask React internals for the Fiber information of that tag. All of this is deeply coupled to React internals, yet the logic is outside of React core in the Inspector.

Reason two is that, for Fabric, the logic for getting the view data is different than Paper. In Fabric, we pass the x,y coordinates to native directly, which returns an instance handle. That handle can be used to measure the ShadowNode, or retrieve the Fiber information.

By moving the logic into the renderer in React core, we decouple the implementation details of looking up view data for a tapped point and allow ourselves the ability to add and change renderer-specific code for the actual lookup without impacting outsiders like the Inspector.

Changelog: [Internal]

Reviewed By: TheSavior

Differential Revision: D20291710

fbshipit-source-id: a125223f2e44a6483120c41dc6146ad75a0e3e68
2020-03-30 14:05:27 -07:00

349 lines
8.9 KiB
JavaScript

/**
* 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<HostComponent<mixed>>;
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<ReactRenderer> {
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 (
<View style={styles.container} pointerEvents="box-none">
{this.state.inspecting && (
<InspectorOverlay
inspected={this.state.inspected}
onTouchPoint={this.onTouchPoint.bind(this)}
/>
)}
<View style={[styles.panelContainer, panelContainerStyle]}>
<InspectorPanel
devtoolsIsOpen={!!this.state.devtoolsAgent}
inspecting={this.state.inspecting}
perfing={this.state.perfing}
setPerfing={this.setPerfing.bind(this)}
setInspecting={this.setInspecting.bind(this)}
inspected={this.state.inspected}
hierarchy={this.state.hierarchy}
selection={this.state.selection}
setSelection={this.setSelection.bind(this)}
touchTargeting={Touchable.TOUCH_TARGET_DEBUG}
setTouchTargeting={this.setTouchTargeting.bind(this)}
networking={this.state.networking}
setNetworking={this.setNetworking.bind(this)}
/>
</View>
</View>
);
}
}
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;