diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx index f0862f7104..7fa0805327 100644 --- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx @@ -17,6 +17,7 @@ import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/w import { useReadinessWatcher } from 'features/queue/store/readiness'; import { configChanged } from 'features/system/store/configSlice'; import { selectLanguage } from 'features/system/store/systemSelectors'; +import { usePanelRegistryInit } from 'features/ui/layouts/panel-registry/use-panel-registry-init'; import i18n from 'i18n'; import { memo, useEffect } from 'react'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -43,6 +44,7 @@ export const GlobalHookIsolator = memo( useGetOpenAPISchemaQuery(); useSyncLoggingConfig(); useCloseChakraTooltipsOnDragFix(); + usePanelRegistryInit(); // Persistent subscription to the queue counts query - canvas relies on this to know if there are pending // and/or in progress canvas sessions. diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx index 527c43e298..f2633d1950 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx @@ -1,5 +1,6 @@ import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library'; import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,10 +13,10 @@ import { LaunchpadUseALayoutImageButton } from './LaunchpadUseALayoutImageButton export const CanvasLaunchpadPanel = memo(() => { const { t } = useTranslation(); - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); const focusCanvas = useCallback(() => { - ctx.focusPanel(WORKSPACE_PANEL_ID); - }, [ctx]); + panelRegistry.focusPanelInTab(tab, WORKSPACE_PANEL_ID); + }, [tab]); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx index a5623b65cb..c9da0d52d3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx @@ -21,10 +21,10 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 }; export const BoardsPanel = memo(() => { const boardSearchText = useAppSelector(selectBoardSearchText); const searchDisclosure = useDisclosure(!!boardSearchText); - const { _$rightPanelApi } = useAutoLayoutContext(); - const gridviewPanelApi = useStore(_$rightPanelApi); + const { tab } = useAutoLayoutContext(); const collapsibleApi = useCollapsibleGridviewPanel( - gridviewPanelApi, + tab, + 'right', BOARDS_PANEL_ID, 'vertical', BOARD_PANEL_DEFAULT_HEIGHT_PX, diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index 2a185fa1d6..5b8748b25a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -32,10 +32,10 @@ const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery export const GalleryPanel = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { _$rightPanelApi } = useAutoLayoutContext(); - const gridviewPanelApi = useStore(_$rightPanelApi); + const { tab } = useAutoLayoutContext(); const collapsibleApi = useCollapsibleGridviewPanel( - gridviewPanelApi, + tab, + 'right', GALLERY_PANEL_ID, 'vertical', GALLERY_PANEL_DEFAULT_HEIGHT_PX, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 5f5c96ce44..eb1d9d6863 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -21,7 +21,7 @@ import { selectSelection, } from 'features/gallery/store/gallerySelectors'; import { imageToCompareChanged, selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice'; -import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared'; import type { MouseEvent, MouseEventHandler } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; @@ -139,7 +139,6 @@ const buildOnClick = export const GalleryImage = memo(({ imageDTO }: Props) => { const store = useAppStore(); - const autoLayoutContext = useAutoLayoutContext(); const [isDragging, setIsDragging] = useState(false); const [dragPreviewState, setDragPreviewState] = useState< DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null @@ -239,8 +238,8 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { const onDoubleClick = useCallback>(() => { store.dispatch(imageToCompareChanged(null)); - autoLayoutContext.focusPanel(VIEWER_PANEL_ID); - }, [autoLayoutContext, store]); + panelRegistry.focusPanelInActiveTab(VIEWER_PANEL_ID); + }, [store]); const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx index 1de72b4026..e363370289 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton.tsx @@ -1,7 +1,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { DndImageIcon } from 'features/dnd/DndImageIcon'; import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice'; -import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,14 +14,13 @@ type Props = { export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) => { const dispatch = useAppDispatch(); - const { focusPanel } = useAutoLayoutContext(); const { t } = useTranslation(); const onClick = useCallback(() => { dispatch(imageToCompareChanged(null)); dispatch(imageSelected(imageDTO.image_name)); - focusPanel(VIEWER_PANEL_ID); - }, [dispatch, focusPanel, imageDTO]); + panelRegistry.focusPanelInActiveTab(VIEWER_PANEL_ID); + }, [dispatch, imageDTO]); return ( { const isStaging = useAppSelector(selectIsStaging); const isUpscalingEnabled = useFeatureStatus('upscaling'); const { getState, dispatch } = useAppStore(); - const autoLayoutContext = useAutoLayoutContext(); const handleEdit = useCallback(async () => { if (!imageDTO) { @@ -59,14 +57,13 @@ export const CurrentImageButtons = memo(() => { getState, dispatch, }); - dispatch(setActiveTab('canvas')); - autoLayoutContext?.focusPanel(WORKSPACE_PANEL_ID); + panelRegistry.focusPanelInTab('canvas', WORKSPACE_PANEL_ID); toast({ id: 'SENT_TO_CANVAS', title: t('toast.sentToCanvas'), status: 'success', }); - }, [imageDTO, getState, dispatch, t, autoLayoutContext]); + }, [imageDTO, getState, dispatch, t]); return ( <> diff --git a/invokeai/frontend/web/src/features/gallery/components/NewGallery.tsx b/invokeai/frontend/web/src/features/gallery/components/NewGallery.tsx index 3ea83d7a25..fce0ba86e3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NewGallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NewGallery.tsx @@ -11,7 +11,6 @@ import { } from 'features/gallery/store/gallerySelectors'; import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; import type { MutableRefObject, RefObject } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -446,15 +445,13 @@ export const NewGallery = memo(() => { const rangeRef = useRef({ startIndex: 0, endIndex: 0 }); const rootRef = useRef(null); - const { isActiveTab } = useAutoLayoutContext(); - // Get the ordered list of image names - this is our primary data source for virtualization const { queryArgs, imageNames, isLoading } = useGalleryImageNames(); // Use range-based fetching for bulk loading image DTOs into cache based on the visible range const { onRangeChanged } = useRangeBasedImageFetching({ imageNames, - enabled: !isLoading && isActiveTab, + enabled: !isLoading, }); useKeepSelectedImageInView(imageNames, virtuosoRef, rootRef, rangeRef); @@ -482,7 +479,7 @@ export const NewGallery = memo(() => { if (isLoading) { return ( - + Loading gallery... @@ -491,7 +488,7 @@ export const NewGallery = memo(() => { if (imageNames.length === 0) { return ( - + No images found ); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowViewEditToggleButton.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowViewEditToggleButton.tsx index 6c91d9d025..4b063209a6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowViewEditToggleButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowViewEditToggleButton.tsx @@ -1,8 +1,8 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice'; -import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; -import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; +import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; import { setActiveTab } from 'features/ui/store/uiSlice'; import type { MouseEventHandler } from 'react'; import { memo, useCallback } from 'react'; @@ -13,7 +13,6 @@ export const WorkflowViewEditToggleButton = memo(() => { const dispatch = useAppDispatch(); const mode = useAppSelector(selectWorkflowMode); const { t } = useTranslation(); - const { focusPanel } = useAutoLayoutContext(); const onClickEdit = useCallback>( (e) => { @@ -22,9 +21,9 @@ export const WorkflowViewEditToggleButton = memo(() => { dispatch(setActiveTab('workflows')); dispatch(workflowModeChanged('edit')); // Focus the Workflow Editor panel - focusPanel(WORKSPACE_PANEL_ID); + panelRegistry.focusPanelInTab('workflows', WORKSPACE_PANEL_ID); }, - [dispatch, focusPanel] + [dispatch] ); const onClickView = useCallback>( @@ -34,9 +33,9 @@ export const WorkflowViewEditToggleButton = memo(() => { dispatch(setActiveTab('workflows')); dispatch(workflowModeChanged('view')); // Focus the Image Viewer panel - focusPanel(VIEWER_PANEL_ID); + panelRegistry.focusPanelInTab('workflows', WORKSPACE_PANEL_ID); }, - [dispatch, focusPanel] + [dispatch] ); if (mode === 'view') { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts index 8002bf1179..8b6f744511 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts @@ -5,7 +5,7 @@ import { withResultAsync } from 'common/util/result'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows'; import { $isReadyToEnqueue } from 'features/queue/store/readiness'; -import { useAutoLayoutContextSafe } from 'features/ui/layouts/auto-layout-context'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; @@ -19,7 +19,6 @@ import { useEnqueueUpscaling } from './useEnqueueUpscaling'; const log = logger('generation'); export const useInvoke = () => { - const ctx = useAutoLayoutContextSafe(); const tabName = useAppSelector(selectActiveTab); const isReady = useStore($isReadyToEnqueue); const isLocked = useIsWorkflowEditorLocked(); @@ -64,20 +63,20 @@ export const useInvoke = () => { const enqueueBack = useCallback(() => { enqueue(false, false); if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') { - ctx?.focusPanel(VIEWER_PANEL_ID); + panelRegistry.focusPanelInTab(tabName, VIEWER_PANEL_ID); } else if (tabName === 'canvas') { - ctx?.focusPanel(WORKSPACE_PANEL_ID); + panelRegistry.focusPanelInTab(tabName, WORKSPACE_PANEL_ID); } - }, [ctx, enqueue, tabName]); + }, [enqueue, tabName]); const enqueueFront = useCallback(() => { enqueue(true, false); if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') { - ctx?.focusPanel(VIEWER_PANEL_ID); + panelRegistry.focusPanelInTab(tabName, VIEWER_PANEL_ID); } else if (tabName === 'canvas') { - ctx?.focusPanel(WORKSPACE_PANEL_ID); + panelRegistry.focusPanelInTab(tabName, WORKSPACE_PANEL_ID); } - }, [ctx, enqueue, tabName]); + }, [enqueue, tabName]); return { enqueueBack, enqueueFront, isLoading, isDisabled: !isReady || isLocked, enqueue }; }; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx index d19f31798b..5d57d9aa37 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx @@ -6,7 +6,8 @@ import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonToolt import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; import { useInvoke } from 'features/queue/hooks/useInvoke'; import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; -import { memo } from 'react'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCircleNotchBold, @@ -52,13 +53,21 @@ export const FloatingCanvasLeftPanelButtons = memo(() => { FloatingCanvasLeftPanelButtons.displayName = 'FloatingCanvasLeftPanelButtons'; const ToggleLeftPanelButton = memo(() => { - const { toggleLeftPanel } = useAutoLayoutContext(); const { t } = useTranslation(); + const { tab } = useAutoLayoutContext(); + + const onClick = useCallback(() => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.toggleLeftPanelInTab(tab); + }, [tab]); + return ( } flexGrow={1} /> diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx index 247f28f351..2bc0938e4f 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingRightPanelButtons.tsx @@ -1,6 +1,7 @@ import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context'; -import { memo } from 'react'; +import { panelRegistry } from 'features/ui/layouts/panel-registry/panelApiRegistry'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImagesSquareBold } from 'react-icons/pi'; @@ -15,13 +16,20 @@ FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons'; const ToggleRightPanelButton = memo(() => { const { t } = useTranslation(); - const { toggleRightPanel } = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + + const onClick = useCallback(() => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.toggleLeftPanelInTab(tab); + }, [tab]); return ( } h={48} /> diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx index 7959d18c1a..459e0564cb 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx @@ -1,171 +1,248 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; import type { FocusRegionName } from 'common/hooks/focus'; -import type { DockviewApi, GridviewApi, IDockviewPanelProps, IGridviewPanelProps } from 'dockview'; +import type { IDockviewPanelProps, IGridviewPanelProps } from 'dockview'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { TabName } from 'features/ui/store/uiTypes'; -import type { WritableAtom } from 'nanostores'; -import { atom } from 'nanostores'; import type { FunctionComponent, PropsWithChildren, RefObject } from 'react'; -import { createContext, memo, useCallback, useContext, useMemo, useState } from 'react'; +import { createContext, memo, useContext, useMemo } from 'react'; import { AutoLayoutPanelContainer } from './AutoLayoutPanelContainer'; -import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared'; +import { panelRegistry } from './panel-registry/panelApiRegistry'; type AutoLayoutContextValue = { tab: TabName; - isActiveTab: boolean; - toggleLeftPanel: () => void; - toggleRightPanel: () => void; - toggleBothPanels: () => void; - resetPanels: () => void; - focusPanel: (id: string) => void; + // isActiveTab: boolean; + // toggleLeftPanel: () => void; + // toggleRightPanel: () => void; + // toggleBothPanels: () => void; + // resetPanels: () => void; + // focusPanel: (id: string) => void; rootRef: RefObject; - _$rootPanelApi: WritableAtom; - _$leftPanelApi: WritableAtom; - _$centerPanelApi: WritableAtom; - _$rightPanelApi: WritableAtom; + // _$rootPanelApi: WritableAtom; + // _$leftPanelApi: WritableAtom; + // _$centerPanelApi: WritableAtom; + // _$rightPanelApi: WritableAtom; + // // Global registry access for cross-tab operations + // registry: typeof panelApiRegistry; }; const AutoLayoutContext = createContext(null); -const expandPanel = (api: GridviewApi, panelId: string, width: number) => { - const panel = api.getPanel(panelId); - if (!panel) { - return; - } - panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width }); - panel.api.setSize({ width: width }); -}; +// const expandPanel = (api: GridviewApi, panelId: string, width: number) => { +// const panel = api.getPanel(panelId); +// if (!panel) { +// return; +// } +// panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width }); +// panel.api.setSize({ width: width }); +// }; -const collapsePanel = (api: GridviewApi, panelId: string) => { - const panel = api.getPanel(panelId); - if (!panel) { - return; - } - panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 }); - panel.api.setSize({ width: 0 }); -}; +// const collapsePanel = (api: GridviewApi, panelId: string) => { +// const panel = api.getPanel(panelId); +// if (!panel) { +// return; +// } +// panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 }); +// panel.api.setSize({ width: 0 }); +// }; -const getIsCollapsed = (api: GridviewApi, panelId: string) => { - const panel = api.getPanel(panelId); - if (!panel) { - return true; // ?? - } - return panel.maximumWidth === 0; -}; +// const getIsCollapsed = (api: GridviewApi, panelId: string) => { +// const panel = api.getPanel(panelId); +// if (!panel) { +// return true; // ?? +// } +// return panel.maximumWidth === 0; +// }; -const activatePanel = (api: GridviewApi | DockviewApi, panelId: string) => { - const panel = api.getPanel(panelId); - if (!panel) { - return; - } - panel.api.setActive(); -}; +// const activatePanel = (api: GridviewApi | DockviewApi, panelId: string) => { +// const panel = api.getPanel(panelId); +// if (!panel) { +// return; +// } +// panel.api.setActive(); +// }; export const AutoLayoutProvider = ( props: PropsWithChildren<{ - $rootApi: WritableAtom; + // $rootApi: WritableAtom; rootRef: RefObject; tab: TabName; }> ) => { - const { $rootApi, rootRef, tab, children } = props; - const selectIsActiveTab = useMemo(() => createSelector(selectActiveTab, (activeTab) => activeTab === tab), [tab]); - const isActiveTab = useAppSelector(selectIsActiveTab); - const $leftApi = useState(() => atom(null))[0]; - const $centerApi = useState(() => atom(null))[0]; - const $rightApi = useState(() => atom(null))[0]; + const { tab, rootRef, children } = props; + // const { $rootApi, rootRef, tab, children } = props; + // const selectIsActiveTab = useMemo(() => createSelector(selectActiveTab, (activeTab) => activeTab === tab), [tab]); + // const isActiveTab = useAppSelector(selectIsActiveTab); + // const $leftApi = useState(() => atom(null))[0]; + // const $centerApi = useState(() => atom(null))[0]; + // const $rightApi = useState(() => atom(null))[0]; - const toggleLeftPanel = useCallback(() => { - const api = $rootApi.get(); - if (!api) { - return; - } - if (getIsCollapsed(api, LEFT_PANEL_ID)) { - expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); - } else { - collapsePanel(api, LEFT_PANEL_ID); - } - }, [$rootApi]); + // // Register this tab with the global panel registry when APIs are available + // useEffect(() => { + // const rootApi = $rootApi.get(); + // const leftApi = $leftApi.get(); + // const centerApi = $centerApi.get(); + // const rightApi = $rightApi.get(); - const toggleRightPanel = useCallback(() => { - const api = $rootApi.get(); - if (!api) { - return; - } - if (getIsCollapsed(api, RIGHT_PANEL_ID)) { - expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); - } else { - collapsePanel(api, RIGHT_PANEL_ID); - } - }, [$rootApi]); + // if (rootApi) { + // panelApiRegistry.registerTab(tab, { + // root: rootApi, + // left: leftApi, + // center: centerApi, + // right: rightApi, + // }); - const toggleBothPanels = useCallback(() => { - const api = $rootApi.get(); - if (!api) { - return; - } - requestAnimationFrame(() => { - if (getIsCollapsed(api, RIGHT_PANEL_ID) || getIsCollapsed(api, LEFT_PANEL_ID)) { - expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); - expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); - } else { - collapsePanel(api, LEFT_PANEL_ID); - collapsePanel(api, RIGHT_PANEL_ID); - } - }); - }, [$rootApi]); + // return () => { + // panelApiRegistry.unregisterTab(tab); + // }; + // } + // }, [tab, $rootApi, $leftApi, $centerApi, $rightApi]); - const resetPanels = useCallback(() => { - const api = $rootApi.get(); - if (!api) { - return; - } - expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); - expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); - }, [$rootApi]); + // // Subscribe to API changes and update registry + // useEffect(() => { + // const unsubscribeRoot = $rootApi.subscribe((rootApi) => { + // if (rootApi) { + // panelApiRegistry.registerTab(tab, { + // root: rootApi, + // left: $leftApi.get(), + // center: $centerApi.get(), + // right: $rightApi.get(), + // }); + // } + // }); - const focusPanel = useCallback( - (id: string) => { - const api = $centerApi.get(); - if (!api) { - return; - } - activatePanel(api, id); - }, - [$centerApi] - ); + // const unsubscribeLeft = $leftApi.subscribe((leftApi) => { + // const rootApi = $rootApi.get(); + // if (rootApi) { + // panelApiRegistry.registerTab(tab, { + // root: rootApi, + // left: leftApi, + // center: $centerApi.get(), + // right: $rightApi.get(), + // }); + // } + // }); + + // const unsubscribeCenter = $centerApi.subscribe((centerApi) => { + // const rootApi = $rootApi.get(); + // if (rootApi) { + // panelApiRegistry.registerTab(tab, { + // root: rootApi, + // left: $leftApi.get(), + // center: centerApi, + // right: $rightApi.get(), + // }); + // } + // }); + + // const unsubscribeRight = $rightApi.subscribe((rightApi) => { + // const rootApi = $rootApi.get(); + // if (rootApi) { + // panelApiRegistry.registerTab(tab, { + // root: rootApi, + // left: $leftApi.get(), + // center: $centerApi.get(), + // right: rightApi, + // }); + // } + // }); + + // return () => { + // unsubscribeRoot(); + // unsubscribeLeft(); + // unsubscribeCenter(); + // unsubscribeRight(); + // }; + // }, [tab, $rootApi, $leftApi, $centerApi, $rightApi]); + + // const toggleLeftPanel = useCallback(() => { + // const api = $rootApi.get(); + // if (!api) { + // return; + // } + // if (getIsCollapsed(api, LEFT_PANEL_ID)) { + // expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); + // } else { + // collapsePanel(api, LEFT_PANEL_ID); + // } + // }, [$rootApi]); + + // const toggleRightPanel = useCallback(() => { + // const api = $rootApi.get(); + // if (!api) { + // return; + // } + // if (getIsCollapsed(api, RIGHT_PANEL_ID)) { + // expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); + // } else { + // collapsePanel(api, RIGHT_PANEL_ID); + // } + // }, [$rootApi]); + + // const toggleBothPanels = useCallback(() => { + // const api = $rootApi.get(); + // if (!api) { + // return; + // } + // requestAnimationFrame(() => { + // if (getIsCollapsed(api, RIGHT_PANEL_ID) || getIsCollapsed(api, LEFT_PANEL_ID)) { + // expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); + // expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); + // } else { + // collapsePanel(api, LEFT_PANEL_ID); + // collapsePanel(api, RIGHT_PANEL_ID); + // } + // }); + // }, [$rootApi]); + + // const resetPanels = useCallback(() => { + // const api = $rootApi.get(); + // if (!api) { + // return; + // } + // expandPanel(api, LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX); + // expandPanel(api, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX); + // }, [$rootApi]); + + // const focusPanel = useCallback( + // (id: string) => { + // const api = $centerApi.get(); + // if (!api) { + // return; + // } + // activatePanel(api, id); + // }, + // [$centerApi] + // ); const value = useMemo( () => ({ tab, - isActiveTab, - toggleLeftPanel, - toggleRightPanel, - toggleBothPanels, - resetPanels, - focusPanel, + // isActiveTab, + // toggleLeftPanel, + // toggleRightPanel, + // toggleBothPanels, + // resetPanels, + // focusPanel, rootRef, - _$rootPanelApi: $rootApi, - _$leftPanelApi: $leftApi, - _$centerPanelApi: $centerApi, - _$rightPanelApi: $rightApi, + // _$rootPanelApi: $rootApi, + // _$leftPanelApi: $leftApi, + // _$centerPanelApi: $centerApi, + // _$rightPanelApi: $rightApi, + // registry: panelApiRegistry, }), [ tab, - isActiveTab, - $centerApi, - $leftApi, - $rightApi, - $rootApi, - focusPanel, - resetPanels, + // isActiveTab, + // $centerApi, + // $leftApi, + // $rightApi, + // $rootApi, + // focusPanel, + // resetPanels, rootRef, - toggleBothPanels, - toggleLeftPanel, - toggleRightPanel, + // toggleBothPanels, + // toggleLeftPanel, + // toggleRightPanel, ] ); return {children}; @@ -185,26 +262,51 @@ export const useAutoLayoutContextSafe = () => { }; export const PanelHotkeysLogical = memo(() => { - const { toggleBothPanels, resetPanels, toggleLeftPanel, toggleRightPanel } = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + useRegisteredHotkeys({ category: 'app', id: 'toggleLeftPanel', - callback: toggleLeftPanel, + callback: () => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.toggleLeftPanelInTab(tab); + }, + dependencies: [tab], }); useRegisteredHotkeys({ category: 'app', id: 'toggleRightPanel', - callback: toggleRightPanel, + callback: () => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.toggleRightPanelInTab(tab); + }, + dependencies: [tab], }); useRegisteredHotkeys({ category: 'app', id: 'resetPanelLayout', - callback: resetPanels, + callback: () => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.resetPanelsInTab(tab); + }, + dependencies: [tab], }); useRegisteredHotkeys({ category: 'app', id: 'togglePanels', - callback: toggleBothPanels, + callback: () => { + if (panelRegistry.tabApi?.getTab() !== tab) { + return; + } + panelRegistry.toggleBothPanelsInTab(tab); + }, + dependencies: [tab], }); return null; @@ -212,6 +314,7 @@ export const PanelHotkeysLogical = memo(() => { PanelHotkeysLogical.displayName = 'PanelHotkeysLogical'; export type PanelParameters = { + tab: TabName; focusRegion: FocusRegionName; }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx index 9995a11c7d..0057cc4525 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx @@ -28,12 +28,13 @@ import { withPanelContainer, } from 'features/ui/layouts/auto-layout-context'; import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; +import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; -import { atom } from 'nanostores'; import { memo, useCallback, useRef, useState } from 'react'; import { CanvasTabLeftPanel } from './CanvasTabLeftPanel'; import { CanvasWorkspacePanel } from './CanvasWorkspacePanel'; +import { panelRegistry } from './panel-registry/panelApiRegistry'; import { BOARD_PANEL_DEFAULT_HEIGHT_PX, BOARD_PANEL_MIN_HEIGHT_PX, @@ -66,66 +67,69 @@ const tabComponents = { [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon, }; -const centerPanelComponents: AutoLayoutDockviewComponents = { +const mainPanelComponents: AutoLayoutDockviewComponents = { [LAUNCHPAD_PANEL_ID]: withPanelContainer(CanvasLaunchpadPanel), [WORKSPACE_PANEL_ID]: withPanelContainer(CanvasWorkspacePanel), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), [PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel), }; -const initializeCenterPanelLayout = (api: DockviewApi) => { - const launchpadPanel = api.addPanel({ +const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => { + const launchpad = api.addPanel({ id: LAUNCHPAD_PANEL_ID, component: LAUNCHPAD_PANEL_ID, title: 'Launchpad', tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID, params: { + tab, focusRegion: 'launchpad', }, }); - const workspacePanel = api.addPanel({ + const workspace = api.addPanel({ id: WORKSPACE_PANEL_ID, component: WORKSPACE_PANEL_ID, title: 'Canvas', tabComponent: DEFAULT_TAB_ID, params: { + tab, focusRegion: 'canvas', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - const viewerPanel = api.addPanel({ + const viewer = api.addPanel({ id: VIEWER_PANEL_ID, component: VIEWER_PANEL_ID, title: 'Image Viewer', tabComponent: DEFAULT_TAB_ID, params: { + tab, focusRegion: 'viewer', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - return { launchpadPanel, workspacePanel, viewerPanel } satisfies Record; + return { launchpad, workspace, viewer } satisfies Record; }; -const CenterPanel = memo(() => { - const ctx = useAutoLayoutContext(); - const onReady = useCallback( - (event) => { - const panels = initializeCenterPanelLayout(event.api); - ctx._$centerPanelApi.set(event.api); +const MainPanel = memo(() => { + const { tab } = useAutoLayoutContext(); - panels.launchpadPanel.api.setActive(); + const onReady = useCallback( + ({ api }) => { + const panels = initializeCenterPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'main', api); + panels.launchpad.api.setActive(); const disposables = [ - event.api.onWillShowOverlay((e) => { + api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { return; } @@ -139,7 +143,7 @@ const CenterPanel = memo(() => { }); }; }, - [ctx._$centerPanelApi] + [tab] ); return ( <> @@ -148,7 +152,7 @@ const CenterPanel = memo(() => { locked={true} disableFloatingGroups={true} dndEdges={false} - components={centerPanelComponents} + components={mainPanelComponents} onReady={onReady} theme={dockviewTheme} tabComponents={tabComponents} @@ -159,7 +163,7 @@ const CenterPanel = memo(() => { ); }); -CenterPanel.displayName = 'CenterPanel'; +MainPanel.displayName = 'MainPanel'; const rightPanelComponents: AutoLayoutGridviewComponents = { [BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel), @@ -167,55 +171,59 @@ const rightPanelComponents: AutoLayoutGridviewComponents = { [LAYERS_PANEL_ID]: withPanelContainer(CanvasLayersPanel), }; -export const initializeRightPanelLayout = (api: GridviewApi) => { - const galleryPanel = api.addPanel({ +export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => { + const gallery = api.addPanel({ id: GALLERY_PANEL_ID, component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'gallery', }, }); - const layersPanel = api.addPanel({ + const layers = api.addPanel({ id: LAYERS_PANEL_ID, component: LAYERS_PANEL_ID, minimumHeight: LAYERS_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'layers', }, position: { direction: 'below', - referencePanel: galleryPanel.id, + referencePanel: gallery.id, }, }); - const boardsPanel = api.addPanel({ + const boards = api.addPanel({ id: BOARDS_PANEL_ID, component: BOARDS_PANEL_ID, minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'boards', }, position: { direction: 'above', - referencePanel: galleryPanel.id, + referencePanel: gallery.id, }, }); - boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); - return { galleryPanel, layersPanel, boardsPanel } satisfies Record; + boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); + return { gallery, layers, boards } satisfies Record; }; const RightPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeRightPanelLayout(event.api); - ctx._$rightPanelApi.set(event.api); + ({ api }) => { + initializeRightPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'right', api); }, - [ctx._$rightPanelApi] + [tab] ); return ( { - const settingsPanel = api.addPanel({ +export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => { + const settings = api.addPanel({ id: SETTINGS_PANEL_ID, component: SETTINGS_PANEL_ID, params: { + tab, focusRegion: 'settings', }, }); - return { settingsPanel } satisfies Record; + return { settings } satisfies Record; }; const LeftPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeLeftPanelLayout(event.api); - ctx._$leftPanelApi.set(event.api); + ({ api }) => { + initializeLeftPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'left', api); }, - [ctx._$leftPanelApi] + [tab] ); return ( { - const mainPanel = api.addPanel({ + const main = api.addPanel({ id: MAIN_PANEL_ID, component: MAIN_PANEL_ID, priority: LayoutPriority.High, }); - const leftPanel = api.addPanel({ + const left = api.addPanel({ id: LEFT_PANEL_ID, component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, position: { direction: 'left', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - const rightPanel = api.addPanel({ + const right = api.addPanel({ id: RIGHT_PANEL_ID, component: RIGHT_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, position: { direction: 'right', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); - return { mainPanel, leftPanel, rightPanel } satisfies Record; + return { main, left, right } satisfies Record; }; export const CanvasTabAutoLayout = memo(() => { const rootRef = useRef(null); - const $rootPanelApi = useState(() => atom(null))[0]; - const onReady = useCallback( - (event) => { - $rootPanelApi.set(event.api); - const { mainPanel } = initializeRootPanelLayout(event.api); - mainPanel.api.setActive(); - }, - [$rootPanelApi] - ); - useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + const { main } = initializeRootPanelLayout(api); + panelRegistry.registerPanel('canvas', 'root', api); + main.api.setActive(); + setRootApi(api); + }, []); + + useResizeMainPanelOnFirstVisit(rootApi, rootRef); return ( - + { - const launchpadPanel = api.addPanel({ +const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => { + const launchpad = api.addPanel({ id: LAUNCHPAD_PANEL_ID, component: LAUNCHPAD_PANEL_ID, title: 'Launchpad', tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID, params: { + tab, focusRegion: 'launchpad', }, }); - const viewerPanel = api.addPanel({ + const viewer = api.addPanel({ id: VIEWER_PANEL_ID, component: VIEWER_PANEL_ID, title: 'Image Viewer', tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, params: { + tab, focusRegion: 'viewer', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - return { launchpadPanel, viewerPanel } satisfies Record; + return { launchpad, viewer } satisfies Record; }; -const CenterPanel = memo(() => { - const ctx = useAutoLayoutContext(); - const onReady = useCallback( - (event) => { - const panels = initializeCenterPanelLayout(event.api); - ctx._$centerPanelApi.set(event.api); +const MainPanel = memo(() => { + const { tab } = useAutoLayoutContext(); - panels.launchpadPanel.api.setActive(); + const onReady = useCallback( + ({ api }) => { + const { launchpad } = initializeMainPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'main', api); + launchpad.api.setActive(); const disposables = [ - event.api.onWillShowOverlay((e) => { + api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { return; } @@ -119,7 +122,7 @@ const CenterPanel = memo(() => { }); }; }, - [ctx._$centerPanelApi] + [tab] ); return ( <> @@ -129,7 +132,7 @@ const CenterPanel = memo(() => { disableFloatingGroups={true} dndEdges={false} tabComponents={tabComponents} - components={centerPanelComponents} + components={mainPanelComponents} onReady={onReady} theme={dockviewTheme} /> @@ -139,50 +142,53 @@ const CenterPanel = memo(() => { ); }); -CenterPanel.displayName = 'CenterPanel'; +MainPanel.displayName = 'MainPanel'; const rightPanelComponents: AutoLayoutGridviewComponents = { [BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel), [GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel), }; -export const initializeRightPanelLayout = (api: GridviewApi) => { - const galleryPanel = api.addPanel({ +export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => { + const gallery = api.addPanel({ id: GALLERY_PANEL_ID, component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'gallery', }, }); - const boardsPanel = api.addPanel({ + const boards = api.addPanel({ id: BOARDS_PANEL_ID, component: BOARDS_PANEL_ID, minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'boards', }, position: { direction: 'above', - referencePanel: galleryPanel.id, + referencePanel: gallery.id, }, }); - boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); + boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); - return { galleryPanel, boardsPanel } satisfies Record; + return { gallery, boards } satisfies Record; }; const RightPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeRightPanelLayout(event.api); - ctx._$rightPanelApi.set(event.api); + ({ api }) => { + initializeRightPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'right', api); }, - [ctx._$rightPanelApi] + [tab] ); return ( { - const settingsPanel = api.addPanel({ +export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => { + const settings = api.addPanel({ id: SETTINGS_PANEL_ID, component: SETTINGS_PANEL_ID, params: { + tab, focusRegion: 'settings', }, }); - return { settingsPanel } satisfies Record; + return { settings } satisfies Record; }; const LeftPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeLeftPanelLayout(event.api); - ctx._$leftPanelApi.set(event.api); + ({ api }) => { + initializeLeftPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'left', api); }, - [ctx._$leftPanelApi] + [tab] ); return ( { - const mainPanel = layoutApi.addPanel({ + const main = layoutApi.addPanel({ id: MAIN_PANEL_ID, component: MAIN_PANEL_ID, priority: LayoutPriority.High, }); - const leftPanel = layoutApi.addPanel({ + const left = layoutApi.addPanel({ id: LEFT_PANEL_ID, component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, position: { direction: 'left', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - const rightPanel = layoutApi.addPanel({ + const right = layoutApi.addPanel({ id: RIGHT_PANEL_ID, component: RIGHT_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, position: { direction: 'right', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); - return { mainPanel, leftPanel, rightPanel } satisfies Record; + return { main, left, right } satisfies Record; }; export const GenerateTabAutoLayout = memo(() => { const rootRef = useRef(null); - const $rootPanelApi = useState(() => atom(null))[0]; - const onReady = useCallback( - (event) => { - $rootPanelApi.set(event.api); - const { mainPanel } = initializeRootPanelLayout(event.api); - mainPanel.api.setActive(); - }, - [$rootPanelApi] - ); - useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + const { main } = initializeRootPanelLayout(api); + panelRegistry.registerPanel('generate', 'root', api); + main.api.setActive(); + setRootApi(api); + }, []); + useResizeMainPanelOnFirstVisit(rootApi, rootRef); return ( - + | null; + left: Readonly | null; + main: Readonly | null; + right: Readonly | null; +}; + +const getInitialTabPanelApis = (): TabPanelApis => ({ + root: null, + left: null, + main: null, + right: null, +}); + +type AppTabApi = { + setTab: (tabName: TabName) => void; + getTab: () => TabName; +}; + +/** + * Global registry for all panel APIs across all tabs + */ +export class PanelRegistry { + private tabPanelApis = new Map(); + + tabApi: AppTabApi | null = null; + + /** + * Set the Redux store reference for tab switching + */ + setTabApi(tabApi: AppTabApi): void { + this.tabApi = tabApi; + } + + /** + * Register panel APIs for a specific tab + */ + registerPanel(tab: TabName, panel: T, api: NonNullable): void { + const current = this.tabPanelApis.get(tab) ?? getInitialTabPanelApis(); + const apis: TabPanelApis = { + ...current, + [panel]: api, + }; + this.tabPanelApis.set(tab, apis); + } + + /** + * Unregister panel APIs for a specific tab + */ + unregisterPanel(tab: TabName, panel: T): void { + const current = this.tabPanelApis.get(tab); + if (!current) { + return; + } + const apis: TabPanelApis = { + ...current, + [panel]: null, + }; + this.tabPanelApis.set(tab, apis); + } + + /** + * Unregister panel APIs for a specific tab + */ + unregisterTab(tabName: TabName): void { + this.tabPanelApis.delete(tabName); + } + + /** + * Get panel APIs for a specific tab + */ + getTabPanelApis(tabName: TabName): TabPanelApis | null { + return this.tabPanelApis.get(tabName) || null; + } + + /** + * Get panel APIs for the currently active tab + */ + getActiveTabPanelApis(): TabPanelApis | null { + if (!this.tabApi) { + return null; + } + + const activeTab = this.tabApi.getTab(); + return this.getTabPanelApis(activeTab); + } + + /** + * Get all registered tab names + */ + getRegisteredTabs(): TabName[] { + return Array.from(this.tabPanelApis.keys()); + } + + /** + * Check if a tab is registered + */ + isTabRegistered(tabName: TabName): boolean { + return this.tabPanelApis.has(tabName); + } + + /** + * Switch to a specific tab + */ + private switchToTab(tabName: TabName): boolean { + if (!this.tabApi) { + log.warn(`Cannot switch to tab "${tabName}": no store reference`); + return false; + } + + this.tabApi.setTab(tabName); + return true; + } + + /** + * Focus a specific panel in a specific tab + * Automatically switches to the target tab if specified + */ + focusPanelInTab(tabName: TabName, panelId: string): boolean { + const apis = this.getTabPanelApis(tabName); + if (!apis) { + log.warn(`Tab "${tabName}" not registered`); + return false; + } + + // Switch to target tab first + if (!this.switchToTab(tabName)) { + return false; + } + + // Try to focus in main panel (dockview) first + if (apis.main) { + const panel = apis.main.getPanel(panelId); + if (panel) { + panel.api.setActive(); + return true; + } + } + + // Try left panel + if (apis.left) { + const panel = apis.left.getPanel(panelId); + if (panel) { + panel.api.setActive(); + return true; + } + } + + // Try right panel + if (apis.right) { + const panel = apis.right.getPanel(panelId); + if (panel) { + panel.api.setActive(); + return true; + } + } + + log.warn(`Panel "${panelId}" not found in tab "${tabName}"`); + return false; + } + + /** + * Focus a panel in the currently active tab + */ + focusPanelInActiveTab(panelId: string): boolean { + if (!this.tabApi) { + return false; + } + + const activeTab = this.tabApi.getTab(); + return this.focusPanelInTab(activeTab, panelId); + } + + expandPanel(panel: IGridviewPanel, width: number) { + panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width }); + panel.api.setSize({ width: width }); + } + + collapsePanel(panel: IGridviewPanel) { + panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 }); + panel.api.setSize({ width: 0 }); + } + + /** + * Toggle the left panel in a specific tab + */ + toggleLeftPanelInTab(tabName: TabName): boolean { + const apis = this.getTabPanelApis(tabName); + if (!apis?.root) { + log.warn(`Root panel API not available for tab "${tabName}"`); + return false; + } + + if (!this.switchToTab(tabName)) { + return false; + } + + const leftPanel = apis.root.getPanel('left'); + if (!leftPanel) { + log.warn(`Left panel not found in tab "${tabName}"`); + return false; + } + + const isCollapsed = leftPanel.maximumWidth === 0; + if (isCollapsed) { + this.expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX); + } else { + this.collapsePanel(leftPanel); + } + return true; + } + + /** + * Toggle the right panel in a specific tab + */ + toggleRightPanelInTab(tabName: TabName): boolean { + const apis = this.getTabPanelApis(tabName); + if (!apis?.root) { + log.warn(`Root panel API not available for tab "${tabName}"`); + return false; + } + + if (!this.switchToTab(tabName)) { + return false; + } + + const rightPanel = apis.root.getPanel('right'); + if (!rightPanel) { + log.warn(`Right panel not found in tab "${tabName}"`); + return false; + } + + const isCollapsed = rightPanel.maximumWidth === 0; + if (isCollapsed) { + this.expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX); + } else { + this.collapsePanel(rightPanel); + } + return true; + } + + toggleBothPanelsInTab(tabName: TabName): boolean { + const apis = this.getTabPanelApis(tabName); + if (!apis?.root) { + log.warn(`Root panel API not available for tab "${tabName}"`); + return false; + } + + const rightPanel = apis.root.getPanel('right'); + const leftPanel = apis.root.getPanel('left'); + if (!rightPanel || !leftPanel) { + log.warn(`Right and/or left panel not found in tab "${tabName}"`); + return false; + } + + const isLeftCollapsed = leftPanel.maximumWidth === 0; + const isRightCollapsed = rightPanel.maximumWidth === 0; + + if (isLeftCollapsed || isRightCollapsed) { + this.expandPanel(leftPanel, LEFT_PANEL_MIN_SIZE_PX); + this.expandPanel(rightPanel, RIGHT_PANEL_MIN_SIZE_PX); + } else { + this.collapsePanel(leftPanel); + this.collapsePanel(rightPanel); + } + return true; + } + + /** + * Toggle the left panel in the currently active tab + */ + toggleLeftPanelInActiveTab(): boolean { + if (!this.tabApi) { + return false; + } + + const activeTab = this.tabApi.getTab(); + return this.toggleLeftPanelInTab(activeTab); + } + + /** + * Toggle the right panel in the currently active tab + */ + toggleRightPanelInActiveTab(): boolean { + if (!this.tabApi) { + return false; + } + + const activeTab = this.tabApi.getTab(); + return this.toggleRightPanelInTab(activeTab); + } + + /** + * Reset panels in a specific tab (expand both left and right) + */ + resetPanelsInTab(tabName: TabName): boolean { + const apis = this.getTabPanelApis(tabName); + if (!apis?.root) { + log.warn(`Root panel API not available for tab "${tabName}"`); + return false; + } + + if (!this.switchToTab(tabName)) { + return false; + } + + const rootApi = apis.root as GridviewApi; + const leftPanel = rootApi.getPanel('left'); + const rightPanel = rootApi.getPanel('right'); + + if (leftPanel) { + leftPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX }); + leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + } + + if (rightPanel) { + rightPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX }); + rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); + } + + return true; + } + + /** + * Reset panels in the currently active tab + */ + resetPanelsInActiveTab(): boolean { + if (!this.tabApi) { + return false; + } + + const activeTab = this.tabApi.getTab(); + return this.resetPanelsInTab(activeTab); + } +} + +// Global singleton instance +export const panelRegistry = new PanelRegistry(); diff --git a/invokeai/frontend/web/src/features/ui/layouts/panel-registry/use-panel-registry-init.tsx b/invokeai/frontend/web/src/features/ui/layouts/panel-registry/use-panel-registry-init.tsx new file mode 100644 index 0000000000..4acddf05ff --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/panel-registry/use-panel-registry-init.tsx @@ -0,0 +1,31 @@ +import { useAppStore } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { useEffect, useMemo } from 'react'; + +import { panelRegistry } from './panelApiRegistry'; + +/** + * Hook that initializes the global panel registry with the Redux store. + */ +export const usePanelRegistryInit = () => { + useAssertSingleton('usePanelRegistryInit'); + const store = useAppStore(); + const tabApi = useMemo( + () => ({ + getTab: () => { + return selectActiveTab(store.getState()); + }, + setTab: (tab: TabName) => { + store.dispatch(setActiveTab(tab)); + }, + }), + [store] + ); + + useEffect(() => { + panelRegistry.setTabApi(tabApi); + }, [store, tabApi]); +}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx index f34b1d3a1b..066d83dc4d 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx @@ -27,10 +27,11 @@ import { withPanelContainer, } from 'features/ui/layouts/auto-layout-context'; import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; +import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; -import { atom } from 'nanostores'; import { memo, useCallback, useRef, useState } from 'react'; +import { panelRegistry } from './panel-registry/panelApiRegistry'; import { BOARD_PANEL_DEFAULT_HEIGHT_PX, BOARD_PANEL_MIN_HEIGHT_PX, @@ -61,51 +62,52 @@ const tabComponents = { [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon, }; -const centerComponents: AutoLayoutDockviewComponents = { +const mainPanelComponents: AutoLayoutDockviewComponents = { [LAUNCHPAD_PANEL_ID]: withPanelContainer(UpscalingLaunchpadPanel), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), [PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel), }; -const initializeCenterPanelLayout = (api: DockviewApi) => { - const launchpadPanel = api.addPanel({ +const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => { + const launchpad = api.addPanel({ id: LAUNCHPAD_PANEL_ID, component: LAUNCHPAD_PANEL_ID, title: 'Launchpad', tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID, params: { + tab, focusRegion: 'launchpad', }, }); - const viewerPanel = api.addPanel({ + const viewer = api.addPanel({ id: VIEWER_PANEL_ID, component: VIEWER_PANEL_ID, title: 'Image Viewer', tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, params: { + tab, focusRegion: 'viewer', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - return { launchpadPanel, viewerPanel } satisfies Record; + return { launchpad, viewer } satisfies Record; }; -const CenterPanel = memo(() => { - const ctx = useAutoLayoutContext(); +const MainPanel = memo(() => { + const { tab } = useAutoLayoutContext(); const onReady = useCallback( - (event) => { - const panels = initializeCenterPanelLayout(event.api); - panels.launchpadPanel.api.setActive(); - - ctx._$centerPanelApi.set(event.api); + ({ api }) => { + const panels = initializeCenterPanelLayout(tab, api); + panels.launchpad.api.setActive(); + panelRegistry.registerPanel(tab, 'main', api); const disposables = [ - event.api.onWillShowOverlay((e) => { + api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { return; } @@ -119,7 +121,7 @@ const CenterPanel = memo(() => { }); }; }, - [ctx._$centerPanelApi] + [tab] ); return ( <> @@ -129,7 +131,7 @@ const CenterPanel = memo(() => { disableFloatingGroups={true} dndEdges={false} tabComponents={tabComponents} - components={centerComponents} + components={mainPanelComponents} onReady={onReady} theme={dockviewTheme} /> @@ -139,50 +141,53 @@ const CenterPanel = memo(() => { ); }); -CenterPanel.displayName = 'CenterPanel'; +MainPanel.displayName = 'MainPanel'; const rightPanelComponents: AutoLayoutGridviewComponents = { [BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel), [GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel), }; -export const initializeRightPanelLayout = (api: GridviewApi) => { - const galleryPanel = api.addPanel({ +export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => { + const gallery = api.addPanel({ id: GALLERY_PANEL_ID, component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'gallery', }, }); - const boardsPanel = api.addPanel({ + const boards = api.addPanel({ id: BOARDS_PANEL_ID, component: BOARDS_PANEL_ID, minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'boards', }, position: { direction: 'above', - referencePanel: galleryPanel.id, + referencePanel: gallery.id, }, }); - boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); + boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); - return { galleryPanel, boardsPanel } satisfies Record; + return { gallery, boards } satisfies Record; }; const RightPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeRightPanelLayout(event.api); - ctx._$rightPanelApi.set(event.api); + ({ api }) => { + initializeRightPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'right', api); }, - [ctx._$rightPanelApi] + [tab] ); return ( { - const settingsPanel = api.addPanel({ +export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => { + const settings = api.addPanel({ id: SETTINGS_PANEL_ID, component: SETTINGS_PANEL_ID, params: { + tab, focusRegion: 'settings', }, }); - return { settingsPanel } satisfies Record; + return { settings } satisfies Record; }; const LeftPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeLeftPanelLayout(event.api); - ctx._$leftPanelApi.set(event.api); + ({ api }) => { + initializeLeftPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'left', api); }, - [ctx._$leftPanelApi] + [tab] ); + return ( { - const mainPanel = layoutApi.addPanel({ + const main = layoutApi.addPanel({ id: MAIN_PANEL_ID, component: MAIN_PANEL_ID, priority: LayoutPriority.High, }); - const leftPanel = layoutApi.addPanel({ + const left = layoutApi.addPanel({ id: LEFT_PANEL_ID, component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, position: { direction: 'left', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - const rightPanel = layoutApi.addPanel({ + const right = layoutApi.addPanel({ id: RIGHT_PANEL_ID, component: RIGHT_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, position: { direction: 'right', - referencePanel: mainPanel.id, + referencePanel: main.id, }, }); - leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); - return { mainPanel, leftPanel, rightPanel } satisfies Record; + return { main, left, right } satisfies Record; }; export const UpscalingTabAutoLayout = memo(() => { const rootRef = useRef(null); - const $rootPanelApi = useState(() => atom(null))[0]; - const onReady = useCallback( - (event) => { - $rootPanelApi.set(event.api); - const { mainPanel } = initializeRootPanelLayout(event.api); - mainPanel.api.setActive(); - }, - [$rootPanelApi] - ); - useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + const { main } = initializeRootPanelLayout(api); + panelRegistry.registerPanel('upscaling', 'root', api); + main.api.setActive(); + setRootApi(api); + }, []); + useResizeMainPanelOnFirstVisit(rootApi, rootRef); return ( - + , orientation: 'vertical' | 'horizontal', @@ -14,7 +18,8 @@ const getIsCollapsed = ( }; export const useCollapsibleGridviewPanel = ( - api: GridviewApi | null, + tab: TabName, + rootPanelId: Exclude, panelId: string, orientation: 'horizontal' | 'vertical', defaultSize: number, @@ -23,6 +28,7 @@ export const useCollapsibleGridviewPanel = ( const $isCollapsed = useState(() => atom(false))[0]; const lastExpandedSizeRef = useRef(0); const collapse = useCallback(() => { + const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId]; if (!api) { return; } @@ -38,9 +44,11 @@ export const useCollapsibleGridviewPanel = ( } else { panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth }); } - }, [api, collapsedSize, orientation, panelId]); + }, [collapsedSize, orientation, panelId, rootPanelId, tab]); const expand = useCallback(() => { + const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId]; + if (!api) { return; } @@ -53,9 +61,11 @@ export const useCollapsibleGridviewPanel = ( } else { panel.api.setSize({ width: lastExpandedSizeRef.current || defaultSize }); } - }, [api, defaultSize, orientation, panelId]); + }, [defaultSize, orientation, panelId, rootPanelId, tab]); const toggle = useCallback(() => { + const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId]; + if (!api) { return; } @@ -69,9 +79,11 @@ export const useCollapsibleGridviewPanel = ( } else { collapse(); } - }, [api, panelId, orientation, collapsedSize, expand, collapse]); + }, [tab, rootPanelId, panelId, orientation, collapsedSize, expand, collapse]); useEffect(() => { + const api = panelRegistry.getTabPanelApis(tab)?.[rootPanelId]; + if (!api) { return; } @@ -90,7 +102,7 @@ export const useCollapsibleGridviewPanel = ( return () => { disposable.dispose(); }; - }, [$isCollapsed, api, collapsedSize, orientation, panelId]); + }, [$isCollapsed, collapsedSize, orientation, panelId, rootPanelId, tab]); return useMemo( () => ({ diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts index af7da97a52..1ea920485c 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts @@ -1,10 +1,22 @@ import type { GridviewApi } from 'dockview'; -import type { Atom } from 'nanostores'; import type { RefObject } from 'react'; import { useCallback, useEffect } from 'react'; import { MAIN_PANEL_ID } from './shared'; +// Find the parent element that has display: none +const findParentWithDisplayNone = (el: HTMLElement): HTMLElement | null => { + let parent = el.parentElement; + while (parent) { + const computedStyle = window.getComputedStyle(parent); + if (computedStyle.display === 'none') { + return parent; + } + parent = parent.parentElement; + } + return null; +}; + export const useOnFirstVisible = (elementRef: RefObject, callback: () => void): void => { useEffect(() => { const element = elementRef.current; @@ -12,20 +24,7 @@ export const useOnFirstVisible = (elementRef: RefObject, callback: return; } - // Find the parent element that has display: none - const findParentWithDisplay = (el: HTMLElement): HTMLElement | null => { - let parent = el.parentElement; - while (parent) { - const computedStyle = window.getComputedStyle(parent); - if (computedStyle.display === 'none') { - return parent; - } - parent = parent.parentElement; - } - return null; - }; - - const targetParent = findParentWithDisplay(element); + const targetParent = findParentWithDisplayNone(element); if (!targetParent) { return; } @@ -51,9 +50,8 @@ export const useOnFirstVisible = (elementRef: RefObject, callback: }, [elementRef, callback]); }; -export const useResizeMainPanelOnFirstVisit = ($api: Atom, ref: RefObject) => { +export const useResizeMainPanelOnFirstVisit = (api: GridviewApi | null, ref: RefObject) => { const resizeMainPanelOnFirstVisible = useCallback(() => { - const api = $api.get(); if (!api) { return; } @@ -76,6 +74,6 @@ export const useResizeMainPanelOnFirstVisit = ($api: Atom, r } }; setSize(); - }, [$api]); + }, [api]); useOnFirstVisible(ref, resizeMainPanelOnFirstVisible); }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx index 3d99b6e454..7d61973f10 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx @@ -29,10 +29,11 @@ import { withPanelContainer, } from 'features/ui/layouts/auto-layout-context'; import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; +import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; -import { atom } from 'nanostores'; import { memo, useCallback, useRef, useState } from 'react'; +import { panelRegistry } from './panel-registry/panelApiRegistry'; import { BOARD_PANEL_DEFAULT_HEIGHT_PX, BOARD_PANEL_MIN_HEIGHT_PX, @@ -63,66 +64,69 @@ const tabComponents = { [TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon, }; -const centerPanelComponents: AutoLayoutDockviewComponents = { +const mainPanelComponents: AutoLayoutDockviewComponents = { [LAUNCHPAD_PANEL_ID]: withPanelContainer(WorkflowsLaunchpadPanel), [WORKSPACE_PANEL_ID]: withPanelContainer(NodeEditor), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), [PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel), }; -const initializeCenterPanelLayout = (api: DockviewApi) => { - const launchpadPanel = api.addPanel({ +const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => { + const launchpad = api.addPanel({ id: LAUNCHPAD_PANEL_ID, component: LAUNCHPAD_PANEL_ID, title: 'Launchpad', tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID, params: { + tab, focusRegion: 'launchpad', }, }); - const workspacePanel = api.addPanel({ + const workspace = api.addPanel({ id: WORKSPACE_PANEL_ID, component: WORKSPACE_PANEL_ID, title: 'Workflow Editor', tabComponent: DEFAULT_TAB_ID, params: { + tab, focusRegion: 'workflows', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - const viewerPanel = api.addPanel({ + const viewer = api.addPanel({ id: VIEWER_PANEL_ID, component: VIEWER_PANEL_ID, title: 'Image Viewer', tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID, params: { + tab, focusRegion: 'viewer', }, position: { direction: 'within', - referencePanel: launchpadPanel.id, + referencePanel: launchpad.id, }, }); - return { launchpadPanel, workspacePanel, viewerPanel } satisfies Record; + return { launchpad, workspace, viewer } satisfies Record; }; -const CenterPanel = memo(() => { - const ctx = useAutoLayoutContext(); - const onReady = useCallback( - (event) => { - const panels = initializeCenterPanelLayout(event.api); - ctx._$centerPanelApi.set(event.api); +const MainPanel = memo(() => { + const { tab } = useAutoLayoutContext(); - panels.launchpadPanel.api.setActive(); + const onReady = useCallback( + ({ api }) => { + const panels = initializeMainPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'main', api); + panels.launchpad.api.setActive(); const disposables = [ - event.api.onWillShowOverlay((e) => { + api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { return; } @@ -136,7 +140,7 @@ const CenterPanel = memo(() => { }); }; }, - [ctx._$centerPanelApi] + [tab] ); return ( <> @@ -146,7 +150,7 @@ const CenterPanel = memo(() => { disableFloatingGroups={true} dndEdges={false} tabComponents={tabComponents} - components={centerPanelComponents} + components={mainPanelComponents} onReady={onReady} theme={dockviewTheme} /> @@ -156,50 +160,53 @@ const CenterPanel = memo(() => { ); }); -CenterPanel.displayName = 'CenterPanel'; +MainPanel.displayName = 'MainPanel'; const rightPanelComponents: AutoLayoutGridviewComponents = { [BOARDS_PANEL_ID]: withPanelContainer(BoardsPanel), [GALLERY_PANEL_ID]: withPanelContainer(GalleryPanel), }; -export const initializeRightPanelLayout = (api: GridviewApi) => { - const galleryPanel = api.addPanel({ +export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => { + const gallery = api.addPanel({ id: GALLERY_PANEL_ID, component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: GALLERY_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'gallery', }, }); - const boardsPanel = api.addPanel({ + const boards = api.addPanel({ id: BOARDS_PANEL_ID, component: BOARDS_PANEL_ID, minimumHeight: BOARD_PANEL_MIN_HEIGHT_PX, params: { + tab, focusRegion: 'boards', }, position: { direction: 'above', - referencePanel: galleryPanel.id, + referencePanel: gallery.id, }, }); - boardsPanel.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); + boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX }); - return { galleryPanel, boardsPanel } satisfies Record; + return { gallery, boards } satisfies Record; }; const RightPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeRightPanelLayout(event.api); - ctx._$rightPanelApi.set(event.api); + ({ api }) => { + initializeRightPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'right', api); }, - [ctx._$rightPanelApi] + [tab] ); return ( { - const settingsPanel = api.addPanel({ +export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => { + const settings = api.addPanel({ id: SETTINGS_PANEL_ID, component: SETTINGS_PANEL_ID, params: { + tab, focusRegion: 'settings', }, }); - return { settingsPanel } satisfies Record; + return { settings } satisfies Record; }; const LeftPanel = memo(() => { - const ctx = useAutoLayoutContext(); + const { tab } = useAutoLayoutContext(); + const onReady = useCallback( - (event) => { - initializeLeftPanelLayout(event.api); - ctx._$leftPanelApi.set(event.api); + ({ api }) => { + initializeLeftPanelLayout(tab, api); + panelRegistry.registerPanel(tab, 'left', api); }, - [ctx._$leftPanelApi] + [tab] ); return ( { - api.addPanel({ + const main = api.addPanel({ id: MAIN_PANEL_ID, component: MAIN_PANEL_ID, priority: LayoutPriority.High, }); - api.addPanel({ + const left = api.addPanel({ id: LEFT_PANEL_ID, component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, @@ -269,7 +278,7 @@ export const initializeRootPanelLayout = (api: GridviewApi) => { referencePanel: MAIN_PANEL_ID, }, }); - api.addPanel({ + const right = api.addPanel({ id: RIGHT_PANEL_ID, component: RIGHT_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, @@ -278,25 +287,25 @@ export const initializeRootPanelLayout = (api: GridviewApi) => { referencePanel: MAIN_PANEL_ID, }, }); - api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - api.getPanel(RIGHT_PANEL_ID)?.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); - api.getPanel(MAIN_PANEL_ID)?.api.setActive(); + left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX }); + + return { main, left, right } satisfies Record; }; export const WorkflowsTabAutoLayout = memo(() => { const rootRef = useRef(null); - const $rootPanelApi = useState(() => atom(null))[0]; - const onReady = useCallback( - (event) => { - $rootPanelApi.set(event.api); - initializeRootPanelLayout(event.api); - }, - [$rootPanelApi] - ); - useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + const panels = initializeRootPanelLayout(api); + panelRegistry.registerPanel('workflows', 'root', api); + panels.main.api.setActive(); + setRootApi(api); + }, []); + useResizeMainPanelOnFirstVisit(rootApi, rootRef); return ( - +