From 477eae1993798813d227d0bcd2cc3562ce21b664 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Mon, 14 Nov 2022 07:05:58 -0800 Subject: [PATCH] Hit test can produce multiple nodes Summary: There are situations where multiple siblings overlap and they are both hit. Previously we picked the first one in the hierachy. Now we produce a list of hit children. The list will not have 2 nodes in the same ancestor path. We store the hovered nodes as a list as we may want to present a modal in future to ask user which node they indented to select. That said simply sorting nodes by area seems to give decent results so we can start with this Reviewed By: lblasa Differential Revision: D41220271 fbshipit-source-id: 643a369113da28e8c4749725a7aee7aa5d08c401 --- .../public/ui-debugger/components/Tree.tsx | 11 ++- .../components/Visualization2D.tsx | 93 +++++++++++-------- desktop/plugins/public/ui-debugger/index.tsx | 6 +- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index 2d66bd345..4b3102c9c 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -21,6 +21,7 @@ import { InteractionMode, TreeEnvironmentRef, } from 'react-complex-tree/lib/esm/types'; +import {head} from 'lodash'; export function Tree(props: { rootId: Id; @@ -32,13 +33,13 @@ export function Tree(props: { const expandedItems = useValue(instance.treeState).expandedNodes; const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]); - const hoveredNode = useValue(instance.hoveredNode); + const hoveredNodes = useValue(instance.hoveredNodes); const treeRef = useRef(); useEffect(() => { //this makes the keyboard arrow controls work always, even when using the visualiser treeRef.current?.focusTree('tree', true); - }, [hoveredNode, props.selectedNode]); + }, [hoveredNodes, props.selectedNode]); return ( { - instance.hoveredNode.set(item.index); + instance.hoveredNodes.set([item.index]); }} onExpandItem={(item) => { instance.treeState.update((draft) => { @@ -89,7 +90,7 @@ export function Tree(props: { }, onMouseOver: () => { - instance.hoveredNode.set(item.index); + instance.hoveredNodes.set([item.index]); }, }), }}> diff --git a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx index 30070bb29..3e1fc2f84 100644 --- a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx @@ -18,9 +18,9 @@ import { UINode, } from '../types'; -import {styled, theme, usePlugin, Atom} from 'flipper-plugin'; +import {styled, theme, usePlugin} from 'flipper-plugin'; import {plugin} from '../index'; -import {throttle} from 'lodash'; +import {throttle, isEqual, head} from 'lodash'; export const Visualization2D: React.FC< { @@ -60,9 +60,13 @@ export const Visualization2D: React.FC< y: offsetMouse.y * pxScaleFactor, }; - const targeted = hitTest(root, scaledMouse, root.bounds); - if (targeted && targeted.id !== instance.hoveredNode.get()) { - instance.hoveredNode.set(targeted.id); + const hitNodes = hitTest(root, scaledMouse).map((node) => node.id); + + if ( + hitNodes.length > 0 && + !isEqual(hitNodes, instance.hoveredNodes.get()) + ) { + instance.hoveredNodes.set(hitNodes); } }, MouseThrottle); window.addEventListener('mousemove', mouseListener); @@ -70,7 +74,7 @@ export const Visualization2D: React.FC< return () => { window.removeEventListener('mousemove', mouseListener); }; - }, [instance.hoveredNode, root]); + }, [instance.hoveredNodes, root]); if (!root) { return null; @@ -81,7 +85,7 @@ export const Visualization2D: React.FC< ref={rootNodeRef as any} onMouseLeave={(e) => { e.stopPropagation(); - instance.hoveredNode.set(undefined); + instance.hoveredNodes.set([]); }} style={{ /** @@ -137,16 +141,16 @@ function Visualization2DNode({ const [isHovered, setIsHovered] = useState(false); useEffect(() => { - const listener = (newValue?: Id, prevValue?: Id) => { - if (prevValue === node.id || newValue === node.id) { - setIsHovered(newValue === node.id); + const listener = (newValue?: Id[], prevValue?: Id[]) => { + if (head(prevValue) === node.id || head(newValue) === node.id) { + setIsHovered(head(newValue) === node.id); } }; - instance.hoveredNode.subscribe(listener); + instance.hoveredNodes.subscribe(listener); return () => { - instance.hoveredNode.unsubscribe(listener); + instance.hoveredNodes.unsubscribe(listener); }; - }, [instance.hoveredNode, node.id]); + }, [instance.hoveredNodes, node.id]); const isSelected = selectedNode === node.id; @@ -198,11 +202,11 @@ function Visualization2DNode({ onClick={(e) => { e.stopPropagation(); - const hoveredNode = instance.hoveredNode.get(); - if (hoveredNode === selectedNode) { + const hoveredNodes = instance.hoveredNodes.get(); + if (hoveredNodes[0] === selectedNode) { onSelectNode(undefined); } else { - onSelectNode(hoveredNode); + onSelectNode(hoveredNodes[0]); } }}> @@ -291,35 +295,48 @@ function toNestedNode( return root ? uiNodeToNestedNode(root) : undefined; } -function hitTest( - node: NestedNode, - mouseCoordinate: Coordinate, - parentBounds: Bounds, -): NestedNode | undefined { - const nodeBounds = node.bounds || parentBounds; +function hitTest(node: NestedNode, mouseCoordinate: Coordinate): NestedNode[] { + const res: NestedNode[] = []; - if (boundsContainsCoordinate(nodeBounds, mouseCoordinate)) { - let children = node.children; + function hitTestRec(node: NestedNode, mouseCoordinate: Coordinate): boolean { + const nodeBounds = node.bounds; - 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; + if (boundsContainsCoordinate(nodeBounds, mouseCoordinate)) { + let children = node.children; + + if (node.activeChildIdx != null) { + children = [node.children[node.activeChildIdx]]; } + const offsetMouseCoord = offsetCoordinate(mouseCoordinate, nodeBounds); + let childHit = false; + + for (const child of children) { + childHit = hitTestRec(child, offsetMouseCoord) || childHit; + } + + if (!childHit) { + res.push(node); + } + + return true; } - return node; + return false; } - return undefined; + hitTestRec(node, mouseCoordinate); + + return res.sort((a, b) => { + const areaA = a.bounds.height * a.bounds.width; + const areaB = b.bounds.height * b.bounds.width; + if (areaA > areaB) { + return 1; + } else if (areaA < areaB) { + return -1; + } else { + return 0; + } + }); } function boundsContainsCoordinate(bounds: Bounds, coordinate: Coordinate) { diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 744a3d901..f7bbd4964 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -52,7 +52,9 @@ export function plugin(client: PluginClient) { const treeState = createState({expandedNodes: []}); - const hoveredNode = createState(undefined); + //The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser. + //The nodes are sorted by area since you most likely want to select the smallest node under your cursor + const hoveredNodes = createState([]); client.onMessage('coordinateUpdate', (event) => { nodes.update((draft) => { @@ -103,7 +105,7 @@ export function plugin(client: PluginClient) { nodes, metadata, snapshots, - hoveredNode, + hoveredNodes, perfEvents, treeState, };