refactor(ui): implement global panel registry, replace context-based panel API

This commit is contained in:
psychedelicious
2025-07-01 19:41:50 +10:00
parent f13ced7ed4
commit 195d6ce893
21 changed files with 969 additions and 440 deletions

View File

@@ -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.

View File

@@ -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 (
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
<Flex flexDir="column" w="full" gap={4} px={14} maxW={768} pt="20vh">

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<MouseEventHandler<HTMLDivElement>>(() => {
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]);

View File

@@ -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 (
<DndImageIcon

View File

@@ -11,10 +11,9 @@ import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
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 { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -45,7 +44,6 @@ export const CurrentImageButtons = memo(() => {
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 (
<>

View File

@@ -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<ListRange>({ startIndex: 0, endIndex: 0 });
const rootRef = useRef<HTMLDivElement>(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 (
<Flex height="100%" alignItems="center" justifyContent="center" gap={4}>
<Flex w="full" h="full" alignItems="center" justifyContent="center" gap={4}>
<Spinner size="lg" opacity={0.3} />
<Text color="base.300">Loading gallery...</Text>
</Flex>
@@ -491,7 +488,7 @@ export const NewGallery = memo(() => {
if (imageNames.length === 0) {
return (
<Flex height="100%" alignItems="center" justifyContent="center">
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Text color="base.300">No images found</Text>
</Flex>
);

View File

@@ -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<MouseEventHandler<HTMLButtonElement>>(
(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<MouseEventHandler<HTMLButtonElement>>(
@@ -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') {

View File

@@ -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 };
};

View File

@@ -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 (
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
<IconButton
aria-label={t('accessibility.toggleLeftPanel')}
onClick={toggleLeftPanel}
onClick={onClick}
icon={<PiSlidersHorizontalBold />}
flexGrow={1}
/>

View File

@@ -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 (
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
<IconButton
aria-label={t('accessibility.toggleRightPanel')}
onClick={toggleRightPanel}
onClick={onClick}
icon={<PiImagesSquareBold />}
h={48}
/>

View File

@@ -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<HTMLDivElement>;
_$rootPanelApi: WritableAtom<GridviewApi | null>;
_$leftPanelApi: WritableAtom<GridviewApi | null>;
_$centerPanelApi: WritableAtom<DockviewApi | null>;
_$rightPanelApi: WritableAtom<GridviewApi | null>;
// _$rootPanelApi: WritableAtom<GridviewApi | null>;
// _$leftPanelApi: WritableAtom<GridviewApi | null>;
// _$centerPanelApi: WritableAtom<DockviewApi | null>;
// _$rightPanelApi: WritableAtom<GridviewApi | null>;
// // Global registry access for cross-tab operations
// registry: typeof panelApiRegistry;
};
const AutoLayoutContext = createContext<AutoLayoutContextValue | null>(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<GridviewApi | null>;
// $rootApi: WritableAtom<GridviewApi | null>;
rootRef: RefObject<HTMLDivElement>;
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<GridviewApi | null>(null))[0];
const $centerApi = useState(() => atom<DockviewApi | null>(null))[0];
const $rightApi = useState(() => atom<GridviewApi | null>(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<GridviewApi | null>(null))[0];
// const $centerApi = useState(() => atom<DockviewApi | null>(null))[0];
// const $rightApi = useState(() => atom<GridviewApi | null>(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<AutoLayoutContextValue>(
() => ({
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 <AutoLayoutContext.Provider value={value}>{children}</AutoLayoutContext.Provider>;
@@ -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;
};

View File

@@ -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<PanelParameters>({
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
const launchpad = api.addPanel<PanelParameters>({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
params: {
tab,
focusRegion: 'launchpad',
},
});
const workspacePanel = api.addPanel<PanelParameters>({
const workspace = api.addPanel<PanelParameters>({
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<PanelParameters>({
const viewer = api.addPanel<PanelParameters>({
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<string, IDockviewPanel>;
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
};
const CenterPanel = memo(() => {
const ctx = useAutoLayoutContext();
const onReady = useCallback<IDockviewReactProps['onReady']>(
(event) => {
const panels = initializeCenterPanelLayout(event.api);
ctx._$centerPanelApi.set(event.api);
const MainPanel = memo(() => {
const { tab } = useAutoLayoutContext();
panels.launchpadPanel.api.setActive();
const onReady = useCallback<IDockviewReactProps['onReady']>(
({ 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<PanelParameters>({
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
const gallery = api.addPanel<PanelParameters>({
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<PanelParameters>({
const layers = api.addPanel<PanelParameters>({
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<PanelParameters>({
const boards = api.addPanel<PanelParameters>({
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<string, IGridviewPanel>;
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
return { gallery, layers, boards } satisfies Record<string, IGridviewPanel>;
};
const RightPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeRightPanelLayout(event.api);
ctx._$rightPanelApi.set(event.api);
({ api }) => {
initializeRightPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'right', api);
},
[ctx._$rightPanelApi]
[tab]
);
return (
<GridviewReact
@@ -232,26 +240,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
[SETTINGS_PANEL_ID]: withPanelContainer(CanvasTabLeftPanel),
};
export const initializeLeftPanelLayout = (api: GridviewApi) => {
const settingsPanel = api.addPanel<PanelParameters>({
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
const settings = api.addPanel<PanelParameters>({
id: SETTINGS_PANEL_ID,
component: SETTINGS_PANEL_ID,
params: {
tab,
focusRegion: 'settings',
},
});
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
return { settings } satisfies Record<string, IGridviewPanel>;
};
const LeftPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeLeftPanelLayout(event.api);
ctx._$leftPanelApi.set(event.api);
({ api }) => {
initializeLeftPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'left', api);
},
[ctx._$leftPanelApi]
[tab]
);
return (
<GridviewReact
@@ -266,58 +276,57 @@ LeftPanel.displayName = 'LeftPanel';
export const rootPanelComponents: RootLayoutGridviewComponents = {
[LEFT_PANEL_ID]: LeftPanel,
[MAIN_PANEL_ID]: CenterPanel,
[MAIN_PANEL_ID]: MainPanel,
[RIGHT_PANEL_ID]: RightPanel,
};
export const initializeRootPanelLayout = (api: GridviewApi) => {
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<string, IGridviewPanel>;
return { main, left, right } satisfies Record<string, IGridviewPanel>;
};
export const CanvasTabAutoLayout = memo(() => {
const rootRef = useRef<HTMLDivElement>(null);
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$rootPanelApi.set(event.api);
const { mainPanel } = initializeRootPanelLayout(event.api);
mainPanel.api.setActive();
},
[$rootPanelApi]
);
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
const { main } = initializeRootPanelLayout(api);
panelRegistry.registerPanel('canvas', 'root', api);
main.api.setActive();
setRootApi(api);
}, []);
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
return (
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="canvas">
<AutoLayoutProvider tab="canvas" rootRef={rootRef}>
<GridviewReact
ref={rootRef}
className="dockview-theme-invoke"

View File

@@ -27,11 +27,12 @@ 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 { GenerateTabLeftPanel } from './GenerateTabLeftPanel';
import { panelRegistry } from './panel-registry/panelApiRegistry';
import {
BOARD_PANEL_DEFAULT_HEIGHT_PX,
BOARD_PANEL_MIN_HEIGHT_PX,
@@ -61,51 +62,53 @@ const tabComponents = {
[TAB_WITH_LAUNCHPAD_ICON_ID]: TabWithLaunchpadIcon,
};
const centerPanelComponents: AutoLayoutDockviewComponents = {
const mainPanelComponents: AutoLayoutDockviewComponents = {
[LAUNCHPAD_PANEL_ID]: withPanelContainer(GenerateLaunchpadPanel),
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
[PROGRESS_PANEL_ID]: withPanelContainer(GenerationProgressPanel),
};
const initializeCenterPanelLayout = (api: DockviewApi) => {
const launchpadPanel = api.addPanel<PanelParameters>({
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
const launchpad = api.addPanel<PanelParameters>({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
params: {
tab,
focusRegion: 'launchpad',
},
});
const viewerPanel = api.addPanel<PanelParameters>({
const viewer = api.addPanel<PanelParameters>({
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<string, IDockviewPanel>;
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
};
const CenterPanel = memo(() => {
const ctx = useAutoLayoutContext();
const onReady = useCallback<IDockviewReactProps['onReady']>(
(event) => {
const panels = initializeCenterPanelLayout(event.api);
ctx._$centerPanelApi.set(event.api);
const MainPanel = memo(() => {
const { tab } = useAutoLayoutContext();
panels.launchpadPanel.api.setActive();
const onReady = useCallback<IDockviewReactProps['onReady']>(
({ 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<PanelParameters>({
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
const gallery = api.addPanel<PanelParameters>({
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<PanelParameters>({
const boards = api.addPanel<PanelParameters>({
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<string, IGridviewPanel>;
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
};
const RightPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeRightPanelLayout(event.api);
ctx._$rightPanelApi.set(event.api);
({ api }) => {
initializeRightPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'right', api);
},
[ctx._$rightPanelApi]
[tab]
);
return (
<GridviewReact
@@ -199,26 +205,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
[SETTINGS_PANEL_ID]: withPanelContainer(GenerateTabLeftPanel),
};
export const initializeLeftPanelLayout = (api: GridviewApi) => {
const settingsPanel = api.addPanel<PanelParameters>({
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
const settings = api.addPanel<PanelParameters>({
id: SETTINGS_PANEL_ID,
component: SETTINGS_PANEL_ID,
params: {
tab,
focusRegion: 'settings',
},
});
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
return { settings } satisfies Record<string, IGridviewPanel>;
};
const LeftPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeLeftPanelLayout(event.api);
ctx._$leftPanelApi.set(event.api);
({ api }) => {
initializeLeftPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'left', api);
},
[ctx._$leftPanelApi]
[tab]
);
return (
<GridviewReact
@@ -233,58 +241,56 @@ LeftPanel.displayName = 'LeftPanel';
export const rootPanelComponents: RootLayoutGridviewComponents = {
[LEFT_PANEL_ID]: LeftPanel,
[MAIN_PANEL_ID]: CenterPanel,
[MAIN_PANEL_ID]: MainPanel,
[RIGHT_PANEL_ID]: RightPanel,
};
export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
const mainPanel = layoutApi.addPanel<PanelParameters>({
const main = layoutApi.addPanel<PanelParameters>({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
priority: LayoutPriority.High,
});
const leftPanel = layoutApi.addPanel<PanelParameters>({
const left = layoutApi.addPanel<PanelParameters>({
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<PanelParameters>({
const right = layoutApi.addPanel<PanelParameters>({
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<string, IGridviewPanel>;
return { main, left, right } satisfies Record<string, IGridviewPanel>;
};
export const GenerateTabAutoLayout = memo(() => {
const rootRef = useRef<HTMLDivElement>(null);
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$rootPanelApi.set(event.api);
const { mainPanel } = initializeRootPanelLayout(event.api);
mainPanel.api.setActive();
},
[$rootPanelApi]
);
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
const { main } = initializeRootPanelLayout(api);
panelRegistry.registerPanel('generate', 'root', api);
main.api.setActive();
setRootApi(api);
}, []);
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
return (
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="generate">
<AutoLayoutProvider tab="generate" rootRef={rootRef}>
<GridviewReact
ref={rootRef}
className="dockview-theme-invoke"

View File

@@ -0,0 +1,345 @@
import { logger } from 'app/logging/logger';
import type { DockviewApi, GridviewApi, IGridviewPanel } from 'dockview';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/layouts/shared';
import type { TabName } from 'features/ui/store/uiTypes';
const log = logger('system');
export type TabPanelApis = {
root: Readonly<GridviewApi> | null;
left: Readonly<GridviewApi> | null;
main: Readonly<DockviewApi> | null;
right: Readonly<GridviewApi> | 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<TabName, TabPanelApis>();
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<T extends keyof TabPanelApis>(tab: TabName, panel: T, api: NonNullable<TabPanelApis[T]>): 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<T extends keyof TabPanelApis>(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();

View File

@@ -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]);
};

View File

@@ -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<PanelParameters>({
const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
const launchpad = api.addPanel<PanelParameters>({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
params: {
tab,
focusRegion: 'launchpad',
},
});
const viewerPanel = api.addPanel<PanelParameters>({
const viewer = api.addPanel<PanelParameters>({
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<string, IDockviewPanel>;
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
};
const CenterPanel = memo(() => {
const ctx = useAutoLayoutContext();
const MainPanel = memo(() => {
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IDockviewReactProps['onReady']>(
(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<PanelParameters>({
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
const gallery = api.addPanel<PanelParameters>({
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<PanelParameters>({
const boards = api.addPanel<PanelParameters>({
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<string, IGridviewPanel>;
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
};
const RightPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeRightPanelLayout(event.api);
ctx._$rightPanelApi.set(event.api);
({ api }) => {
initializeRightPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'right', api);
},
[ctx._$rightPanelApi]
[tab]
);
return (
<GridviewReact
@@ -199,27 +204,30 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
[SETTINGS_PANEL_ID]: withPanelContainer(UpscalingTabLeftPanel),
};
export const initializeLeftPanelLayout = (api: GridviewApi) => {
const settingsPanel = api.addPanel<PanelParameters>({
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
const settings = api.addPanel<PanelParameters>({
id: SETTINGS_PANEL_ID,
component: SETTINGS_PANEL_ID,
params: {
tab,
focusRegion: 'settings',
},
});
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
return { settings } satisfies Record<string, IGridviewPanel>;
};
const LeftPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeLeftPanelLayout(event.api);
ctx._$leftPanelApi.set(event.api);
({ api }) => {
initializeLeftPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'left', api);
},
[ctx._$leftPanelApi]
[tab]
);
return (
<GridviewReact
className="dockview-theme-invoke"
@@ -233,58 +241,56 @@ LeftPanel.displayName = 'LeftPanel';
export const rootPanelComponents: RootLayoutGridviewComponents = {
[LEFT_PANEL_ID]: LeftPanel,
[MAIN_PANEL_ID]: CenterPanel,
[MAIN_PANEL_ID]: MainPanel,
[RIGHT_PANEL_ID]: RightPanel,
};
export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
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<string, IGridviewPanel>;
return { main, left, right } satisfies Record<string, IGridviewPanel>;
};
export const UpscalingTabAutoLayout = memo(() => {
const rootRef = useRef<HTMLDivElement>(null);
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$rootPanelApi.set(event.api);
const { mainPanel } = initializeRootPanelLayout(event.api);
mainPanel.api.setActive();
},
[$rootPanelApi]
);
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
const { main } = initializeRootPanelLayout(api);
panelRegistry.registerPanel('upscaling', 'root', api);
main.api.setActive();
setRootApi(api);
}, []);
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
return (
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="upscaling">
<AutoLayoutProvider tab="upscaling" rootRef={rootRef}>
<GridviewReact
ref={rootRef}
className="dockview-theme-invoke"

View File

@@ -1,7 +1,11 @@
import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview';
import type { GridviewPanelApi, IGridviewPanel } from 'dockview';
import type { TabName } from 'features/ui/store/uiTypes';
import { atom } from 'nanostores';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { TabPanelApis } from './panel-registry/panelApiRegistry';
import { panelRegistry } from './panel-registry/panelApiRegistry';
const getIsCollapsed = (
panel: IGridviewPanel<GridviewPanelApi>,
orientation: 'vertical' | 'horizontal',
@@ -14,7 +18,8 @@ const getIsCollapsed = (
};
export const useCollapsibleGridviewPanel = (
api: GridviewApi | null,
tab: TabName,
rootPanelId: Exclude<keyof TabPanelApis, 'main'>,
panelId: string,
orientation: 'horizontal' | 'vertical',
defaultSize: number,
@@ -23,6 +28,7 @@ export const useCollapsibleGridviewPanel = (
const $isCollapsed = useState(() => atom(false))[0];
const lastExpandedSizeRef = useRef<number>(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(
() => ({

View File

@@ -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<HTMLElement>, callback: () => void): void => {
useEffect(() => {
const element = elementRef.current;
@@ -12,20 +24,7 @@ export const useOnFirstVisible = (elementRef: RefObject<HTMLElement>, 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<HTMLElement>, callback:
}, [elementRef, callback]);
};
export const useResizeMainPanelOnFirstVisit = ($api: Atom<GridviewApi | null>, ref: RefObject<HTMLElement>) => {
export const useResizeMainPanelOnFirstVisit = (api: GridviewApi | null, ref: RefObject<HTMLElement>) => {
const resizeMainPanelOnFirstVisible = useCallback(() => {
const api = $api.get();
if (!api) {
return;
}
@@ -76,6 +74,6 @@ export const useResizeMainPanelOnFirstVisit = ($api: Atom<GridviewApi | null>, r
}
};
setSize();
}, [$api]);
}, [api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
};

View File

@@ -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<PanelParameters>({
const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
const launchpad = api.addPanel<PanelParameters>({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
tabComponent: TAB_WITH_LAUNCHPAD_ICON_ID,
params: {
tab,
focusRegion: 'launchpad',
},
});
const workspacePanel = api.addPanel<PanelParameters>({
const workspace = api.addPanel<PanelParameters>({
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<PanelParameters>({
const viewer = api.addPanel<PanelParameters>({
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<string, IDockviewPanel>;
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
};
const CenterPanel = memo(() => {
const ctx = useAutoLayoutContext();
const onReady = useCallback<IDockviewReactProps['onReady']>(
(event) => {
const panels = initializeCenterPanelLayout(event.api);
ctx._$centerPanelApi.set(event.api);
const MainPanel = memo(() => {
const { tab } = useAutoLayoutContext();
panels.launchpadPanel.api.setActive();
const onReady = useCallback<IDockviewReactProps['onReady']>(
({ 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<PanelParameters>({
export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
const gallery = api.addPanel<PanelParameters>({
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<PanelParameters>({
const boards = api.addPanel<PanelParameters>({
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<string, IGridviewPanel>;
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
};
const RightPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeRightPanelLayout(event.api);
ctx._$rightPanelApi.set(event.api);
({ api }) => {
initializeRightPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'right', api);
},
[ctx._$rightPanelApi]
[tab]
);
return (
<GridviewReact
@@ -216,26 +223,28 @@ const leftPanelComponents: AutoLayoutGridviewComponents = {
[SETTINGS_PANEL_ID]: withPanelContainer(WorkflowsTabLeftPanel),
};
export const initializeLeftPanelLayout = (api: GridviewApi) => {
const settingsPanel = api.addPanel<PanelParameters>({
export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
const settings = api.addPanel<PanelParameters>({
id: SETTINGS_PANEL_ID,
component: SETTINGS_PANEL_ID,
params: {
tab,
focusRegion: 'settings',
},
});
return { settingsPanel } satisfies Record<string, IGridviewPanel>;
return { settings } satisfies Record<string, IGridviewPanel>;
};
const LeftPanel = memo(() => {
const ctx = useAutoLayoutContext();
const { tab } = useAutoLayoutContext();
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
initializeLeftPanelLayout(event.api);
ctx._$leftPanelApi.set(event.api);
({ api }) => {
initializeLeftPanelLayout(tab, api);
panelRegistry.registerPanel(tab, 'left', api);
},
[ctx._$leftPanelApi]
[tab]
);
return (
<GridviewReact
@@ -250,17 +259,17 @@ LeftPanel.displayName = 'LeftPanel';
export const rootPanelComponents: RootLayoutGridviewComponents = {
[LEFT_PANEL_ID]: LeftPanel,
[MAIN_PANEL_ID]: CenterPanel,
[MAIN_PANEL_ID]: MainPanel,
[RIGHT_PANEL_ID]: RightPanel,
};
export const initializeRootPanelLayout = (api: GridviewApi) => {
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<string, IGridviewPanel>;
};
export const WorkflowsTabAutoLayout = memo(() => {
const rootRef = useRef<HTMLDivElement>(null);
const $rootPanelApi = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$rootPanelApi.set(event.api);
initializeRootPanelLayout(event.api);
},
[$rootPanelApi]
);
useResizeMainPanelOnFirstVisit($rootPanelApi, rootRef);
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
const panels = initializeRootPanelLayout(api);
panelRegistry.registerPanel('workflows', 'root', api);
panels.main.api.setActive();
setRootApi(api);
}, []);
useResizeMainPanelOnFirstVisit(rootApi, rootRef);
return (
<AutoLayoutProvider $rootApi={$rootPanelApi} rootRef={rootRef} tab="workflows">
<AutoLayoutProvider tab="workflows" rootRef={rootRef}>
<GridviewReact
ref={rootRef}
className="dockview-theme-invoke"