Draw selected and hovered node borders in overlay layer
Summary: The previous approach of setting some of the borders to be thicker and different colours was flakey, sometimes parts of the border would be cut off by a parent With this approach we figure out the offset relative to the root of the visualiser, and draw a box that is definatley on top. It works much more reliably Also fixed a couple of other niggles: 1. Can unselect when clicking again 2. Going into focus mode clears selection since your selection may not be in the focused area and there is a phantom box Reviewed By: lblasa Differential Revision: D46224034 fbshipit-source-id: 24bed8db38cddab796f786e7e0a4acfe7c6a9089
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2815ba0fb8
commit
c13180a929
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {Bounds, Coordinate, Id, NestedNode, Tag, UINode} from '../types';
|
import {Bounds, Coordinate, Id, NestedNode, Tag, UINode} from '../types';
|
||||||
|
|
||||||
import {produce, styled, theme, usePlugin, useValue} from 'flipper-plugin';
|
import {produce, styled, theme, usePlugin, useValue} from 'flipper-plugin';
|
||||||
@@ -15,7 +15,6 @@ import {plugin} from '../index';
|
|||||||
import {head, isEqual, throttle} from 'lodash';
|
import {head, isEqual, throttle} from 'lodash';
|
||||||
import {Dropdown, Menu, Tooltip} from 'antd';
|
import {Dropdown, Menu, Tooltip} from 'antd';
|
||||||
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
|
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
|
||||||
import {useFilteredValue} from '../hooks/usefilteredValue';
|
|
||||||
|
|
||||||
export const Visualization2D: React.FC<
|
export const Visualization2D: React.FC<
|
||||||
{
|
{
|
||||||
@@ -31,6 +30,8 @@ export const Visualization2D: React.FC<
|
|||||||
const snapshotNode = snapshot && nodes.get(snapshot.nodeId);
|
const snapshotNode = snapshot && nodes.get(snapshot.nodeId);
|
||||||
const focusedNodeId = useValue(instance.uiState.focusedNode);
|
const focusedNodeId = useValue(instance.uiState.focusedNode);
|
||||||
|
|
||||||
|
const selectedNodeId = useValue(instance.uiState.selectedNode);
|
||||||
|
const hoveredNodeId = head(useValue(instance.uiState.hoveredNodes));
|
||||||
const focusState = useMemo(() => {
|
const focusState = useMemo(() => {
|
||||||
//use the snapshot node as root since we cant realistically visualise any node above this
|
//use the snapshot node as root since we cant realistically visualise any node above this
|
||||||
const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes);
|
const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes);
|
||||||
@@ -107,6 +108,16 @@ export const Visualization2D: React.FC<
|
|||||||
height: toPx(focusState.actualRoot.bounds.height),
|
height: toPx(focusState.actualRoot.bounds.height),
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}>
|
}>
|
||||||
|
{hoveredNodeId && (
|
||||||
|
<OverlayBorder type="hovered" nodeId={hoveredNodeId} nodes={nodes} />
|
||||||
|
)}
|
||||||
|
{selectedNodeId && (
|
||||||
|
<OverlayBorder
|
||||||
|
type="selected"
|
||||||
|
nodeId={selectedNodeId}
|
||||||
|
nodes={nodes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
ref={rootNodeRef as any}
|
ref={rootNodeRef as any}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
@@ -170,19 +181,7 @@ function Visualization2DNode({
|
|||||||
}) {
|
}) {
|
||||||
const instance = usePlugin(plugin);
|
const instance = usePlugin(plugin);
|
||||||
|
|
||||||
const wasOrIsSelected = useCallback(
|
const {isLongHovered} = useHoverStates(node.id);
|
||||||
(curValue?: Id, prevValue?: Id) =>
|
|
||||||
curValue === node.id || prevValue === node.id,
|
|
||||||
[node.id],
|
|
||||||
);
|
|
||||||
const selectedNode = useFilteredValue(
|
|
||||||
instance.uiState.selectedNode,
|
|
||||||
wasOrIsSelected,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSelected = selectedNode === node.id;
|
|
||||||
|
|
||||||
const {isHovered, isLongHovered} = useHoverStates(node.id);
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
let nestedChildren: NestedNode[];
|
let nestedChildren: NestedNode[];
|
||||||
|
|
||||||
@@ -234,22 +233,62 @@ function Visualization2DNode({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const hoveredNodes = instance.uiState.hoveredNodes.get();
|
const hoveredNodes = instance.uiState.hoveredNodes.get();
|
||||||
if (hoveredNodes[0] === selectedNode) {
|
|
||||||
onSelectNode(undefined);
|
onSelectNode(hoveredNodes[0]);
|
||||||
} else {
|
|
||||||
onSelectNode(hoveredNodes[0]);
|
|
||||||
}
|
|
||||||
}}>
|
}}>
|
||||||
<NodeBorder
|
<NodeBorder />
|
||||||
hovered={isHovered}
|
|
||||||
selected={isSelected}
|
|
||||||
tags={node.tags}></NodeBorder>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OverlayBorder = styled.div<{
|
||||||
|
type: 'selected' | 'hovered';
|
||||||
|
nodeId: Id;
|
||||||
|
nodes: Map<Id, UINode>;
|
||||||
|
}>(({type, nodeId, nodes}) => {
|
||||||
|
const offset = getTotalOffset(nodeId, nodes);
|
||||||
|
const node = nodes.get(nodeId);
|
||||||
|
return {
|
||||||
|
zIndex: 100,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'absolute',
|
||||||
|
top: toPx(offset.y),
|
||||||
|
left: toPx(offset.x),
|
||||||
|
width: toPx(node?.bounds?.width ?? 0),
|
||||||
|
height: toPx(node?.bounds?.height ?? 0),
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
color: 'transparent',
|
||||||
|
borderColor:
|
||||||
|
type === 'selected' ? theme.primaryColor : theme.selectionBackgroundColor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* computes the x,y offset of a given node from the root of the visualization
|
||||||
|
* in node coordinates
|
||||||
|
*/
|
||||||
|
function getTotalOffset(id: Id, nodes: Map<Id, UINode>): Coordinate {
|
||||||
|
const offset = {x: 0, y: 0};
|
||||||
|
let curId: Id | undefined = id;
|
||||||
|
|
||||||
|
while (curId != null) {
|
||||||
|
const cur = nodes.get(curId);
|
||||||
|
if (cur != null) {
|
||||||
|
offset.x += cur.bounds.x;
|
||||||
|
offset.y += cur.bounds.y;
|
||||||
|
}
|
||||||
|
curId = cur?.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
function useHoverStates(nodeId: Id) {
|
function useHoverStates(nodeId: Id) {
|
||||||
const instance = usePlugin(plugin);
|
const instance = usePlugin(plugin);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@@ -309,7 +348,7 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
|
|||||||
key="focus"
|
key="focus"
|
||||||
text={`Focus ${hoveredNode?.name}`}
|
text={`Focus ${hoveredNode?.name}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
instance.uiState.focusedNode.set(hoveredNode?.id);
|
instance.uiActions.onFocusNode(hoveredNode?.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -335,33 +374,25 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
|
|||||||
* node itself so that it has the same size but the border doesnt affect the sizing of its children
|
* node itself so that it has the same size but the border doesnt affect the sizing of its children
|
||||||
* as border is part of the box model
|
* as border is part of the box model
|
||||||
*/
|
*/
|
||||||
const NodeBorder = styled.div<{
|
const NodeBorder = styled.div({
|
||||||
tags: Tag[];
|
|
||||||
hovered: boolean;
|
|
||||||
selected: boolean;
|
|
||||||
}>((props) => ({
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
borderWidth: props.selected || props.hovered ? '2px' : '1px',
|
borderWidth: '1px',
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
color: 'transparent',
|
color: 'transparent',
|
||||||
borderColor: props.selected
|
borderColor: theme.disabledColor,
|
||||||
? theme.primaryColor
|
});
|
||||||
: props.hovered
|
|
||||||
? theme.selectionBackgroundColor
|
|
||||||
: theme.disabledColor,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const longHoverDelay = 200;
|
const longHoverDelay = 200;
|
||||||
const pxScaleFactorCssVar = '--pxScaleFactor';
|
const pxScaleFactorCssVar = '--pxScaleFactor';
|
||||||
const MouseThrottle = 32;
|
const MouseThrottle = 32;
|
||||||
|
|
||||||
function toPx(n: number) {
|
function toPx(n: number) {
|
||||||
return `calc(${n}px / var(${pxScaleFactorCssVar})`;
|
return `calc(${n}px / var(${pxScaleFactorCssVar}))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNestedNode(
|
function toNestedNode(
|
||||||
|
|||||||
@@ -375,7 +375,12 @@ function uiActions(uiState: UIState, nodes: Atom<Map<Id, UINode>>): UIActions {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onSelectNode = (node?: Id) => {
|
const onSelectNode = (node?: Id) => {
|
||||||
uiState.selectedNode.set(node);
|
if (uiState.selectedNode.get() === node) {
|
||||||
|
uiState.selectedNode.set(undefined);
|
||||||
|
} else {
|
||||||
|
uiState.selectedNode.set(node);
|
||||||
|
}
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
const selectedNode = nodes.get().get(node);
|
const selectedNode = nodes.get().get(node);
|
||||||
const tags = selectedNode?.tags;
|
const tags = selectedNode?.tags;
|
||||||
@@ -410,12 +415,14 @@ function uiActions(uiState: UIState, nodes: Atom<Map<Id, UINode>>): UIActions {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFocusNode = (node?: Id) => {
|
const onFocusNode = (node?: Id) => {
|
||||||
if (node) {
|
if (node != null) {
|
||||||
const focusedNode = nodes.get().get(node);
|
const focusedNode = nodes.get().get(node);
|
||||||
const tags = focusedNode?.tags;
|
const tags = focusedNode?.tags;
|
||||||
if (tags) {
|
if (tags) {
|
||||||
tracker.track('node-focused', {name: focusedNode.name, tags});
|
tracker.track('node-focused', {name: focusedNode.name, tags});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uiState.selectedNode.set(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.focusedNode.set(node);
|
uiState.focusedNode.set(node);
|
||||||
|
|||||||
Reference in New Issue
Block a user