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 000000000..33ec0c304 Binary files /dev/null and b/desktop/static/icons/android-logo.png differ diff --git a/desktop/static/icons/ios-logo.png b/desktop/static/icons/ios-logo.png new file mode 100644 index 000000000..5e67e19db Binary files /dev/null and b/desktop/static/icons/ios-logo.png differ