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

View File

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