feat(ui): use nanostores for useMouseOverNode

This greatly reduces the weight of the event handlers.
This commit is contained in:
psychedelicious 2023-12-31 18:34:47 +11:00 committed by Kent Keirsey
parent b490c8ae27
commit 5168415999
5 changed files with 48 additions and 87 deletions

View File

@ -1,9 +1,8 @@
import { useToken } from '@chakra-ui/react'; import { useToken } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger'; import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { import {
connectionEnded, connectionEnded,
connectionMade, connectionMade,
@ -67,14 +66,6 @@ const nodeTypes = {
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app // TODO: can we support reactflow? if not, we could style the attribution so it matches the app
const proOptions: ProOptions = { hideAttribution: true }; const proOptions: ProOptions = { hideAttribution: true };
const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
const { shouldSnapToGrid, selectionMode } = nodes;
return {
shouldSnapToGrid,
selectionMode,
};
});
const snapGrid: [number, number] = [25, 25]; const snapGrid: [number, number] = [25, 25];
export const Flow = memo(() => { export const Flow = memo(() => {
@ -82,9 +73,12 @@ export const Flow = memo(() => {
const nodes = useAppSelector((state) => state.nodes.nodes); const nodes = useAppSelector((state) => state.nodes.nodes);
const edges = useAppSelector((state) => state.nodes.edges); const edges = useAppSelector((state) => state.nodes.edges);
const viewport = useAppSelector((state) => state.nodes.viewport); const viewport = useAppSelector((state) => state.nodes.viewport);
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector); const shouldSnapToGrid = useAppSelector(
(state) => state.nodes.shouldSnapToGrid
);
const selectionMode = useAppSelector((state) => state.nodes.selectionMode);
const flowWrapper = useRef<HTMLDivElement>(null); const flowWrapper = useRef<HTMLDivElement>(null);
const cursorPosition = useRef<XYPosition>(); const cursorPosition = useRef<XYPosition | null>(null);
const isValidConnection = useIsValidConnection(); const isValidConnection = useIsValidConnection();
const [borderRadius] = useToken('radii', ['base']); const [borderRadius] = useToken('radii', ['base']);
@ -125,7 +119,15 @@ export const Flow = memo(() => {
); );
const onConnectEnd: OnConnectEnd = useCallback(() => { const onConnectEnd: OnConnectEnd = useCallback(() => {
dispatch(connectionEnded({ cursorPosition: cursorPosition.current })); if (!cursorPosition.current) {
return;
}
dispatch(
connectionEnded({
cursorPosition: cursorPosition.current,
mouseOverNodeId: $mouseOverNode.get(),
})
);
}, [dispatch]); }, [dispatch]);
const onEdgesDelete: OnEdgesDelete = useCallback( const onEdgesDelete: OnEdgesDelete = useCallback(
@ -169,10 +171,11 @@ export const Flow = memo(() => {
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => { const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
if (flowWrapper.current?.getBoundingClientRect()) { if (flowWrapper.current?.getBoundingClientRect()) {
cursorPosition.current = $flow.get()?.screenToFlowPosition({ cursorPosition.current =
x: event.clientX, $flow.get()?.screenToFlowPosition({
y: event.clientY, x: event.clientX,
}); y: event.clientY,
}) ?? null;
} }
}, []); }, []);
@ -245,6 +248,9 @@ export const Flow = memo(() => {
}); });
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
if (!cursorPosition.current) {
return;
}
e.preventDefault(); e.preventDefault();
dispatch(selectionPasted({ cursorPosition: cursorPosition.current })); dispatch(selectionPasted({ cursorPosition: cursorPosition.current }));
}); });

View File

@ -1,31 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { mouseOverFieldChanged } from 'features/nodes/store/nodesSlice';
import { useCallback, useMemo } from 'react';
export const useIsMouseOverField = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createMemoizedSelector(
stateSelector,
({ nodes }) =>
nodes.mouseOverField?.nodeId === nodeId &&
nodes.mouseOverField?.fieldName === fieldName
),
[fieldName, nodeId]
);
const isMouseOverField = useAppSelector(selector);
const handleMouseOver = useCallback(() => {
dispatch(mouseOverFieldChanged({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]);
const handleMouseOut = useCallback(() => {
dispatch(mouseOverFieldChanged(null));
}, [dispatch]);
return { isMouseOverField, handleMouseOver, handleMouseOut };
};

View File

@ -1,29 +1,24 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useStore } from '@nanostores/react';
import { stateSelector } from 'app/store/store'; import { atom } from 'nanostores';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { mouseOverNodeChanged } from 'features/nodes/store/nodesSlice';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
export const $mouseOverNode = atom<string | null>(null);
export const useMouseOverNode = (nodeId: string) => { export const useMouseOverNode = (nodeId: string) => {
const dispatch = useAppDispatch(); const mouseOverNode = useStore($mouseOverNode);
const selector = useMemo(
() => const isMouseOverNode = useMemo(
createMemoizedSelector( () => mouseOverNode === nodeId,
stateSelector, [mouseOverNode, nodeId]
({ nodes }) => nodes.mouseOverNode === nodeId
),
[nodeId]
); );
const isMouseOverNode = useAppSelector(selector);
const handleMouseOver = useCallback(() => { const handleMouseOver = useCallback(() => {
!isMouseOverNode && dispatch(mouseOverNodeChanged(nodeId)); $mouseOverNode.set(nodeId);
}, [dispatch, nodeId, isMouseOverNode]); }, [nodeId]);
const handleMouseOut = useCallback(() => { const handleMouseOut = useCallback(() => {
isMouseOverNode && dispatch(mouseOverNodeChanged(null)); $mouseOverNode.set(null);
}, [dispatch, isMouseOverNode]); }, []);
return { isMouseOverNode, handleMouseOver, handleMouseOut }; return { isMouseOverNode, handleMouseOver, handleMouseOut };
}; };

View File

@ -8,7 +8,6 @@ import type {
ColorFieldValue, ColorFieldValue,
ControlNetModelFieldValue, ControlNetModelFieldValue,
EnumFieldValue, EnumFieldValue,
FieldIdentifier,
FieldValue, FieldValue,
FloatFieldValue, FloatFieldValue,
ImageFieldValue, ImageFieldValue,
@ -116,8 +115,6 @@ export const initialNodesState: NodesState = {
selectedEdges: [], selectedEdges: [],
nodeExecutionStates: {}, nodeExecutionStates: {},
viewport: { x: 0, y: 0, zoom: 1 }, viewport: { x: 0, y: 0, zoom: 1 },
mouseOverField: null,
mouseOverNode: null,
nodesToCopy: [], nodesToCopy: [],
edgesToCopy: [], edgesToCopy: [],
selectionMode: SelectionMode.Partial, selectionMode: SelectionMode.Partial,
@ -272,11 +269,18 @@ const nodesSlice = createSlice({
state.connectionMade = true; state.connectionMade = true;
}, },
connectionEnded: (state, action) => { connectionEnded: (
state,
action: PayloadAction<{
cursorPosition: XYPosition;
mouseOverNodeId: string | null;
}>
) => {
const { cursorPosition, mouseOverNodeId } = action.payload;
if (!state.connectionMade) { if (!state.connectionMade) {
if (state.mouseOverNode) { if (mouseOverNodeId) {
const nodeIndex = state.nodes.findIndex( const nodeIndex = state.nodes.findIndex(
(n) => n.id === state.mouseOverNode (n) => n.id === mouseOverNodeId
); );
const mouseOverNode = state.nodes?.[nodeIndex]; const mouseOverNode = state.nodes?.[nodeIndex];
if (mouseOverNode && state.connectionStartParams) { if (mouseOverNode && state.connectionStartParams) {
@ -308,7 +312,7 @@ const nodesSlice = createSlice({
state.connectionStartParams = null; state.connectionStartParams = null;
state.connectionStartFieldType = null; state.connectionStartFieldType = null;
} else { } else {
state.addNewNodePosition = action.payload.cursorPosition; state.addNewNodePosition = cursorPosition;
state.isAddNodePopoverOpen = true; state.isAddNodePopoverOpen = true;
} }
} else { } else {
@ -681,15 +685,6 @@ const nodesSlice = createSlice({
viewportChanged: (state, action: PayloadAction<Viewport>) => { viewportChanged: (state, action: PayloadAction<Viewport>) => {
state.viewport = action.payload; state.viewport = action.payload;
}, },
mouseOverFieldChanged: (
state,
action: PayloadAction<FieldIdentifier | null>
) => {
state.mouseOverField = action.payload;
},
mouseOverNodeChanged: (state, action: PayloadAction<string | null>) => {
state.mouseOverNode = action.payload;
},
selectedAll: (state) => { selectedAll: (state) => {
state.nodes = applyNodeChanges( state.nodes = applyNodeChanges(
state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })), state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })),
@ -929,8 +924,6 @@ export const {
fieldSchedulerValueChanged, fieldSchedulerValueChanged,
fieldStringValueChanged, fieldStringValueChanged,
fieldVaeModelValueChanged, fieldVaeModelValueChanged,
mouseOverFieldChanged,
mouseOverNodeChanged,
nodeAdded, nodeAdded,
nodeReplaced, nodeReplaced,
nodeEditorReset, nodeEditorReset,

View File

@ -1,4 +1,4 @@
import type { FieldIdentifier, FieldType } from 'features/nodes/types/field'; import type { FieldType } from 'features/nodes/types/field';
import type { import type {
AnyNode, AnyNode,
InvocationNodeEdge, InvocationNodeEdge,
@ -32,8 +32,6 @@ export type NodesState = {
nodeExecutionStates: Record<string, NodeExecutionState>; nodeExecutionStates: Record<string, NodeExecutionState>;
viewport: Viewport; viewport: Viewport;
isReady: boolean; isReady: boolean;
mouseOverField: FieldIdentifier | null;
mouseOverNode: string | null;
nodesToCopy: AnyNode[]; nodesToCopy: AnyNode[];
edgesToCopy: InvocationNodeEdge[]; edgesToCopy: InvocationNodeEdge[];
isAddNodePopoverOpen: boolean; isAddNodePopoverOpen: boolean;