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;
}) {
const instance = usePlugin(plugin);
const expandedItems = useValue(instance.treeState).expandedNodes;
const focused = useValue(instance.focusedNode);
const expandedItems = useValue(instance.uiState.treeState).expandedNodes;
const focused = useValue(instance.uiState.focusedNode);
const items = useMemo(
() => toComplexTree(focused || props.rootId, props.nodes),
[focused, props.nodes, props.rootId],
);
const hoveredNodes = useValue(instance.hoveredNodes);
const hoveredNodes = useValue(instance.uiState.hoveredNodes);
const treeEnvRef = useRef<TreeEnvironmentRef>();
const searchTerm = useValue(instance.searchTerm);
const searchTerm = useValue(instance.uiState.searchTerm);
useEffect(() => {
//this makes the keyboard arrow controls work always, even when using the visualiser
@@ -75,15 +75,15 @@ export function Tree(props: {
},
}}
onFocusItem={(item) => {
instance.hoveredNodes.set([item.index]);
instance.uiState.hoveredNodes.set([item.index]);
}}
onExpandItem={(item) => {
instance.treeState.update((draft) => {
instance.uiState.treeState.update((draft) => {
draft.expandedNodes.push(item.index);
});
}}
onCollapseItem={(item) =>
instance.treeState.update((draft) => {
instance.uiState.treeState.update((draft) => {
draft.expandedNodes = draft.expandedNodes.filter(
(expandedItemIndex) => expandedItemIndex !== item.index,
);
@@ -109,8 +109,8 @@ export function Tree(props: {
},
onMouseOver: () => {
if (!instance.isContextMenuOpen.get()) {
instance.hoveredNodes.set([item.index]);
if (!instance.uiState.isContextMenuOpen.get()) {
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 instance = usePlugin(plugin);
const focusedNode = instance.focusedNode.get();
const focusedNode = instance.uiState.focusedNode.get();
return (
<Dropdown
onVisibleChange={(visible) => {
instance.isContextMenuOpen.set(visible);
instance.uiState.isContextMenuOpen.set(visible);
}}
overlay={() => (
<Menu>
{focusedNode !== head(instance.hoveredNodes.get()) && (
{focusedNode !== head(instance.uiState.hoveredNodes.get()) && (
<UIDebuggerMenuItem
key="focus"
text={`Focus ${title}`}
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"
text="Remove focus"
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 snapshot = useValue(instance.snapshot);
const focusedNodeId = useValue(instance.focusedNode);
const focusedNodeId = useValue(instance.uiState.focusedNode);
const focusState = useMemo(() => {
const rootNode = toNestedNode(rootId, nodes);
@@ -40,7 +40,7 @@ export const Visualization2D: React.FC<
const mouseListener = throttle((ev: MouseEvent) => {
const domRect = rootNodeRef.current?.getBoundingClientRect();
if (!focusState || !domRect || instance.isContextMenuOpen.get()) {
if (!focusState || !domRect || instance.uiState.isContextMenuOpen.get()) {
return;
}
const rawMouse = {x: ev.clientX, y: ev.clientY};
@@ -63,9 +63,9 @@ export const Visualization2D: React.FC<
if (
hitNodes.length > 0 &&
!isEqual(hitNodes, instance.hoveredNodes.get())
!isEqual(hitNodes, instance.uiState.hoveredNodes.get())
) {
instance.hoveredNodes.set(hitNodes);
instance.uiState.hoveredNodes.set(hitNodes);
}
}, MouseThrottle);
window.addEventListener('mousemove', mouseListener);
@@ -73,7 +73,12 @@ export const Visualization2D: React.FC<
return () => {
window.removeEventListener('mousemove', mouseListener);
};
}, [instance.hoveredNodes, focusState, nodes, instance.isContextMenuOpen]);
}, [
instance.uiState.hoveredNodes,
focusState,
nodes,
instance.uiState.isContextMenuOpen,
]);
if (!focusState) {
return null;
@@ -94,8 +99,8 @@ export const Visualization2D: React.FC<
onMouseLeave={(e) => {
e.stopPropagation();
//the context menu triggers this callback but we dont want to remove hover effect
if (!instance.isContextMenuOpen.get()) {
instance.hoveredNodes.set([]);
if (!instance.uiState.isContextMenuOpen.get()) {
instance.uiState.hoveredNodes.set([]);
}
}}
style={{
@@ -169,11 +174,11 @@ function Visualization2DNode({
setIsHovered(head(newValue) === node.id);
}
};
instance.hoveredNodes.subscribe(listener);
instance.uiState.hoveredNodes.subscribe(listener);
return () => {
instance.hoveredNodes.unsubscribe(listener);
instance.uiState.hoveredNodes.unsubscribe(listener);
};
}, [instance.hoveredNodes, node.id]);
}, [instance.uiState.hoveredNodes, node.id]);
const isSelected = selectedNode === node.id;
@@ -224,7 +229,7 @@ function Visualization2DNode({
onClick={(e) => {
e.stopPropagation();
const hoveredNodes = instance.hoveredNodes.get();
const hoveredNodes = instance.uiState.hoveredNodes.get();
if (hoveredNodes[0] === selectedNode) {
onSelectNode(undefined);
} else {
@@ -241,15 +246,15 @@ function Visualization2DNode({
const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
const instance = usePlugin(plugin);
const focusedNodeId = useValue(instance.focusedNode);
const hoveredNodeId = head(useValue(instance.hoveredNodes));
const focusedNodeId = useValue(instance.uiState.focusedNode);
const hoveredNodeId = head(useValue(instance.uiState.hoveredNodes));
const nodes = useValue(instance.nodes);
const hoveredNode = hoveredNodeId ? nodes.get(hoveredNodeId) : null;
return (
<Dropdown
onVisibleChange={(open) => {
instance.isContextMenuOpen.set(open);
instance.uiState.isContextMenuOpen.set(open);
}}
trigger={['contextMenu']}
overlay={() => {
@@ -260,7 +265,7 @@ const ContextMenu: React.FC<{nodes: Map<Id, UINode>}> = ({children}) => {
key="focus"
text={`Focus ${hoveredNode?.name}`}
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"
text="Remove focus"
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));
const searchTerm = useValue(instance.searchTerm);
const searchTerm = useValue(instance.uiState.searchTerm);
const {ctrlPressed} = useKeyboardModifiers();
function renderSidebar(
@@ -55,7 +55,7 @@ export function Component() {
<Layout.Container grow pad="medium" gap="small">
<Input
value={searchTerm}
onChange={(e) => instance.searchTerm.set(e.target.value)}
onChange={(e) => instance.uiState.searchTerm.set(e.target.value)}
/>
<Layout.ScrollContainer>
<Tree

View File

@@ -24,7 +24,7 @@ export const UIDebuggerMenuItem: React.FC<{
}> = ({text, onClick}) => {
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.
* 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
onClick={() => {
onClick();
instance.isContextMenuOpen.set(false);
instance.uiState.isContextMenuOpen.set(false);
}}>
{text}
</Menu.Item>

View File

@@ -23,7 +23,6 @@ import './node_modules/react-complex-tree/lib/style.css';
export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined);
const metadata = createState<Map<MetadataId, Metadata>>(new Map());
const searchTerm = createState<string>('');
client.onMessage('init', (event) => {
rootId.set(event.rootId);
@@ -48,21 +47,23 @@ export function plugin(client: PluginClient<Events>) {
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 snapshot = createState<{nodeId: Id; base64Image: Snapshot} | 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 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) => {
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) {
if (!seenNodes.has(node.id)) {
draft.expandedNodes.push(node.id);
@@ -111,15 +112,11 @@ export function plugin(client: PluginClient<Events>) {
return {
rootId,
isContextMenuOpen,
uiState,
nodes,
metadata,
focusedNode,
snapshot,
hoveredNodes,
perfEvents,
treeState,
searchTerm,
};
}