diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index 02461c43b..07091083a 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -8,76 +8,108 @@ */ import {Id, UINode} from '../types'; -import {Tree as AntTree, TreeDataNode} from 'antd'; -import {DownOutlined} from '@ant-design/icons'; -import React from 'react'; +import React, {useEffect, useRef} from 'react'; +import { + Tree as ComplexTree, + ControlledTreeEnvironment, + TreeItem, +} from 'react-complex-tree'; + +import {plugin} from '../index'; +import {usePlugin, useValue} from 'flipper-plugin'; +import { + InteractionMode, + TreeEnvironmentRef, +} from 'react-complex-tree/lib/esm/types'; export function Tree(props: { rootId: Id; nodes: Map; selectedNode?: Id; + hoveredNode?: Id; onSelectNode: (id: Id) => void; onHoveredNode: (id?: Id) => void; }) { - const [antTree, inactive] = nodesToAntTree(props.rootId, props.nodes); + const instance = usePlugin(plugin); + const expandedItems = useValue(instance.treeState).expandedNodes; + const items = toComplexTree(props.nodes); + const treeRef = useRef(); + useEffect(() => { + //this makes the keyboard arrow controls work always, even when using the visualiser + treeRef.current?.focusTree('tree', true); + }, [props.hoveredNode, props.selectedNode]); return ( -
{ - //This div exists so when mouse exits the entire tree then unhover - props.onHoveredNode(undefined); - }}> - { - return ( -
{ - props.onHoveredNode(node.key as Id); - }}> - {node.title} -
+ item.data.name} + canRename={false} + canDragAndDrop={false} + canSearch + autoFocus + viewState={{ + tree: { + focusedItem: props.hoveredNode, + expandedItems, + selectedItems: props.selectedNode ? [props.selectedNode] : [], + }, + }} + onFocusItem={(item) => props.onHoveredNode(item.index)} + onExpandItem={(item) => { + instance.treeState.update((draft) => { + draft.expandedNodes.push(item.index); + }); + }} + onCollapseItem={(item) => + instance.treeState.update((draft) => { + draft.expandedNodes = draft.expandedNodes.filter( + (expandedItemIndex) => expandedItemIndex !== item.index, ); - }} - selectedKeys={[props.selectedNode ?? '']} - onSelect={(selected) => { - props.onSelectNode(selected[0] as Id); - }} - defaultExpandAll - expandedKeys={[...props.nodes.keys()].filter( - (key) => !inactive.includes(key), - )} - switcherIcon={} - treeData={[antTree]} + }) + } + onSelectItems={(items) => props.onSelectNode(items[0])} + defaultInteractionMode={{ + mode: 'custom', + extends: InteractionMode.DoubleClickItemToExpand, + createInteractiveElementProps: ( + item, + treeId, + actions, + renderFlags, + ) => ({ + onClick: () => { + if (renderFlags.isSelected) { + actions.unselectItem(); + } else { + actions.selectItem(); + } + }, + onMouseOver: () => { + props.onHoveredNode(item.index); + }, + }), + }}> + -
+ ); } -function nodesToAntTree( - root: Id, - nodes: Map, -): [TreeDataNode, Id[]] { - const inactive: Id[] = []; - - function uiNodeToAntNode(id: Id): TreeDataNode { - const node = nodes.get(id); - - if (node?.activeChild) { - for (const child of node.children) { - if (child !== node?.activeChild) { - inactive.push(child); - } - } - } - - return { - key: id, - title: node?.name, - children: node?.children.map((id) => uiNodeToAntNode(id)), +function toComplexTree(nodes: Map): Record> { + const res: Record> = {}; + for (const node of nodes.values()) { + res[node.id] = { + index: node.id, + canMove: false, + canRename: false, + children: node.children, + data: node, + hasChildren: node.children.length > 0, }; } - - return [uiNodeToAntNode(root), inactive]; + return res; } diff --git a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx index 67bec8390..e0e388a35 100644 --- a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx @@ -10,11 +10,10 @@ import React from 'react'; import {Id, Snapshot, Tag, UINode} from '../types'; import {styled, Layout, theme} from 'flipper-plugin'; -import {Typography} from 'antd'; export const Visualization2D: React.FC< { - root: Id; + rootId: Id; nodes: Map; snapshots: Map; hoveredNode?: Id; @@ -24,7 +23,7 @@ export const Visualization2D: React.FC< modifierPressed: boolean; } & React.HTMLAttributes > = ({ - root, + rootId, nodes, snapshots, hoveredNode, @@ -34,53 +33,49 @@ export const Visualization2D: React.FC< modifierPressed, }) => { //todo, do a bfs search for the first bounds found - const rootBounds = nodes.get(root)?.bounds; - const rootSnapshot = snapshots.get(root); + const rootBounds = nodes.get(rootId)?.bounds; + const rootSnapshot = snapshots.get(rootId); if (!rootBounds) { return null; } return ( - - Visualizer - -
{ - e.stopPropagation(); - onHoverNode(undefined); - }} - style={{ - /** - * This relative position is so the root visualization 2DNode and outer border has a non static element to - * position itself relative to. - * - * Subsequent Visualization2DNode are positioned relative to their parent as each one is position absolute - * which despite the name acts are a reference point for absolute positioning... - */ - position: 'relative', - width: toPx(rootBounds.width), - height: toPx(rootBounds.height), - overflow: 'hidden', - }}> - - {rootSnapshot ? ( - - ) : null} - { + e.stopPropagation(); + onHoverNode(undefined); + }} + style={{ + /** + * This relative position is so the root visualization 2DNode and outer border has a non static element to + * position itself relative to. + * + * Subsequent Visualization2DNode are positioned relative to their parent as each one is position absolute + * which despite the name acts are a reference point for absolute positioning... + */ + position: 'relative', + width: toPx(rootBounds.width), + height: toPx(rootBounds.height), + overflow: 'hidden', + }}> + + {rootSnapshot ? ( + -
-
+ ) : null} + + ); }; diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 265363fea..83dd4e633 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -47,32 +47,31 @@ export function Component() { if (rootId) { return ( - <> + - - - - + + {selectedNode && renderSidebar(nodes.get(selectedNode))} - + ); } - return
Nothing yet
; + return
Loading...
; } diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 6032ee3d8..2bfa99e32 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -8,7 +8,7 @@ */ import {PluginClient, createState, createDataSource} from 'flipper-plugin'; -import {Events, Id, PerfStatsEvent, Snapshot, UINode} from './types'; +import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types'; export function plugin(client: PluginClient) { const rootId = createState(undefined); @@ -25,6 +25,8 @@ export function plugin(client: PluginClient) { const nodesAtom = createState>(new Map()); const snapshotsAtom = createState>(new Map()); + const treeState = createState({expandedNodes: []}); + client.onMessage('coordinateUpdate', (event) => { nodesAtom.update((draft) => { const node = draft.get(event.nodeId); @@ -36,6 +38,8 @@ export function plugin(client: PluginClient) { } }); }); + + const seenNodes = new Set(); client.onMessage('subtreeUpdate', (event) => { snapshotsAtom.update((draft) => { draft.set(event.rootId, event.snapshot); @@ -45,9 +49,35 @@ export function plugin(client: PluginClient) { draft.set(node.id, node); } }); + + treeState.update((draft) => { + for (const node of event.nodes) { + if (!seenNodes.has(node.id)) { + draft.expandedNodes.push(node.id); + } + seenNodes.add(node.id); + + if (node.activeChild) { + const inactiveChildren = node.children.filter( + (child) => child !== node.activeChild, + ); + + draft.expandedNodes = draft.expandedNodes.filter( + (nodeId) => !inactiveChildren.includes(nodeId), + ); + draft.expandedNodes.push(node.activeChild); + } + } + }); }); - return {rootId, snapshots: snapshotsAtom, nodes: nodesAtom, perfEvents}; + return { + rootId, + snapshots: snapshotsAtom, + nodes: nodesAtom, + perfEvents, + treeState, + }; } export {Component} from './components/main'; diff --git a/desktop/plugins/public/ui-debugger/package.json b/desktop/plugins/public/ui-debugger/package.json index fef2532ac..f51191943 100644 --- a/desktop/plugins/public/ui-debugger/package.json +++ b/desktop/plugins/public/ui-debugger/package.json @@ -14,6 +14,7 @@ ], "dependencies": { "react-color": "^2.19.3", + "react-complex-tree" : "^1.1.11", "react-hotkeys-hook": "^3.4.7" }, "bugs": { diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index 557fb26ee..48dbbd3f6 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -7,6 +7,8 @@ * @format */ +import {TreeItemIndex} from 'react-complex-tree'; + export type Events = { init: InitEvent; subtreeUpdate: SubtreeUpdateEvent; @@ -89,7 +91,9 @@ export type Color = { }; export type Snapshot = string; -export type Id = number; +export type Id = number | TreeItemIndex; + +export type TreeState = {expandedNodes: Id[]}; export type Tag = 'Native' | 'Declarative' | 'Android' | 'Litho '; diff --git a/desktop/plugins/public/yarn.lock b/desktop/plugins/public/yarn.lock index 573cd4398..9d8cf2472 100644 --- a/desktop/plugins/public/yarn.lock +++ b/desktop/plugins/public/yarn.lock @@ -1644,6 +1644,11 @@ react-color@^2.19.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" +react-complex-tree@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/react-complex-tree/-/react-complex-tree-1.1.11.tgz#430520d12908b033a4b278be0dfd8d0aa6654a85" + integrity sha512-hAkm2ZRH2lwZd7NEzZMQI8db/jI5T2fJsbwHX8oNPrG/WPdakc3eNpm2A4gLk2SBa88HeU6mnauVXg6Q6fJLow== + react-devtools-core@^4.26.1: version "4.26.1" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.26.1.tgz#2893fea58089be64c5356d5bd0eebda8d1bbf317" diff --git a/desktop/themes/base.less b/desktop/themes/base.less index 2a31b08ad..dbb02ef92 100644 --- a/desktop/themes/base.less +++ b/desktop/themes/base.less @@ -7,6 +7,7 @@ */ @import '../node_modules/antd/dist/antd.less'; @import './typography.less'; +@import (inline) './plugins/public/node_modules/react-complex-tree/lib/style.css'; /* Based on: https://www.figma.com/file/4e6BMdm2SuZ1L7FSuOPQVC/Flipper?node-id=620%3A84636 */ @background-transparent-hover: rgba(0, 0, 0, 0.1);