feat(ui): revise app layout strategy, add interaction scopes for hotkeys

This commit is contained in:
psychedelicious
2024-08-18 23:37:49 +10:00
parent 50051ee147
commit 4c66a0dcd0
33 changed files with 807 additions and 613 deletions

View File

@@ -0,0 +1,181 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
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 FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
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 { 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 type { InvokeTabName } from 'features/ui/store/tabMap';
import { $isGalleryPanelOpen, $isParametersPanelOpen } from 'features/ui/store/uiSlice';
import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import ResizeHandle from './tabs/ResizeHandle';
const TABS_WITH_GALLERY_PANEL: InvokeTabName[] = ['generation', 'upscaling', 'workflows'] as const;
const TABS_WITH_OPTIONS_PANEL: InvokeTabName[] = ['generation', 'upscaling', 'workflows'] as const;
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;
export const onGalleryPanelCollapse = (isCollapsed: boolean) => $isGalleryPanelOpen.set(!isCollapsed);
export const onParametersPanelCollapse = (isCollapsed: boolean) => $isParametersPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const isImageViewerOpen = useIsImageViewerOpen();
const shouldShowGalleryPanel = useAppSelector((s) => TABS_WITH_GALLERY_PANEL.includes(s.ui.activeTab));
const shouldShowOptionsPanel = useAppSelector((s) => TABS_WITH_OPTIONS_PANEL.includes(s.ui.activeTab));
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 panelStorage = usePanelStorage();
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
useHotkeys(
'shift+r',
() => {
optionsPanel.reset();
galleryPanel.reset();
},
[optionsPanel.reset, galleryPanel.reset]
);
useHotkeys(
'f',
() => {
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
optionsPanel.expand();
galleryPanel.expand();
} else {
optionsPanel.collapse();
galleryPanel.collapse();
}
},
[
optionsPanel.isCollapsed,
galleryPanel.isCollapsed,
optionsPanel.expand,
galleryPanel.expand,
optionsPanel.collapse,
galleryPanel.collapse,
]
);
return (
<Flex ref={ref} id="invoke-app-tabs" w="full" h="full" gap={4} p={4}>
<VerticalNavBar />
<Flex position="relative" w="full" h="full" gap={4}>
<PanelGroup
ref={panelGroupRef}
id="app-panel-group"
autoSaveId="app"
direction="horizontal"
style={panelStyles}
storage={panelStorage}
>
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
<TabMountGate tab="generation">
<TabVisibilityGate tab="generation">
<ParametersPanelTextToImage />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="upscaling">
<TabVisibilityGate tab="upscaling">
<ParametersPanelUpscale />
</TabVisibilityGate>
</TabMountGate>
<TabMountGate tab="workflows">
<TabVisibilityGate tab="workflows">
<NodeEditorPanelGroup />
</TabVisibilityGate>
</TabMountGate>
</Panel>
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
<TabMountGate tab="generation">
<TabVisibilityGate tab="generation">
<CanvasEditor />
</TabVisibilityGate>
</TabMountGate>
{/* upscaling tab has no content of its own - uses image viewer only */}
<TabMountGate tab="workflows">
<TabVisibilityGate tab="workflows">
<NodesTab />
</TabVisibilityGate>
</TabMountGate>
{isImageViewerOpen && <ImageViewer />}
</Panel>
<ResizeHandle id="main-gallery-handle" orientation="vertical" {...galleryPanel.resizeHandleProps} />
<Panel order={2} style={panelStyles} collapsible {...galleryPanel.panelProps}>
<GalleryPanelContent />
</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>
</Flex>
</Flex>
);
});
AppContent.displayName = 'AppContent';

View File

@@ -1,303 +0,0 @@
import { Flex, IconButton, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs, Tooltip } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
import { selectConfigSlice } from 'features/system/store/configSlice';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
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 TextToImageTab from 'features/ui/components/tabs/TextToImageTab';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { TAB_NUMBER_MAP } from 'features/ui/store/tabMap';
import { activeTabIndexSelector, activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { MdZoomOutMap } from 'react-icons/md';
import { PiFlowArrowBold } from 'react-icons/pi';
import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import ResizeHandle from './tabs/ResizeHandle';
import UpscalingTab from './tabs/UpscalingTab';
type TabData = {
id: InvokeTabName;
translationKey: string;
icon: ReactElement;
content: ReactNode;
parametersPanel?: ReactNode;
};
const TAB_DATA: Record<InvokeTabName, TabData> = {
generation: {
id: 'generation',
translationKey: 'ui.tabs.generation',
icon: <RiInputMethodLine />,
content: <TextToImageTab />,
parametersPanel: <ParametersPanelTextToImage />,
},
upscaling: {
id: 'upscaling',
translationKey: 'ui.tabs.upscaling',
icon: <MdZoomOutMap />,
content: <UpscalingTab />,
parametersPanel: <ParametersPanelUpscale />,
},
workflows: {
id: 'workflows',
translationKey: 'ui.tabs.workflows',
icon: <PiFlowArrowBold />,
content: <NodesTab />,
parametersPanel: <NodeEditorPanelGroup />,
},
models: {
id: 'models',
translationKey: 'ui.tabs.models',
icon: <RiBox2Line />,
content: <ModelManagerTab />,
},
queue: {
id: 'queue',
translationKey: 'ui.tabs.queue',
icon: <RiPlayList2Fill />,
content: <QueueTab />,
},
};
const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) =>
TAB_NUMBER_MAP.map((tabName) => TAB_DATA[tabName]).filter((tab) => !config.disabledTabs.includes(tab.id))
);
const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['models', 'queue'];
const panelStyles: CSSProperties = { 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 appPanelGroupId = 'app-panel-group';
const InvokeTabs = () => {
const activeTabIndex = useAppSelector(activeTabIndexSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const enabledTabs = useAppSelector(enabledTabsSelector);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const customNavComponent = useStore($customNavComponent);
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => {
if (e.target instanceof HTMLElement) {
e.target.blur();
}
}, []);
const shouldShowGalleryPanel = useMemo(() => !NO_GALLERY_PANEL_TABS.includes(activeTabName), [activeTabName]);
const tabs = useMemo(
() =>
enabledTabs.map((tab) => (
<Tooltip key={tab.id} label={t(tab.translationKey)} placement="end">
<Tab
as={IconButton}
p={0}
onClick={handleClickTab}
icon={tab.icon}
size="md"
fontSize="24px"
variant="appTab"
data-selected={activeTabName === tab.id}
aria-label={t(tab.translationKey)}
data-testid={t(tab.translationKey)}
/>
</Tooltip>
)),
[enabledTabs, t, handleClickTab, activeTabName]
);
const tabPanels = useMemo(
() => enabledTabs.map((tab) => <TabPanel key={tab.id}>{tab.content}</TabPanel>),
[enabledTabs]
);
const handleTabChange = useCallback(
(index: number) => {
const tab = enabledTabs[index];
if (!tab) {
return;
}
dispatch(setActiveTab(tab.id));
},
[dispatch, enabledTabs]
);
const optionsPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
unit: 'pixels',
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
fallbackMinSizePct: OPTIONS_PANEL_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
}),
[]
);
const galleryPanelUsePanelOptions = useMemo<UsePanelOptions>(
() => ({
unit: 'pixels',
minSize: GALLERY_MIN_SIZE_PX,
fallbackMinSizePct: GALLERY_MIN_SIZE_PCT,
panelGroupRef,
panelGroupDirection: 'horizontal',
}),
[]
);
const panelStorage = usePanelStorage();
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
useHotkeys(
'shift+r',
() => {
optionsPanel.reset();
galleryPanel.reset();
},
[optionsPanel.reset, galleryPanel.reset]
);
useHotkeys(
'f',
() => {
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
optionsPanel.expand();
galleryPanel.expand();
} else {
optionsPanel.collapse();
galleryPanel.collapse();
}
},
[
optionsPanel.isCollapsed,
galleryPanel.isCollapsed,
optionsPanel.expand,
galleryPanel.expand,
optionsPanel.collapse,
galleryPanel.collapse,
]
);
return (
<Tabs
id="invoke-app-tabs"
variant="appTabs"
defaultIndex={activeTabIndex}
index={activeTabIndex}
onChange={handleTabChange}
w="full"
h="full"
gap={4}
p={4}
isLazy
>
<Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
<InvokeAILogoComponent />
<TabList gap={4} pt={6} h="full" flexDir="column">
{tabs}
</TabList>
<Spacer />
<StatusIndicator />
{customNavComponent ? customNavComponent : <SettingsMenu />}
</Flex>
<PanelGroup
ref={panelGroupRef}
id={appPanelGroupId}
autoSaveId="app"
direction="horizontal"
style={panelStyles}
storage={panelStorage}
>
{!!TAB_DATA[activeTabName].parametersPanel && (
<>
<Panel
id="options-panel"
ref={optionsPanel.ref}
order={0}
defaultSize={optionsPanel.minSize}
minSize={optionsPanel.minSize}
onCollapse={optionsPanel.onCollapse}
onExpand={optionsPanel.onExpand}
collapsible
>
{TAB_DATA[activeTabName].parametersPanel}
</Panel>
<ResizeHandle
id="options-main-handle"
onDoubleClick={optionsPanel.onDoubleClickHandle}
orientation="vertical"
/>
</>
)}
<Panel id="main-panel" order={1} minSize={20}>
<TabPanels w="full" h="full">
{tabPanels}
</TabPanels>
</Panel>
{shouldShowGalleryPanel && (
<>
<ResizeHandle
id="main-gallery-handle"
orientation="vertical"
onDoubleClick={galleryPanel.onDoubleClickHandle}
/>
<Panel
id="gallery-panel"
ref={galleryPanel.ref}
order={2}
defaultSize={galleryPanel.minSize}
minSize={galleryPanel.minSize}
onCollapse={galleryPanel.onCollapse}
onExpand={galleryPanel.onExpand}
collapsible
>
<ImageGalleryContent />
</Panel>
</>
)}
</PanelGroup>
{!!TAB_DATA[activeTabName].parametersPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
</Tabs>
);
};
export default memo(InvokeTabs);
const ParametersPanelComponent = memo(() => {
const activeTabName = useAppSelector(activeTabNameSelector);
if (activeTabName === 'workflows') {
return <NodeEditorPanelGroup />;
} else {
return <ParametersPanelTextToImage />;
}
});
ParametersPanelComponent.displayName = 'ParametersPanelComponent';

View File

@@ -0,0 +1,32 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, type ReactElement, useCallback } from 'react';
export const TabButton = memo(({ tab, icon, label }: { tab: InvokeTabName; icon: ReactElement; label: string }) => {
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const onClick = useCallback(() => {
dispatch(setActiveTab(tab));
}, [dispatch, tab]);
return (
<Tooltip label={label} placement="end">
<IconButton
p={0}
onClick={onClick}
icon={icon}
size="md"
fontSize="24px"
variant="appTab"
data-selected={activeTabName === tab}
aria-label={label}
data-testid={label}
/>
</Tooltip>
);
});
TabButton.displayName = 'TabButton';

View File

@@ -0,0 +1,22 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectConfigSlice } from 'features/system/store/configSlice';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
export const TabMountGate = memo(({ tab, children }: PropsWithChildren<{ tab: InvokeTabName }>) => {
const selectIsTabEnabled = useMemo(
() => createSelector(selectConfigSlice, (config) => !config.disabledTabs.includes(tab)),
[tab]
);
const isEnabled = useAppSelector(selectIsTabEnabled);
if (!isEnabled) {
return null;
}
return children;
});
TabMountGate.displayName = 'TabMountGate';

View File

@@ -0,0 +1,29 @@
import { Box } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
export const TabVisibilityGate = memo(({ tab, children }: PropsWithChildren<{ tab: InvokeTabName }>) => {
const activeTabName = useAppSelector((s) => s.ui.activeTab);
return (
<Box
display={activeTabName === tab ? undefined : 'none'}
pointerEvents={activeTabName === tab ? undefined : 'none'}
userSelect={activeTabName === tab ? undefined : 'none'}
hidden={activeTabName !== tab}
w="full"
h="full"
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
>
{children}
</Box>
);
});
TabVisibilityGate.displayName = 'TabVisibilityGate';

View File

@@ -0,0 +1,47 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
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 { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
import { TabButton } from './TabButton';
export const VerticalNavBar = memo(() => {
const { t } = useTranslation();
const customNavComponent = useStore($customNavComponent);
return (
<Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
<InvokeAILogoComponent />
<Flex gap={4} pt={6} h="full" flexDir="column">
<TabMountGate tab="generation">
<TabButton tab="generation" icon={<RiInputMethodLine />} label={t('ui.tabs.generation')} />
</TabMountGate>
<TabMountGate tab="upscaling">
<TabButton tab="upscaling" icon={<MdZoomOutMap />} label={t('ui.tabs.upscaling')} />
</TabMountGate>
<TabMountGate tab="workflows">
<TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />
</TabMountGate>
<TabMountGate tab="models">
<TabButton tab="models" icon={<RiBox2Line />} label={t('ui.tabs.models')} />
</TabMountGate>
<TabMountGate tab="queue">
<TabButton tab="queue" icon={<RiPlayList2Fill />} label={t('ui.tabs.queue')} />
</TabMountGate>
</Flex>
<Spacer />
<StatusIndicator />
{customNavComponent ? customNavComponent : <SettingsMenu />}
</Flex>
);
});
VerticalNavBar.displayName = 'VerticalNavBar';

View File

@@ -1,27 +1,34 @@
import { Box } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import NodeEditor from 'features/nodes/components/NodeEditor';
import { memo } from 'react';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useRef } from 'react';
import { ReactFlowProvider } from 'reactflow';
const NodesTab = () => {
const mode = useAppSelector((s) => s.workflow.mode);
if (mode === 'view') {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ImageViewer />
<ImageComparisonDroppable />
</Box>
);
}
const activeTabName = useAppSelector(activeTabNameSelector);
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('workflows', ref);
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
<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>
);
};

View File

@@ -1,8 +1,13 @@
import { Flex } from '@invoke-ai/ui-library';
import QueueTabContent from 'features/queue/components/QueueTabContent';
import { memo } from 'react';
const QueueTab = () => {
return <QueueTabContent />;
return (
<Flex w="full" h="full">
<QueueTabContent />
</Flex>
);
};
export default memo(QueueTab);

View File

@@ -17,7 +17,6 @@ const ResizeHandle = (props: ResizeHandleProps) => {
<ChakraPanelResizeHandle {...rest}>
<Flex sx={sx} data-orientation={orientation}>
<Box className="resize-handle-inner" data-orientation={orientation} />
<Box className="resize-handle-drag-handle" data-orientation={orientation} />
</Flex>
</ChakraPanelResizeHandle>
);
@@ -59,22 +58,4 @@ const sx: SystemStyleObject = {
transitionProperty: 'inherit',
transitionDuration: 'inherit',
},
'.resize-handle-drag-handle': {
pos: 'absolute',
borderRadius: '1px',
transitionProperty: 'inherit',
transitionDuration: 'inherit',
'&[data-orientation="horizontal"]': {
w: '30px',
h: '6px',
insetInlineStart: '50%',
transform: 'translate(-50%, 0)',
},
'&[data-orientation="vertical"]': {
w: '6px',
h: '30px',
insetBlockStart: '50%',
transform: 'translate(0, -50%)',
},
},
};

View File

@@ -1,21 +1,11 @@
import { Box } from '@invoke-ai/ui-library';
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
import { memo } from 'react';
const TextToImageTab = () => {
const imageViewer = useImageViewer();
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ControlLayersEditor />
{imageViewer.isOpen && (
<>
<ImageViewer />
<ImageComparisonDroppable />
</>
)}
<CanvasEditor />
</Box>
);
};

View File

@@ -1,11 +1,23 @@
import { Box } from '@invoke-ai/ui-library';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
const UpscalingTab = () => {
const activeTabName = useAppSelector(activeTabNameSelector);
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ImageViewer />
<Box
display={activeTabName === 'upscaling' ? undefined : 'none'}
hidden={activeTabName !== 'upscaling'}
layerStyle="first"
position="relative"
w="full"
h="full"
p={2}
borderRadius="base"
>
{/* <ImageViewer /> */}
</Box>
);
};

View File

@@ -5,6 +5,8 @@ import type {
ImperativePanelHandle,
PanelOnCollapse,
PanelOnExpand,
PanelProps,
PanelResizeHandleProps,
} from 'react-resizable-panels';
import { getPanelGroupElement, getResizeHandleElementsForGroup } from 'react-resizable-panels';
@@ -12,6 +14,7 @@ type Direction = 'horizontal' | 'vertical';
export type UsePanelOptions =
| {
id: string;
/**
* The minimum size of the panel as a percentage.
*/
@@ -24,8 +27,10 @@ export type UsePanelOptions =
* The unit of the minSize
*/
unit: 'percentages';
onCollapse?: (isCollapsed: boolean) => void;
}
| {
id: string;
/**
* The minimum size of the panel in pixels.
*/
@@ -47,44 +52,18 @@ export type UsePanelOptions =
* A ref to the panel group.
*/
panelGroupRef: RefObject<ImperativePanelGroupHandle>;
onCollapse?: (isCollapsed: boolean) => void;
};
export type UsePanelReturn = {
/**
* The ref to the panel handle.
*/
ref: RefObject<ImperativePanelHandle>;
/**
* The dynamically calculated minimum size of the panel.
*/
minSize: number;
/**
* The dynamically calculated default size of the panel.
*/
defaultSize: number;
/**
* Whether the panel is collapsed.
*/
isCollapsed: boolean;
/**
* The onCollapse callback. This is required to update the isCollapsed state.
* This should be passed to the panel as the onCollapse prop. Wrap it if additional logic is required.
*/
onCollapse: PanelOnCollapse;
/**
* The onExpand callback. This is required to update the isCollapsed state.
* This should be passed to the panel as the onExpand prop. Wrap it if additional logic is required.
*/
onExpand: PanelOnExpand;
/**
* Reset the panel to the minSize.
*/
reset: () => void;
/**
* Reset the panel to the minSize. If the panel is already at the minSize, collapse it.
* This should be passed to the `onDoubleClick` prop of the panel's nearest resize handle.
*/
onDoubleClickHandle: () => void;
/**
* Toggle the panel between collapsed and expanded.
*/
@@ -101,6 +80,8 @@ export type UsePanelReturn = {
* Resize the panel to the given size in the same units as the minSize.
*/
resize: (size: number) => void;
panelProps: Partial<PanelProps & { ref: RefObject<ImperativePanelHandle> }>;
resizeHandleProps: Partial<PanelResizeHandleProps>;
};
export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
@@ -128,12 +109,11 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
const minSizePct = getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
_setMinSize(minSizePct);
const defaultSizePct = getSizeAsPercentage(
arg.defaultSize ?? arg.minSize,
arg.panelGroupRef,
arg.panelGroupDirection
);
_setDefaultSize(defaultSizePct);
if (arg.defaultSize && arg.defaultSize > minSizePct) {
_setDefaultSize(defaultSizePct);
} else {
_setDefaultSize(minSizePct);
}
if (!panelHandleRef.current.isCollapsed() && panelHandleRef.current.getSize() < minSizePct && minSizePct > 0) {
panelHandleRef.current.resize(minSizePct);
@@ -144,11 +124,8 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
panelGroupHandleElements.forEach((el) => resizeObserver.observe(el));
// Resize the panel to the min size once on startup
const defaultSizePct = getSizeAsPercentage(
arg.defaultSize ?? arg.minSize,
arg.panelGroupRef,
arg.panelGroupDirection
);
const defaultSizePct =
arg.defaultSize ?? getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
panelHandleRef.current?.resize(defaultSizePct);
return () => {
@@ -160,11 +137,13 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
const onCollapse = useCallback<PanelOnCollapse>(() => {
setIsCollapsed(true);
}, []);
arg.onCollapse?.(true);
}, [arg]);
const onExpand = useCallback<PanelOnExpand>(() => {
setIsCollapsed(false);
}, []);
arg.onCollapse?.(false);
}, [arg]);
const toggle = useCallback(() => {
if (panelHandleRef.current?.isCollapsed()) {
@@ -201,7 +180,7 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
panelHandleRef.current?.resize(_minSize);
}, [_minSize]);
const onDoubleClickHandle = useCallback(() => {
const cycleState = useCallback(() => {
// If the panel is really super close to the min size, collapse it
if (Math.abs((panelHandleRef.current?.getSize() ?? 0) - _defaultSize) < 0.01) {
collapse();
@@ -213,18 +192,23 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
}, [_defaultSize, collapse]);
return {
ref: panelHandleRef,
minSize: _minSize,
isCollapsed,
onCollapse,
onExpand,
reset,
toggle,
expand,
collapse,
resize,
onDoubleClickHandle,
defaultSize: _defaultSize,
panelProps: {
id: arg.id,
defaultSize: _defaultSize,
onCollapse,
onExpand,
ref: panelHandleRef,
minSize: _minSize,
},
resizeHandleProps: {
onDoubleClick: cycleState,
},
};
};

View File

@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { atom } from 'nanostores';
import type { InvokeTabName } from './tabMap';
import type { UIState } from './uiTypes';
@@ -77,3 +78,6 @@ export const uiPersistConfig: PersistConfig<UIState> = {
migrate: migrateUIState,
persistDenylist: ['shouldShowImageDetails'],
};
export const $isGalleryPanelOpen = atom(true);
export const $isParametersPanelOpen = atom(true);