diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index d5e70366a..1a6c0a76b 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -23,34 +23,36 @@ export function Tree(props: { const [antTree, inactive] = nodesToAntTree(props.rootId, props.nodes); return ( - { - //when mouse exits the entire tree then unhover + //This div exists so when mouse exits the entire tree then unhover props.onHoveredNode(undefined); - }} - showIcon - showLine - titleRender={(node) => { - return ( -
{ - props.onHoveredNode(node.key as Id); - }}> - {node.title} -
- ); - }} - selectedKeys={[props.selectedNode ?? '']} - onSelect={(selected) => { - props.onSelectNode(selected[0] as Id); - }} - defaultExpandAll - expandedKeys={[...props.nodes.keys()].filter( - (key) => !inactive.includes(key), - )} - switcherIcon={} - treeData={[antTree]} - /> + }}> + { + return ( +
{ + props.onHoveredNode(node.key as Id); + }}> + {node.title} +
+ ); + }} + selectedKeys={[props.selectedNode ?? '']} + onSelect={(selected) => { + props.onSelectNode(selected[0] as Id); + }} + defaultExpandAll + expandedKeys={[...props.nodes.keys()].filter( + (key) => !inactive.includes(key), + )} + switcherIcon={} + treeData={[antTree]} + /> + ); } diff --git a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx index 2b404caff..6c3e49bfa 100644 --- a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import {Bounds, Id, Tag, UINode} from '../types'; +import {Id, Tag, UINode} from '../types'; import {styled, Layout, theme} from 'flipper-plugin'; import {Typography} from 'antd'; @@ -17,44 +17,80 @@ export const Visualization2D: React.FC< root: Id; nodes: Map; hoveredNode?: Id; + selectedNode?: Id; onSelectNode: (id: Id) => void; + onHoverNode: (id?: Id) => void; + modifierPressed: boolean; } & React.HTMLAttributes -> = ({root, nodes, hoveredNode, onSelectNode}) => { +> = ({ + root, + nodes, + hoveredNode, + selectedNode, + onSelectNode, + onHoverNode, + modifierPressed, +}) => { + //todo, do a bfs search for the first bounds found + const rootBounds = nodes.get(root)?.bounds; + + if (!rootBounds) { + return null; + } return ( Visualizer
{ + e.stopPropagation(); + onHoverNode(undefined); + }} style={{ - //this sets the reference frame for the absolute positioning - //of the individual absolutely positioned nodes + /** + * 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), }}> + - ;
); }; function Visualization2DNode({ + parentId, nodeId, nodes, - isRoot, hoveredNode, + selectedNode, onSelectNode, + onHoverNode, + modifierPressed, }: { - isRoot: boolean; nodeId: Id; + parentId?: Id; nodes: Map; + modifierPressed: boolean; hoveredNode?: Id; + selectedNode?: Id; onSelectNode: (id: Id) => void; + onHoverNode: (id?: Id) => void; }) { const node = nodes.get(nodeId); @@ -63,27 +99,34 @@ function Visualization2DNode({ } const isHovered = hoveredNode === nodeId; + const isSelected = selectedNode === nodeId; let childrenIds: Id[] = []; - if (!isHovered) { - //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; - } + //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) => ( )); @@ -93,50 +136,88 @@ function Visualization2DNode({ const isZeroWidthOrHeight = node.bounds?.height === 0 || node.bounds?.width === 0; + + const bounds = node.bounds ?? {x: 0, y: 0, width: 0, height: 0}; + return ( - { + e.stopPropagation(); + onHoverNode(nodeId); + }} + onMouseLeave={(e) => { + e.stopPropagation(); + onHoverNode(parentId); + }} onClick={(e) => { e.stopPropagation(); onSelectNode(nodeId); - }} - bounds={node.bounds} - isRoot={isRoot} - tags={node.tags} - isHovered={isHovered}> + }}> + + {/* Dirty hack to avoid showing highly overlapping text */} {!hasOverlappingChild && !isZeroWidthOrHeight && node.bounds ? node.name : null} {children} - + ); } -const BoundsBox = styled.div<{ - bounds?: Bounds; - isRoot: boolean; - isHovered: boolean; - tags: Tag[]; -}>((props) => { - const bounds = props.bounds ?? {x: 0, y: 0, width: 0, height: 0}; - return { - // borderWidth: props.isRoot ? '5px' : '1px', - cursor: 'pointer', - borderWidth: '1px', - //to offset the border - margin: '-1px', - borderColor: props.tags.includes('Declarative') - ? 'green' - : props.tags.includes('Native') - ? 'blue' - : 'black', - borderStyle: 'solid', - position: 'absolute', - backgroundColor: props.isHovered ? theme.selectionBackgroundColor : 'white', - //todo need to understand why its so big and needs halving - left: bounds.x / 2, - top: bounds.y / 2, - width: bounds.width / 2, - height: bounds.height / 2, - }; +/** + * 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`; +} diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index a299cbd8c..f3874607c 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -23,6 +23,7 @@ import {Id, UINode} from '../types'; import {PerfStats} from './PerfStats'; import {Tree} from './Tree'; import {Visualization2D} from './Visualization2D'; +import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers'; export function Component() { const instance = usePlugin(plugin); @@ -35,6 +36,8 @@ export function Component() { useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show)); + const {ctrlPressed} = useKeyboardModifiers(); + function renderAttributesInspector(node: UINode | undefined) { if (!node) { return; @@ -69,7 +72,10 @@ export function Component() { root={rootId} nodes={nodes} hoveredNode={hoveredNode} + onHoverNode={setHoveredNode} + selectedNode={selectedNode} onSelectNode={setSelectedNode} + modifierPressed={ctrlPressed} /> diff --git a/desktop/plugins/public/ui-debugger/hooks/useKeyboardModifiers.tsx b/desktop/plugins/public/ui-debugger/hooks/useKeyboardModifiers.tsx new file mode 100644 index 000000000..cfbbdcdf9 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/hooks/useKeyboardModifiers.tsx @@ -0,0 +1,29 @@ +/** + * 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 {useEffect, useState} from 'react'; + +export function useKeyboardModifiers() { + const [state, setState] = useState({altPressed: false, ctrlPressed: false}); + + function handler(event: KeyboardEvent) { + setState({altPressed: event.altKey, ctrlPressed: event.ctrlKey}); + } + + useEffect(() => { + window.addEventListener('keydown', handler); + window.addEventListener('keyup', handler); + return () => { + window.removeEventListener('keydown', handler); + window.removeEventListener('keyup', handler); + }; + }, []); + + return state; +}