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

@@ -375,6 +375,7 @@
"useCache": "Use Cache"
},
"gallery": {
"gallery": "Gallery",
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"assets": "Assets",
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
@@ -387,11 +388,11 @@
"deleteImage_one": "Delete Image",
"deleteImage_other": "Delete {{count}} Images",
"deleteImagePermanent": "Deleted images cannot be restored.",
"displayBoardSearch": "Display Board Search",
"displaySearch": "Display Search",
"displayBoardSearch": "Board Search",
"displaySearch": "Image Search",
"download": "Download",
"exitBoardSearch": "Exit Board Search",
"exitSearch": "Exit Search",
"exitSearch": "Exit Image Search",
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
"galleryImageSize": "Image Size",
"gallerySettings": "Gallery Settings",
@@ -437,7 +438,8 @@
"compareHelp1": "Hold <Kbd>Alt</Kbd> while clicking a gallery image or using the arrow keys to change the compare image.",
"compareHelp2": "Press <Kbd>M</Kbd> to cycle through comparison modes.",
"compareHelp3": "Press <Kbd>C</Kbd> to swap the compared images.",
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit."
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.",
"toggleMiniViewer": "Toggle Mini Viewer"
},
"hotkeys": {
"searchHotkeys": "Search Hotkeys",
@@ -1049,8 +1051,8 @@
"scaledHeight": "Scaled H",
"scaledWidth": "Scaled W",
"scheduler": "Scheduler",
"seamlessXAxis": "Seamless Tiling X Axis",
"seamlessYAxis": "Seamless Tiling Y Axis",
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
"seed": "Seed",
"imageActions": "Image Actions",
"sendToCanvas": "Send To Canvas",
@@ -1714,6 +1716,8 @@
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"ipAdapter": "IP Adapter",
"sendingToCanvas": "Sending to Canvas",
"sendingToGallery": "Sending to Gallery",
"sendToGallery": "Send To Gallery",
"sendToGalleryDesc": "Generations will be sent to the gallery.",
"sendToCanvas": "Send To Canvas",

View File

@@ -21,10 +21,16 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
direction,
shadows: {
..._theme.shadows,
selected:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
hoverSelected:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
hoverUnselected:
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
selectedForCompare:
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-400)',
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
hoverSelectedForCompare:
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-300)',
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
});
}, [direction]);

View File

@@ -1,6 +1,5 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
import { queueApi } from 'services/api/endpoints/queue';
@@ -11,7 +10,6 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
enqueueRequested.match(action) && action.payload.tabName === 'upscaling',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { shouldShowProgressInViewer } = state.ui;
const { prepend } = action.payload;
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
@@ -25,9 +23,6 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
);
try {
await req.unwrap();
if (shouldShowProgressInViewer) {
dispatch(isImageViewerOpenChanged(true));
}
} finally {
req.reset();
}

View File

@@ -13,7 +13,7 @@ import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/c
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -146,7 +146,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { imageDTO } = activeData.payload;
dispatch(imageToCompareChanged(imageDTO));
dispatch(isImageViewerOpenChanged(true));
return;
}

View File

@@ -6,18 +6,51 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable';
import SelectionOverlay from './SelectionOverlay';
const defaultUploadElement = <Icon as={PiUploadSimpleBold} boxSize={16} />;
const defaultNoContentFallback = <IAINoContentFallback icon={PiImageBold} />;
const sx: SystemStyleObject = {
'.gallery-image-container::before': {
content: '""',
display: 'inline-block',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
borderRadius: 'base',
},
'&[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
'&:hover>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
};
type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
@@ -75,13 +108,11 @@ const IAIDndImage = (props: IAIDndImageProps) => {
...rest
} = props;
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (onMouseOver) {
onMouseOver(e);
}
setIsHovered(true);
},
[onMouseOver]
);
@@ -90,7 +121,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
if (onMouseOut) {
onMouseOut(e);
}
setIsHovered(false);
},
[onMouseOut]
);
@@ -141,10 +171,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
minH={minSize ? minSize : undefined}
userSelect="none"
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
sx={withHoverOverlay ? sx : undefined}
data-selected={isSelectedForCompare ? 'selectedForCompare' : isSelected ? 'selected' : undefined}
{...rest}
>
{imageDTO && (
<Flex
className="gallery-image-container"
w="full"
h="full"
position={fitContainer ? 'absolute' : 'relative'}
@@ -167,11 +200,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
data-testid={dataTestId}
/>
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
<SelectionOverlay
isSelected={isSelected}
isSelectedForCompare={isSelectedForCompare}
isHovered={withHoverOverlay ? isHovered : false}
/>
</Flex>
)}
{!imageDTO && !isUploadDisabled && (

View File

@@ -114,4 +114,13 @@ export const useGlobalHotkeys = () => {
},
[dispatch, isModelManagerEnabled]
);
useHotkeys(
isModelManagerEnabled ? '6' : '5',
() => {
dispatch(setActiveTab('gallery'));
setScopes([]);
},
[dispatch, isModelManagerEnabled]
);
};

View File

@@ -1,7 +1,6 @@
import { Flex } from '@invoke-ai/ui-library';
import IAIDroppable from 'common/components/IAIDroppable';
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
@@ -15,12 +14,6 @@ const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
};
export const CanvasDropArea = memo(() => {
const isImageViewerOpen = useIsImageViewerOpen();
if (isImageViewerOpen) {
return null;
}
return (
<>
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">

View File

@@ -1,24 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import type { Meta, StoryObj } from '@storybook/react';
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
const meta: Meta<typeof CanvasEditor> = {
title: 'Feature/ControlLayers',
tags: ['autodocs'],
component: CanvasEditor,
};
export default meta;
type Story = StoryObj<typeof CanvasEditor>;
const Component = () => {
return (
<Flex w={1500} h={1500}>
<CanvasEditor />
</Flex>
);
};
export const Default: Story = {
render: Component,
};

View File

@@ -6,7 +6,7 @@ import { memo } from 'react';
export const EntityListGlobalActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<Flex w="full" gap={2} alignItems="center">
<EntityListGlobalActionBarDenoisingStrength />
<Spacer />
<Flex>

View File

@@ -8,7 +8,7 @@ import { memo } from 'react';
export const EntityListSelectedEntityActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<Flex w="full" gap={2} alignItems="center" ps={1}>
<EntityListSelectedEntityActionBarOpacity />
<Spacer />
<EntityListSelectedEntityActionBarFill />

View File

@@ -2,7 +2,6 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListGlobalActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
@@ -14,8 +13,6 @@ export const CanvasPanelContent = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListGlobalActionBar />
<Divider py={0} />
<EntityListSelectedEntityActionBar />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}

View File

@@ -0,0 +1,83 @@
import { useDndContext } from '@dnd-kit/core';
import { Box, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { memo, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasRightPanelContent = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const [tab, setTab] = useState(0);
useScopeOnFocus('gallery', ref);
return (
<Tabs index={tab} onChange={setTab} w="full" h="full" display="flex" flexDir="column">
<TabList alignItems="center">
<PanelTabs setTab={setTab} />
<Spacer />
<CanvasSendToToggle />
</TabList>
<TabPanels w="full" h="full">
<TabPanel w="full" h="full" p={0} pt={2}>
<GalleryPanelContent />
</TabPanel>
<TabPanel w="full" h="full" p={0} pt={2}>
<CanvasPanelContent />
</TabPanel>
</TabPanels>
</Tabs>
);
});
CanvasRightPanelContent.displayName = 'CanvasRightPanelContent';
const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
const { t } = useTranslation();
const sendToCanvas = useAppSelector(selectSendToCanvas);
const tabTimeout = useRef<number | null>(null);
const dndCtx = useDndContext();
const onOnMouseOverLayersTab = useCallback(() => {
tabTimeout.current = window.setTimeout(() => {
if (dndCtx.active) {
setTab(1);
}
}, 300);
}, [dndCtx.active, setTab]);
const onOnMouseOverGalleryTab = useCallback(() => {
tabTimeout.current = window.setTimeout(() => {
if (dndCtx.active) {
setTab(0);
}
}, 300);
}, [dndCtx.active, setTab]);
const onMouseOut = useCallback(() => {
if (tabTimeout.current) {
clearTimeout(tabTimeout.current);
}
}, []);
return (
<>
<Tab position="relative" onMouseOver={onOnMouseOverGalleryTab} onMouseOut={onMouseOut}>
{t('gallery.gallery')}
{!sendToCanvas && (
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
)}
</Tab>
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut}>
{t('controlLayers.layer_other')}
{sendToCanvas && (
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
)}
</Tab>
</>
);
});
PanelTabs.displayName = 'PanelTabs';

View File

@@ -1,64 +1,76 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IconSwitch } from 'common/components/IconSwitch';
import {
selectCanvasSettingsSlice,
settingsSendToCanvasChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
Button,
Flex,
Icon,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Text,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
const TooltipSendToGallery = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
</Flex>
);
});
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
const TooltipSendToCanvas = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
</Flex>
);
});
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas);
import { PiCaretDownBold, PiCheckBold } from 'react-icons/pi';
export const CanvasSendToToggle = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const sendToCanvas = useAppSelector(selectSendToCanvas);
const dispatch = useAppDispatch();
const onChange = useCallback(
(isChecked: boolean) => {
dispatch(settingsSendToCanvasChanged(isChecked));
},
[dispatch]
);
const enableSendToCanvas = useCallback(() => {
dispatch(settingsSendToCanvasChanged(true));
}, [dispatch]);
const disableSendToCanvas = useCallback(() => {
dispatch(settingsSendToCanvasChanged(false));
}, [dispatch]);
return (
<IconSwitch
isChecked={sendToCanvas}
onChange={onChange}
iconUnchecked={<PiImageBold />}
tooltipUnchecked={<TooltipSendToGallery />}
iconChecked={<PiPaintBrushBold />}
tooltipChecked={<TooltipSendToCanvas />}
ariaLabel="Toggle canvas mode"
/>
<Popover isLazy>
<PopoverTrigger>
<Button
size="sm"
variant="link"
data-testid="toggle-viewer-menu-button"
pointerEvents="auto"
rightIcon={<PiCaretDownBold />}
>
{sendToCanvas ? t('controlLayers.sendingToCanvas') : t('controlLayers.sendingToGallery')}
</Button>
</PopoverTrigger>
<PopoverContent p={2} pointerEvents="auto">
<PopoverArrow />
<PopoverBody>
<Flex flexDir="column">
<Button onClick={disableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={!sendToCanvas ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
<Text fontWeight="normal" variant="subtext">
{t('controlLayers.sendToGalleryDesc')}
</Text>
</Flex>
</Flex>
</Button>
<Button onClick={enableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={sendToCanvas ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
<Text fontWeight="normal" variant="subtext">
{t('controlLayers.sendToCanvasDesc')}
</Text>
</Flex>
</Flex>
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});

View File

@@ -11,7 +11,7 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useRef } from 'react';
export const CanvasEditor = memo(() => {
export const CanvasTabContent = memo(() => {
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('canvas', ref);
@@ -48,4 +48,4 @@ export const CanvasEditor = memo(() => {
);
});
CanvasEditor.displayName = 'CanvasEditor';
CanvasTabContent.displayName = 'CanvasTabContent';

View File

@@ -85,7 +85,7 @@ export const IPAdapterImagePreview = memo(
/>
{controlImage && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}

View File

@@ -13,8 +13,6 @@ import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/u
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
import { useNextPrevEntityHotkeys } from 'features/controlLayers/hooks/useNextPrevEntity';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { memo } from 'react';
export const CanvasToolbar = memo(() => {
@@ -27,7 +25,6 @@ export const CanvasToolbar = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex w="full" gap={2} alignItems="center">
<ToggleProgressButton />
<ToolChooser />
<Spacer />
<ToolSettings />
@@ -38,7 +35,6 @@ export const CanvasToolbar = memo(() => {
<ToolColorPicker />
<CanvasToolbarSaveToGalleryButton />
<CanvasSettingsPopover />
<ViewerToggle />
</Flex>
</CanvasManagerProviderGate>
);

View File

@@ -160,3 +160,4 @@ export const selectDynamicGrid = createCanvasSettingsSelector((settings) => sett
export const selectShowHUD = createCanvasSettingsSelector((settings) => settings.showHUD);
export const selectAutoProcessFilter = createCanvasSettingsSelector((settings) => settings.autoProcessFilter);
export const selectSnapToGrid = createCanvasSettingsSelector((settings) => settings.snapToGrid);
export const selectSendToCanvas = createCanvasSettingsSelector((canvasSettings) => canvasSettings.sendToCanvas);

View File

@@ -35,6 +35,7 @@ const dragOverlayStyles: CSSProperties = {
width: 'min-content',
height: 'min-content',
cursor: 'grabbing',
pointerEvents: 'none',
userSelect: 'none',
// expand overlay to prevent cursor from going outside it and displaying
padding: '10rem',

View File

@@ -14,12 +14,14 @@ import {
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import CurrentImageButtons from 'features/gallery/components/ImageViewer/CurrentImageButtons';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
import { selectIsMiniViewerOpen, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { galleryViewChanged, isMiniViewerOpenToggled, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { CSSProperties } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMagnifyingGlassBold } from 'react-icons/pi';
import { PiEyeBold, PiEyeClosedBold, PiMagnifyingGlassBold } from 'react-icons/pi';
import { useBoardName } from 'services/api/hooks/useBoardName';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
@@ -38,7 +40,7 @@ const SELECTED_STYLES: ChakraProps['sx'] = {
color: 'invokeBlue.300',
};
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '100%' };
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
@@ -50,7 +52,11 @@ export const Gallery = () => {
const initialSearchTerm = useAppSelector(selectSearchTerm);
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
const isMiniViewerOpen = useAppSelector(selectIsMiniViewerOpen);
const toggleMiniViewer = useCallback(() => {
dispatch(isMiniViewerOpenToggled());
}, [dispatch]);
const handleClickImages = useCallback(() => {
dispatch(galleryViewChanged('images'));
}, [dispatch]);
@@ -68,7 +74,7 @@ export const Gallery = () => {
const boardName = useBoardName(selectedBoardId);
return (
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1} minH={0}>
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
@@ -81,28 +87,54 @@ export const Gallery = () => {
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
{t('gallery.assets')}
</Tab>
<IconButton
onClick={handleClickSearch}
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
aria-label={t('gallery.displaySearch')}
icon={<PiMagnifyingGlassBold />}
colorScheme={searchDisclosure.isOpen ? 'invokeBlue' : 'base'}
variant="link"
/>
<Flex h="full">
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
onClick={toggleMiniViewer}
tooltip={t('gallery.toggleMiniViewer')}
aria-label={t('gallery.toggleMiniViewer')}
icon={isMiniViewerOpen ? <PiEyeBold /> : <PiEyeClosedBold />}
colorScheme={isMiniViewerOpen ? 'invokeBlue' : 'base'}
/>
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
onClick={handleClickSearch}
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
aria-label={t('gallery.displaySearch')}
icon={<PiMagnifyingGlassBold />}
/>
</Flex>
</TabList>
</Tabs>
<Box w="full">
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<GallerySearch
searchTerm={searchTerm}
onChangeSearchTerm={onChangeSearchTerm}
onResetSearchTerm={onResetSearchTerm}
/>
</Box>
</Collapse>
</Box>
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<GallerySearch
searchTerm={searchTerm}
onChangeSearchTerm={onChangeSearchTerm}
onResetSearchTerm={onResetSearchTerm}
/>
</Box>
</Collapse>
<Collapse in={isMiniViewerOpen} style={COLLAPSE_STYLES}>
<Box position="relative" w="full" mt={2} aspectRatio="1/1">
<CurrentImagePreview />
<Flex
position="absolute"
top={2}
gap={2}
justifyContent="space-between"
left="50%"
transform="translateX(-50%)"
>
<CurrentImageButtons />
</Flex>
</Box>
</Collapse>
<GalleryImageGrid />
<GalleryPagination />
</Flex>

View File

@@ -1,4 +1,4 @@
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
import { Box, Button, Collapse, Divider, Flex, IconButton, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
@@ -60,36 +60,31 @@ const GalleryPanelContent = () => {
return (
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
<Flex alignItems="center" gap={0}>
<GalleryHeader />
<Flex alignItems="center" justifyContent="space-between" w="full">
<Button
<GalleryHeader />
<Flex alignItems="center" w="full">
<Button
size="sm"
variant="ghost"
onClick={handleToggleBoardPanel}
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
>
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
</Button>
<Spacer />
<Flex h="full">
<GallerySettingsPopover />
<IconButton
size="sm"
variant="ghost"
onClick={handleToggleBoardPanel}
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
>
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
</Button>
<Flex alignItems="center" justifyContent="space-between">
<GallerySettingsPopover />
<Flex>
<IconButton
w="full"
h="full"
onClick={handleClickBoardSearch}
tooltip={
boardSearchDisclosure.isOpen
? `${t('gallery.exitBoardSearch')}`
: `${t('gallery.displayBoardSearch')}`
}
aria-label={t('gallery.displayBoardSearch')}
icon={<PiMagnifyingGlassBold />}
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
variant="link"
/>
</Flex>
</Flex>
variant="link"
alignSelf="stretch"
onClick={handleClickBoardSearch}
tooltip={
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
}
aria-label={t('gallery.displayBoardSearch')}
icon={<PiMagnifyingGlassBold />}
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
/>
</Flex>
</Flex>

View File

@@ -17,7 +17,13 @@ const GallerySettingsPopover = () => {
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton aria-label={t('gallery.gallerySettings')} icon={<RiSettings4Fill />} variant="link" h="full" />
<IconButton
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('gallery.gallerySettings')}
icon={<RiSettings4Fill />}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>

View File

@@ -1,8 +1,8 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold } from 'react-icons/pi';
@@ -11,13 +11,12 @@ export const ImageMenuItemOpenInViewer = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
dispatch(imageToCompareChanged(null));
dispatch(imageSelected(imageDTO));
imageViewer.onOpen();
}, [dispatch, imageDTO, imageViewer]);
dispatch(setActiveTab('gallery'));
}, [dispatch, imageDTO]);
return (
<MenuItem icon={<PiEyeBold />} onClick={onClick}>

View File

@@ -5,7 +5,6 @@ import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
@@ -21,7 +20,6 @@ export const ImageMenuItemSendToCanvas = memo(() => {
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const bboxRect = useAppSelector(selectBboxRect);
const imageViewer = useImageViewer();
const handleSendToCanvas = useCallback(() => {
const imageObject = imageDTOToImageObject(imageDTO);
@@ -32,13 +30,12 @@ export const ImageMenuItemSendToCanvas = memo(() => {
dispatch(sentImageToCanvas());
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
dispatch(setActiveTab('generation'));
imageViewer.onClose();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, t]);
return (
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">

View File

@@ -13,11 +13,8 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import {
imageToCompareChanged,
isImageViewerOpenChanged,
selectGallerySlice,
} from 'features/gallery/store/gallerySlice';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import type { MouseEvent, MouseEventHandler } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -116,7 +113,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
}, []);
const onDoubleClick = useCallback(() => {
dispatch(isImageViewerOpenChanged(true));
dispatch(setActiveTab('gallery'));
dispatch(imageToCompareChanged(null));
}, [dispatch]);
@@ -150,7 +147,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
}
return (
<Box w="full" h="full" p={1.5} className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
<Box w="full" h="full" className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
<Flex
ref={imageContainerRef}
userSelect="none"
@@ -183,13 +180,12 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
color="base.50"
fontSize="sm"
fontWeight="semibold"
bottom={0}
left={0}
bottom={1}
left={1}
opacity={0.7}
px={2}
lineHeight={1.25}
borderTopEndRadius="base"
borderBottomStartRadius="base"
sx={badgeSx}
pointerEvents="none"
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
@@ -199,8 +195,8 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
icon={starIcon}
tooltip={starTooltip}
position="absolute"
top={1}
insetInlineEnd={1}
top={2}
insetInlineEnd={2}
/>
{isHovered && <DeleteIcon onClick={handleDelete} />}
@@ -227,8 +223,8 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
icon={<PiTrashSimpleFill size="16px" />}
tooltip={t('gallery.deleteImage_one')}
position="absolute"
bottom={1}
insetInlineEnd={1}
bottom={2}
insetInlineEnd={2}
/>
);
};

View File

@@ -78,24 +78,52 @@ const Content = () => {
// Managing refs for dynamically rendered components is a bit tedious:
// - https://react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback
// As a easy workaround, we can just grab the first gallery image element directly.
const galleryImageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
if (!galleryImageEl) {
const imageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
if (!imageEl) {
// No images in gallery?
return;
}
const galleryImageRect = galleryImageEl.getBoundingClientRect();
const gridEl = document.querySelector(`.${GALLERY_GRID_CLASS_NAME}`);
if (!gridEl) {
return;
}
const imageRect = imageEl.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (!galleryImageRect.width || !galleryImageRect.height || !containerRect.width || !containerRect.height) {
// We need to account for the gap between images
const gridElStyle = window.getComputedStyle(gridEl);
const gap = parseFloat(gridElStyle.gap);
if (!imageRect.width || !imageRect.height || !containerRect.width || !containerRect.height) {
// Gallery is too small to fit images or not rendered yet
return;
}
// Floating-point precision requires we round to get the correct number of images per row
const imagesPerRow = Math.round(containerRect.width / galleryImageRect.width);
// However, when calculating the number of images per column, we want to floor the value to not overflow the container
const imagesPerColumn = Math.floor(containerRect.height / galleryImageRect.height);
let imagesPerColumn = 0;
let spaceUsed = 0;
while (spaceUsed + imageRect.height <= containerRect.height) {
imagesPerColumn++; // Increment the number of images
spaceUsed += imageRect.height; // Add image size to the used space
if (spaceUsed + gap <= containerRect.height) {
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
}
}
let imagesPerRow = 0;
spaceUsed = 0;
while (spaceUsed + imageRect.width <= containerRect.width) {
imagesPerRow++; // Increment the number of images
spaceUsed += imageRect.width; // Add image size to the used space
if (spaceUsed + gap <= containerRect.width) {
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
}
}
// Always load at least 1 row of images
const limit = Math.max(imagesPerRow, imagesPerRow * imagesPerColumn);
dispatch(limitChanged(limit));
@@ -139,6 +167,7 @@ const Content = () => {
<Grid
className={GALLERY_GRID_CLASS_NAME}
gridTemplateColumns={`repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr))`}
gap={1}
>
{imageDTOs.map((imageDTO, index) => (
<GalleryImage key={imageDTO.image_name} imageDTO={imageDTO} index={index} />

View File

@@ -4,13 +4,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $activeScopes } from 'common/hooks/interactionScopes';
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
import { selectionChanged } from 'features/gallery/store/gallerySlice';
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
import { computed } from 'nanostores';
import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
const $isSelectAllEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
const $isSelectAllEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen;
});

View File

@@ -21,7 +21,8 @@ export const ImageViewer = memo(() => {
<Flex
ref={ref}
tabIndex={-1}
layerStyle="body"
layerStyle="first"
p={2}
borderRadius="base"
position="absolute"
flexDirection="column"

View File

@@ -1,61 +0,0 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { IconSwitch } from 'common/components/IconSwitch';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
const TooltipEdit = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.edit')}</Text>
<Text fontWeight="normal">{t('common.editDesc')}</Text>
</Flex>
);
});
TooltipEdit.displayName = 'TooltipEdit';
const TooltipView = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.view')}</Text>
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
</Flex>
);
});
TooltipView.displayName = 'TooltipView';
export const ViewerToggle = memo(() => {
const imageViewer = useImageViewer();
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
const onChange = useCallback(
(isChecked: boolean) => {
if (isChecked) {
imageViewer.onClose();
} else {
imageViewer.onOpen();
}
},
[imageViewer]
);
return (
<IconSwitch
isChecked={!imageViewer.isOpen}
onChange={onChange}
iconUnchecked={<PiEyeBold />}
tooltipUnchecked={<TooltipView />}
iconChecked={<PiPencilBold />}
tooltipChecked={<TooltipEdit />}
ariaLabel="Toggle viewer"
/>
);
});
ViewerToggle.displayName = 'ViewerToggle';

View File

@@ -1,23 +1,11 @@
import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import CurrentImageButtons from './CurrentImageButtons';
import { ViewerToggle } from './ViewerToggleMenu';
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
if (tab === 'upscaling' || tab === 'workflows') {
return false;
}
return true;
});
export const ViewerToolbar = memo(() => {
const showToggle = useAppSelector(selectShowToggle);
return (
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
@@ -30,9 +18,7 @@ export const ViewerToolbar = memo(() => {
<CurrentImageButtons />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
{showToggle && <ViewerToggle />}
</Flex>
<Flex gap={2} marginInlineStart="auto" />
</Flex>
</Flex>
);

View File

@@ -1,73 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectHasImageToCompare, selectIsImageViewerOpen } from 'features/gallery/store/gallerySelectors';
import {
imageToCompareChanged,
isImageViewerOpenChanged,
selectGallerySlice,
} from 'features/gallery/store/gallerySlice';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { selectUiSlice } from 'features/ui/store/uiSlice';
import { useCallback } from 'react';
const selectIsOpen = createSelector(selectUiSlice, selectWorkflowSlice, selectGallerySlice, (ui, workflow, gallery) => {
const tab = ui.activeTab;
const workflowsMode = workflow.mode;
if (tab === 'models' || tab === 'queue') {
return false;
}
if (tab === 'workflows' && workflowsMode === 'edit') {
return false;
}
if (tab === 'workflows' && workflowsMode === 'view') {
return true;
}
if (tab === 'upscaling') {
return true;
}
return gallery.isImageViewerOpen;
});
export const useIsImageViewerOpen = () => {
const isOpen = useAppSelector(selectIsOpen);
return isOpen;
};
const selectIsForcedOpen = createSelector(selectUiSlice, selectWorkflowSlice, (ui, workflow) => {
return ui.activeTab === 'upscaling' || (ui.activeTab === 'workflows' && workflow.mode === 'view');
});
export const useImageViewer = () => {
const dispatch = useAppDispatch();
const isComparing = useAppSelector(selectHasImageToCompare);
const isNaturallyOpen = useAppSelector(selectIsImageViewerOpen);
const isForcedOpen = useAppSelector(selectIsForcedOpen);
const onClose = useCallback(() => {
if (isForcedOpen) {
return;
}
if (isComparing && isNaturallyOpen) {
dispatch(imageToCompareChanged(null));
} else {
dispatch(isImageViewerOpenChanged(false));
}
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
const onOpen = useCallback(() => {
dispatch(isImageViewerOpenChanged(true));
}, [dispatch]);
const onToggle = useCallback(() => {
if (isForcedOpen) {
return;
}
if (isComparing && isNaturallyOpen) {
dispatch(imageToCompareChanged(null));
} else {
dispatch(isImageViewerOpenChanged(!isNaturallyOpen));
}
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
return { isOpen: isNaturallyOpen || isForcedOpen, onOpen, onClose, onToggle, isComparing };
};

View File

@@ -5,7 +5,7 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
import { computed } from 'nanostores';
import { useHotkeys } from 'react-hotkeys-hook';
import { useListImagesQuery } from 'services/api/endpoints/images';
@@ -15,7 +15,7 @@ const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => {
return !activeScopes.has('canvas') || activeScopes.has('imageViewer');
});
const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
const $upDownHotkeysEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
// The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open.
return !activeScopes.has('canvas') && isGalleryPanelOpen;
});

View File

@@ -57,4 +57,4 @@ export const selectImageToCompare = createSelector(selectGallerySlice, (gallery)
export const selectHasImageToCompare = createSelector(selectImageToCompare, (imageToCompare) =>
Boolean(imageToCompare)
);
export const selectIsImageViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isImageViewerOpen);
export const selectIsMiniViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isMiniViewerOpen);

View File

@@ -21,11 +21,11 @@ const initialGalleryState: GalleryState = {
starredFirst: true,
orderDir: 'DESC',
searchTerm: '',
isImageViewerOpen: true,
imageToCompare: null,
comparisonMode: 'slider',
comparisonFit: 'fill',
shouldShowArchivedBoards: false,
isMiniViewerOpen: false,
};
export const gallerySlice = createSlice({
@@ -40,9 +40,6 @@ export const gallerySlice = createSlice({
},
imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.imageToCompare = action.payload;
if (action.payload) {
state.isImageViewerOpen = true;
}
},
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
state.comparisonMode = action.payload;
@@ -91,8 +88,8 @@ export const gallerySlice = createSlice({
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
state.alwaysShowImageSizeBadge = action.payload;
},
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
state.isImageViewerOpen = action.payload;
isMiniViewerOpenToggled: (state) => {
state.isMiniViewerOpen = !state.isMiniViewerOpen;
},
comparedImagesSwapped: (state) => {
if (state.imageToCompare) {
@@ -138,7 +135,6 @@ export const {
selectionChanged,
boardSearchTextChanged,
alwaysShowImageSizeBadgeChanged,
isImageViewerOpenChanged,
imageToCompareChanged,
comparisonModeChanged,
comparedImagesSwapped,
@@ -150,6 +146,7 @@ export const {
starredFirstChanged,
shouldShowArchivedBoardsChanged,
searchTermChanged,
isMiniViewerOpenToggled,
} = gallerySlice.actions;
export const selectGallerySlice = (state: RootState) => state.gallery;
@@ -166,13 +163,5 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
name: gallerySlice.name,
initialState: initialGalleryState,
migrate: migrateGalleryState,
persistDenylist: [
'selection',
'selectedBoardId',
'galleryView',
'offset',
'limit',
'isImageViewerOpen',
'imageToCompare',
],
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'imageToCompare'],
};

View File

@@ -27,6 +27,6 @@ export type GalleryState = {
imageToCompare: ImageDTO | null;
comparisonMode: ComparisonMode;
comparisonFit: ComparisonFit;
isImageViewerOpen: boolean;
shouldShowArchivedBoards: boolean;
isMiniViewerOpen: boolean;
};

View File

@@ -2,12 +2,13 @@ import 'reactflow/dist/style.css';
import { Flex } from '@invoke-ai/ui-library';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
import { memo } from 'react';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -19,8 +20,13 @@ import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
const NodeEditor = () => {
const { data, isLoading } = useGetOpenAPISchemaQuery();
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('workflows', ref);
return (
<Flex
tabIndex={-1}
ref={ref}
layerStyle="first"
position="relative"
width="full"

View File

@@ -34,5 +34,5 @@ export const BboxSettings = memo(() => {
BboxSettings.displayName = 'BboxSettings';
const formLabelProps: FormLabelProps = {
minW: 14,
minW: 10,
};

View File

@@ -0,0 +1,53 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 0.5, 1];
export const ParamDenoisingStrength = memo(() => {
const img2imgStrength = useAppSelector(selectImg2imgStrength);
const dispatch = useAppDispatch();
const onChange = useCallback(
(v: number) => {
dispatch(setImg2imgStrength(v));
},
[dispatch]
);
const config = useAppSelector(selectImg2imgStrengthConfig);
const { t } = useTranslation();
return (
<FormControl>
<InformationalPopover feature="paramDenoisingStrength">
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
</InformationalPopover>
<CompositeSlider
step={config.coarseStep}
fineStep={config.fineStep}
min={config.sliderMin}
max={config.sliderMax}
defaultValue={config.initial}
onChange={onChange}
value={img2imgStrength}
marks={marks}
/>
<CompositeNumberInput
step={config.coarseStep}
fineStep={config.fineStep}
min={config.numberInputMin}
max={config.numberInputMax}
defaultValue={config.initial}
onChange={onChange}
value={img2imgStrength}
/>
</FormControl>
);
});
ParamDenoisingStrength.displayName = 'ParamDenoisingStrength';

View File

@@ -18,7 +18,7 @@ export const ParamSeedRandomize = memo(() => {
return (
<FormControl w="min-content">
<FormLabel>{t('common.random')}</FormLabel>
<FormLabel m={0}>{t('common.random')}</FormLabel>
<Switch isChecked={shouldRandomizeSeed} onChange={handleChangeShouldRandomizeSeed} />
</FormControl>
);

View File

@@ -1,4 +1,4 @@
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
@@ -40,16 +40,18 @@ const ParamVAEModelSelect = () => {
return (
<FormControl isDisabled={!options.length} isInvalid={!options.length}>
<InformationalPopover feature="paramVAE">
<FormLabel>{t('modelManager.vae')}</FormLabel>
<FormLabel m={0}>{t('modelManager.vae')}</FormLabel>
</InformationalPopover>
<Combobox
isClearable
value={value}
placeholder={value ? value.value : t('models.defaultVAE')}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
<Box w="full" minW={0}>
<Combobox
isClearable
value={value}
placeholder={value ? value.value : t('models.defaultVAE')}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</Box>
</FormControl>
);
};

View File

@@ -12,7 +12,7 @@ const options = [
{ label: 'FP32', value: 'fp32' },
];
const ParamVAEModelSelect = () => {
const ParamVAEPrecision = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const vaePrecision = useAppSelector(selectVAEPrecision);
@@ -31,13 +31,13 @@ const ParamVAEModelSelect = () => {
const value = useMemo(() => options.find((o) => o.value === vaePrecision), [vaePrecision]);
return (
<FormControl w="14rem" flexShrink={0}>
<FormControl w={24}>
<InformationalPopover feature="paramVAEPrecision">
<FormLabel>{t('modelManager.vaePrecision')}</FormLabel>
<FormLabel m={0}>{t('modelManager.vaePrecision')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} onChange={onChange} />
</FormControl>
);
};
export default memo(ParamVAEModelSelect);
export default memo(ParamVAEPrecision);

View File

@@ -1,25 +1,21 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
import ProgressBar from 'features/system/components/ProgressBar';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
const QueueControls = () => {
const isPrependEnabled = useFeatureStatus('prependQueue');
const tab = useAppSelector(selectActiveTab);
return (
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
<Flex gap={2}>
{isPrependEnabled && <QueueFrontButton />}
<InvokeQueueBackButton />
<Spacer />
{tab === 'generation' && <CanvasSendToToggle />}
<ClearQueueIconButton />
</Flex>
<ProgressBar />

View File

@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { $isParametersPanelOpen, TABS_WITH_OPTIONS_PANEL } from 'features/ui/store/uiSlice';
import { $isLeftPanelOpen, TABS_WITH_LEFT_PANEL } from 'features/ui/store/uiSlice';
import type { RefObject } from 'react';
import { memo, useEffect, useState } from 'react';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
@@ -13,13 +13,13 @@ type Props = {
};
const selectActiveTabShouldShowBadge = createSelector(selectActiveTab, (activeTab) =>
TABS_WITH_OPTIONS_PANEL.includes(activeTab)
TABS_WITH_LEFT_PANEL.includes(activeTab)
);
export const QueueCountBadge = memo(({ targetRef }: Props) => {
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
const activeTabShouldShowBadge = useAppSelector(selectActiveTabShouldShowBadge);
const isParametersPanelOpen = useStore($isParametersPanelOpen);
const isParametersPanelOpen = useStore($isLeftPanelOpen);
const { queueSize } = useGetQueueStatusQuery(undefined, {
selectFromResult: (res) => ({
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
@@ -39,7 +39,7 @@ export const QueueCountBadge = memo(({ targetRef }: Props) => {
}
const cb = () => {
if (!$isParametersPanelOpen.get()) {
if (!$isLeftPanelOpen.get()) {
return;
}
const { x, y } = target.getBoundingClientRect();

View File

@@ -1,4 +1,4 @@
import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
@@ -6,6 +6,7 @@ import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/
import { zModelIdentifierField } from 'features/nodes/types/common';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useRefinerModels } from 'services/api/hooks/modelsByType';
import type { MainModelConfig } from 'services/api/types';
@@ -33,21 +34,32 @@ const ParamSDXLRefinerModelSelect = () => {
isLoading,
optionsFilter,
});
const onReset = useCallback(() => {
_onChange(null);
}, [_onChange]);
return (
<FormControl isDisabled={!options.length} isInvalid={!options.length} w="full">
<InformationalPopover feature="refinerModel">
<FormLabel>{t('sdxl.refinermodel')}</FormLabel>
</InformationalPopover>
<Box w="full" minW={0}>
<Flex w="full" minW={0} gap={2} alignItems="center">
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
isClearable
/>
</Box>
<IconButton
size="sm"
variant="ghost"
icon={<PiXBold />}
aria-label={t('common.reset')}
onClick={onReset}
isDisabled={!value}
/>
</Flex>
</FormControl>
);
};

View File

@@ -9,6 +9,7 @@ import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeig
import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth';
import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod';
import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings';
import { ParamDenoisingStrength } from 'features/parameters/components/Core/ParamDenoisingStrength';
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
@@ -67,11 +68,12 @@ export const ImageSettingsAccordion = memo(() => {
>
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
<BboxSettings />
<Flex pt={4} gap={4} alignItems="center">
<Flex py={3} gap={4} alignItems="center">
<ParamSeedNumberInput />
<ParamSeedShuffle />
<ParamSeedRandomize />
</Flex>
<ParamDenoisingStrength />
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} pb={4} flexDir="column">
<BboxScaleMethod />

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 {
/**