mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 17:45:14 -05:00
feat(ui): revise app layout strategy, add interaction scopes for hotkeys
This commit is contained in:
181
invokeai/frontend/web/src/features/ui/components/AppContent.tsx
Normal file
181
invokeai/frontend/web/src/features/ui/components/AppContent.tsx
Normal 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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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%)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user