// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { useOnResize } from "@/app/hook/useDimensions"; import { atoms, globalStore, WOS } from "@/app/store/global"; import { fireAndForget } from "@/util/util"; import { Atom, useAtomValue } from "jotai"; import { CSSProperties, useCallback, useEffect, useState } from "react"; import { withLayoutTreeStateAtomFromTab } from "./layoutAtom"; import { LayoutModel } from "./layoutModel"; import { LayoutNode, NodeModel, TileLayoutContents } from "./types"; const layoutModelMap: Map = new Map(); export function getLayoutModelForTab(tabAtom: Atom): LayoutModel { const tabData = globalStore.get(tabAtom); if (!tabData) return; const tabId = tabData.oid; if (layoutModelMap.has(tabId)) { const layoutModel = layoutModelMap.get(tabData.oid); if (layoutModel) { return layoutModel; } } const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom); const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set); globalStore.sub(layoutTreeStateAtom, () => fireAndForget(layoutModel.onTreeStateAtomUpdated.bind(layoutModel))); layoutModelMap.set(tabId, layoutModel); return layoutModel; } export function getLayoutModelForTabById(tabId: string) { const tabOref = WOS.makeORef("tab", tabId); const tabAtom = WOS.getWaveObjectAtom(tabOref); return getLayoutModelForTab(tabAtom); } export function getLayoutModelForStaticTab() { const tabId = globalStore.get(atoms.staticTabId); return getLayoutModelForTabById(tabId); } export function deleteLayoutModelForTab(tabId: string) { if (layoutModelMap.has(tabId)) layoutModelMap.delete(tabId); } export function useLayoutModel(tabAtom: Atom): LayoutModel { return getLayoutModelForTab(tabAtom); } export function useTileLayout(tabAtom: Atom, tileContent: TileLayoutContents): LayoutModel { // Use tab data to ensure we can reload if the tab is disposed and remade (such as during Hot Module Reloading) useAtomValue(tabAtom); const layoutModel = useLayoutModel(tabAtom); useOnResize(layoutModel?.displayContainerRef, layoutModel?.onContainerResize); // Once the TileLayout is mounted, re-run the state update to get all the nodes to flow in the layout. useEffect(() => fireAndForget(() => layoutModel.onTreeStateAtomUpdated(true)), []); useEffect(() => layoutModel.registerTileLayout(tileContent), [tileContent]); return layoutModel; } export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): NodeModel { return layoutModel.getNodeModel(layoutNode); } export function useDebouncedNodeInnerRect(nodeModel: NodeModel): CSSProperties { const nodeInnerRect = useAtomValue(nodeModel.innerRect); const animationTimeS = useAtomValue(nodeModel.animationTimeS); const isMagnified = useAtomValue(nodeModel.isMagnified); const isResizing = useAtomValue(nodeModel.isResizing); const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); const [innerRect, setInnerRect] = useState(); const [innerRectDebounceTimeout, setInnerRectDebounceTimeout] = useState(); const setInnerRectDebounced = useCallback( (nodeInnerRect: CSSProperties) => { clearInnerRectDebounce(); setInnerRectDebounceTimeout( setTimeout(() => { setInnerRect(nodeInnerRect); }, animationTimeS * 1000) ); }, [animationTimeS] ); const clearInnerRectDebounce = useCallback(() => { if (innerRectDebounceTimeout) { clearTimeout(innerRectDebounceTimeout); setInnerRectDebounceTimeout(undefined); } }, [innerRectDebounceTimeout]); useEffect(() => { if (prefersReducedMotion || isMagnified || isResizing) { clearInnerRectDebounce(); setInnerRect(nodeInnerRect); } else { setInnerRectDebounced(nodeInnerRect); } }, [nodeInnerRect]); return innerRect; }