Group app wide ui state into one object

Summary: Should be a bit easier to see what UI state we are holding at the plugin instance level

Reviewed By: lblasa

Differential Revision: D41498272

fbshipit-source-id: 6d88086766efd9c39f71be7e2ce32c5058494c96
This commit is contained in:
Luke De Feo
2022-11-24 09:23:16 -08:00
committed by Facebook GitHub Bot
parent 7fc64adfd4
commit 6bb541a33f
5 changed files with 52 additions and 50 deletions

View File

@@ -40,17 +40,17 @@ export function Tree(props: {
onSelectNode: (id: Id) => void; onSelectNode: (id: Id) => void;
}) { }) {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const expandedItems = useValue(instance.treeState).expandedNodes; const expandedItems = useValue(instance.uiState.treeState).expandedNodes;
const focused = useValue(instance.focusedNode); const focused = useValue(instance.uiState.focusedNode);
const items = useMemo( const items = useMemo(
() => toComplexTree(focused || props.rootId, props.nodes), () => toComplexTree(focused || props.rootId, props.nodes),
[focused, props.nodes, props.rootId], [focused, props.nodes, props.rootId],
); );
const hoveredNodes = useValue(instance.hoveredNodes); const hoveredNodes = useValue(instance.uiState.hoveredNodes);
const treeEnvRef = useRef<TreeEnvironmentRef>(); const treeEnvRef = useRef<TreeEnvironmentRef>();
const searchTerm = useValue(instance.searchTerm); const searchTerm = useValue(instance.uiState.searchTerm);
useEffect(() => { useEffect(() => {
//this makes the keyboard arrow controls work always, even when using the visualiser //this makes the keyboard arrow controls work always, even when using the visualiser
@@ -75,15 +75,15 @@ export function Tree(props: {
}, },
}} }}
onFocusItem={(item) => { onFocusItem={(item) => {
instance.hoveredNodes.set([item.index]); instance.uiState.hoveredNodes.set([item.index]);
}} }}
onExpandItem={(item) => { onExpandItem={(item) => {
instance.treeState.update((draft) => { instance.uiState.treeState.update((draft) => {
draft.expandedNodes.push(item.index); draft.expandedNodes.push(item.index);
}); });
}} }}
onCollapseItem={(item) => onCollapseItem={(item) =>
instance.treeState.update((draft) => { instance.uiState.treeState.update((draft) => {
draft.expandedNodes = draft.expandedNodes.filter( draft.expandedNodes = draft.expandedNodes.filter(
(expandedItemIndex) => expandedItemIndex !== item.index, (expandedItemIndex) => expandedItemIndex !== item.index,
); );
@@ -109,8 +109,8 @@ export function Tree(props: {
}, },
onMouseOver: () => { onMouseOver: () => {
if (!instance.isContextMenuOpen.get()) { if (!instance.uiState.isContextMenuOpen.get()) {
instance.hoveredNodes.set([item.index]); instance.uiState.hoveredNodes.set([item.index]);
} }
}, },
}), }),
@@ -200,21 +200,21 @@ type ContextMenuProps = {node: UINode; id: Id; title: string};
const ContextMenu: React.FC<ContextMenuProps> = ({id, title, children}) => { const ContextMenu: React.FC<ContextMenuProps> = ({id, title, children}) => {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const focusedNode = instance.focusedNode.get(); const focusedNode = instance.uiState.focusedNode.get();
return ( return (
<Dropdown <Dropdown
onVisibleChange={(visible) => { onVisibleChange={(visible) => {
instance.isContextMenuOpen.set(visible); instance.uiState.isContextMenuOpen.set(visible);
}} }}
overlay={() => ( overlay={() => (
<Menu> <Menu>
{focusedNode !== head(instance.hoveredNodes.get()) && ( {focusedNode !== head(instance.uiState.hoveredNodes.get()) && (
<UIDebuggerMenuItem <UIDebuggerMenuItem
key="focus" key="focus"
text={`Focus ${title}`} text={`Focus ${title}`}
onClick={() => { onClick={() => {
instance.focusedNode.set(id); instance.uiState.focusedNode.set(id);
}} }}
/> />
)} )}
@@ -224,7 +224,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({id, title, children}) => {
key="remove-focus" key="remove-focus"
text="Remove focus" text="Remove focus"
onClick={() => { onClick={() => {
instance.focusedNode.set(undefined); instance.uiState.focusedNode.set(undefined);
}} }}
/> />
)} )}

View File

@@ -29,7 +29,7 @@ export const Visualization2D: React.FC<
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const snapshot = useValue(instance.snapshot); const snapshot = useValue(instance.snapshot);
const focusedNodeId = useValue(instance.focusedNode); const focusedNodeId = useValue(instance.uiState.focusedNode);
const focusState = useMemo(() => { const focusState = useMemo(() => {
const rootNode = toNestedNode(rootId, nodes); const rootNode = toNestedNode(rootId, nodes);
@@ -40,7 +40,7 @@ export const Visualization2D: React.FC<
const mouseListener = throttle((ev: MouseEvent) => { const mouseListener = throttle((ev: MouseEvent) => {
const domRect = rootNodeRef.current?.getBoundingClientRect(); const domRect = rootNodeRef.current?.getBoundingClientRect();
if (!focusState || !domRect || instance.isContextMenuOpen.get()) { if (!focusState || !domRect || instance.uiState.isContextMenuOpen.get()) {
return; return;
} }
const rawMouse = {x: ev.clientX, y: ev.clientY}; const rawMouse = {x: ev.clientX, y: ev.clientY};
@@ -63,9 +63,9 @@ export const Visualization2D: React.FC<
if ( if (
hitNodes.length > 0 && hitNodes.length > 0 &&
!isEqual(hitNodes, instance.hoveredNodes.get()) !isEqual(hitNodes, instance.uiState.hoveredNodes.get())
) { ) {
instance.hoveredNodes.set(hitNodes); instance.uiState.hoveredNodes.set(hitNodes);
} }
}, MouseThrottle); }, MouseThrottle);
window.addEventListener('mousemove', mouseListener); window.addEventListener('mousemove', mouseListener);
@@ -73,7 +73,12 @@ export const Visualization2D: React.FC<
return () => { return () => {
window.removeEventListener('mousemove', mouseListener); window.removeEventListener('mousemove', mouseListener);
}; };
}, [instance.hoveredNodes, focusState, nodes, instance.isContextMenuOpen]); }, [
instance.uiState.hoveredNodes,
focusState,
nodes,
instance.uiState.isContextMenuOpen,
]);
if (!focusState) { if (!focusState) {
return null; return null;
@@ -94,8 +99,8 @@ export const Visualization2D: React.FC<
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.stopPropagation(); e.stopPropagation();
//the context menu triggers this callback but we dont want to remove hover effect //the context menu triggers this callback but we dont want to remove hover effect
if (!instance.isContextMenuOpen.get()) { if (!instance.uiState.isContextMenuOpen.get()) {
instance.hoveredNodes.set([]); instance.uiState.hoveredNodes.set([]);
} }
}} }}
style={{ style={{
@@ -169,11 +174,11 @@ function Visualization2DNode({
setIsHovered(head(newValue) === node.id); setIsHovered(head(newValue) === node.id);
} }
}; };
instance.hoveredNodes.subscribe(listener); instance.uiState.hoveredNodes.subscribe(listener);
return () => { return () => {
instance.hoveredNodes.unsubscribe(listener); instance.uiState.hoveredNodes.unsubscribe(listener);
}; };
}, [instance.hoveredNodes, node.id]); }, [instance.uiState.hoveredNodes, node.id]);
const isSelected = selectedNode === node.id; const isSelected = selectedNode === node.id;
@@ -224,7 +229,7 @@ function Visualization2DNode({
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const hoveredNodes = instance.hoveredNodes.get(); const hoveredNodes = instance.uiState.hoveredNodes.get();
if (hoveredNodes[0] === selectedNode) { if (hoveredNodes[0] === selectedNode) {
onSelectNode(undefined); onSelectNode(undefined);
} else { } else {
@@ -241,15 +246,15 @@ function Visualization2DNode({
const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => { const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const focusedNodeId = useValue(instance.focusedNode); const focusedNodeId = useValue(instance.uiState.focusedNode);
const hoveredNodeId = head(useValue(instance.hoveredNodes)); const hoveredNodeId = head(useValue(instance.uiState.hoveredNodes));
const nodes = useValue(instance.nodes); const nodes = useValue(instance.nodes);
const hoveredNode = hoveredNodeId ? nodes.get(hoveredNodeId) : null; const hoveredNode = hoveredNodeId ? nodes.get(hoveredNodeId) : null;
return ( return (
<Dropdown <Dropdown
onVisibleChange={(open) => { onVisibleChange={(open) => {
instance.isContextMenuOpen.set(open); instance.uiState.isContextMenuOpen.set(open);
}} }}
trigger={['contextMenu']} trigger={['contextMenu']}
overlay={() => { overlay={() => {
@@ -260,7 +265,7 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
key="focus" key="focus"
text={`Focus ${hoveredNode?.name}`} text={`Focus ${hoveredNode?.name}`}
onClick={() => { onClick={() => {
instance.focusedNode.set(hoveredNode?.id); instance.uiState.focusedNode.set(hoveredNode?.id);
}} }}
/> />
)} )}
@@ -269,7 +274,7 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
key="remove-focus" key="remove-focus"
text="Remove focus" text="Remove focus"
onClick={() => { onClick={() => {
instance.focusedNode.set(undefined); instance.uiState.focusedNode.set(undefined);
}} }}
/> />
)} )}

View File

@@ -30,7 +30,7 @@ export function Component() {
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show)); useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
const searchTerm = useValue(instance.searchTerm); const searchTerm = useValue(instance.uiState.searchTerm);
const {ctrlPressed} = useKeyboardModifiers(); const {ctrlPressed} = useKeyboardModifiers();
function renderSidebar( function renderSidebar(
@@ -55,7 +55,7 @@ export function Component() {
<Layout.Container grow pad="medium" gap="small"> <Layout.Container grow pad="medium" gap="small">
<Input <Input
value={searchTerm} value={searchTerm}
onChange={(e) => instance.searchTerm.set(e.target.value)} onChange={(e) => instance.uiState.searchTerm.set(e.target.value)}
/> />
<Layout.ScrollContainer> <Layout.ScrollContainer>
<Tree <Tree

View File

@@ -24,7 +24,7 @@ export const UIDebuggerMenuItem: React.FC<{
}> = ({text, onClick}) => { }> = ({text, onClick}) => {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const isMenuOpen = useValue(instance.isContextMenuOpen); const isMenuOpen = useValue(instance.uiState.isContextMenuOpen);
/** /**
* The menu is not a controlled component and seems to be a bit slow to close when user clicks on it. * The menu is not a controlled component and seems to be a bit slow to close when user clicks on it.
* React may rerender the menu before it has time to close resulting in seeing an incorrect context menu for a frame. * React may rerender the menu before it has time to close resulting in seeing an incorrect context menu for a frame.
@@ -37,7 +37,7 @@ export const UIDebuggerMenuItem: React.FC<{
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
onClick(); onClick();
instance.isContextMenuOpen.set(false); instance.uiState.isContextMenuOpen.set(false);
}}> }}>
{text} {text}
</Menu.Item> </Menu.Item>

View File

@@ -23,7 +23,6 @@ import './node_modules/react-complex-tree/lib/style.css';
export function plugin(client: PluginClient<Events>) { export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined); const rootId = createState<Id | undefined>(undefined);
const metadata = createState<Map<MetadataId, Metadata>>(new Map()); const metadata = createState<Map<MetadataId, Metadata>>(new Map());
const searchTerm = createState<string>('');
client.onMessage('init', (event) => { client.onMessage('init', (event) => {
rootId.set(event.rootId); rootId.set(event.rootId);
@@ -48,21 +47,23 @@ export function plugin(client: PluginClient<Events>) {
perfEvents.append(event); perfEvents.append(event);
}); });
//used to disabled hover effects which cause rerenders and mess up the existing context menu
const isContextMenuOpen = createState<boolean>(false);
const focusedNode = createState<Id | undefined>(undefined);
const nodes = createState<Map<Id, UINode>>(new Map()); const nodes = createState<Map<Id, UINode>>(new Map());
const snapshot = createState<{nodeId: Id; base64Image: Snapshot} | null>( const snapshot = createState<{nodeId: Id; base64Image: Snapshot} | null>(
null, null,
); );
const treeState = createState<TreeState>({expandedNodes: []}); const uiState = {
//used to disabled hover effects which cause rerenders and mess up the existing context menu
isContextMenuOpen: createState<boolean>(false),
//The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser. //The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
//The nodes are sorted by area since you most likely want to select the smallest node under your cursor //The nodes are sorted by area since you most likely want to select the smallest node under your cursor
const hoveredNodes = createState<Id[]>([]); hoveredNodes: createState<Id[]>([]),
searchTerm: createState<string>(''),
focusedNode: createState<Id | undefined>(undefined),
treeState: createState<TreeState>({expandedNodes: []}),
};
client.onMessage('coordinateUpdate', (event) => { client.onMessage('coordinateUpdate', (event) => {
nodes.update((draft) => { nodes.update((draft) => {
@@ -88,7 +89,7 @@ export function plugin(client: PluginClient<Events>) {
}); });
}); });
treeState.update((draft) => { uiState.treeState.update((draft) => {
for (const node of event.nodes) { for (const node of event.nodes) {
if (!seenNodes.has(node.id)) { if (!seenNodes.has(node.id)) {
draft.expandedNodes.push(node.id); draft.expandedNodes.push(node.id);
@@ -111,15 +112,11 @@ export function plugin(client: PluginClient<Events>) {
return { return {
rootId, rootId,
isContextMenuOpen, uiState,
nodes, nodes,
metadata, metadata,
focusedNode,
snapshot, snapshot,
hoveredNodes,
perfEvents, perfEvents,
treeState,
searchTerm,
}; };
} }