From 3ed29a16a8564e710895500cfc4f6391aec06d9d Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 9 Sep 2024 21:53:36 +1000
Subject: [PATCH] feat(ui): reworked layout (wip)
---
invokeai/frontend/web/public/locales/en.json | 16 +-
.../app/components/ThemeLocaleProvider.tsx | 10 +-
.../listeners/enqueueRequestedUpscale.ts | 5 -
.../listeners/imageDropped.ts | 3 +-
.../web/src/common/components/IAIDndImage.tsx | 48 +++-
.../web/src/common/hooks/useGlobalHotkeys.ts | 9 +
.../components/CanvasDropArea.tsx | 7 -
.../components/CanvasEditor.stories.tsx | 24 --
.../EntityListGlobalActionBar.tsx | 2 +-
.../EntityListSelectedEntityActionBar.tsx | 2 +-
.../components/CanvasPanelContent.tsx | 3 -
.../components/CanvasRightPanel.tsx | 83 +++++++
.../components/CanvasSendToToggle.tsx | 116 +++++----
...{CanvasEditor.tsx => CanvasTabContent.tsx} | 4 +-
.../IPAdapter/IPAdapterImagePreview.tsx | 2 +-
.../components/Toolbar/CanvasToolbar.tsx | 4 -
.../store/canvasSettingsSlice.ts | 1 +
.../features/dnd/components/DndOverlay.tsx | 1 +
.../features/gallery/components/Gallery.tsx | 80 ++++--
.../components/GalleryPanelContent.tsx | 55 ++--
.../GallerySettingsPopover.tsx | 8 +-
.../ImageMenuItemOpenInViewer.tsx | 7 +-
.../ImageMenuItemSendToCanvas.tsx | 5 +-
.../components/ImageGrid/GalleryImage.tsx | 24 +-
.../components/ImageGrid/GalleryImageGrid.tsx | 45 +++-
.../ImageGrid/GallerySelectionCountTag.tsx | 4 +-
.../components/ImageViewer/ImageViewer.tsx | 3 +-
.../ImageViewer/ViewerToggleMenu.tsx | 61 -----
.../components/ImageViewer/ViewerToolbar.tsx | 16 +-
.../components/ImageViewer/useImageViewer.ts | 73 ------
.../gallery/hooks/useGalleryHotkeys.ts | 4 +-
.../gallery/store/gallerySelectors.ts | 2 +-
.../features/gallery/store/gallerySlice.ts | 21 +-
.../web/src/features/gallery/store/types.ts | 2 +-
.../features/nodes/components/NodeEditor.tsx | 8 +-
.../components/Bbox/BboxSettings.tsx | 2 +-
.../Core/ParamDenoisingStrength.tsx | 53 ++++
.../components/Seed/ParamSeedRandomize.tsx | 2 +-
.../VAEModel/ParamVAEModelSelect.tsx | 22 +-
.../components/VAEModel/ParamVAEPrecision.tsx | 8 +-
.../queue/components/QueueControls.tsx | 6 +-
.../queue/components/QueueCountBadge.tsx | 8 +-
.../ParamSDXLRefinerModelSelect.tsx | 20 +-
.../ImageSettingsAccordion.tsx | 4 +-
.../src/features/ui/components/AppContent.tsx | 234 ++++++++++--------
.../ParametersPanelTextToImage.tsx | 78 +-----
.../features/ui/components/VerticalNavBar.tsx | 5 +-
.../features/ui/components/tabs/NodesTab.tsx | 37 ---
.../components/tabs/WorkflowsTabContent.tsx | 22 ++
.../web/src/features/ui/hooks/usePanel.ts | 6 +
.../web/src/features/ui/store/uiSlice.ts | 17 +-
.../web/src/features/ui/store/uiTypes.ts | 2 +-
52 files changed, 656 insertions(+), 628 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.stories.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{CanvasEditor.tsx => CanvasTabContent.tsx} (94%)
delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx
delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Core/ParamDenoisingStrength.tsx
delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index ad22a875d7..d3c7edbd46 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -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 Alt while clicking a gallery image or using the arrow keys to change the compare image.",
"compareHelp2": "Press M to cycle through comparison modes.",
"compareHelp3": "Press C to swap the compared images.",
- "compareHelp4": "Press Z or Esc to exit."
+ "compareHelp4": "Press Z or Esc 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",
diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
index aa3a24209c..325db314d5 100644
--- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
+++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx
@@ -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]);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts
index cbfaac6227..624e9e54b3 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts
@@ -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();
}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
index e09cd9589e..4bdea623b2 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
@@ -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;
}
diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
index f16aa3d4b4..c4e5f01486 100644
--- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
@@ -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 = ;
const defaultNoContentFallback = ;
+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) => void;
@@ -75,13 +108,11 @@ const IAIDndImage = (props: IAIDndImageProps) => {
...rest
} = props;
- const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(
(e: MouseEvent) => {
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 && (
{
data-testid={dataTestId}
/>
{withMetadataOverlay && }
-
)}
{!imageDTO && !isUploadDisabled && (
diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
index 634e2ead39..9a40d55841 100644
--- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
+++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
@@ -114,4 +114,13 @@ export const useGlobalHotkeys = () => {
},
[dispatch, isModelManagerEnabled]
);
+
+ useHotkeys(
+ isModelManagerEnabled ? '6' : '5',
+ () => {
+ dispatch(setActiveTab('gallery'));
+ setScopes([]);
+ },
+ [dispatch, isModelManagerEnabled]
+ );
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
index d341c1c6d9..499cbcba7d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx
@@ -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 (
<>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.stories.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.stories.tsx
deleted file mode 100644
index 6fde45c040..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.stories.tsx
+++ /dev/null
@@ -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 = {
- title: 'Feature/ControlLayers',
- tags: ['autodocs'],
- component: CanvasEditor,
-};
-
-export default meta;
-type Story = StoryObj;
-
-const Component = () => {
- return (
-
-
-
- );
-};
-
-export const Default: Story = {
- render: Component,
-};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx
index 3d495db228..4803a3f353 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx
@@ -6,7 +6,7 @@ import { memo } from 'react';
export const EntityListGlobalActionBar = memo(() => {
return (
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx
index b9e511e3e5..0b100d62fc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx
@@ -8,7 +8,7 @@ import { memo } from 'react';
export const EntityListSelectedEntityActionBar = memo(() => {
return (
-
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx
index 90d20222b6..87971e31f0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx
@@ -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 (
-
-
{!hasEntities && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
new file mode 100644
index 0000000000..2e118f2f80
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx
@@ -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(null);
+ const [tab, setTab] = useState(0);
+ useScopeOnFocus('gallery', ref);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+CanvasRightPanelContent.displayName = 'CanvasRightPanelContent';
+
+const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
+ const { t } = useTranslation();
+ const sendToCanvas = useAppSelector(selectSendToCanvas);
+ const tabTimeout = useRef(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 (
+ <>
+
+ {t('gallery.gallery')}
+ {!sendToCanvas && (
+
+ )}
+
+
+ {t('controlLayers.layer_other')}
+ {sendToCanvas && (
+
+ )}
+
+ >
+ );
+});
+
+PanelTabs.displayName = 'PanelTabs';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx
index ad6311ffb2..8e44f44a7f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx
@@ -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 (
-
- {t('controlLayers.sendToGallery')}
- {t('controlLayers.sendToGalleryDesc')}
-
- );
-});
-
-TooltipSendToGallery.displayName = 'TooltipSendToGallery';
-
-const TooltipSendToCanvas = memo(() => {
- const { t } = useTranslation();
-
- return (
-
- {t('controlLayers.sendToCanvas')}
- {t('controlLayers.sendToCanvasDesc')}
-
- );
-});
-
-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 (
- }
- tooltipUnchecked={}
- iconChecked={}
- tooltipChecked={}
- ariaLabel="Toggle canvas mode"
- />
+
+
+ }
+ >
+ {sendToCanvas ? t('controlLayers.sendingToCanvas') : t('controlLayers.sendingToGallery')}
+
+
+
+
+
+
+
+
+
+
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx
similarity index 94%
rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx
index 8b346c1891..c19bdd3217 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEditor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx
@@ -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(null);
useScopeOnFocus('canvas', ref);
@@ -48,4 +48,4 @@ export const CanvasEditor = memo(() => {
);
});
-CanvasEditor.displayName = 'CanvasEditor';
+CanvasTabContent.displayName = 'CanvasTabContent';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
index efffc0ff0a..699ea23fe6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx
@@ -85,7 +85,7 @@ export const IPAdapterImagePreview = memo(
/>
{controlImage && (
-
+
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
index 6662631873..b7800051a4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx
@@ -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 (
-
@@ -38,7 +35,6 @@ export const CanvasToolbar = memo(() => {
-
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
index 9edcc482bc..cea62ced15 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts
@@ -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);
diff --git a/invokeai/frontend/web/src/features/dnd/components/DndOverlay.tsx b/invokeai/frontend/web/src/features/dnd/components/DndOverlay.tsx
index 34067c9b46..883d79c87f 100644
--- a/invokeai/frontend/web/src/features/dnd/components/DndOverlay.tsx
+++ b/invokeai/frontend/web/src/features/dnd/components/DndOverlay.tsx
@@ -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',
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 5f4ffdf015..fdbe7de50f 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -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 (
-
+
@@ -81,28 +87,54 @@ export const Gallery = () => {
{t('gallery.assets')}
- }
- colorScheme={searchDisclosure.isOpen ? 'invokeBlue' : 'base'}
- variant="link"
- />
+
+ : }
+ colorScheme={isMiniViewerOpen ? 'invokeBlue' : 'base'}
+ />
+ }
+ />
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
index 2406be7a08..d459689832 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx
@@ -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 (
-
-
-
-
+
+ : }
+ >
+ {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
+
+
+
+
+ : }
- >
- {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
-
-
-
-
- }
- colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
- variant="link"
- />
-
-
+ variant="link"
+ alignSelf="stretch"
+ onClick={handleClickBoardSearch}
+ tooltip={
+ boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
+ }
+ aria-label={t('gallery.displayBoardSearch')}
+ icon={}
+ colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
+ />
diff --git a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
index c6a4005f15..301a182831 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover/GallerySettingsPopover.tsx
@@ -17,7 +17,13 @@ const GallerySettingsPopover = () => {
return (
- } variant="link" h="full" />
+ }
+ />
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx
index cb5313cd18..6f4962a3c7 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx
@@ -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 (
} onClick={onClick}>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas.tsx
index 6960a19071..75732a134f 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas.tsx
@@ -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 (
} onClickCapture={handleSendToCanvas} id="send-to-canvas">
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index e2fa43ef6f..7dca9ecac2 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -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 (
-
+
{
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}`}
@@ -199,8 +195,8 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
icon={starIcon}
tooltip={starTooltip}
position="absolute"
- top={1}
- insetInlineEnd={1}
+ top={2}
+ insetInlineEnd={2}
/>
{isHovered && }
@@ -227,8 +223,8 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
icon={}
tooltip={t('gallery.deleteImage_one')}
position="absolute"
- bottom={1}
- insetInlineEnd={1}
+ bottom={2}
+ insetInlineEnd={2}
/>
);
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
index 9d40eb6afc..68a009afad 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx
@@ -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 = () => {
{imageDTOs.map((imageDTO, index) => (
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx
index 439388e2c5..bd7faf86e0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx
@@ -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;
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index 431af16e23..414539942c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -21,7 +21,8 @@ export const ImageViewer = memo(() => {
{
- const { t } = useTranslation();
-
- return (
-
- {t('common.edit')}
- {t('common.editDesc')}
-
- );
-});
-TooltipEdit.displayName = 'TooltipEdit';
-
-const TooltipView = memo(() => {
- const { t } = useTranslation();
-
- return (
-
- {t('common.view')}
- {t('common.viewDesc')}
-
- );
-});
-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 (
- }
- tooltipUnchecked={}
- iconChecked={}
- tooltipChecked={}
- ariaLabel="Toggle viewer"
- />
- );
-});
-
-ViewerToggle.displayName = 'ViewerToggle';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx
index 0d00941077..5a66e34039 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx
@@ -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 (
@@ -30,9 +18,7 @@ export const ViewerToolbar = memo(() => {
-
- {showToggle && }
-
+
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
deleted file mode 100644
index 985e2c7a38..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts
+++ /dev/null
@@ -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 };
-};
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
index ec71b84462..a84c81769d 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
@@ -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;
});
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
index a8c327494e..8f1e866875 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts
@@ -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);
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index 487c07dbc2..69e8d6ac4c 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -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) => {
state.imageToCompare = action.payload;
- if (action.payload) {
- state.isImageViewerOpen = true;
- }
},
comparisonModeChanged: (state, action: PayloadAction) => {
state.comparisonMode = action.payload;
@@ -91,8 +88,8 @@ export const gallerySlice = createSlice({
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction) => {
state.alwaysShowImageSizeBadge = action.payload;
},
- isImageViewerOpenChanged: (state, action: PayloadAction) => {
- 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 = {
name: gallerySlice.name,
initialState: initialGalleryState,
migrate: migrateGalleryState,
- persistDenylist: [
- 'selection',
- 'selectedBoardId',
- 'galleryView',
- 'offset',
- 'limit',
- 'isImageViewerOpen',
- 'imageToCompare',
- ],
+ persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'imageToCompare'],
};
diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts
index 7d7a321515..4ded8c494e 100644
--- a/invokeai/frontend/web/src/features/gallery/store/types.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/types.ts
@@ -27,6 +27,6 @@ export type GalleryState = {
imageToCompare: ImageDTO | null;
comparisonMode: ComparisonMode;
comparisonFit: ComparisonFit;
- isImageViewerOpen: boolean;
shouldShowArchivedBoards: boolean;
+ isMiniViewerOpen: boolean;
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index 0a118b9d44..e1c41daba6 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -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(null);
+ useScopeOnFocus('workflows', ref);
+
return (
{
BboxSettings.displayName = 'BboxSettings';
const formLabelProps: FormLabelProps = {
- minW: 14,
+ minW: 10,
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamDenoisingStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamDenoisingStrength.tsx
new file mode 100644
index 0000000000..a840573647
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamDenoisingStrength.tsx
@@ -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 (
+
+
+ {`${t('parameters.denoisingStrength')}`}
+
+
+
+
+ );
+});
+
+ParamDenoisingStrength.displayName = 'ParamDenoisingStrength';
diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx
index bd523089d4..ba41887e74 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx
@@ -18,7 +18,7 @@ export const ParamSeedRandomize = memo(() => {
return (
- {t('common.random')}
+ {t('common.random')}
);
diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
index 3544bfe9bd..4f6eda60c8 100644
--- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx
@@ -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 (
- {t('modelManager.vae')}
+ {t('modelManager.vae')}
-
+
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx
index aaafa894e4..670b9e1af9 100644
--- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx
@@ -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 (
-
+
- {t('modelManager.vaePrecision')}
+ {t('modelManager.vaePrecision')}
);
};
-export default memo(ParamVAEModelSelect);
+export default memo(ParamVAEPrecision);
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
index b12cc7705f..bf323a8c8d 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx
@@ -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 (
{isPrependEnabled && }
- {tab === 'generation' && }
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueCountBadge.tsx b/invokeai/frontend/web/src/features/queue/components/QueueCountBadge.tsx
index 2facdb3c96..7af8739d9a 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueCountBadge.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueCountBadge.tsx
@@ -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();
diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
index e9f05d20ad..75b00624a6 100644
--- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx
@@ -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 (
{t('sdxl.refinermodel')}
-
+
-
+ }
+ aria-label={t('common.reset')}
+ onClick={onReset}
+ isDisabled={!value}
+ />
+
);
};
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx
index 31649cb35b..10f6f05269 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx
@@ -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(() => {
>
-
+
+
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 73841a89c4..f881d86097 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -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(null);
- const isImageViewerOpen = useIsImageViewerOpen();
- const shouldShowGalleryPanel = useAppSelector(selectShouldShowGalleryPanel);
- const shouldShowOptionsPanel = useAppSelector(selectShouldShowOptionsPanel);
const ref = useRef(null);
useScopeOnFocus('gallery', ref);
- const optionsPanelUsePanelOptions = useMemo(
- () => ({
- 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(
- () => ({
- id: 'gallery-panel',
- unit: 'pixels',
- minSize: GALLERY_MIN_SIZE_PX,
- defaultSize: GALLERY_MIN_SIZE_PCT,
- panelGroupRef,
- panelGroupDirection: 'horizontal',
- onCollapse: onGalleryPanelCollapse,
- }),
- []
- );
-
+ const panelGroupRef = useRef(null);
const panelStorage = usePanelStorage();
- const optionsPanel = usePanel(optionsPanelUsePanelOptions);
+ const withLeftPanel = useAppSelector(selectWithLeftPanel);
+ const leftPanelUsePanelOptions = useMemo(
+ () => ({
+ 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(
+ () => ({
+ 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}
>
-
-
-
-
+ {withLeftPanel && (
+ <>
+
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
+
+
+ >
+ )}
-
+
+
+
+
+
+
- {/* upscaling tab has no content of its own - uses image viewer only */}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {isImageViewerOpen && }
-
-
-
-
+ {withRightPanel && (
+ <>
+
+
+
+
+ >
+ )}
- {shouldShowOptionsPanel && }
- {shouldShowGalleryPanel && }
-
-
-
-
-
-
-
-
-
-
+ {withLeftPanel && }
+ {withRightPanel && }
);
});
AppContent.displayName = 'AppContent';
+
+const RightPanelContent = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+
+ if (tab === 'generation') {
+ return ;
+ }
+
+ return ;
+});
+RightPanelContent.displayName = 'RightPanelContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx
index 986d9f0b33..9eb3447225 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx
@@ -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(null);
const isMenuOpen = useStore($isMenuOpen);
return (
-
+
{isMenuOpen && (
@@ -68,43 +40,11 @@ const ParametersPanelTextToImage = () => {
-
-
-
- {t('common.settingsLabel')}
-
-
- {t('controlLayers.layer_other')}
-
-
-
-
-
-
-
-
- {isSDXL && }
-
-
-
-
-
-
-
-
+
+
+
+ {isSDXL && }
+
diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
index 28168b563e..cdf78f4469 100644
--- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx
@@ -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(() => {
} label={t('ui.tabs.queue')} />
+
+ } label={t('ui.tabs.gallery')} />
+
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx
deleted file mode 100644
index 157a875cfd..0000000000
--- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx
+++ /dev/null
@@ -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(null);
- useScopeOnFocus('workflows', ref);
-
- return (
-
- {mode === 'edit' && (
-
-
-
- )}
-
- );
-};
-
-export default memo(NodesTab);
diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
new file mode 100644
index 0000000000..72c6082316
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/components/tabs/WorkflowsTabContent.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ return ;
+});
+
+WorkflowsTabContent.displayName = 'WorkflowsTabContent';
diff --git a/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts b/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts
index d3711d1344..23d7b0aab5 100644
--- a/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts
+++ b/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts
@@ -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) {
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index cc24ff9c11..2877bc3b80 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -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 = {
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));
diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
index 4e863fffdc..c7c34a91fe 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts
@@ -1,4 +1,4 @@
-export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue';
+export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue' | 'gallery';
export interface UIState {
/**