Summary: ^ This change allows to take different snapshots for different nodes and render them each on the visualiser. At the moment, more than likely, this is not really used. At the same time, it fixes an issue whereas any subtree update can override and set the only visible snapshot. Reviewed By: LukeDefeo, antonk52 Differential Revision: D39821920 fbshipit-source-id: ab8f6a4a2a5e96801c951a4e3009cc571a617f22
246 lines
6.3 KiB
TypeScript
246 lines
6.3 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),
|
|
}}>
|
|
<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 hasOverlappingChild = childrenIds
|
|
.map((id) => nodes.get(id))
|
|
.find((child) => child?.bounds?.x === 0 || child?.bounds?.y === 0);
|
|
|
|
const isZeroWidthOrHeight =
|
|
node.bounds?.height === 0 || node.bounds?.width === 0;
|
|
|
|
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),
|
|
backgroundColor: isSelected
|
|
? theme.primaryColor
|
|
: isHovered
|
|
? theme.selectionBackgroundColor
|
|
: 'transparent',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.stopPropagation();
|
|
onHoverNode(nodeId);
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.stopPropagation();
|
|
onHoverNode(parentId);
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onSelectNode(nodeId);
|
|
}}>
|
|
<NodeBorder tags={node.tags}></NodeBorder>
|
|
{snapshot ? (
|
|
<img
|
|
src={'data:image/jpeg;base64,' + snapshot}
|
|
style={{maxWidth: '100%'}}
|
|
/>
|
|
) : (
|
|
<>
|
|
{/* Dirty hack to avoid showing highly overlapping text */}
|
|
{!hasOverlappingChild && !isZeroWidthOrHeight && node.bounds
|
|
? node.name
|
|
: null}
|
|
</>
|
|
)}
|
|
{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`;
|
|
}
|