import { useMemo, useRef } from "react"; import type { ImageDocument } from "@core/document"; import type { AppState, MaskEditState } from "@editor/state"; import type { AppStore } from "@editor/store"; import type { InteractionMode } from "@editor/tools"; import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index"; import { brushUnavailableHint, canPreviewBrush, type BrushTargetEditorState } from "@operations/paint/brush"; import { canvasCursorClass } from "./canvas/cursor"; import { useCanvasInput } from "./canvas/useCanvasInput"; import { useCanvasRenderer } from "./canvas/useCanvasRenderer"; import { useCanvasResize } from "./canvas/useCanvasResize"; import { useAppState } from "./useAppState"; const ignoreGlobalKeybind: GlobalKeybindConsumer = () => false; const ignoreGlobalPointer: GlobalPointerConsumer = () => false; const ignoreGlobalWheel: GlobalWheelConsumer = () => false; export type CanvasViewportProps = { store: AppStore; globalKeybindConsumer?: GlobalKeybindConsumer; globalPointerConsumer?: GlobalPointerConsumer; globalWheelConsumer?: GlobalWheelConsumer; }; export function CanvasViewport({ store, globalKeybindConsumer = ignoreGlobalKeybind, globalPointerConsumer = ignoreGlobalPointer, globalWheelConsumer = ignoreGlobalWheel, }: CanvasViewportProps) { const canvasRef = useRef(null); const cursorState = useAppState(store, selectCanvasCursorState, canvasCursorStatesEqual); const inputOptions = useMemo( () => ({ globalKeybindConsumer, globalPointerConsumer, globalWheelConsumer }), [globalKeybindConsumer, globalPointerConsumer, globalWheelConsumer], ); useCanvasRenderer(canvasRef, store); useCanvasResize(canvasRef, store.dispatch); useCanvasInput(canvasRef, store, inputOptions); const brushHint = brushUnavailableHint(cursorState.document, cursorState.editor); const hasBrushPreview = Boolean(cursorState.hasBrushPreview && !brushHint && canPreviewBrush(cursorState.document, cursorState.editor)); const cursorClass = canvasCursorClass(cursorState.editor.tools.interactionMode, cursorState.isPanning, hasBrushPreview, !brushHint); return ; } type CanvasCursorState = { document: ImageDocument; editor: BrushTargetEditorState; hasBrushPreview: boolean; isPanning: boolean; }; function selectCanvasCursorState(state: AppState): CanvasCursorState { return { document: state.document, editor: { selection: state.editor.selection, tools: { activeTool: state.editor.tools.activeTool, interactionMode: state.editor.tools.interactionMode, }, maskEdit: state.editor.maskEdit, }, hasBrushPreview: Boolean(state.editor.brushPreview), isPanning: state.editor.pointerSession?.type === "pan", }; } function canvasCursorStatesEqual(a: CanvasCursorState, b: CanvasCursorState): boolean { return ( a.document === b.document && a.hasBrushPreview === b.hasBrushPreview && a.isPanning === b.isPanning && a.editor.selection === b.editor.selection && a.editor.tools.activeTool === b.editor.tools.activeTool && interactionModesEqual(a.editor.tools.interactionMode, b.editor.tools.interactionMode) && maskEditStatesEqual(a.editor.maskEdit, b.editor.maskEdit) ); } function interactionModesEqual(a: InteractionMode, b: InteractionMode): boolean { if (a.type !== b.type) return false; if (a.type === "temporary-pan") return b.type === "temporary-pan" && a.previousTool === b.previousTool; return b.type === "tool" && a.tool === b.tool; } function maskEditStatesEqual(a: MaskEditState | undefined, b: MaskEditState | undefined): boolean { if (a === b) return true; if (!a || !b) return false; return a.targetLayerId === b.targetLayerId && a.maskLayerId === b.maskLayerId && a.viewMode === b.viewMode; }