mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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
349 lines
8.9 KiB
JavaScript
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;
|