From 0e15dce0330d23f2efb2c1c77a1cee1ed795b369 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Wed, 26 Jul 2023 03:22:38 -0700 Subject: [PATCH] New Tree design Summary: 1. Add indent guidess to all tree depths 2. Monospaced font 3. cleaned up alignment of icons 4. Gave everything a bit more space to breathe Changelog: UI Debugger Tree UI refresh, added indent guides and fixed alignment Reviewed By: mweststrate Differential Revision: D47626869 fbshipit-source-id: e4509621cda6c254f7dd5a7ec9b99c13efb577f4 --- .../ui-debugger/components/tree/Tree.tsx | 215 ++++++++++++------ .../components/tree/toTreeList.tsx | 69 ++++-- desktop/static/icons/android-logo.png | Bin 0 -> 6264 bytes desktop/static/icons/ios-logo.png | Bin 0 -> 7342 bytes 4 files changed, 196 insertions(+), 88 deletions(-) create mode 100644 desktop/static/icons/android-logo.png create mode 100644 desktop/static/icons/ios-logo.png diff --git a/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx b/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx index 819281804..181a1cf7e 100644 --- a/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx @@ -20,6 +20,7 @@ import React, { import { HighlightManager, HighlightProvider, + Layout, styled, theme, useHighlighter, @@ -27,30 +28,28 @@ import { useValue, } from 'flipper-plugin'; import {plugin} from '../../index'; -import {Glyph} from 'flipper'; -import {head} from 'lodash'; +import {head, last} from 'lodash'; import {Badge, Typography} from 'antd'; import {useVirtualizer} from '@tanstack/react-virtual'; import {ContextMenu} from './ContextMenu'; import {MillisSinceEpoch, useKeyboardControls} from './useKeyboardControls'; import {toTreeList} from './toTreeList'; +import {CaretDownOutlined} from '@ant-design/icons'; const {Text} = Typography; -type LineStyle = 'ToParent' | 'ToChildren'; - type NodeIndentGuide = { depth: number; - style: LineStyle; addHorizontalMarker: boolean; trimBottom: boolean; + color: 'primary' | 'secondary'; }; export type TreeNode = ClientNode & { depth: number; idx: number; isExpanded: boolean; - indentGuide: NodeIndentGuide | null; + indentGuides: NodeIndentGuide[]; frameworkEvents: number | null; }; export function Tree2({ @@ -111,7 +110,7 @@ export function Tree2({ const rowVirtualizer = useVirtualizer({ count: treeNodes.length, getScrollElement: () => parentRef.current, - estimateSize: () => 26, + estimateSize: () => TreeItemHeightNumber, overscan: 20, }); @@ -261,31 +260,82 @@ export function Tree2({ ); } -function IndentGuide({indentGuide}: {indentGuide: NodeIndentGuide}) { - const verticalLinePadding = `${renderDepthOffset * indentGuide.depth + 8}px`; +const secondaryColor = theme.buttonDefaultBackground; +const GuideOffset = 11; - return ( -
-
- {indentGuide.addHorizontalMarker && ( -
- )} -
- ); -} +const IndentGuides = React.memo( + ({ + isSelected, + indentGuides, + hasExpandChildrenIcon, + }: { + isSelected: boolean; + hasExpandChildrenIcon: boolean; + indentGuides: NodeIndentGuide[]; + }) => { + const lastGuide = last(indentGuides); + + const lastGuidePadding = `${ + renderDepthOffset * (lastGuide?.depth ?? 0) + GuideOffset + }px`; + + return ( +
+ {indentGuides.map((guide, idx) => { + const indentGuideLinePadding = `${ + renderDepthOffset * guide.depth + GuideOffset + }px`; + + const isLastGuide = idx === indentGuides.length - 1; + const drawHalfprimary = isSelected && isLastGuide; + + const firstHalf = + guide.color === 'primary' ? theme.primaryColor : secondaryColor; + + const secondHalf = guide.trimBottom + ? 'transparent' + : guide.color === 'primary' && !drawHalfprimary + ? theme.primaryColor + : secondaryColor; + + return ( +
+ ); + })} + {lastGuide?.addHorizontalMarker && ( +
+ )} +
+ ); + }, + (props, nextProps) => + props.hasExpandChildrenIcon === nextProps.hasExpandChildrenIcon && + props.indentGuides === nextProps.indentGuides && + props.isSelected === nextProps.isSelected, +); function TreeNodeRow({ transform, @@ -314,6 +364,8 @@ function TreeNodeRow({ onCollapseNode: (node: Id) => void; onHoverNode: (node: Id) => void; }) { + const showExpandChildrenIcon = treeNode.children.length > 0; + const isSelected = treeNode.id === selectedNode; return (
- {treeNode.indentGuide != null && ( - - )} + + { const kbIsNoLongerReservingScroll = @@ -347,7 +402,7 @@ function TreeNodeRow({ style={{overflow: 'visible'}}> 0} + showIcon={showExpandChildrenIcon} onClick={() => { if (treeNode.isExpanded) { onCollapseNode(treeNode.id); @@ -356,8 +411,8 @@ function TreeNodeRow({ } }} /> - {nodeIcon(treeNode)} + {nodeIcon(treeNode)} {treeNode.frameworkEvents && ( + - + ); } @@ -404,7 +463,8 @@ function InlineAttributes({attributes}: {attributes: Record}) { ); } -const TreeItemHeight = '26px'; +const TreeItemHeightNumber = 28; +const TreeItemHeight = `${TreeItemHeightNumber}px`; const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`; const TreeNodeContent = styled.li<{ @@ -414,12 +474,12 @@ const TreeNodeContent = styled.li<{ isHighlighted: boolean; }>(({item, isHovered, isSelected, isHighlighted}) => ({ display: 'flex', - alignItems: 'baseline', + alignItems: 'center', height: TreeItemHeight, paddingLeft: `${item.depth * renderDepthOffset}px`, borderWidth: '1px', borderRadius: '3px', - borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent', + borderColor: 'transparent', borderStyle: 'solid', overflow: 'hidden', whiteSpace: 'nowrap', @@ -427,6 +487,8 @@ const TreeNodeContent = styled.li<{ ? 'rgba(255,0,0,.3)' : isSelected ? theme.selectionBackgroundColor + : isHovered + ? theme.backgroundWash : theme.backgroundDefault, })); @@ -440,57 +502,82 @@ function ExpandedIconOrSpace(props: {
{ e.stopPropagation(); props.onClick(); }}> -
); } else { - return
; + return ( +
+ ); } } function HighlightedText(props: {text: string}) { const highlightManager: HighlightManager = useHighlighter(); - return {highlightManager.render(props.text)} ; + return ( + {highlightManager.render(props.text)} + ); } -function nodeIcon(node: ClientNode) { +function nodeIcon(node: TreeNode) { if (node.tags.includes('LithoMountable')) { - return ; + return ; } else if (node.tags.includes('Litho')) { - return ; + return ; } else if (node.tags.includes('CK')) { if (node.tags.includes('iOS')) { - return ; + return ; } - return ; + return ; } else if (node.tags.includes('BloksBoundTree')) { - return ; + return ; } else if (node.tags.includes('BloksDerived')) { - return ; + return ; + } else { + return ( +
+ ); } } -const DecorationImage = styled.img({ - height: 12, - marginRight: 5, - width: 12, +const NodeIconSize = 14; +const IconRightMargin = '4px'; +const NodeIconImage = styled.img({ + height: NodeIconSize, + width: NodeIconSize, + marginRight: IconRightMargin, + userSelect: 'none', }); -const renderDepthOffset = 12; +const renderDepthOffset = 14; //due to virtualisation the out of the box dom based scrolling doesnt work function findSearchMatchingIndexes( diff --git a/desktop/plugins/public/ui-debugger/components/tree/toTreeList.tsx b/desktop/plugins/public/ui-debugger/components/tree/toTreeList.tsx index 804567ab2..f6fb1faa5 100644 --- a/desktop/plugins/public/ui-debugger/components/tree/toTreeList.tsx +++ b/desktop/plugins/public/ui-debugger/components/tree/toTreeList.tsx @@ -13,13 +13,14 @@ import { ClientNode, } from '../../ClientTypes'; import {DataSource} from 'flipper-plugin'; -import {last} from 'lodash'; +import {concat, last} from 'lodash'; import {reverse} from 'lodash/fp'; import {TreeNode} from './Tree'; type TreeListStackItem = { node: ClientNode; depth: number; + parentIndentGuideDepths: number[]; isChildOfSelectedNode: boolean; selectedNodeDepth: number; }; @@ -37,7 +38,13 @@ export function toTreeList( return []; } const stack = [ - {node: root, depth: 0, isChildOfSelectedNode: false, selectedNodeDepth: 0}, + { + node: root, + depth: 0, + isChildOfSelectedNode: false, + selectedNodeDepth: 0, + parentIndentGuideDepths: [], + }, ] as TreeListStackItem[]; const treeNodes = [] as TreeNode[]; @@ -48,11 +55,12 @@ export function toTreeList( 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 prevItemLine = last(treeNodes); + //trim all the guides that have now ended + if (prevItemLine != null) { + for (let i = depth; i < prevItemLine.depth; i++) { + prevItemLine.indentGuides[i].trimBottom = true; + } } const isExpanded = expandedNodes.has(node.id); @@ -73,15 +81,23 @@ export function toTreeList( depth, isExpanded, frameworkEvents: events.length > 0 ? events.length : null, - indentGuide: stackItem.isChildOfSelectedNode - ? { - depth: stackItem.selectedNodeDepth, - style: 'ToChildren', - //if first child of selected node add horizontal marker - addHorizontalMarker: depth === stackItem.selectedNodeDepth + 1, + indentGuides: stackItem.parentIndentGuideDepths.map( + (parentGuideDepth, idx) => { + const isLastGuide = + idx === stackItem.parentIndentGuideDepths.length - 1; + return { + depth: parentGuideDepth, + addHorizontalMarker: isLastGuide, trimBottom: false, - } - : null, + + color: + stackItem.isChildOfSelectedNode && + parentGuideDepth === stackItem.selectedNodeDepth + ? 'primary' + : 'secondary', + }; + }, + ), }); i++; @@ -97,12 +113,11 @@ export function toTreeList( if (prevNode.depth < depth) { break; } - prevNode.indentGuide = { - depth: selectedNodeDepth - 1, - style: 'ToParent', - addHorizontalMarker: prevNode.depth == depth, - trimBottom: prevNode.id === selectedNode, - }; + const selectedDepthIndentGuide = + prevNode.indentGuides[selectedNodeDepth - 1]; + if (selectedDepthIndentGuide) { + selectedDepthIndentGuide.color = 'primary'; + } } } @@ -114,6 +129,10 @@ export function toTreeList( stack.push({ node: child, depth: depth + 1, + parentIndentGuideDepths: concat( + stackItem.parentIndentGuideDepths, + depth, + ), isChildOfSelectedNode: isChildOfSelectedNode, selectedNodeDepth: selectedNodeDepth, }); @@ -122,10 +141,12 @@ export function toTreeList( } } - //always trim last indent guide - const prevItemLine = last(treeNodes)?.indentGuide; + //always trim last indent guides since they have 'ended' + const prevItemLine = last(treeNodes); if (prevItemLine != null) { - prevItemLine.trimBottom = true; + prevItemLine.indentGuides.forEach((guide) => { + guide.trimBottom = true; + }); } return treeNodes; diff --git a/desktop/static/icons/android-logo.png b/desktop/static/icons/android-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..33ec0c30417831057edb06b1dd8029ee72519c18 GIT binary patch literal 6264 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 RUj2HKXsBo_e}GwG{smreCmjF) literal 0 HcmV?d00001 diff --git a/desktop/static/icons/ios-logo.png b/desktop/static/icons/ios-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e67e19db03f343bfebb8f47a495937c493ca994 GIT binary patch literal 7342 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 literal 0 HcmV?d00001