diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index 07091083a..2d66bd345 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -8,7 +8,7 @@ */ import {Id, UINode} from '../types'; -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useMemo, useRef} from 'react'; import { Tree as ComplexTree, ControlledTreeEnvironment, @@ -26,19 +26,19 @@ export function Tree(props: { rootId: Id; nodes: Map; selectedNode?: Id; - hoveredNode?: Id; onSelectNode: (id: Id) => void; - onHoveredNode: (id?: Id) => void; }) { const instance = usePlugin(plugin); const expandedItems = useValue(instance.treeState).expandedNodes; - const items = toComplexTree(props.nodes); + const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]); + const hoveredNode = useValue(instance.hoveredNode); const treeRef = useRef(); + useEffect(() => { //this makes the keyboard arrow controls work always, even when using the visualiser treeRef.current?.focusTree('tree', true); - }, [props.hoveredNode, props.selectedNode]); + }, [hoveredNode, props.selectedNode]); return ( props.onHoveredNode(item.index)} + onFocusItem={(item) => { + instance.hoveredNode.set(item.index); + }} onExpandItem={(item) => { instance.treeState.update((draft) => { draft.expandedNodes.push(item.index); @@ -85,8 +87,9 @@ export function Tree(props: { actions.selectItem(); } }, + onMouseOver: () => { - props.onHoveredNode(item.index); + instance.hoveredNode.set(item.index); }, }), }}> diff --git a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx index 8725a057b..30070bb29 100644 --- a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx @@ -7,47 +7,81 @@ * @format */ -import React from 'react'; -import {Id, NestedNode, Snapshot, Tag, UINode} from '../types'; -import {styled, Layout, theme} from 'flipper-plugin'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; +import { + Bounds, + Coordinate, + Id, + NestedNode, + Snapshot, + Tag, + UINode, +} from '../types'; + +import {styled, theme, usePlugin, Atom} from 'flipper-plugin'; +import {plugin} from '../index'; +import {throttle} from 'lodash'; export const Visualization2D: React.FC< { rootId: Id; nodes: Map; snapshots: Map; - hoveredNode?: Id; selectedNode?: Id; onSelectNode: (id?: Id) => void; - onHoverNode: (id?: Id) => void; modifierPressed: boolean; } & React.HTMLAttributes > = ({ rootId, nodes, snapshots, - hoveredNode, selectedNode, onSelectNode, - onHoverNode, modifierPressed, }) => { - //todo, do a bfs search for the first bounds found + const root = useMemo(() => toNestedNode(rootId, nodes), [rootId, nodes]); + const rootNodeRef = useRef(); + const instance = usePlugin(plugin); - const rootSnapshot = snapshots.get(rootId); - const root = toNestedNode(rootId, nodes); + useEffect(() => { + const mouseListener = throttle((ev: MouseEvent) => { + const domRect = rootNodeRef.current?.getBoundingClientRect(); + if (!root || !domRect) { + return; + } + + //make the mouse coord relative to the dom rect of the visualizer + const offsetMouse = offsetCoordinate( + {x: ev.clientX, y: ev.clientY}, + domRect, + ); + const scaledMouse = { + x: offsetMouse.x * pxScaleFactor, + y: offsetMouse.y * pxScaleFactor, + }; + + const targeted = hitTest(root, scaledMouse, root.bounds); + if (targeted && targeted.id !== instance.hoveredNode.get()) { + instance.hoveredNode.set(targeted.id); + } + }, MouseThrottle); + window.addEventListener('mousemove', mouseListener); + + return () => { + window.removeEventListener('mousemove', mouseListener); + }; + }, [instance.hoveredNode, root]); if (!root) { return null; } - const rootBounds = root.bounds; - return (
{ e.stopPropagation(); - onHoverNode(undefined); + instance.hoveredNode.set(undefined); }} style={{ /** @@ -58,50 +92,62 @@ export const Visualization2D: React.FC< * which despite the name acts are a reference point for absolute positioning... */ position: 'relative', - width: toPx(rootBounds.width), - height: toPx(rootBounds.height), + width: toPx(root.bounds.width), + height: toPx(root.bounds.height), overflow: 'hidden', }}> - {rootSnapshot ? ( - - ) : null} -
); }; +const MemoedVisualizationNode2D = React.memo( + Visualization2DNode, + (prev, next) => { + return ( + prev.node === next.node && + prev.modifierPressed === next.modifierPressed && + prev.selectedNode === next.selectedNode + ); + }, +); + function Visualization2DNode({ node, snapshots, - hoveredNode, selectedNode, onSelectNode, - onHoverNode, modifierPressed, }: { node: NestedNode; snapshots: Map; modifierPressed: boolean; - hoveredNode?: Id; selectedNode?: Id; onSelectNode: (id?: Id) => void; - onHoverNode: (id?: Id) => void; }) { const snapshot = snapshots.get(node.id); + const instance = usePlugin(plugin); + + const [isHovered, setIsHovered] = useState(false); + useEffect(() => { + const listener = (newValue?: Id, prevValue?: Id) => { + if (prevValue === node.id || newValue === node.id) { + setIsHovered(newValue === node.id); + } + }; + instance.hoveredNode.subscribe(listener); + return () => { + instance.hoveredNode.unsubscribe(listener); + }; + }, [instance.hoveredNode, node.id]); - const isHovered = hoveredNode === node.id; const isSelected = selectedNode === node.id; let nestedChildren: NestedNode[]; @@ -121,13 +167,11 @@ function Visualization2DNode({ } const children = nestedChildren.map((child) => ( - @@ -151,21 +195,13 @@ function Visualization2DNode({ ? theme.selectionBackgroundColor : 'transparent', }} - onMouseEnter={(e) => { - e.stopPropagation(); - onHoverNode(node.id); - }} - onMouseLeave={(e) => { - e.stopPropagation(); - // onHoverNode(parentId); - }} onClick={(e) => { e.stopPropagation(); + const hoveredNode = instance.hoveredNode.get(); 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); } }}> @@ -221,8 +257,11 @@ const OuterBorder = styled.div({ borderRadius: '10px', }); +const pxScaleFactor = 2; +const MouseThrottle = 32; + function toPx(n: number) { - return `${n / 2}px`; + return `${n / pxScaleFactor}px`; } function toNestedNode( @@ -251,3 +290,53 @@ function toNestedNode( const root = nodes.get(rootId); return root ? uiNodeToNestedNode(root) : undefined; } + +function hitTest( + node: NestedNode, + mouseCoordinate: Coordinate, + parentBounds: Bounds, +): NestedNode | undefined { + const nodeBounds = node.bounds || parentBounds; + + if (boundsContainsCoordinate(nodeBounds, mouseCoordinate)) { + let children = node.children; + + if (node.activeChildIdx) { + children = [node.children[node.activeChildIdx]]; + } + const offsetMouseCoord = offsetCoordinate(mouseCoordinate, nodeBounds); + for (const child of children) { + const childHit = hitTest( + child, + offsetMouseCoord, + (parentBounds = nodeBounds), + ); + if (childHit) { + return childHit; + } + } + + return node; + } + + return undefined; +} + +function boundsContainsCoordinate(bounds: Bounds, coordinate: Coordinate) { + return ( + coordinate.x >= bounds.x && + coordinate.x <= bounds.x + bounds.width && + coordinate.y >= bounds.y && + coordinate.y <= bounds.y + bounds.height + ); +} + +function offsetCoordinate( + coordinate: Coordinate, + offset: Coordinate, +): Coordinate { + return { + x: coordinate.x - offset.x, + y: coordinate.y - offset.y, + }; +} diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index dc781c970..0c4b45cbe 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -27,7 +27,6 @@ export function Component() { const [showPerfStats, setShowPerfStats] = useState(false); const [selectedNode, setSelectedNode] = useState(undefined); - const [hoveredNode, setHoveredNode] = useState(undefined); useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show)); @@ -55,9 +54,7 @@ export function Component() { @@ -66,8 +63,6 @@ export function Component() { rootId={rootId} nodes={nodes} snapshots={snapshots} - hoveredNode={hoveredNode} - onHoverNode={setHoveredNode} selectedNode={selectedNode} onSelectNode={setSelectedNode} modifierPressed={ctrlPressed} diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index bcdfbb11a..744a3d901 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -52,6 +52,8 @@ export function plugin(client: PluginClient) { const treeState = createState({expandedNodes: []}); + const hoveredNode = createState(undefined); + client.onMessage('coordinateUpdate', (event) => { nodes.update((draft) => { const node = draft.get(event.nodeId); @@ -101,6 +103,7 @@ export function plugin(client: PluginClient) { nodes, metadata, snapshots, + hoveredNode, perfEvents, treeState, }; diff --git a/desktop/plugins/public/ui-debugger/package.json b/desktop/plugins/public/ui-debugger/package.json index f51191943..beba5b62b 100644 --- a/desktop/plugins/public/ui-debugger/package.json +++ b/desktop/plugins/public/ui-debugger/package.json @@ -13,6 +13,7 @@ "flipper-plugin" ], "dependencies": { + "lodash": "^4.17.21", "react-color": "^2.19.3", "react-complex-tree" : "^1.1.11", "react-hotkeys-hook": "^3.4.7"