This commit is contained in:
psychedelicious
2025-06-12 12:08:31 +10:00
parent 9cc8bcf749
commit 81363b904b
4 changed files with 687 additions and 462 deletions

View File

@@ -4,16 +4,19 @@ import {
MenuButton,
MenuGroup,
MenuItem,
MenuItemOption,
MenuList,
MenuOptionGroup,
Portal,
useDisclosure,
useGlobalMenuClose,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import AboutModal from 'features/system/components/AboutModal/AboutModal';
import HotkeysModal from 'features/system/components/HotkeysModal/HotkeysModal';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { discordLink, githubLink } from 'features/system/store/constants';
import { $panels } from 'features/ui/components/AppContent';
import { $advancedLayout, $panels, toggleAdvancedLayout } from 'features/ui/components/AppContent';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -38,6 +41,7 @@ const SettingsMenu = () => {
const isBugLinkEnabled = useFeatureStatus('bugLink');
const isDiscordLinkEnabled = useFeatureStatus('discordLink');
const isGithubLinkEnabled = useFeatureStatus('githubLink');
const advancedLayout = useStore($advancedLayout);
const resetLayout = useCallback(() => {
$panels.get()?.resetLayout();
@@ -79,6 +83,15 @@ const SettingsMenu = () => {
<MenuItem as="button" onClick={resetLayout} icon={<PiLayoutBold />}>
Reset Layout
</MenuItem>
<MenuOptionGroup
value={advancedLayout ? 'advanced' : 'simple'}
onChange={toggleAdvancedLayout}
title="Layout"
type="radio"
>
<MenuItemOption value="simple">Simple</MenuItemOption>
<MenuItemOption value="advanced">Advanced</MenuItemOption>
</MenuOptionGroup>
<MenuGroup title={t('common.settingsLabel')}>
<HotkeysModal>
<MenuItem as="button" icon={<PiKeyboardBold />}>

View File

@@ -1,451 +1,86 @@
import 'dockview/dist/styles/dockview.css';
import './dockview_theme_invoke.css';
import { Divider, Flex, IconButton } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import type {
DockviewApi,
DockviewTheme,
IDockviewHeaderActionsProps,
IDockviewPanelHeaderProps,
IDockviewReactProps,
} from 'dockview';
import { DockviewDefaultTab, DockviewReact } from 'dockview';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { DockviewApi } from 'dockview';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ProgressImage from 'features/gallery/components/ImageViewer/ProgressImage';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { MainPanelContent } from 'features/ui/components/MainPanelContent';
import { GridviewWrapper } from 'features/ui/components/GridviewWrapper';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isLeftPanelOpen,
$isRightPanelOpen,
LEFT_PANEL_MIN_SIZE_PX,
RIGHT_PANEL_MIN_SIZE_PX,
selectWithLeftPanel,
selectWithRightPanel,
} from 'features/ui/store/uiSlice';
import { atom } from 'nanostores';
import type { CSSProperties } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PiArrowSquareOutBold, PiCornersInBold, PiCornersOutBold } from 'react-icons/pi';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
const MyCustomTab = (props: IDockviewPanelHeaderProps) => {
const onDragEnter = useCallback(() => {
if (!props.api.isActive) {
props.api.setActive();
}
}, [props.api]);
return <DockviewDefaultTab hideClose {...props} onDragEnter={onDragEnter} />;
};
const RightHeaderActions = (props: IDockviewHeaderActionsProps) => {
const [isMaximized, setIsMaximized] = useState(false);
const popOutToFloating = useCallback(() => {
props.containerApi.addFloatingGroup(props.group);
}, [props.containerApi, props.group]);
const maximize = useCallback(() => {
if (!props.group.activePanel) {
props.group.panels.at(0)?.api.setActive();
}
if (!props.group.activePanel) {
return;
}
props.containerApi.maximizeGroup(props.group.activePanel);
}, [props.containerApi, props.group]);
const exitMaximized = useCallback(() => {
if (!props.group.activePanel) {
props.group.panels.at(0)?.api.setActive();
}
if (!props.group.activePanel) {
return;
}
props.containerApi.exitMaximizedGroup();
}, [props.containerApi, props.group]);
useEffect(() => {
const subscription = props.containerApi.onDidMaximizedGroupChange((e) => {
if (e.group.id === props.group.id) {
setIsMaximized(e.isMaximized);
}
});
return () => {
subscription.dispose();
};
}, [props.containerApi, props.group.id]);
return (
<Flex h="full" alignItems="center" pe={1}>
{!isMaximized && (
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
icon={<PiCornersOutBold />}
aria-label="Maximize Panel"
tooltip="Maximize Panel"
onClick={maximize}
opacity={0.7}
/>
)}
{isMaximized && (
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
icon={<PiCornersInBold />}
aria-label="Maximize Panel"
tooltip="Maximize Panel"
onClick={exitMaximized}
opacity={0.7}
/>
)}
{props.group.api.location.type !== 'floating' && (
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
icon={<PiArrowSquareOutBold />}
aria-label="Pop out Panel"
tooltip="Pop out Panel"
onClick={popOutToFloating}
opacity={0.7}
/>
)}
</Flex>
);
};
const components: IDockviewReactProps['components'] = {
main: MainPanelContent,
boards: BoardsListPanelContent,
gallery: Gallery,
layers: () => (
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
),
queueControls: QueueControls,
prompts: Prompts,
imageSettings: ImageSettingsAccordion,
generationSettings: GenerationSettingsAccordion,
compositingSettings: CompositingSettingsAccordion,
advancedSettings: AdvancedSettingsAccordion,
refinerSettings: RefinerSettingsAccordion,
viewer: () => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
),
progress: () => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
),
};
const theme: DockviewTheme = {
className: 'dockview-theme-invoke',
name: 'Invoke',
};
import { memo } from 'react';
export const $panels = atom<{ api: DockviewApi; resetLayout: () => void } | null>(null);
const canvasLayout = (api: DockviewApi) => {
api.clear();
const mainPanel = api.addPanel({
id: 'main',
component: 'main',
title: 'Workspace',
minimumWidth: 200,
});
api.addPanel({
id: 'viewer',
component: 'viewer',
title: 'Viewer',
position: {
direction: 'within',
referencePanel: mainPanel,
},
});
api.addPanel({
id: 'progress',
component: 'progress',
title: 'Progress',
position: {
direction: 'within',
referencePanel: mainPanel,
},
});
const queueControls = api.addPanel({
id: 'queue-controls',
title: 'Queue',
component: 'queueControls',
// floating: true,
// initialHeight: 48 + 24,
initialHeight: 48,
maximumHeight: 48,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
initialWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: mainPanel,
},
});
const promptsPanel = api.addPanel({
id: 'prompts',
title: 'Prompts',
component: 'prompts',
position: {
direction: 'below',
referencePanel: queueControls,
},
});
const imagePanel = api.addPanel({
id: 'imageSettings',
title: 'Image',
component: 'imageSettings',
position: {
direction: 'below',
referencePanel: promptsPanel,
},
});
api.addPanel({
id: 'generationSettings',
title: 'Generation',
component: 'generationSettings',
position: {
direction: 'within',
referencePanel: imagePanel,
},
});
const compPanel = api.addPanel({
id: 'compositingSettings',
title: 'Compositing',
component: 'compositingSettings',
position: {
direction: 'below',
referencePanel: imagePanel,
},
});
const advancedPanel = api.addPanel({
id: 'advancedSettings',
title: 'Advanced',
component: 'advancedSettings',
position: {
direction: 'within',
referencePanel: compPanel,
},
});
api.addPanel({
id: 'refinerSettings',
title: 'Refiner',
component: 'refinerSettings',
position: {
direction: 'within',
referencePanel: advancedPanel,
},
});
const boardsPanel = api.addPanel({
id: 'boards',
component: 'boards',
title: 'Boards',
initialWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: mainPanel,
},
});
const galleryPanel = api.addPanel({
id: 'gallery',
component: 'gallery',
title: 'Gallery',
position: {
direction: 'below',
referencePanel: boardsPanel,
},
});
api.addPanel({
id: 'layers',
component: 'layers',
title: 'Layers',
position: {
direction: 'below',
referencePanel: galleryPanel,
},
});
mainPanel.api.setActive();
};
const galleryLayout = (api: DockviewApi) => {
api.clear();
const viewer = api.addPanel({
id: 'gallery-viewer',
title: 'Viewer',
component: 'viewer',
});
api.addPanel({
id: 'gallery-progress',
title: 'Progress',
component: 'progress',
position: {
direction: 'within',
referencePanel: viewer,
},
});
const gallery = api.addPanel({
id: 'gallery-gallery',
title: 'Gallery',
component: 'gallery',
initialWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: viewer,
},
});
api.addPanel({
id: 'gallery-boards',
title: 'Boards',
component: 'boards',
position: {
direction: 'above',
referencePanel: gallery,
},
});
viewer.api.setActive();
export const $advancedLayout = atom<boolean>(false);
export const toggleAdvancedLayout = () => {
$advancedLayout.set(!$advancedLayout.get());
};
export const AppContent = memo(() => {
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const tab = useAppSelector(selectActiveTab);
useDndMonitor();
const tab = useAppSelector(selectActiveTab);
const advancedLayout = useStore($advancedLayout);
// useRegisteredHotkeys({
// id: 'toggleLeftPanel',
// category: 'app',
// callback: leftPanel.toggle,
// options: { enabled: withLeftPanel },
// dependencies: [leftPanel.toggle, withLeftPanel],
// });
const withLeftPanel = useAppSelector(selectWithLeftPanel);
const leftPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'left-panel',
minSizePx: LEFT_PANEL_MIN_SIZE_PX,
defaultSizePx: LEFT_PANEL_MIN_SIZE_PX,
imperativePanelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onLeftPanelCollapse,
}),
[]
);
const leftPanel = usePanel(leftPanelUsePanelOptions);
useRegisteredHotkeys({
id: 'toggleLeftPanel',
category: 'app',
callback: leftPanel.toggle,
options: { enabled: withLeftPanel },
dependencies: [leftPanel.toggle, withLeftPanel],
});
// useRegisteredHotkeys({
// id: 'toggleRightPanel',
// category: 'app',
// callback: rightPanel.toggle,
// options: { enabled: withRightPanel },
// dependencies: [rightPanel.toggle, withRightPanel],
// });
const withRightPanel = useAppSelector(selectWithRightPanel);
const rightPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'right-panel',
minSizePx: RIGHT_PANEL_MIN_SIZE_PX,
defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX,
imperativePanelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onRightPanelCollapse,
}),
[]
);
const rightPanel = usePanel(rightPanelUsePanelOptions);
useRegisteredHotkeys({
id: 'toggleRightPanel',
category: 'app',
callback: rightPanel.toggle,
options: { enabled: withRightPanel },
dependencies: [rightPanel.toggle, withRightPanel],
});
useRegisteredHotkeys({
id: 'resetPanelLayout',
category: 'app',
callback: () => {
leftPanel.reset();
rightPanel.reset();
},
dependencies: [leftPanel.reset, rightPanel.reset],
});
useRegisteredHotkeys({
id: 'togglePanels',
category: 'app',
callback: () => {
if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
leftPanel.expand();
rightPanel.expand();
} else {
leftPanel.collapse();
rightPanel.collapse();
}
},
dependencies: [
leftPanel.isCollapsed,
rightPanel.isCollapsed,
leftPanel.expand,
rightPanel.expand,
leftPanel.collapse,
rightPanel.collapse,
],
});
const onReady = useCallback<IDockviewReactProps['onReady']>((event) => {
const _resetToDefaults = () => canvasLayout(event.api);
$panels.set({ api: event.api, resetLayout: _resetToDefaults });
_resetToDefaults();
}, []);
useEffect(() => {
const panels = $panels.get();
if (!panels) {
return;
}
if (tab === 'gallery') {
galleryLayout(panels.api);
} else {
canvasLayout(panels.api);
}
}, [tab]);
// useRegisteredHotkeys({
// id: 'resetPanelLayout',
// category: 'app',
// callback: () => {
// leftPanel.reset();
// rightPanel.reset();
// },
// dependencies: [leftPanel.reset, rightPanel.reset],
// });
// useRegisteredHotkeys({
// id: 'togglePanels',
// category: 'app',
// callback: () => {
// if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
// leftPanel.expand();
// rightPanel.expand();
// } else {
// leftPanel.collapse();
// rightPanel.collapse();
// }
// },
// dependencies: [
// leftPanel.isCollapsed,
// rightPanel.isCollapsed,
// leftPanel.expand,
// rightPanel.expand,
// leftPanel.collapse,
// rightPanel.collapse,
// ],
// });
return (
<Flex id="invoke-app-tabs" w="full" h="full" overflow="hidden">
<Flex id="invoke-app-tabs" w="full" h="full" overflow="hidden" position="relative">
<VerticalNavBar />
<DockviewReact
{/* <DockviewReact
components={components}
onReady={onReady}
theme={theme}
defaultTabComponent={MyCustomTab}
rightHeaderActionsComponent={RightHeaderActions}
/>
/> */}
<GridviewWrapper />
{/* <PanelGroup
ref={imperativePanelGroupRef}
id="app-panel-group"
@@ -479,41 +114,3 @@ export const AppContent = memo(() => {
);
});
AppContent.displayName = 'AppContent';
const GalleryTab = () => {
const onReady = useCallback<IDockviewReactProps['onReady']>((event) => {
const viewer = event.api.addPanel({
id: 'gallery-viewer',
title: 'Viewer',
component: 'viewer',
});
const gallery = event.api.addPanel({
id: 'gallery-gallery',
title: 'Gallery',
component: 'gallery',
initialWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: viewer,
},
});
event.api.addPanel({
id: 'gallery-boards',
title: 'Boards',
component: 'boards',
position: {
direction: 'above',
referencePanel: gallery,
},
});
}, []);
return (
<DockviewReact
onReady={onReady}
components={components}
theme={theme}
defaultTabComponent={MyCustomTab}
rightHeaderActionsComponent={RightHeaderActions}
/>
);
};

View File

@@ -0,0 +1,373 @@
import 'dockview/dist/styles/dockview.css';
import './dockview_theme_invoke.css';
import { Divider, Flex, IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import {
type DockviewApi,
DockviewDefaultTab,
DockviewReact,
type DockviewTheme,
type IDockviewHeaderActionsProps,
type IDockviewPanelHeaderProps,
type IDockviewReactProps,
} from 'dockview';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ProgressImage from 'features/gallery/components/ImageViewer/ProgressImage';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { MainPanelContent } from 'features/ui/components/MainPanelContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { atom } from 'nanostores';
import { memo, useCallback, useEffect, useState } from 'react';
import { PiArrowSquareOutBold, PiCornersInBold, PiCornersOutBold } from 'react-icons/pi';
const MyCustomTab = (props: IDockviewPanelHeaderProps) => {
const onDragEnter = useCallback(() => {
if (!props.api.isActive) {
props.api.setActive();
}
}, [props.api]);
return <DockviewDefaultTab hideClose {...props} onDragEnter={onDragEnter} />;
};
const RightHeaderActions = (props: IDockviewHeaderActionsProps) => {
const [isMaximized, setIsMaximized] = useState(false);
const popOutToFloating = useCallback(() => {
props.containerApi.addFloatingGroup(props.group);
}, [props.containerApi, props.group]);
const maximize = useCallback(() => {
if (!props.group.activePanel) {
props.group.panels.at(0)?.api.setActive();
}
if (!props.group.activePanel) {
return;
}
props.containerApi.maximizeGroup(props.group.activePanel);
}, [props.containerApi, props.group]);
const exitMaximized = useCallback(() => {
if (!props.group.activePanel) {
props.group.panels.at(0)?.api.setActive();
}
if (!props.group.activePanel) {
return;
}
props.containerApi.exitMaximizedGroup();
}, [props.containerApi, props.group]);
useEffect(() => {
const subscription = props.containerApi.onDidMaximizedGroupChange((e) => {
if (e.group.id === props.group.id) {
setIsMaximized(e.isMaximized);
}
});
return () => {
subscription.dispose();
};
}, [props.containerApi, props.group.id]);
return (
<Flex h="full" alignItems="center" pe={1}>
{!isMaximized && (
<IconButton
size="xs"
variant="link"
alignSelf="stretch"
icon={<PiCornersOutBold />}
aria-label="Maximize Panel"
tooltip="Maximize Panel"
onClick={maximize}
opacity={0.7}
/>
)}
{isMaximized && (
<IconButton
size="xs"
variant="link"
alignSelf="stretch"
icon={<PiCornersInBold />}
aria-label="Maximize Panel"
tooltip="Maximize Panel"
onClick={exitMaximized}
opacity={0.7}
/>
)}
{props.group.api.location.type !== 'floating' && (
<IconButton
size="xs"
variant="link"
alignSelf="stretch"
icon={<PiArrowSquareOutBold />}
aria-label="Pop out Panel"
tooltip="Pop out Panel"
onClick={popOutToFloating}
opacity={0.7}
/>
)}
</Flex>
);
};
const LayersPanelContent = memo(() => (
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
));
LayersPanelContent.displayName = 'LayersPanelContent';
const ViewerPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
));
ViewerPanelContent.displayName = 'ViewerPanelContent';
const ProgressPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
));
ProgressPanelContent.displayName = 'ProgressPanelContent';
const components: IDockviewReactProps['components'] = {
main: MainPanelContent,
boards: BoardsListPanelContent,
gallery: Gallery,
layers: LayersPanelContent,
queueControls: QueueControls,
prompts: Prompts,
imageSettings: ImageSettingsAccordion,
generationSettings: GenerationSettingsAccordion,
compositingSettings: CompositingSettingsAccordion,
advancedSettings: AdvancedSettingsAccordion,
refinerSettings: RefinerSettingsAccordion,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
const theme: DockviewTheme = {
className: 'dockview-theme-invoke',
name: 'Invoke',
};
export const $panels = atom<{ api: DockviewApi; resetLayout: () => void } | null>(null);
const canvasLayout = (api: DockviewApi) => {
api.clear();
const mainPanel = api.addPanel({
id: 'main',
component: 'main',
title: 'Workspace',
minimumWidth: 200,
});
api.addPanel({
id: 'viewer',
component: 'viewer',
title: 'Viewer',
position: {
direction: 'within',
referencePanel: mainPanel,
},
});
api.addPanel({
id: 'progress',
component: 'progress',
title: 'Progress',
position: {
direction: 'within',
referencePanel: mainPanel,
},
});
const queueControls = api.addPanel({
id: 'queue-controls',
title: 'Queue',
component: 'queueControls',
// floating: true,
// initialHeight: 48 + 24,
initialHeight: 48,
maximumHeight: 48,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
initialWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: mainPanel,
},
});
const promptsPanel = api.addPanel({
id: 'prompts',
title: 'Prompts',
component: 'prompts',
position: {
direction: 'below',
referencePanel: queueControls,
},
});
const imagePanel = api.addPanel({
id: 'imageSettings',
title: 'Image',
component: 'imageSettings',
position: {
direction: 'below',
referencePanel: promptsPanel,
},
});
api.addPanel({
id: 'generationSettings',
title: 'Generation',
component: 'generationSettings',
position: {
direction: 'within',
referencePanel: imagePanel,
},
});
const compPanel = api.addPanel({
id: 'compositingSettings',
title: 'Compositing',
component: 'compositingSettings',
position: {
direction: 'below',
referencePanel: imagePanel,
},
});
const advancedPanel = api.addPanel({
id: 'advancedSettings',
title: 'Advanced',
component: 'advancedSettings',
position: {
direction: 'within',
referencePanel: compPanel,
},
});
api.addPanel({
id: 'refinerSettings',
title: 'Refiner',
component: 'refinerSettings',
position: {
direction: 'within',
referencePanel: advancedPanel,
},
});
const boardsPanel = api.addPanel({
id: 'boards',
component: 'boards',
title: 'Boards',
initialWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: mainPanel,
},
});
const galleryPanel = api.addPanel({
id: 'gallery',
component: 'gallery',
title: 'Gallery',
position: {
direction: 'below',
referencePanel: boardsPanel,
},
});
api.addPanel({
id: 'layers',
component: 'layers',
title: 'Layers',
position: {
direction: 'below',
referencePanel: galleryPanel,
},
});
mainPanel.api.setActive();
};
const galleryLayout = (api: DockviewApi) => {
api.clear();
const viewer = api.addPanel({
id: 'gallery-viewer',
title: 'Viewer',
component: 'viewer',
});
api.addPanel({
id: 'gallery-progress',
title: 'Progress',
component: 'progress',
position: {
direction: 'within',
referencePanel: viewer,
},
});
const gallery = api.addPanel({
id: 'gallery-gallery',
title: 'Gallery',
component: 'gallery',
initialWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: viewer,
},
});
api.addPanel({
id: 'gallery-boards',
title: 'Boards',
component: 'boards',
position: {
direction: 'above',
referencePanel: gallery,
},
});
viewer.api.setActive();
};
export const DockviewWrapper = memo(() => {
const tab = useAppSelector(selectActiveTab);
const onTabChange = useCallback((tab: TabName, api: DockviewApi) => {
if (tab === 'gallery') {
galleryLayout(api);
} else {
canvasLayout(api);
}
}, []);
const onReady = useCallback<IDockviewReactProps['onReady']>(
(event) => {
const _resetToDefaults = () => canvasLayout(event.api);
$panels.set({ api: event.api, resetLayout: _resetToDefaults });
onTabChange(tab, event.api);
},
[onTabChange, tab]
);
useEffect(() => {
const panels = $panels.get();
if (!panels) {
return;
}
onTabChange(tab, panels.api);
}, [onTabChange, tab]);
return (
<DockviewReact
components={components}
onReady={onReady}
theme={theme}
defaultTabComponent={MyCustomTab}
rightHeaderActionsComponent={RightHeaderActions}
/>
);
});
DockviewWrapper.displayName = 'DockviewWrapper';

View File

@@ -0,0 +1,242 @@
import 'dockview/dist/styles/dockview.css';
import './dockview_theme_invoke.css';
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IGridviewReactProps } from 'dockview';
import { GridviewReact, LayoutPriority, Orientation } from 'dockview';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ProgressImage from 'features/gallery/components/ImageViewer/ProgressImage';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { MainPanelContent } from 'features/ui/components/MainPanelContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { atom } from 'nanostores';
import { memo, useCallback, useEffect } from 'react';
const LayersPanelContent = memo(() => (
<CanvasManagerProviderGate>
<CanvasLayersPanelContent />
</CanvasManagerProviderGate>
));
LayersPanelContent.displayName = 'LayersPanelContent';
const ViewerPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
));
ViewerPanelContent.displayName = 'ViewerPanelContent';
const ProgressPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
));
ProgressPanelContent.displayName = 'ProgressPanelContent';
const components: IGridviewReactProps['components'] = {
main: MainPanelContent,
boards: BoardsListPanelContent,
gallery: Gallery,
layers: LayersPanelContent,
queueControls: QueueControls,
prompts: Prompts,
imageSettings: ImageSettingsAccordion,
generationSettings: GenerationSettingsAccordion,
compositingSettings: CompositingSettingsAccordion,
advancedSettings: AdvancedSettingsAccordion,
refinerSettings: RefinerSettingsAccordion,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
};
export const $panels = atom<{ api: GridviewApi; resetLayout: () => void } | null>(null);
const canvasLayout = (api: GridviewApi) => {
api.clear();
const mainPanel = api.addPanel({
id: 'main',
component: 'main',
minimumWidth: 200,
priority: LayoutPriority.High,
});
// api.addPanel({
// id: 'viewer',
// component: 'viewer',
// position: {
// direction: 'within',
// referencePanel: mainPanel.id,
// },
// });
// api.addPanel({
// id: 'progress',
// component: 'progress',
// position: {
// direction: 'within',
// referencePanel: mainPanel.id,
// },
// });
const queueControls = api.addPanel({
id: 'queue-controls',
component: 'queueControls',
// floating: true,
// initialHeight: 48 + 24,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: mainPanel.id,
},
priority: LayoutPriority.Low,
snap: true,
});
const promptsPanel = api.addPanel({
id: 'prompts',
component: 'prompts',
position: {
direction: 'below',
referencePanel: queueControls.id,
},
});
const imagePanel = api.addPanel({
id: 'imageSettings',
component: 'imageSettings',
position: {
direction: 'below',
referencePanel: promptsPanel.id,
},
});
const genPanel = api.addPanel({
id: 'generationSettings',
component: 'generationSettings',
position: {
direction: 'below',
referencePanel: imagePanel.id,
},
});
const compPanel = api.addPanel({
id: 'compositingSettings',
component: 'compositingSettings',
position: {
direction: 'below',
referencePanel: genPanel.id,
},
});
const advancedPanel = api.addPanel({
id: 'advancedSettings',
component: 'advancedSettings',
position: {
direction: 'below',
referencePanel: compPanel.id,
},
});
api.addPanel({
id: 'refinerSettings',
component: 'refinerSettings',
position: {
direction: 'below',
referencePanel: advancedPanel.id,
},
});
const galleryPanel = api.addPanel({
id: 'gallery',
component: 'gallery',
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: mainPanel.id,
},
snap: true,
});
const boardsPanel = api.addPanel({
id: 'boards',
component: 'boards',
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'above',
referencePanel: galleryPanel.id,
},
priority: LayoutPriority.Low,
});
mainPanel.api.setActive();
galleryPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
boardsPanel.api.setSize({ height: 200 });
queueControls.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
};
const galleryLayout = (api: GridviewApi) => {
api.clear();
const viewer = api.addPanel({
id: 'gallery-viewer',
component: 'viewer',
});
const gallery = api.addPanel({
id: 'gallery-gallery',
component: 'gallery',
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
position: {
direction: 'right',
referencePanel: viewer.id,
},
});
api.addPanel({
id: 'gallery-boards',
component: 'boards',
position: {
direction: 'above',
referencePanel: gallery.id,
},
});
viewer.api.setActive();
};
export const GridviewWrapper = memo(() => {
const tab = useAppSelector(selectActiveTab);
const onTabChange = useCallback((tab: TabName, api: GridviewApi) => {
if (tab === 'gallery') {
galleryLayout(api);
} else {
canvasLayout(api);
}
}, []);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
const _resetToDefaults = () => canvasLayout(event.api);
$panels.set({ api: event.api, resetLayout: _resetToDefaults });
_resetToDefaults();
}, []);
useEffect(() => {
const panels = $panels.get();
if (!panels) {
return;
}
onTabChange(tab, panels.api);
}, [onTabChange, tab]);
return (
<GridviewReact
className="dockview-theme-invoke"
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
);
});
GridviewWrapper.displayName = 'GridviewWrapper';