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 {
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 (
<div>
<div
style={{
position: 'absolute',
width: verticalLinePadding,
height: indentGuide.trimBottom ? HalfTreeItemHeight : TreeItemHeight,
borderRight: `1px solid ${theme.primaryColor}`,
}}></div>
{indentGuide.addHorizontalMarker && (
<div
style={{
position: 'absolute',
width: renderDepthOffset / 3,
height: HalfTreeItemHeight,
borderBottom: `1px solid ${theme.primaryColor}`,
marginLeft: verticalLinePadding,
}}></div>
)}
</div>
);
}
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 (
<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
style={{
position: 'absolute',
width: hasExpandChildrenIcon
? renderDepthOffset / 2
: renderDepthOffset,
height: HalfTreeItemHeight,
borderBottom: `1px solid ${
lastGuide.color === 'primary'
? theme.primaryColor
: secondaryColor
}`,
marginLeft: lastGuidePadding,
}}></div>
)}
</div>
);
},
(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 (
<div
ref={innerRef}
@@ -325,12 +377,15 @@ function TreeNodeRow({
transform: transform,
//Due to absolute positioning width is set outside of react via a useLayoutEffect in parent
}}>
{treeNode.indentGuide != null && (
<IndentGuide indentGuide={treeNode.indentGuide} />
)}
<IndentGuides
isSelected={isSelected}
hasExpandChildrenIcon={showExpandChildrenIcon}
indentGuides={treeNode.indentGuides}
/>
<TreeNodeContent
isHighlighted={highlightedNodes.has(treeNode.id)}
isSelected={treeNode.id === selectedNode}
isSelected={isSelected}
isHovered={hoveredNode === treeNode.id}
onMouseEnter={() => {
const kbIsNoLongerReservingScroll =
@@ -347,7 +402,7 @@ function TreeNodeRow({
style={{overflow: 'visible'}}>
<ExpandedIconOrSpace
expanded={treeNode.isExpanded}
showIcon={treeNode.children.length > 0}
showIcon={showExpandChildrenIcon}
onClick={() => {
if (treeNode.isExpanded) {
onCollapseNode(treeNode.id);
@@ -356,8 +411,8 @@ function TreeNodeRow({
}
}}
/>
{nodeIcon(treeNode)}
{nodeIcon(treeNode)}
<TreeNodeTextContent treeNode={treeNode} />
{treeNode.frameworkEvents && (
<Badge
@@ -375,10 +430,14 @@ function TreeNodeRow({
function TreeNodeTextContent({treeNode}: {treeNode: TreeNode}) {
return (
<>
<Layout.Horizontal
style={{
alignItems: 'baseline',
userSelect: 'none',
}}>
<HighlightedText text={treeNode.name} />
<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 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: {
<div
role="button"
tabIndex={0}
style={{display: 'flex'}}
style={{
display: 'flex',
height: TreeItemHeight,
width: 20,
alignItems: 'center',
justifyContent: 'center',
}}
onClick={(e) => {
e.stopPropagation();
props.onClick();
}}>
<Glyph
<CaretDownOutlined
style={{
transform: props.expanded ? 'rotate(90deg)' : '',
marginRight: '4px',
marginBottom: props.expanded ? '2px' : '',
cursor: 'pointer',
color: theme.textColorPlaceholder,
fontSize: 14,
transform: props.expanded ? '' : 'rotate(-90deg)',
}}
name="chevron-right"
size={12}
color="grey"
/>
</div>
);
} else {
return <div style={{width: '16px'}}></div>;
return (
<div
style={{
width: 20,
height: TreeItemHeight,
}}></div>
);
}
}
function HighlightedText(props: {text: string}) {
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')) {
return <DecorationImage src="icons/litho-logo-blue.png" />;
return <NodeIconImage src="icons/litho-logo-blue.png" />;
} 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')) {
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')) {
return <DecorationImage src="facebook/bloks-logo-orange.png" />;
return <NodeIconImage src="facebook/bloks-logo-orange.png" />;
} 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({
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(