feat(ui): get layouts working

This commit is contained in:
psychedelicious
2025-06-19 23:49:37 +10:00
parent fcaeba290e
commit e7e1142c77
22 changed files with 641 additions and 352 deletions

View File

@@ -14,7 +14,7 @@ export const CanvasLayersPanel = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<Flex flexDir="column" gap={2} w="full" h="full" p={2}>
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />

View File

@@ -11,7 +11,7 @@ export const CanvasLaunchpadPanel = memo(() => {
return (
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
<Flex flexDir="column" w="full" h="full" gap={4} px={12} maxW={768} pt="20%">
<Heading mb={4}>Get started with Invoke.</Heading>
<Heading mb={4}>Edit and refine on Canvas.</Heading>
<Flex flexDir="column" gap={8}>
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
<InitialStateMainModelPicker />

View File

@@ -16,7 +16,7 @@ export const GenerateLaunchpadPanel = memo(() => {
return (
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={12} maxW={768}>
<Heading mb={4}>Get started with Invoke.</Heading>
<Heading mb={4}>Generate images from text prompts.</Heading>
<Flex flexDir="column" gap={8}>
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
<InitialStateMainModelPicker />

View File

@@ -0,0 +1,13 @@
import { Flex, Heading } from '@invoke-ai/ui-library';
import { memo } from 'react';
export const UpscalingLaunchpadPanel = memo(() => {
return (
<Flex flexDir="column" h="full" w="full" alignItems="center" justifyContent="center" gap={2}>
<Flex flexDir="column" w="full" h="full" justifyContent="center" gap={4} px={12} maxW={768}>
<Heading mb={4}>Upscale and add detail.</Heading>
</Flex>
</Flex>
);
});
UpscalingLaunchpadPanel.displayName = 'UpscalingLaunchpadPanel';

View File

@@ -1,4 +1,4 @@
import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
import { Divider, Flex } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
@@ -29,10 +29,6 @@ export const CanvasToolbar = memo(() => {
return (
<Flex w="full" gap={2} alignItems="center" px={2}>
<Heading size="sm" me={2}>
Canvas
</Heading>
<Divider orientation="vertical" />
<ToolColorPicker />
<ToolSettings />
<Flex alignItems="center" h="full" flexGrow={1} justifyContent="flex-end">
@@ -48,7 +44,6 @@ export const CanvasToolbar = memo(() => {
<CanvasToolbarNewSessionMenuButton />
<CanvasSettingsPopover />
</Flex>
<Divider orientation="vertical" />
</Flex>
);
});

View File

@@ -2,6 +2,8 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai';
import type { TabName } from 'features/ui/store/uiTypes';
import { ALL_TABS } from 'features/ui/store/uiTypes';
import { merge } from 'lodash-es';
const baseDimensionConfig: NumericalParameterConfig = {
@@ -225,3 +227,12 @@ export const selectIsClientSideUploadEnabled = createConfigSelector((config) =>
export const selectAllowPublishWorkflows = createConfigSelector((config) => config.allowPublishWorkflows);
export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);
export const selectShouldShowCredits = createConfigSelector((config) => config.shouldShowCredits);
export const selectEnabledTabs = createConfigSelector((config) => {
const enabledTabs: TabName[] = [];
for (const tab of ALL_TABS) {
if (!config.disabledTabs.includes(tab)) {
enabledTabs.push(tab);
}
}
return enabledTabs;
});

View File

@@ -1,10 +1,14 @@
import 'dockview/dist/styles/dockview.css';
import 'features/ui/styles/dockview-theme-invoke.css';
import { Flex } from '@invoke-ai/ui-library';
import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import { AutoLayout } from 'features/ui/layouts/AutoLayout';
import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout';
import { selectActiveTabIndex } from 'features/ui/store/uiSelectors';
import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo } from 'react';
@@ -16,6 +20,7 @@ const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!is
export const AppContent = memo(() => {
// const tab = useAppSelector(selectActiveTab);
const tabIndex = useAppSelector(selectActiveTabIndex);
// const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
useDndMonitor();
@@ -93,10 +98,22 @@ export const AppContent = memo(() => {
// });
return (
<Flex id="invoke-app-tabs" w="full" h="full" p={0} overflow="hidden">
<VerticalNavBar />
<AutoLayout />
</Flex>
<Tabs index={tabIndex} display="flex" w="full" h="full" p={0} overflow="hidden">
<TabList>
<VerticalNavBar />
</TabList>
<TabPanels w="full" h="full" p={0}>
<TabPanel w="full" h="full" p={0}>
<GenerateTabAutoLayout />
</TabPanel>
<TabPanel w="full" h="full" p={0}>
<CanvasTabAutoLayout />
</TabPanel>
<TabPanel w="full" h="full" p={0}>
<UpscalingTabAutoLayout />
</TabPanel>
</TabPanels>
</Tabs>
);
});
AppContent.displayName = 'AppContent';

View File

@@ -22,7 +22,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
const ParametersPanelTextToImage = () => {
export const ParametersPanelCanvas = memo(() => {
const isSDXL = useAppSelector(selectIsSDXL);
const isCogview4 = useAppSelector(selectIsCogView4);
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
@@ -55,6 +55,6 @@ const ParametersPanelTextToImage = () => {
</Flex>
</Flex>
);
};
});
export default memo(ParametersPanelTextToImage);
ParametersPanelCanvas.displayName = 'ParametersPanelCanvas';

View File

@@ -0,0 +1,58 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
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 { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { $isStylePresetsMenuOpen } from 'features/stylePresets/store/stylePresetSlice';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};
export const ParametersPanelGenerate = memo(() => {
const isSDXL = useAppSelector(selectIsSDXL);
const isCogview4 = useAppSelector(selectIsCogView4);
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
const isApiModel = useIsApiModel();
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{isStylePresetsMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
{!isCogview4 && !isApiModel && <AdvancedSettingsAccordion />}
</Flex>
</OverlayScrollbarsComponent>
</Box>
</Flex>
</Flex>
);
});
ParametersPanelGenerate.displayName = 'ParametersPanelGenerate';

View File

@@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};
const ParametersPanelUpscale = () => {
export const ParametersPanelUpscale = memo(() => {
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
return (
@@ -44,6 +44,6 @@ const ParametersPanelUpscale = () => {
</Flex>
</Flex>
);
};
});
export default memo(ParametersPanelUpscale);
ParametersPanelUpscale.displayName = 'ParametersPanelUpscale';

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -26,13 +26,14 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
<Tooltip label={label} placement="end">
<IconButton
as={Tab}
p={0}
ref={ref}
onClick={selectTab}
icon={icon}
size="md"
fontSize="24px"
variant="appTab"
variant="link"
data-selected={activeTabName === tab}
aria-label={label}
data-testid={label}

View File

@@ -0,0 +1,16 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import QueueControls from 'features/queue/components/QueueControls';
import { ParametersPanelCanvas } from 'features/ui/components/ParametersPanels/ParametersPanelCanvas';
import { memo } from 'react';
export const CanvasTabLeftPanel = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelCanvas />
</Box>
</Flex>
);
});
CanvasTabLeftPanel.displayName = 'CanvasTabLeftPanel';

View File

@@ -66,6 +66,7 @@ export const CanvasWorkspacePanel = memo(() => {
alignItems="center"
justifyContent="center"
overflow="hidden"
p={2}
>
<CanvasManagerProviderGate>
<CanvasToolbar />

View File

@@ -0,0 +1,16 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import QueueControls from 'features/queue/components/QueueControls';
import { ParametersPanelGenerate } from 'features/ui/components/ParametersPanels/ParametersPanelGenerate';
import { memo } from 'react';
export const GenerateTabLeftPanel = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelGenerate />
</Box>
</Flex>
);
});
GenerateTabLeftPanel.displayName = 'GenerateTabLeftPanel';

View File

@@ -0,0 +1,16 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import QueueControls from 'features/queue/components/QueueControls';
import { ParametersPanelUpscale } from 'features/ui/components/ParametersPanels/ParametersPanelUpscale';
import { memo } from 'react';
export const UpscalingTabLeftPanel = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelUpscale />
</Box>
</Flex>
);
});
UpscalingTabLeftPanel.displayName = 'UpscalingTabLeftPanel';

View File

@@ -1,225 +1,71 @@
import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
import { useAppSelector } from 'app/store/storeHooks';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { components } from 'features/ui/layouts/components';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useState } from 'react';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { atom } from 'nanostores';
import { memo, useCallback, useRef, useState } from 'react';
const MenuContent = memo(() => {
return (
<CanvasManagerProviderGate>
<MenuList>
<CanvasContextMenuSelectedEntityMenuItems />
<CanvasContextMenuGlobalMenuItems />
</MenuList>
</CanvasManagerProviderGate>
);
});
MenuContent.displayName = 'MenuContent';
import { CanvasTabLeftPanel } from './CanvasTabLeftPanel';
import { CanvasWorkspacePanel } from './CanvasWorkspacePanel';
import { useOnFirstVisible } from './use-on-first-visible';
const canvasBgSx = {
position: 'relative',
w: 'full',
h: 'full',
borderRadius: 'base',
overflow: 'hidden',
bg: 'base.900',
'&[data-dynamic-grid="true"]': {
bg: 'base.850',
},
};
export const CanvasWorkspacePanel = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
const canvasId = useAppSelector(selectCanvasSessionId);
const renderMenu = useCallback(() => {
return <MenuContent />;
}, []);
return (
<Flex
tabIndex={-1}
borderRadius="base"
position="relative"
flexDirection="column"
height="full"
width="full"
gap={2}
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<CanvasManagerProviderGate>
<CanvasToolbar />
</CanvasManagerProviderGate>
<Divider />
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
{(ref) => (
<Flex ref={ref} sx={canvasBgSx} data-dynamic-grid={dynamicGrid}>
<InvokeCanvasComponent />
<CanvasManagerProviderGate>
<Flex
position="absolute"
flexDir="column"
top={1}
insetInlineStart={1}
pointerEvents="none"
gap={2}
alignItems="flex-start"
>
{showHUD && <CanvasHUD />}
<CanvasAlertsSelectedEntityStatus />
<CanvasAlertsPreserveMask />
<CanvasAlertsInvocationProgress />
</Flex>
<Flex position="absolute" top={1} insetInlineEnd={1}>
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
<MenuContent />
</Menu>
</Flex>
</CanvasManagerProviderGate>
</Flex>
)}
</ContextMenu>
{canvasId !== null && (
<CanvasManagerProviderGate>
<CanvasSessionContextProvider type="advanced" id={canvasId}>
<Flex
position="absolute"
flexDir="column"
bottom={4}
gap={2}
align="center"
justify="center"
left={4}
right={4}
>
<Flex position="relative" maxW="full" w="full" h={108}>
<StagingAreaItemsList />
</Flex>
<Flex gap={2}>
<StagingAreaToolbar />
</Flex>
</Flex>
</CanvasSessionContextProvider>
</CanvasManagerProviderGate>
)}
<Flex position="absolute" bottom={4}>
<CanvasManagerProviderGate>
<Filter />
<Transform />
<SelectObject />
</CanvasManagerProviderGate>
</Flex>
<CanvasManagerProviderGate>
<CanvasDropArea />
</CanvasManagerProviderGate>
</Flex>
);
});
CanvasWorkspacePanel.displayName = 'CanvasPanel';
const LayersPanelContent = memo(() => (
<CanvasManagerProviderGate>
<CanvasLayersPanel />
</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 LAUNCHPAD_PANEL_ID = 'launchpad';
const WORKSPACE_PANEL_ID = 'workspace';
const VIEWER_PANEL_ID = 'viewer';
const PROGRESS_PANEL_ID = 'progress';
const mainPanelComponents: IDockviewReactProps['components'] = {
canvasLaunchpad: GenerateLaunchpadPanel,
canvas: CanvasWorkspacePanel,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
[LAUNCHPAD_PANEL_ID]: CanvasLaunchpadPanel,
[WORKSPACE_PANEL_ID]: CanvasWorkspacePanel,
[VIEWER_PANEL_ID]: ImageViewerPanel,
[PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: 'canvasLaunchpad',
component: 'canvasLaunchpad',
title: 'canvasLaunchpad',
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
});
api.addPanel({
id: 'canvas',
component: 'canvas',
id: WORKSPACE_PANEL_ID,
component: WORKSPACE_PANEL_ID,
title: 'Canvas',
position: {
direction: 'within',
referencePanel: 'canvasLaunchpad',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: 'viewer',
component: 'viewer',
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: 'canvasLaunchpad',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: 'progress',
component: 'progress',
id: PROGRESS_PANEL_ID,
component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: 'canvasLaunchpad',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
@@ -238,102 +84,122 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
<Flex w="full" h="full">
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={components}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
</Flex>
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={mainPanelComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
);
});
MainPanel.displayName = 'MainPanel';
const Left = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelTextToImage />
</Box>
</Flex>
);
});
Left.displayName = 'Left';
const Null = () => null;
const LEFT_PANEL_ID = 'left';
const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
const LAYERS_PANEL_ID = 'layers';
export const canvasTabComponents: IGridviewReactProps['components'] = {
left: Left,
main: MainPanel,
boards: BoardsPanel,
gallery: GalleryPanel,
layers: LayersPanelContent,
[LEFT_PANEL_ID]: CanvasTabLeftPanel,
[MAIN_PANEL_ID]: MainPanel,
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
[LAYERS_PANEL_ID]: CanvasLayersPanel,
};
export const initializeCanvasTabLayout = (api: GridviewApi) => {
const main = api.addPanel({
id: 'main',
component: 'main',
minimumWidth: 256,
export const initializeLayout = (api: GridviewApi) => {
api.addPanel({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
});
const left = api.addPanel({
id: 'left',
component: 'left',
api.addPanel({
id: LEFT_PANEL_ID,
component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: 'main',
referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
id: 'gallery',
component: 'gallery',
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: 'main',
referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
id: 'layers',
component: 'layers',
id: LAYERS_PANEL_ID,
component: LAYERS_PANEL_ID,
minimumHeight: 256,
position: {
direction: 'below',
referencePanel: 'gallery',
referencePanel: GALLERY_PANEL_ID,
},
});
const boards = api.addPanel({
id: 'boards',
component: 'boards',
api.addPanel({
id: BOARDS_PANEL_ID,
component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: 'gallery',
referencePanel: GALLERY_PANEL_ID,
},
});
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const CanvasTabAutoLayout = memo(() => {
const [api, setApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
$isLayoutLoading.set(true);
setApi(event.api);
initializeCanvasTabLayout(event.api);
$isLayoutLoading.set(false);
}, []);
const ref = useRef<HTMLDivElement>(null);
const $api = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$api.set(event.api);
initializeLayout(event.api);
},
[$api]
);
const resizeMainPanelOnFirstVisible = useCallback(() => {
const api = $api.get();
if (!api) {
return;
}
const mainPanel = api.getPanel(MAIN_PANEL_ID);
if (!mainPanel) {
return;
}
if (mainPanel.width !== 0) {
return;
}
let count = 0;
const setSize = () => {
if (count++ > 50) {
return;
}
mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
if (mainPanel.width === 0) {
requestAnimationFrame(setSize);
return;
}
};
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
return (
<AutoLayoutProvider api={api}>
<AutoLayoutProvider $api={$api}>
<GridviewReact
ref={ref}
className="dockview-theme-invoke"
components={canvasTabComponents}
onReady={onReady}

View File

@@ -1,69 +1,58 @@
import { Box, Divider, Flex } from '@invoke-ai/ui-library';
import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading';
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
import QueueControls from 'features/queue/components/QueueControls';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { memo, useCallback, useState } from 'react';
import { atom } from 'nanostores';
import { memo, useCallback, useRef, useState } from 'react';
const ViewerPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2} gap={2}>
<ViewerToolbar />
<Divider />
<ImageViewer />
</Flex>
));
ViewerPanelContent.displayName = 'ViewerPanelContent';
import { GenerateTabLeftPanel } from './GenerateTabLeftPanel';
import { useOnFirstVisible } from './use-on-first-visible';
const ProgressPanelContent = memo(() => (
<Flex flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage />
</Flex>
));
ProgressPanelContent.displayName = 'ProgressPanelContent';
const LAUNCHPAD_PANEL_ID = 'launchpad';
const VIEWER_PANEL_ID = 'viewer';
const PROGRESS_PANEL_ID = 'progress';
const mainPanelComponents: IDockviewReactProps['components'] = {
welcome: GenerateLaunchpadPanel,
viewer: ViewerPanelContent,
progress: ProgressPanelContent,
[LAUNCHPAD_PANEL_ID]: GenerateLaunchpadPanel,
[VIEWER_PANEL_ID]: ImageViewerPanel,
[PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: 'welcome',
component: 'welcome',
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
});
api.addPanel({
id: 'viewer',
component: 'viewer',
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: 'welcome',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: 'progress',
component: 'progress',
id: PROGRESS_PANEL_ID,
component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: 'welcome',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
@@ -82,90 +71,111 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const MainPanel = memo(() => {
return (
<Flex w="full" h="full">
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={mainPanelComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
</Flex>
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={mainPanelComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
);
});
MainPanel.displayName = 'MainPanel';
export const GenerateLeftPanel = memo(() => {
return (
<Flex flexDir="column" w="full" h="full" gap={2} py={2} pe={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelTextToImage />
</Box>
</Flex>
);
});
GenerateLeftPanel.displayName = 'GenerateLeftPanel';
const LEFT_PANEL_ID = 'left';
const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
export const generateTabComponents: IGridviewReactProps['components'] = {
left: GenerateLeftPanel,
main: MainPanel,
boards: BoardsPanel,
gallery: GalleryPanel,
[LEFT_PANEL_ID]: GenerateTabLeftPanel,
[MAIN_PANEL_ID]: MainPanel,
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
export const initializeGenerateTabLayout = (api: GridviewApi) => {
const main = api.addPanel({
id: 'main',
component: 'main',
minimumWidth: 256,
export const initializeLayout = (api: GridviewApi) => {
api.addPanel({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
});
const left = api.addPanel({
id: 'left',
component: 'left',
api.addPanel({
id: LEFT_PANEL_ID,
component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: 'main',
referencePanel: MAIN_PANEL_ID,
},
});
api.addPanel({
id: 'gallery',
component: 'gallery',
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: 'main',
referencePanel: MAIN_PANEL_ID,
},
});
const boards = api.addPanel({
id: 'boards',
component: 'boards',
api.addPanel({
id: BOARDS_PANEL_ID,
component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: 'gallery',
referencePanel: GALLERY_PANEL_ID,
},
});
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const GenerateTabAutoLayout = memo(() => {
const [api, setApi] = useState<GridviewApi | null>(null);
const onReady = useCallback<IGridviewReactProps['onReady']>((event) => {
$isLayoutLoading.set(true);
setApi(event.api);
initializeGenerateTabLayout(event.api);
$isLayoutLoading.set(false);
}, []);
const ref = useRef<HTMLDivElement>(null);
const $api = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$api.set(event.api);
initializeLayout(event.api);
},
[$api]
);
const resizeMainPanelOnFirstVisible = useCallback(() => {
const api = $api.get();
if (!api) {
return;
}
const mainPanel = api.getPanel(MAIN_PANEL_ID);
if (!mainPanel) {
return;
}
if (mainPanel.width !== 0) {
return;
}
let count = 0;
const setSize = () => {
if (count++ > 50) {
return;
}
mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
if (mainPanel.width === 0) {
requestAnimationFrame(setSize);
return;
}
};
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
return (
<AutoLayoutProvider api={api}>
<AutoLayoutProvider $api={$api}>
<GridviewReact
ref={ref}
className="dockview-theme-invoke"
components={generateTabComponents}
onReady={onReady}

View File

@@ -0,0 +1,191 @@
import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
import { DockviewReact, GridviewReact, Orientation } from 'dockview';
import { UpscalingLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel';
import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent';
import { GalleryPanel } from 'features/gallery/components/Gallery';
import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel';
import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel';
import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
import { dockviewTheme } from 'features/ui/styles/theme';
import { atom } from 'nanostores';
import { memo, useCallback, useRef, useState } from 'react';
import { UpscalingTabLeftPanel } from './UpscalingTabLeftPanel';
import { useOnFirstVisible } from './use-on-first-visible';
const LAUNCHPAD_PANEL_ID = 'launchpad';
const VIEWER_PANEL_ID = 'viewer';
const PROGRESS_PANEL_ID = 'progress';
const dockviewComponents: IDockviewReactProps['components'] = {
[LAUNCHPAD_PANEL_ID]: UpscalingLaunchpadPanel,
[VIEWER_PANEL_ID]: ImageViewerPanel,
[PROGRESS_PANEL_ID]: GenerationProgressPanel,
};
const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
const { api } = event;
api.addPanel({
id: LAUNCHPAD_PANEL_ID,
component: LAUNCHPAD_PANEL_ID,
title: 'Launchpad',
});
api.addPanel({
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.addPanel({
id: PROGRESS_PANEL_ID,
component: PROGRESS_PANEL_ID,
title: 'Generation Progress',
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,
},
});
api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive();
const disposables = [
api.onWillShowOverlay((e) => {
if (e.kind === 'header_space' || e.kind === 'tab') {
return;
}
e.preventDefault();
}),
];
return () => {
disposables.forEach((disposable) => {
disposable.dispose();
});
};
};
const MainPanel = memo(() => {
return (
<DockviewReact
disableDnd={true}
locked={true}
disableFloatingGroups={true}
dndEdges={false}
defaultTabComponent={TabWithoutCloseButton}
components={dockviewComponents}
onReady={onReadyMainPanel}
theme={dockviewTheme}
/>
);
});
MainPanel.displayName = 'MainPanel';
const LEFT_PANEL_ID = 'left';
const MAIN_PANEL_ID = 'main';
const BOARDS_PANEL_ID = 'boards';
const GALLERY_PANEL_ID = 'gallery';
export const gridviewComponents: IGridviewReactProps['components'] = {
[LEFT_PANEL_ID]: UpscalingTabLeftPanel,
[MAIN_PANEL_ID]: MainPanel,
[BOARDS_PANEL_ID]: BoardsPanel,
[GALLERY_PANEL_ID]: GalleryPanel,
};
export const initializeLayout = (api: GridviewApi) => {
api.addPanel({
id: MAIN_PANEL_ID,
component: MAIN_PANEL_ID,
// priority: LayoutPriority.High,
});
api.addPanel({
id: LEFT_PANEL_ID,
component: LEFT_PANEL_ID,
minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
position: {
direction: 'left',
referencePanel: MAIN_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.addPanel({
id: GALLERY_PANEL_ID,
component: GALLERY_PANEL_ID,
minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
minimumHeight: 232,
position: {
direction: 'right',
referencePanel: MAIN_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.addPanel({
id: BOARDS_PANEL_ID,
component: BOARDS_PANEL_ID,
minimumHeight: 36,
position: {
direction: 'above',
referencePanel: GALLERY_PANEL_ID,
},
// priority: LayoutPriority.High,
});
api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
api.getPanel(MAIN_PANEL_ID)?.api.setActive();
};
export const UpscalingTabAutoLayout = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const $api = useState(() => atom<GridviewApi | null>(null))[0];
const onReady = useCallback<IGridviewReactProps['onReady']>(
(event) => {
$api.set(event.api);
initializeLayout(event.api);
},
[$api]
);
const resizeMainPanelOnFirstVisible = useCallback(() => {
const api = $api.get();
if (!api) {
return;
}
const mainPanel = api.getPanel(MAIN_PANEL_ID);
if (!mainPanel) {
return;
}
if (mainPanel.width !== 0) {
return;
}
let count = 0;
const setSize = () => {
if (count++ > 50) {
return;
}
mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER });
if (mainPanel.width === 0) {
requestAnimationFrame(setSize);
return;
}
};
setSize();
}, [$api]);
useOnFirstVisible(ref, resizeMainPanelOnFirstVisible);
return (
<AutoLayoutProvider $api={$api}>
<GridviewReact
ref={ref}
className="dockview-theme-invoke"
components={gridviewComponents}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</AutoLayoutProvider>
);
});
UpscalingTabAutoLayout.displayName = 'UpscalingTabAutoLayout';

View File

@@ -0,0 +1,25 @@
import { addAppListener } from 'app/store/middleware/listenerMiddleware';
import { useAppDispatch } from 'app/store/storeHooks';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { useEffect } from 'react';
export const useOnFirstVisitToTab = (tab: TabName, cb: () => void) => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(
addAppListener({
predicate: (action) => {
if (!setActiveTab.match(action)) {
return false;
}
return action.payload === tab;
},
effect: (_, api) => {
cb();
api.unsubscribe();
},
})
);
}, [cb, dispatch, tab]);
};

View File

@@ -0,0 +1,48 @@
import type { RefObject } from 'react';
import { useEffect } from 'react';
export const useOnFirstVisible = (elementRef: RefObject<HTMLElement>, callback: () => void): void => {
useEffect(() => {
const element = elementRef.current;
if (!element) {
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);
if (!targetParent) {
return;
}
const observerCallback = () => {
if (window.getComputedStyle(targetParent).display === 'none') {
return;
}
observer.disconnect();
callback();
};
const observer = new MutationObserver(observerCallback);
observer.observe(targetParent, {
attributes: true,
attributeFilter: ['hidden', 'style', 'class'],
});
observerCallback();
return () => {
observer.disconnect();
};
}, [elementRef, callback]);
};

View File

@@ -1,7 +1,11 @@
import { createSelector } from '@reduxjs/toolkit';
import { selectEnabledTabs } from 'features/system/store/configSlice';
import { selectUiSlice } from 'features/ui/store/uiSlice';
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
export const selectActiveTabIndex = createSelector(selectActiveTab, selectEnabledTabs, (activeTab, enabledTabs) => {
return enabledTabs.indexOf(activeTab);
});
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);

View File

@@ -2,6 +2,7 @@ import { deepClone } from 'common/util/deepClone';
import { z } from 'zod';
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
export const ALL_TABS = zTabName.options;
export type TabName = z.infer<typeof zTabName>;
const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
export type CanvasRightPanelTabName = z.infer<typeof zCanvasRightPanelTabName>;