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
This commit is contained in:
Luke De Feo
2023-07-26 03:22:38 -07:00
committed by Facebook GitHub Bot
parent 3891a5d61b
commit 0e15dce033
4 changed files with 196 additions and 88 deletions

View File

@@ -20,6 +20,7 @@ import React, {
import { import {
HighlightManager, HighlightManager,
HighlightProvider, HighlightProvider,
Layout,
styled, styled,
theme, theme,
useHighlighter, useHighlighter,
@@ -27,30 +28,28 @@ import {
useValue, useValue,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {plugin} from '../../index'; import {plugin} from '../../index';
import {Glyph} from 'flipper'; import {head, last} from 'lodash';
import {head} from 'lodash';
import {Badge, Typography} from 'antd'; import {Badge, Typography} from 'antd';
import {useVirtualizer} from '@tanstack/react-virtual'; import {useVirtualizer} from '@tanstack/react-virtual';
import {ContextMenu} from './ContextMenu'; import {ContextMenu} from './ContextMenu';
import {MillisSinceEpoch, useKeyboardControls} from './useKeyboardControls'; import {MillisSinceEpoch, useKeyboardControls} from './useKeyboardControls';
import {toTreeList} from './toTreeList'; import {toTreeList} from './toTreeList';
import {CaretDownOutlined} from '@ant-design/icons';
const {Text} = Typography; const {Text} = Typography;
type LineStyle = 'ToParent' | 'ToChildren';
type NodeIndentGuide = { type NodeIndentGuide = {
depth: number; depth: number;
style: LineStyle;
addHorizontalMarker: boolean; addHorizontalMarker: boolean;
trimBottom: boolean; trimBottom: boolean;
color: 'primary' | 'secondary';
}; };
export type TreeNode = ClientNode & { export type TreeNode = ClientNode & {
depth: number; depth: number;
idx: number; idx: number;
isExpanded: boolean; isExpanded: boolean;
indentGuide: NodeIndentGuide | null; indentGuides: NodeIndentGuide[];
frameworkEvents: number | null; frameworkEvents: number | null;
}; };
export function Tree2({ export function Tree2({
@@ -111,7 +110,7 @@ export function Tree2({
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: treeNodes.length, count: treeNodes.length,
getScrollElement: () => parentRef.current, getScrollElement: () => parentRef.current,
estimateSize: () => 26, estimateSize: () => TreeItemHeightNumber,
overscan: 20, overscan: 20,
}); });
@@ -261,31 +260,82 @@ export function Tree2({
); );
} }
function IndentGuide({indentGuide}: {indentGuide: NodeIndentGuide}) { const secondaryColor = theme.buttonDefaultBackground;
const verticalLinePadding = `${renderDepthOffset * indentGuide.depth + 8}px`; const GuideOffset = 11;
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 ( return (
<div> <div style={{pointerEvents: 'none'}}>
{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 (
<div
key={guide.depth}
style={{
position: 'absolute',
width: indentGuideLinePadding,
height: TreeItemHeight,
borderRight: `1px solid`,
borderImage: `linear-gradient(to bottom, ${firstHalf} 50%, ${secondHalf} 50%) 1`,
borderImageSlice: 1,
}}
/>
);
})}
{lastGuide?.addHorizontalMarker && (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
width: verticalLinePadding, width: hasExpandChildrenIcon
height: indentGuide.trimBottom ? HalfTreeItemHeight : TreeItemHeight, ? renderDepthOffset / 2
borderRight: `1px solid ${theme.primaryColor}`, : renderDepthOffset,
}}></div>
{indentGuide.addHorizontalMarker && (
<div
style={{
position: 'absolute',
width: renderDepthOffset / 3,
height: HalfTreeItemHeight, height: HalfTreeItemHeight,
borderBottom: `1px solid ${theme.primaryColor}`, borderBottom: `1px solid ${
marginLeft: verticalLinePadding, lastGuide.color === 'primary'
? theme.primaryColor
: secondaryColor
}`,
marginLeft: lastGuidePadding,
}}></div> }}></div>
)} )}
</div> </div>
); );
} },
(props, nextProps) =>
props.hasExpandChildrenIcon === nextProps.hasExpandChildrenIcon &&
props.indentGuides === nextProps.indentGuides &&
props.isSelected === nextProps.isSelected,
);
function TreeNodeRow({ function TreeNodeRow({
transform, transform,
@@ -314,6 +364,8 @@ function TreeNodeRow({
onCollapseNode: (node: Id) => void; onCollapseNode: (node: Id) => void;
onHoverNode: (node: Id) => void; onHoverNode: (node: Id) => void;
}) { }) {
const showExpandChildrenIcon = treeNode.children.length > 0;
const isSelected = treeNode.id === selectedNode;
return ( return (
<div <div
ref={innerRef} ref={innerRef}
@@ -325,12 +377,15 @@ function TreeNodeRow({
transform: transform, transform: transform,
//Due to absolute positioning width is set outside of react via a useLayoutEffect in parent //Due to absolute positioning width is set outside of react via a useLayoutEffect in parent
}}> }}>
{treeNode.indentGuide != null && ( <IndentGuides
<IndentGuide indentGuide={treeNode.indentGuide} /> isSelected={isSelected}
)} hasExpandChildrenIcon={showExpandChildrenIcon}
indentGuides={treeNode.indentGuides}
/>
<TreeNodeContent <TreeNodeContent
isHighlighted={highlightedNodes.has(treeNode.id)} isHighlighted={highlightedNodes.has(treeNode.id)}
isSelected={treeNode.id === selectedNode} isSelected={isSelected}
isHovered={hoveredNode === treeNode.id} isHovered={hoveredNode === treeNode.id}
onMouseEnter={() => { onMouseEnter={() => {
const kbIsNoLongerReservingScroll = const kbIsNoLongerReservingScroll =
@@ -347,7 +402,7 @@ function TreeNodeRow({
style={{overflow: 'visible'}}> style={{overflow: 'visible'}}>
<ExpandedIconOrSpace <ExpandedIconOrSpace
expanded={treeNode.isExpanded} expanded={treeNode.isExpanded}
showIcon={treeNode.children.length > 0} showIcon={showExpandChildrenIcon}
onClick={() => { onClick={() => {
if (treeNode.isExpanded) { if (treeNode.isExpanded) {
onCollapseNode(treeNode.id); onCollapseNode(treeNode.id);
@@ -356,8 +411,8 @@ function TreeNodeRow({
} }
}} }}
/> />
{nodeIcon(treeNode)}
{nodeIcon(treeNode)}
<TreeNodeTextContent treeNode={treeNode} /> <TreeNodeTextContent treeNode={treeNode} />
{treeNode.frameworkEvents && ( {treeNode.frameworkEvents && (
<Badge <Badge
@@ -375,10 +430,14 @@ function TreeNodeRow({
function TreeNodeTextContent({treeNode}: {treeNode: TreeNode}) { function TreeNodeTextContent({treeNode}: {treeNode: TreeNode}) {
return ( return (
<> <Layout.Horizontal
style={{
alignItems: 'baseline',
userSelect: 'none',
}}>
<HighlightedText text={treeNode.name} /> <HighlightedText text={treeNode.name} />
<InlineAttributes attributes={treeNode.inlineAttributes} /> <InlineAttributes attributes={treeNode.inlineAttributes} />
</> </Layout.Horizontal>
); );
} }
@@ -404,7 +463,8 @@ function InlineAttributes({attributes}: {attributes: Record<string, string>}) {
); );
} }
const TreeItemHeight = '26px'; const TreeItemHeightNumber = 28;
const TreeItemHeight = `${TreeItemHeightNumber}px`;
const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`; const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`;
const TreeNodeContent = styled.li<{ const TreeNodeContent = styled.li<{
@@ -414,12 +474,12 @@ const TreeNodeContent = styled.li<{
isHighlighted: boolean; isHighlighted: boolean;
}>(({item, isHovered, isSelected, isHighlighted}) => ({ }>(({item, isHovered, isSelected, isHighlighted}) => ({
display: 'flex', display: 'flex',
alignItems: 'baseline', alignItems: 'center',
height: TreeItemHeight, height: TreeItemHeight,
paddingLeft: `${item.depth * renderDepthOffset}px`, paddingLeft: `${item.depth * renderDepthOffset}px`,
borderWidth: '1px', borderWidth: '1px',
borderRadius: '3px', borderRadius: '3px',
borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent', borderColor: 'transparent',
borderStyle: 'solid', borderStyle: 'solid',
overflow: 'hidden', overflow: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@@ -427,6 +487,8 @@ const TreeNodeContent = styled.li<{
? 'rgba(255,0,0,.3)' ? 'rgba(255,0,0,.3)'
: isSelected : isSelected
? theme.selectionBackgroundColor ? theme.selectionBackgroundColor
: isHovered
? theme.backgroundWash
: theme.backgroundDefault, : theme.backgroundDefault,
})); }));
@@ -440,57 +502,82 @@ function ExpandedIconOrSpace(props: {
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
style={{display: 'flex'}} style={{
display: 'flex',
height: TreeItemHeight,
width: 20,
alignItems: 'center',
justifyContent: 'center',
}}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
props.onClick(); props.onClick();
}}> }}>
<Glyph <CaretDownOutlined
style={{ style={{
transform: props.expanded ? 'rotate(90deg)' : '', cursor: 'pointer',
marginRight: '4px', color: theme.textColorPlaceholder,
marginBottom: props.expanded ? '2px' : '', fontSize: 14,
transform: props.expanded ? '' : 'rotate(-90deg)',
}} }}
name="chevron-right"
size={12}
color="grey"
/> />
</div> </div>
); );
} else { } else {
return <div style={{width: '16px'}}></div>; return (
<div
style={{
width: 20,
height: TreeItemHeight,
}}></div>
);
} }
} }
function HighlightedText(props: {text: string}) { function HighlightedText(props: {text: string}) {
const highlightManager: HighlightManager = useHighlighter(); const highlightManager: HighlightManager = useHighlighter();
return <span>{highlightManager.render(props.text)} </span>; return (
<Typography.Text>{highlightManager.render(props.text)} </Typography.Text>
);
} }
function nodeIcon(node: ClientNode) { function nodeIcon(node: TreeNode) {
if (node.tags.includes('LithoMountable')) { if (node.tags.includes('LithoMountable')) {
return <DecorationImage src="icons/litho-logo-blue.png" />; return <NodeIconImage src="icons/litho-logo-blue.png" />;
} else if (node.tags.includes('Litho')) { } else if (node.tags.includes('Litho')) {
return <DecorationImage src="icons/litho-logo.png" />; return <NodeIconImage src="icons/litho-logo.png" />;
} else if (node.tags.includes('CK')) { } else if (node.tags.includes('CK')) {
if (node.tags.includes('iOS')) { if (node.tags.includes('iOS')) {
return <DecorationImage src="icons/ck-mounted-logo.png" />; return <NodeIconImage src="icons/ck-mounted-logo.png" />;
} }
return <DecorationImage src="icons/ck-logo.png" />; return <NodeIconImage src="icons/ck-logo.png" />;
} else if (node.tags.includes('BloksBoundTree')) { } else if (node.tags.includes('BloksBoundTree')) {
return <DecorationImage src="facebook/bloks-logo-orange.png" />; return <NodeIconImage src="facebook/bloks-logo-orange.png" />;
} else if (node.tags.includes('BloksDerived')) { } else if (node.tags.includes('BloksDerived')) {
return <DecorationImage src="facebook/bloks-logo-blue.png" />; return <NodeIconImage src="facebook/bloks-logo-blue.png" />;
} else {
return (
<div
style={{
height: NodeIconSize,
width: 0,
marginRight: IconRightMargin,
}}
/>
);
} }
} }
const DecorationImage = styled.img({ const NodeIconSize = 14;
height: 12, const IconRightMargin = '4px';
marginRight: 5, const NodeIconImage = styled.img({
width: 12, 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 //due to virtualisation the out of the box dom based scrolling doesnt work
function findSearchMatchingIndexes( function findSearchMatchingIndexes(

View File

@@ -13,13 +13,14 @@ import {
ClientNode, ClientNode,
} from '../../ClientTypes'; } from '../../ClientTypes';
import {DataSource} from 'flipper-plugin'; import {DataSource} from 'flipper-plugin';
import {last} from 'lodash'; import {concat, last} from 'lodash';
import {reverse} from 'lodash/fp'; import {reverse} from 'lodash/fp';
import {TreeNode} from './Tree'; import {TreeNode} from './Tree';
type TreeListStackItem = { type TreeListStackItem = {
node: ClientNode; node: ClientNode;
depth: number; depth: number;
parentIndentGuideDepths: number[];
isChildOfSelectedNode: boolean; isChildOfSelectedNode: boolean;
selectedNodeDepth: number; selectedNodeDepth: number;
}; };
@@ -37,7 +38,13 @@ export function toTreeList(
return []; return [];
} }
const stack = [ const stack = [
{node: root, depth: 0, isChildOfSelectedNode: false, selectedNodeDepth: 0}, {
node: root,
depth: 0,
isChildOfSelectedNode: false,
selectedNodeDepth: 0,
parentIndentGuideDepths: [],
},
] as TreeListStackItem[]; ] as TreeListStackItem[];
const treeNodes = [] as TreeNode[]; const treeNodes = [] as TreeNode[];
@@ -48,11 +55,12 @@ export function toTreeList(
const {node, depth} = stackItem; const {node, depth} = stackItem;
//if the previous item has an indent guide but we don't then it was the last segment const prevItemLine = last(treeNodes);
//so we trim the bottom //trim all the guides that have now ended
const prevItemLine = last(treeNodes)?.indentGuide; if (prevItemLine != null) {
if (prevItemLine != null && stackItem.isChildOfSelectedNode === false) { for (let i = depth; i < prevItemLine.depth; i++) {
prevItemLine.trimBottom = true; prevItemLine.indentGuides[i].trimBottom = true;
}
} }
const isExpanded = expandedNodes.has(node.id); const isExpanded = expandedNodes.has(node.id);
@@ -73,15 +81,23 @@ export function toTreeList(
depth, depth,
isExpanded, isExpanded,
frameworkEvents: events.length > 0 ? events.length : null, frameworkEvents: events.length > 0 ? events.length : null,
indentGuide: stackItem.isChildOfSelectedNode indentGuides: stackItem.parentIndentGuideDepths.map(
? { (parentGuideDepth, idx) => {
depth: stackItem.selectedNodeDepth, const isLastGuide =
style: 'ToChildren', idx === stackItem.parentIndentGuideDepths.length - 1;
//if first child of selected node add horizontal marker return {
addHorizontalMarker: depth === stackItem.selectedNodeDepth + 1, depth: parentGuideDepth,
addHorizontalMarker: isLastGuide,
trimBottom: false, trimBottom: false,
}
: null, color:
stackItem.isChildOfSelectedNode &&
parentGuideDepth === stackItem.selectedNodeDepth
? 'primary'
: 'secondary',
};
},
),
}); });
i++; i++;
@@ -97,12 +113,11 @@ export function toTreeList(
if (prevNode.depth < depth) { if (prevNode.depth < depth) {
break; break;
} }
prevNode.indentGuide = { const selectedDepthIndentGuide =
depth: selectedNodeDepth - 1, prevNode.indentGuides[selectedNodeDepth - 1];
style: 'ToParent', if (selectedDepthIndentGuide) {
addHorizontalMarker: prevNode.depth == depth, selectedDepthIndentGuide.color = 'primary';
trimBottom: prevNode.id === selectedNode, }
};
} }
} }
@@ -114,6 +129,10 @@ export function toTreeList(
stack.push({ stack.push({
node: child, node: child,
depth: depth + 1, depth: depth + 1,
parentIndentGuideDepths: concat(
stackItem.parentIndentGuideDepths,
depth,
),
isChildOfSelectedNode: isChildOfSelectedNode, isChildOfSelectedNode: isChildOfSelectedNode,
selectedNodeDepth: selectedNodeDepth, selectedNodeDepth: selectedNodeDepth,
}); });
@@ -122,10 +141,12 @@ export function toTreeList(
} }
} }
//always trim last indent guide //always trim last indent guides since they have 'ended'
const prevItemLine = last(treeNodes)?.indentGuide; const prevItemLine = last(treeNodes);
if (prevItemLine != null) { if (prevItemLine != null) {
prevItemLine.trimBottom = true; prevItemLine.indentGuides.forEach((guide) => {
guide.trimBottom = true;
});
} }
return treeNodes; return treeNodes;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB