Summary: Clicking on a node a second time will unselect which was impossible before. The behaviour of the on click handler doesnt always line up what is currently hovered, this is a temporary work around. There are deeper issues with this in that on exit we hover the parent, but in some situations the parent is too small to propagate. In future we will use the mouse position and do a hit test and drive selection / hover from that Reviewed By: lblasa Differential Revision: D40637356 fbshipit-source-id: 9df19dbf619845891bb46624730d7cf74f73cf25
242 lines
6.2 KiB
TypeScript
242 lines
6.2 KiB
TypeScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {Id, Snapshot, Tag, UINode} from '../types';
|
|
import {styled, Layout, theme} from 'flipper-plugin';
|
|
import {Typography} from 'antd';
|
|
|
|
export const Visualization2D: React.FC<
|
|
{
|
|
root: Id;
|
|
nodes: Map<Id, UINode>;
|
|
snapshots: Map<Id, Snapshot>;
|
|
hoveredNode?: Id;
|
|
selectedNode?: Id;
|
|
onSelectNode: (id?: Id) => void;
|
|
onHoverNode: (id?: Id) => void;
|
|
modifierPressed: boolean;
|
|
} & React.HTMLAttributes<HTMLDivElement>
|
|
> = ({
|
|
root,
|
|
nodes,
|
|
snapshots,
|
|
hoveredNode,
|
|
selectedNode,
|
|
onSelectNode,
|
|
onHoverNode,
|
|
modifierPressed,
|
|
}) => {
|
|
//todo, do a bfs search for the first bounds found
|
|
const rootBounds = nodes.get(root)?.bounds;
|
|
const rootSnapshot = snapshots.get(root);
|
|
|
|
if (!rootBounds) {
|
|
return null;
|
|
}
|
|
return (
|
|
<Layout.Container gap="large">
|
|
<Typography.Title>Visualizer</Typography.Title>
|
|
|
|
<div
|
|
onMouseLeave={(e) => {
|
|
e.stopPropagation();
|
|
onHoverNode(undefined);
|
|
}}
|
|
style={{
|
|
/**
|
|
* This relative position is so the root visualization 2DNode and outer border has a non static element to
|
|
* position itself relative to.
|
|
*
|
|
* Subsequent Visualization2DNode are positioned relative to their parent as each one is position absolute
|
|
* which despite the name acts are a reference point for absolute positioning...
|
|
*/
|
|
position: 'relative',
|
|
width: toPx(rootBounds.width),
|
|
height: toPx(rootBounds.height),
|
|
overflow: 'hidden',
|
|
}}>
|
|
<OuterBorder />
|
|
{rootSnapshot ? (
|
|
<img
|
|
src={'data:image/jpeg;base64,' + rootSnapshot}
|
|
style={{maxWidth: '100%'}}
|
|
/>
|
|
) : null}
|
|
<Visualization2DNode
|
|
nodeId={root}
|
|
nodes={nodes}
|
|
snapshots={snapshots}
|
|
hoveredNode={hoveredNode}
|
|
selectedNode={selectedNode}
|
|
onSelectNode={onSelectNode}
|
|
onHoverNode={onHoverNode}
|
|
modifierPressed={modifierPressed}
|
|
/>
|
|
</div>
|
|
</Layout.Container>
|
|
);
|
|
};
|
|
|
|
function Visualization2DNode({
|
|
parentId,
|
|
nodeId,
|
|
nodes,
|
|
snapshots,
|
|
hoveredNode,
|
|
selectedNode,
|
|
onSelectNode,
|
|
onHoverNode,
|
|
modifierPressed,
|
|
}: {
|
|
nodeId: Id;
|
|
parentId?: Id;
|
|
nodes: Map<Id, UINode>;
|
|
snapshots: Map<Id, Snapshot>;
|
|
modifierPressed: boolean;
|
|
hoveredNode?: Id;
|
|
selectedNode?: Id;
|
|
onSelectNode: (id?: Id) => void;
|
|
onHoverNode: (id?: Id) => void;
|
|
}) {
|
|
const node = nodes.get(nodeId);
|
|
const snapshot = snapshots.get(nodeId);
|
|
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
const isHovered = hoveredNode === nodeId;
|
|
const isSelected = selectedNode === nodeId;
|
|
|
|
let childrenIds: Id[] = [];
|
|
|
|
//if there is an active child don't draw the other children
|
|
//this means we don't draw overlapping activities / tabs etc
|
|
if (node.activeChild) {
|
|
childrenIds = [node.activeChild];
|
|
} else {
|
|
childrenIds = node.children;
|
|
}
|
|
|
|
// stop drawing children if hovered with the modifier so you
|
|
// can see parent views without their children getting in the way
|
|
if (isHovered && modifierPressed) {
|
|
childrenIds = [];
|
|
}
|
|
|
|
const children = childrenIds.map((childId) => (
|
|
<Visualization2DNode
|
|
parentId={nodeId}
|
|
key={childId}
|
|
nodeId={childId}
|
|
nodes={nodes}
|
|
snapshots={snapshots}
|
|
hoveredNode={hoveredNode}
|
|
onSelectNode={onSelectNode}
|
|
onHoverNode={onHoverNode}
|
|
selectedNode={selectedNode}
|
|
modifierPressed={modifierPressed}
|
|
/>
|
|
));
|
|
|
|
const bounds = node.bounds ?? {x: 0, y: 0, width: 0, height: 0};
|
|
|
|
return (
|
|
<div
|
|
role="button"
|
|
tabIndex={0}
|
|
style={{
|
|
position: 'absolute',
|
|
cursor: 'pointer',
|
|
left: toPx(bounds.x),
|
|
top: toPx(bounds.y),
|
|
width: toPx(bounds.width),
|
|
height: toPx(bounds.height),
|
|
opacity: isSelected || isHovered ? 0.5 : 1,
|
|
backgroundColor: isSelected
|
|
? theme.primaryColor
|
|
: isHovered
|
|
? theme.selectionBackgroundColor
|
|
: 'transparent',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.stopPropagation();
|
|
onHoverNode(nodeId);
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.stopPropagation();
|
|
onHoverNode(parentId);
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
|
|
if (hoveredNode === selectedNode) {
|
|
onSelectNode(undefined);
|
|
} else {
|
|
//the way click is resolved doesn't always match what is hovered, this is a way to ensure what is hovered is selected
|
|
onSelectNode(hoveredNode);
|
|
}
|
|
}}>
|
|
<NodeBorder tags={node.tags}></NodeBorder>
|
|
{snapshot && (
|
|
<img
|
|
src={'data:image/jpeg;base64,' + snapshot}
|
|
style={{maxWidth: '100%'}}
|
|
/>
|
|
)}
|
|
{isHovered && <p style={{float: 'right'}}>{node.name}</p>}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* this is the border that shows the green or blue line, it is implemented as a sibling to the
|
|
* 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[]}>((props) => ({
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
bottom: 0,
|
|
right: 0,
|
|
borderWidth: '1px',
|
|
borderStyle: 'solid',
|
|
color: 'transparent',
|
|
borderColor: props.tags.includes('Declarative')
|
|
? 'green'
|
|
: props.tags.includes('Native')
|
|
? 'blue'
|
|
: 'black',
|
|
}));
|
|
|
|
const outerBorderWidth = '10px';
|
|
const outerBorderOffset = `-${outerBorderWidth}`;
|
|
|
|
//this is the thick black border around the whole vizualization, the border goes around the content
|
|
//hence the top,left,right,botton being negative to increase its size
|
|
const OuterBorder = styled.div({
|
|
boxSizing: 'border-box',
|
|
position: 'absolute',
|
|
top: outerBorderOffset,
|
|
left: outerBorderOffset,
|
|
right: outerBorderOffset,
|
|
bottom: outerBorderOffset,
|
|
borderWidth: outerBorderWidth,
|
|
borderStyle: 'solid',
|
|
borderColor: 'black',
|
|
borderRadius: '10px',
|
|
});
|
|
|
|
function toPx(n: number) {
|
|
return `${n / 2}px`;
|
|
}
|