From b686567e2bc0c73881db2fa300cd4dda322ec543 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Tue, 13 Dec 2022 08:21:22 -0800 Subject: [PATCH] Added indent guides to parent and children for selected node Reviewed By: lblasa Differential Revision: D41995460 fbshipit-source-id: cd4574caa6aa164d2b3a026f656609585cae65c0 --- .../public/ui-debugger/components/Tree.tsx | 217 ++++++++++++++---- 1 file changed, 168 insertions(+), 49 deletions(-) diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index bf26e669a..244442d97 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -21,13 +21,27 @@ import { } from 'flipper-plugin'; import {plugin} from '../index'; import {Glyph} from 'flipper'; -import {head} from 'lodash'; +import {head, last} from 'lodash'; import {reverse} from 'lodash/fp'; import {Dropdown, Menu, Typography} from 'antd'; import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem'; const {Text} = Typography; +type LineStyle = 'ToParent' | 'ToChildren'; + +type NodeIndentGuide = { + depth: number; + style: LineStyle; + addHorizontalMarker: boolean; + trimBottom: boolean; +}; +export type TreeNode = UINode & { + depth: number; + isExpanded: boolean; + indentGuide: NodeIndentGuide | null; +}; + export function Tree2({ nodes, rootId, @@ -47,14 +61,19 @@ export function Tree2({ const hoveredNode = head(useValue(instance.uiState.hoveredNodes)); const {treeNodes, refs} = useMemo(() => { - const treeNodes = toTreeList(nodes, focusedNode || rootId, expandedNodes); + const treeNodes = toTreeNodes( + nodes, + focusedNode || rootId, + expandedNodes, + selectedNode, + ); const refs: React.RefObject[] = treeNodes.map(() => React.createRef(), ); return {treeNodes, refs}; - }, [expandedNodes, focusedNode, nodes, rootId]); + }, [expandedNodes, focusedNode, nodes, rootId, selectedNode]); const isUsingKBToScroll = useRef(false); @@ -116,11 +135,6 @@ export function Tree2({ ); } -export type TreeNode = UINode & { - depth: number; - isExpanded: boolean; -}; - const MemoTreeItemContainer = React.memo( TreeItemContainer, (prevProps, nextProps) => { @@ -137,6 +151,41 @@ const MemoTreeItemContainer = React.memo( }, ); +function IndentGuide({indentGuide}: {indentGuide: NodeIndentGuide}) { + const verticalLinePadding = `${renderDepthOffset * indentGuide.depth + 8}px`; + + const verticalLineStyle = `${ + indentGuide.style === 'ToParent' ? 'dashed' : 'solid' + }`; + const horizontalLineStyle = `${ + indentGuide.style === 'ToParent' ? 'dotted' : 'solid' + }`; + + const color = indentGuide.style === 'ToParent' ? '#B0B0B0' : '#C0C0C0'; + + return ( +
+
+ {indentGuide.addHorizontalMarker && ( +
+ )} +
+ ); +} + function TreeItemContainer({ innerRef, treeNode, @@ -161,34 +210,42 @@ function TreeItemContainer({ onHoverNode: (node: Id) => void; }) { return ( - { - if (isUsingKBToScroll.current === false && isContextMenuOpen == false) { - onHoverNode(treeNode.id); - } - }} - onClick={() => { - onSelectNode(treeNode.id); - }} - item={treeNode}> - 0} - onClick={() => { - if (treeNode.isExpanded) { - onCollapseNode(treeNode.id); - } else { - onExpandNode(treeNode.id); +
+ {treeNode.indentGuide != null && ( + + )} + { + if ( + isUsingKBToScroll.current === false && + isContextMenuOpen == false + ) { + onHoverNode(treeNode.id); } }} - /> - {nodeIcon(treeNode)} - - - + onClick={() => { + onSelectNode(treeNode.id); + }} + item={treeNode}> + 0} + onClick={() => { + if (treeNode.isExpanded) { + onCollapseNode(treeNode.id); + } else { + onExpandNode(treeNode.id); + } + }} + /> + {nodeIcon(treeNode)} + + + +
); } @@ -216,15 +273,18 @@ function InlineAttributes({attributes}: {attributes: Record}) { ); } -const TreeItem = styled.li<{ +const TreeItemHeight = '26px'; +const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`; + +const TreeItemContent = styled.li<{ item: TreeNode; isHovered: boolean; isSelected: boolean; }>(({item, isHovered, isSelected}) => ({ display: 'flex', alignItems: 'baseline', - height: '26px', - paddingLeft: `${(item.depth + 1) * renderDepthOffset}px`, + height: TreeItemHeight, + paddingLeft: `${item.depth * renderDepthOffset}px`, borderWidth: '1px', borderRadius: '3px', borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent', @@ -243,7 +303,10 @@ function ExpandedIconOrSpace(props: { role="button" tabIndex={0} style={{display: 'flex'}} - onClick={props.onClick}> + onClick={(e) => { + e.stopPropagation(); + props.onClick(); + }}> ; @@ -328,41 +391,97 @@ const ContextMenu: React.FC<{ ); }; -function toTreeList( +type TreeListStackItem = { + node: UINode; + depth: number; + isChildOfSelectedNode: boolean; + selectedNodeDepth: number; +}; + +function toTreeNodes( nodes: Map, rootId: Id, expandedNodes: Set, + selectedNode: Id | undefined, ): TreeNode[] { const root = nodes.get(rootId); if (root == null) { return []; } - const stack = [[root, 0]] as [UINode, number][]; + const stack = [ + {node: root, depth: 0, isChildOfSelectedNode: false, selectedNodeDepth: 0}, + ] as TreeListStackItem[]; - const res = [] as TreeNode[]; + const treeNodes = [] as TreeNode[]; while (stack.length > 0) { - const [cur, depth] = stack.pop()!!; + const stackItem = stack.pop()!!; - const isExpanded = expandedNodes.has(cur.id); - res.push({ - ...cur, + const {node, depth} = stackItem; + + //if the previous item has an indent guide but we don't then it was the last segment + //so we trim the bottom + const prevItemLine = last(treeNodes)?.indentGuide; + if (prevItemLine != null && stackItem.isChildOfSelectedNode === false) { + prevItemLine.trimBottom = true; + } + + const isExpanded = expandedNodes.has(node.id); + const isSelected = node.id === selectedNode; + + treeNodes.push({ + ...node, depth, isExpanded, + indentGuide: stackItem.isChildOfSelectedNode + ? { + depth: stackItem.selectedNodeDepth, + style: 'ToChildren', + //if first child of selected node add horizontal marker + addHorizontalMarker: depth === stackItem.selectedNodeDepth + 1, + trimBottom: false, + } + : null, }); + let isChildOfSelectedNode = stackItem.isChildOfSelectedNode; + let selectedNodeDepth = stackItem.selectedNodeDepth; + if (isSelected) { + isChildOfSelectedNode = true; + selectedNodeDepth = depth; + // walk back through tree nodes, while depth is greater or equal than current it is your + // parents child / your previous cousin so set dashed line + for (let i = treeNodes.length - 1; i >= 0; i--) { + const prevNode = treeNodes[i]; + if (prevNode.depth < depth) { + break; + } + prevNode.indentGuide = { + depth: selectedNodeDepth - 1, + style: 'ToParent', + addHorizontalMarker: prevNode.depth == depth, + trimBottom: prevNode.id === selectedNode, + }; + } + } + if (isExpanded) { //since we do dfs and use a stack we have to reverse children to get the order correct - for (const childId of reverse(cur.children)) { + for (const childId of reverse(node.children)) { const child = nodes.get(childId); if (child != null) { - stack.push([child, depth + 1]); + stack.push({ + node: child, + depth: depth + 1, + isChildOfSelectedNode: isChildOfSelectedNode, + selectedNodeDepth: selectedNodeDepth, + }); } } } } - return res; + return treeNodes; } function useKeyboardShortcuts(