import { Size } from "@noya-app/noya-geometry"; import { getCurrentPlatform, handleKeyboardEvent, KeyMap, } from "@noya-app/noya-keymap"; import { isDeepEqual } from "@noya-app/noya-utils"; import { assignRef, useLateReferenceState } from "@noya-app/react-utils"; import React, { ComponentProps, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { InfiniteCanvas } from "../components/InfiniteCanvas"; import { convertOffsetPoint, convertPoint } from "../utils/pointUtils"; import { BaseMode, Plugin, PluginSystem, PluginSystemContextProperties, PluginSystemData, PluginSystemEventHandlerMap, PluginSystemMode, PluginSystemOptions, } from "./PluginSystem"; import { AnyCanvasPlugin, AnyCanvasPluginSystem, BaseCanvasData, getCanvasTransform, initialBaseCanvasData, initialPointerData, ReactEventHandlerMap, } from "./baseCanvas"; import { BaseContextProperties, DOMContextProperties, } from "./contextProperties"; import { PointerData, pointerPlugin, UniquePointerContextProperties, } from "./pointerPlugin"; type AllBaseContextProperties< Mode extends BaseMode, Data extends BaseCanvasData & PointerData, > = BaseContextProperties & DOMContextProperties; type ContextPropertiesWithoutBase< Mode extends BaseMode, Data extends BaseCanvasData & PointerData, ContextProperties extends object, > = Omit>; // Fix covariance/contravariance type ContextPropertiesBaseFixType< Mode extends BaseMode, Data extends BaseCanvasData & PointerData, ContextProperties extends object, > = ContextPropertiesWithoutBase & AllBaseContextProperties; type PluginSystemToReactCanvas = ReactCanvas< PluginSystemMode, PluginSystemData, PluginSystemContextProperties, PluginSystemEventHandlerMap >; const AnyDataContext = React.createContext< (BaseCanvasData & PointerData) | undefined >(undefined); const AnyModeContext = React.createContext(undefined); const AnyContextPropertiesContext = React.createContext< ContextPropertiesBaseFixType | undefined >(undefined); export class ReactCanvas< Mode extends BaseMode, Data extends BaseCanvasData & PointerData, ContextProperties extends object, EventHandlerMap extends ReactEventHandlerMap, > { private constructor( public pluginSystem: PluginSystem< Mode, Data, ContextProperties, EventHandlerMap > ) {} static create() { return new ReactCanvas( PluginSystem.create() .addData(initialBaseCanvasData) .addData(initialPointerData) ); } addPlugin< M extends BaseMode, D extends BaseCanvasData & PointerData, H extends object, E extends ReactEventHandlerMap, >(plugin: Plugin) { return new ReactCanvas(this.pluginSystem.clone().addPlugin(plugin)); } addPlugins

( ...plugins: P ): PluginSystemToReactCanvas>; addPlugins(...plugins: AnyCanvasPlugin[]) { const newPluginSystem = plugins.reduce( (pluginSystem, plugin) => pluginSystem.addPlugin(plugin), this.pluginSystem.clone() as AnyCanvasPluginSystem ); return new ReactCanvas(newPluginSystem); } addOptions(options: PluginSystemOptions) { return new ReactCanvas(this.pluginSystem.clone().addOptions(options)); } get initialMode() { return this.pluginSystem.mode; } get initialData() { return this.pluginSystem.data; } PropertiesRefContext = AnyContextPropertiesContext as React.Context< ContextPropertiesBaseFixType | undefined >; ModeContext = AnyModeContext as React.Context; DataContext = AnyDataContext as React.Context; ModeProvider = this.ModeContext.Provider; DataProvider = this.DataContext.Provider; useContextPropertiesRef = () => { const context = useContext(this.PropertiesRefContext); if (!context) { throw new Error( "usePropertiesRefContext must be used within a CanvasProvider" ); } return context; }; useMode = () => { const context = useContext(this.ModeContext); if (!context) { throw new Error("useModeContext must be used within a CanvasProvider"); } return context; }; useData = () => { const context = useContext(this.DataContext); if (!context) { throw new Error("useDataContext must be used within a CanvasProvider"); } return context; }; Canvas = forwardRef( ( { mode: modeProp, data: dataProp, contextProperties: withoutBase, setMode: setModeProp, setData: setDataProp, children, InfiniteCanvasComponent = InfiniteCanvas, ...rest }: { mode?: Mode; data?: Data; contextProperties: ( base: BaseContextProperties ) => ContextPropertiesWithoutBase & UniquePointerContextProperties; setMode?: (mode: Mode) => void; setData?: (data: Data) => void; children?: React.ReactNode; InfiniteCanvasComponent?: typeof InfiniteCanvas; } & Omit< React.ComponentProps, "onChangeSize" | "children" >, forwardedRef: React.Ref ) => { const [internalMode, setInternalMode] = useState( modeProp ?? this.initialMode ); const [internalData, setInternalData] = useState( dataProp ?? this.initialData ); const [mode, setMode_] = modeProp !== undefined && setModeProp ? [modeProp, setModeProp] : [internalMode, setInternalMode]; const [data, setData_] = dataProp !== undefined && setDataProp ? [dataProp, setDataProp] : [internalData, setInternalData]; const [getMode, setMode] = useLateReferenceState(mode, setMode_); const [getData, setData] = useLateReferenceState(data, setData_); const { PropertiesRefContext: HandlerContext, ModeContext, DataContext, } = this; const canvasRef = useRef(null); const base = useMemo( (): BaseContextProperties => ({ getMode, setMode, getData, setData, }), [getMode, setMode, getData, setData] ); const baseRef = useRef(base); useEffect(() => { baseRef.current = base; }, [base]); const [readOnlyInternal, setReadOnlyInternal] = useState(false); const [shouldHandleKeyboardEvents, setShouldHandleKeyboardEvents] = useState(true); const domContextProperties: DOMContextProperties = useMemo( () => ({ canvasRef, handleKeyboardEvent: (keyMap: KeyMap) => (event: React.KeyboardEvent) => { const platform = getCurrentPlatform(navigator); return handleKeyboardEvent(event.nativeEvent, platform, keyMap); }, setReadOnly: setReadOnlyInternal, setShouldHandleKeyboardEvents, getScreenPoint: convertOffsetPoint, getCanvasPoint(input) { return this.convertPoint(this.getScreenPoint(input), "canvas"); }, convertPoint(point, targetCoordinateSystem) { const data = baseRef.current.getData(); return convertPoint( data.origin, data.zoom, point, targetCoordinateSystem ); }, setPointerCapture: (pointerId: number) => { canvasRef.current?.setPointerCapture(pointerId); }, releasePointerCapture: (pointerId: number) => { canvasRef.current?.releasePointerCapture(pointerId); }, }), [] ); const contextProperties = useMemo( (): ContextPropertiesBaseFixType => ({ ...base, ...domContextProperties, ...withoutBase(base), }), [base, domContextProperties, withoutBase] ); const pluginSystemWithPointer = useMemo( () => this.pluginSystem.clone().addPlugin(pointerPlugin()), [] ); const eventHandlers = pluginSystemWithPointer .clone() .getHandlers(contextProperties as any); const contextPropertiesRef = useRef(contextProperties); useEffect(() => { contextPropertiesRef.current = contextProperties; }, [contextProperties]); const handleSizeChange = useCallback((size: Size) => { const { getData, setData } = contextPropertiesRef.current; const data = getData(); if (isDeepEqual(size, data.size)) return; setData({ ...data, size }); }, []); const handleRef = useCallback( (value: HTMLDivElement | null) => { assignRef(canvasRef, value); assignRef(forwardedRef, value); }, [forwardedRef] ); const OwnProvider = this.ReactCanvasProvider; return ( {children} ); } ); ReactCanvasProvider = ({ children }: { children: React.ReactNode }) => { return ( } > {children} ); }; createContextProperties = ( value: ComponentProps["contextProperties"] ) => { return value; }; getTransform = getCanvasTransform; useTransform = () => { const data = this.useData(); return useMemo( () => this.getTransform({ origin: data.origin, zoom: data.zoom }), [data.origin, data.zoom] ); }; } export const ReactCanvasContext = React.createContext< | ReactCanvas< BaseMode, BaseCanvasData & PointerData, {}, ReactEventHandlerMap > | undefined >(undefined); export const useReactCanvas = () => { const context = useContext(ReactCanvasContext); if (!context) { throw new Error("useReactCanvas must be used within a ReactCanvasProvider"); } return context; }; export function createCanvas() { return ReactCanvas.create(); } export type CanvasPluginsToSystem = CanvasPluginsToSystemRecurse< // identity for a union is never never, BaseCanvasData & PointerData, {}, ReactEventHandlerMap, Plugins >; type CanvasPluginsToSystemRecurse< Mode extends BaseMode, Data extends BaseCanvasData & PointerData, UniqueContextProperties extends object, EventHandlerMap extends ReactEventHandlerMap, Plugins extends readonly AnyCanvasPlugin[], > = Plugins extends readonly [ Plugin, ...infer Tail extends AnyCanvasPlugin[], ] ? CanvasPluginsToSystemRecurse< Mode | M, Data & D, // don't include context shared across all plugins UniqueContextProperties & Omit>, EventHandlerMap & E, Tail > : PluginSystem< Mode, Data, BaseContextProperties & UniqueContextProperties, EventHandlerMap >;