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" - /> + + + + + + + + + + + + + + ); }); 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')} - - - - - } - 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 ( - - ); -}; - -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 { /**