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:
Luke De Feo
2023-06-07 06:20:13 -07:00
committed by Facebook GitHub Bot
parent 2815ba0fb8
commit c13180a929
2 changed files with 78 additions and 40 deletions

View File

@@ -7,7 +7,7 @@
* @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 {produce, styled, theme, usePlugin, useValue} from 'flipper-plugin';
@@ -15,7 +15,6 @@ import {plugin} from '../index';
import {head, isEqual, throttle} from 'lodash';
import {Dropdown, Menu, Tooltip} from 'antd';
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
import {useFilteredValue} from '../hooks/usefilteredValue';
export const Visualization2D: React.FC<
{
@@ -31,6 +30,8 @@ export const Visualization2D: React.FC<
const snapshotNode = snapshot && nodes.get(snapshot.nodeId);
const focusedNodeId = useValue(instance.uiState.focusedNode);
const selectedNodeId = useValue(instance.uiState.selectedNode);
const hoveredNodeId = head(useValue(instance.uiState.hoveredNodes));
const focusState = useMemo(() => {
//use the snapshot node as root since we cant realistically visualise any node above this
const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes);
@@ -107,6 +108,16 @@ export const Visualization2D: React.FC<
height: toPx(focusState.actualRoot.bounds.height),
} as React.CSSProperties
}>
{hoveredNodeId && (
<OverlayBorder type="hovered" nodeId={hoveredNodeId} nodes={nodes} />
)}
{selectedNodeId && (
<OverlayBorder
type="selected"
nodeId={selectedNodeId}
nodes={nodes}
/>
)}
<div
ref={rootNodeRef as any}
onMouseLeave={(e) => {
@@ -170,19 +181,7 @@ function Visualization2DNode({
}) {
const instance = usePlugin(plugin);
const wasOrIsSelected = useCallback(
(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 {isLongHovered} = useHoverStates(node.id);
const ref = useRef<HTMLDivElement>(null);
let nestedChildren: NestedNode[];
@@ -234,22 +233,62 @@ function Visualization2DNode({
e.stopPropagation();
const hoveredNodes = instance.uiState.hoveredNodes.get();
if (hoveredNodes[0] === selectedNode) {
onSelectNode(undefined);
} else {
onSelectNode(hoveredNodes[0]);
}
onSelectNode(hoveredNodes[0]);
}}>
<NodeBorder
hovered={isHovered}
selected={isSelected}
tags={node.tags}></NodeBorder>
<NodeBorder />
{children}
</div>
</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) {
const instance = usePlugin(plugin);
const [isHovered, setIsHovered] = useState(false);
@@ -309,7 +348,7 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
key="focus"
text={`Focus ${hoveredNode?.name}`}
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
* as border is part of the box model
*/
const NodeBorder = styled.div<{
tags: Tag[];
hovered: boolean;
selected: boolean;
}>((props) => ({
const NodeBorder = styled.div({
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
boxSizing: 'border-box',
borderWidth: props.selected || props.hovered ? '2px' : '1px',
borderWidth: '1px',
borderStyle: 'solid',
color: 'transparent',
borderColor: props.selected
? theme.primaryColor
: props.hovered
? theme.selectionBackgroundColor
: theme.disabledColor,
}));
borderColor: theme.disabledColor,
});
const longHoverDelay = 200;
const pxScaleFactorCssVar = '--pxScaleFactor';
const MouseThrottle = 32;
function toPx(n: number) {
return `calc(${n}px / var(${pxScaleFactorCssVar})`;
return `calc(${n}px / var(${pxScaleFactorCssVar}))`;
}
function toNestedNode(

View File

@@ -375,7 +375,12 @@ function uiActions(uiState: UIState, nodes: Atom<Map<Id, UINode>>): UIActions {
});
};
const onSelectNode = (node?: Id) => {
uiState.selectedNode.set(node);
if (uiState.selectedNode.get() === node) {
uiState.selectedNode.set(undefined);
} else {
uiState.selectedNode.set(node);
}
if (node) {
const selectedNode = nodes.get().get(node);
const tags = selectedNode?.tags;
@@ -410,12 +415,14 @@ function uiActions(uiState: UIState, nodes: Atom<Map<Id, UINode>>): UIActions {
};
const onFocusNode = (node?: Id) => {
if (node) {
if (node != null) {
const focusedNode = nodes.get().get(node);
const tags = focusedNode?.tags;
if (tags) {
tracker.track('node-focused', {name: focusedNode.name, tags});
}
uiState.selectedNode.set(undefined);
}
uiState.focusedNode.set(node);