mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): use nanostores for useMouseOverNode
This greatly reduces the weight of the event handlers.
This commit is contained in:
parent
b490c8ae27
commit
5168415999
@ -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 }));
|
||||
});
|
||||
|
@ -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 };
|
||||
};
|
@ -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 };
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user