feat(ui): reworked layout (wip)

This commit is contained in:
psychedelicious
2024-09-09 21:53:36 +10:00
parent b67c369bdb
commit 3ed29a16a8
52 changed files with 656 additions and 628 deletions

View File

@@ -1,11 +1,10 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
import { CanvasRightPanelContent } from 'features/controlLayers/components/CanvasRightPanel';
import { CanvasTabContent } from 'features/controlLayers/components/CanvasTabContent';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import QueueControls from 'features/queue/components/QueueControls';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
@@ -13,19 +12,23 @@ import FloatingParametersPanelButtons from 'features/ui/components/FloatingParam
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
import { TabMountGate } from 'features/ui/components/TabMountGate';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import NodesTab from 'features/ui/components/tabs/NodesTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsTabContent } from 'features/ui/components/tabs/WorkflowsTabContent';
import { TabVisibilityGate } from 'features/ui/components/TabVisibilityGate';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isGalleryPanelOpen,
$isParametersPanelOpen,
selectUiSlice,
TABS_WITH_GALLERY_PANEL,
TABS_WITH_OPTIONS_PANEL,
$isLeftPanelOpen,
$isRightPanelOpen,
LEFT_PANEL_MIN_SIZE_PCT,
LEFT_PANEL_MIN_SIZE_PX,
RIGHT_PANEL_MIN_SIZE_PCT,
RIGHT_PANEL_MIN_SIZE_PX,
selectWithLeftPanel,
selectWithRightPanel,
} from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
@@ -37,95 +40,75 @@ import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import ResizeHandle from './tabs/ResizeHandle';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%' };
const GALLERY_MIN_SIZE_PX = 310;
const GALLERY_MIN_SIZE_PCT = 20;
const OPTIONS_PANEL_MIN_SIZE_PX = 430;
const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
const onGalleryPanelCollapse = (isCollapsed: boolean) => $isGalleryPanelOpen.set(!isCollapsed);
const onParametersPanelCollapse = (isCollapsed: boolean) => $isParametersPanelOpen.set(!isCollapsed);
const selectShouldShowGalleryPanel = createSelector(selectUiSlice, (ui) =>
TABS_WITH_GALLERY_PANEL.includes(ui.activeTab)
);
const selectShouldShowOptionsPanel = createSelector(selectUiSlice, (ui) =>
TABS_WITH_OPTIONS_PANEL.includes(ui.activeTab)
);
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const isImageViewerOpen = useIsImageViewerOpen();
const shouldShowGalleryPanel = useAppSelector(selectShouldShowGalleryPanel);
const shouldShowOptionsPanel = useAppSelector(selectShouldShowOptionsPanel);
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('gallery', ref);
const optionsPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'options-panel',
unit: 'pixels',
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
defaultSize: OPTIONS_PANEL_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onParametersPanelCollapse,
}),
[]
);
const galleryPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'gallery-panel',
unit: 'pixels',
minSize: GALLERY_MIN_SIZE_PX,
defaultSize: GALLERY_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onGalleryPanelCollapse,
}),
[]
);
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const panelStorage = usePanelStorage();
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
const withLeftPanel = useAppSelector(selectWithLeftPanel);
const leftPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'left-panel',
unit: 'pixels',
minSize: LEFT_PANEL_MIN_SIZE_PX,
defaultSize: LEFT_PANEL_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onLeftPanelCollapse,
}),
[]
);
const leftPanel = usePanel(leftPanelUsePanelOptions);
useHotkeys(['t', 'o'], leftPanel.toggle, { enabled: withLeftPanel }, [leftPanel.toggle, withLeftPanel]);
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
const withRightPanel = useAppSelector(selectWithRightPanel);
const rightPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
id: 'right-panel',
unit: 'pixels',
minSize: RIGHT_PANEL_MIN_SIZE_PX,
defaultSize: RIGHT_PANEL_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
onCollapse: onRightPanelCollapse,
}),
[]
);
const rightPanel = usePanel(rightPanelUsePanelOptions);
useHotkeys('g', rightPanel.toggle, { enabled: withRightPanel }, [rightPanel.toggle, withRightPanel]);
useHotkeys('g', galleryPanel.toggle, { enabled: shouldShowGalleryPanel }, [
galleryPanel.toggle,
shouldShowGalleryPanel,
]);
useHotkeys(['t', 'o'], optionsPanel.toggle, { enabled: shouldShowOptionsPanel }, [
optionsPanel.toggle,
shouldShowOptionsPanel,
]);
useHotkeys(
'shift+r',
() => {
optionsPanel.reset();
galleryPanel.reset();
leftPanel.reset();
rightPanel.reset();
},
[optionsPanel.reset, galleryPanel.reset]
[leftPanel.reset, rightPanel.reset]
);
useHotkeys(
'f',
() => {
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
optionsPanel.expand();
galleryPanel.expand();
if (leftPanel.isCollapsed || rightPanel.isCollapsed) {
leftPanel.expand();
rightPanel.expand();
} else {
optionsPanel.collapse();
galleryPanel.collapse();
leftPanel.collapse();
rightPanel.collapse();
}
},
[
optionsPanel.isCollapsed,
galleryPanel.isCollapsed,
optionsPanel.expand,
galleryPanel.expand,
optionsPanel.collapse,
galleryPanel.collapse,
leftPanel.isCollapsed,
rightPanel.isCollapsed,
leftPanel.expand,
rightPanel.expand,
leftPanel.collapse,
rightPanel.collapse,
]
);
@@ -141,63 +124,100 @@ export const AppContent = memo(() => {
style={panelStyles}
storage={panelStorage}
>
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
{withLeftPanel && (
<>
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
<TabMountGate tab="generation">
<TabVisibilityGate tab="generation">
<ParametersPanelTextToImage />
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelTextToImage />
</Box>
</Flex>
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="upscaling">
<TabVisibilityGate tab="upscaling">
<ParametersPanelUpscale />
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<ParametersPanelUpscale />
</Box>
</Flex>
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="workflows">
<TabVisibilityGate tab="workflows">
<NodeEditorPanelGroup />
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
<NodeEditorPanelGroup />
</Box>
</Flex>
</TabVisibilityGate>
</TabMountGate>
</Box>
</Flex>
</Panel>
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
</Panel>
<ResizeHandle id="left-main-handle" orientation="vertical" {...leftPanel.resizeHandleProps} />
</>
)}
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
<TabMountGate tab="generation">
<TabVisibilityGate tab="generation">
<CanvasEditor />
<CanvasTabContent />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="upscaling">
<TabVisibilityGate tab="upscaling">
<ImageViewer />
</TabVisibilityGate>
</TabMountGate>
{/* upscaling tab has no content of its own - uses image viewer only */}
<TabMountGate tab="workflows">
<TabVisibilityGate tab="workflows">
<NodesTab />
<WorkflowsTabContent />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="gallery">
<TabVisibilityGate tab="gallery">
<ImageViewer />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="models">
<TabVisibilityGate tab="models">
<ModelManagerTab />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="queue">
<TabVisibilityGate tab="queue">
<QueueTab />
</TabVisibilityGate>
</TabMountGate>
{isImageViewerOpen && <ImageViewer />}
</Panel>
<ResizeHandle id="main-gallery-handle" orientation="vertical" {...galleryPanel.resizeHandleProps} />
<Panel order={2} style={panelStyles} collapsible {...galleryPanel.panelProps}>
<GalleryPanelContent />
</Panel>
{withRightPanel && (
<>
<ResizeHandle id="main-right-handle" orientation="vertical" {...rightPanel.resizeHandleProps} />
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
<RightPanelContent />
</Panel>
</>
)}
</PanelGroup>
{shouldShowOptionsPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
<TabMountGate tab="models">
<TabVisibilityGate tab="models">
<ModelManagerTab />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="models">
<TabVisibilityGate tab="queue">
<QueueTab />
</TabVisibilityGate>
</TabMountGate>
{withLeftPanel && <FloatingParametersPanelButtons panelApi={leftPanel} />}
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
</Flex>
</Flex>
);
});
AppContent.displayName = 'AppContent';
const RightPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
if (tab === 'generation') {
return <CanvasRightPanelContent />;
}
return <GalleryPanelContent />;
});
RightPanelContent.displayName = 'RightPanelContent';

View File

@@ -1,11 +1,8 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
@@ -17,47 +14,22 @@ import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePr
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};
const baseStyles: ChakraProps['sx'] = {
fontWeight: 'semibold',
fontSize: 'sm',
color: 'base.300',
};
const selectedStyles: ChakraProps['sx'] = {
borderColor: 'base.800',
borderBottomColor: 'base.900',
color: 'invokeBlue.300',
};
const ParametersPanelTextToImage = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isSDXL = useAppSelector(selectIsSDXL);
const onChangeTabs = useCallback(
(i: number) => {
if (i === 1) {
dispatch(isImageViewerOpenChanged(false));
}
},
[dispatch]
);
const ref = useRef<HTMLDivElement>(null);
const isMenuOpen = useStore($isMenuOpen);
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} ref={ref}>
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{isMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
@@ -68,43 +40,11 @@ const ParametersPanelTextToImage = () => {
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<Tabs
defaultIndex={0}
variant="enclosed"
display="flex"
flexDir="column"
w="full"
h="full"
gap={2}
onChange={onChangeTabs}
>
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full" pe={1}>
<Tab sx={baseStyles} _selected={selectedStyles} data-testid="generation-tab-settings-tab-button">
{t('common.settingsLabel')}
</Tab>
<Tab
sx={baseStyles}
_selected={selectedStyles}
data-testid="generation-tab-control-layers-tab-button"
>
{t('controlLayers.layer_other')}
</Tab>
</TabList>
<TabPanels w="full" h="full">
<TabPanel p={0} w="full" h="full">
<Flex gap={2} flexDirection="column" h="full" w="full">
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
</TabPanel>
<TabPanel p={0} w="full" h="full">
<CanvasPanelContent />
</TabPanel>
</TabPanels>
</Tabs>
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
</Box>

View File

@@ -8,7 +8,7 @@ import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdZoomOutMap } from 'react-icons/md';
import { PiFlowArrowBold } from 'react-icons/pi';
import { PiFlowArrowBold, PiImageBold } from 'react-icons/pi';
import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
import { TabButton } from './TabButton';
@@ -36,6 +36,9 @@ export const VerticalNavBar = memo(() => {
<TabMountGate tab="queue">
<TabButton tab="queue" icon={<RiPlayList2Fill />} label={t('ui.tabs.queue')} />
</TabMountGate>
<TabMountGate tab="gallery">
<TabButton tab="gallery" icon={<PiImageBold />} label={t('ui.tabs.gallery')} />
</TabMountGate>
</Flex>
<Spacer />
<StatusIndicator />

View File

@@ -1,37 +0,0 @@
import { Box } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import NodeEditor from 'features/nodes/components/NodeEditor';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useRef } from 'react';
import { ReactFlowProvider } from 'reactflow';
const NodesTab = () => {
const mode = useAppSelector(selectWorkflowMode);
const activeTabName = useAppSelector(selectActiveTab);
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('workflows', ref);
return (
<Box
display={activeTabName === 'workflows' ? undefined : 'none'}
hidden={activeTabName !== 'workflows'}
ref={ref}
layerStyle="first"
position="relative"
w="full"
h="full"
p={2}
borderRadius="base"
>
{mode === 'edit' && (
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
)}
</Box>
);
};
export default memo(NodesTab);

View File

@@ -0,0 +1,22 @@
import { useAppSelector } from 'app/store/storeHooks';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import NodeEditor from 'features/nodes/components/NodeEditor';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
import { ReactFlowProvider } from 'reactflow';
export const WorkflowsTabContent = memo(() => {
const mode = useAppSelector(selectWorkflowMode);
if (mode === 'edit') {
return (
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
);
}
return <ImageViewer />;
});
WorkflowsTabContent.displayName = 'WorkflowsTabContent';

View File

@@ -107,6 +107,12 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
}
const minSizePct = getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
if (minSizePct > 100) {
// This can happen when the panel is hidden
return;
}
_setMinSize(minSizePct);
if (arg.defaultSize && arg.defaultSize > minSizePct) {

View File

@@ -1,5 +1,5 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { atom } from 'nanostores';
@@ -78,7 +78,14 @@ export const uiPersistConfig: PersistConfig<UIState> = {
persistDenylist: ['shouldShowImageDetails'],
};
export const $isGalleryPanelOpen = atom(true);
export const $isParametersPanelOpen = atom(true);
export const TABS_WITH_GALLERY_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
export const TABS_WITH_OPTIONS_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
export const LEFT_PANEL_MIN_SIZE_PX = 390;
export const LEFT_PANEL_MIN_SIZE_PCT = 20;
export const TABS_WITH_LEFT_PANEL: TabName[] = ['generation', 'upscaling', 'workflows'] as const;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
export const TABS_WITH_RIGHT_PANEL: TabName[] = ['generation', 'upscaling', 'workflows', 'gallery'] as const;
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
export const RIGHT_PANEL_MIN_SIZE_PCT = 20;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));

View File

@@ -1,4 +1,4 @@
export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue';
export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue' | 'gallery';
export interface UIState {
/**