From e7e1142c77b27d2edddef26ac1381841ddc6f24e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:49:37 +1000 Subject: [PATCH] feat(ui): get layouts working --- .../components/CanvasLayersPanelContent.tsx | 2 +- .../SimpleSession/CanvasLaunchpadPanel.tsx | 2 +- .../SimpleSession/GenerateLaunchpadPanel.tsx | 2 +- .../SimpleSession/UpscalingLaunchpadPanel.tsx | 13 + .../components/Toolbar/CanvasToolbar.tsx | 7 +- .../src/features/system/store/configSlice.ts | 11 + .../src/features/ui/components/AppContent.tsx | 29 +- ...tToImage.tsx => ParametersPanelCanvas.tsx} | 6 +- .../ParametersPanelGenerate.tsx | 58 +++ .../ParametersPanelUpscale.tsx | 6 +- .../src/features/ui/components/TabButton.tsx | 5 +- .../ui/layouts/CanvasTabLeftPanel.tsx | 16 + .../ui/layouts/CanvasWorkspacePanel.tsx | 1 + .../ui/layouts/GenerateTabLeftPanel.tsx | 16 + .../ui/layouts/UpscalingTabLeftPanel.tsx | 16 + .../ui/layouts/canvas-tab-auto-layout.tsx | 352 ++++++------------ .../ui/layouts/generate-tab-auto-layout.tsx | 182 ++++----- .../ui/layouts/upscaling-tab-auto-layout.tsx | 191 ++++++++++ .../features/ui/layouts/use-did-render-tab.ts | 25 ++ .../ui/layouts/use-on-first-visible.ts | 48 +++ .../web/src/features/ui/store/uiSelectors.ts | 4 + .../web/src/features/ui/store/uiTypes.ts | 1 + 22 files changed, 641 insertions(+), 352 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx rename invokeai/frontend/web/src/features/ui/components/ParametersPanels/{ParametersPanelTextToImage.tsx => ParametersPanelCanvas.tsx} (96%) create mode 100644 invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/CanvasTabLeftPanel.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx index 3b28be2799..49ae65205b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx @@ -14,7 +14,7 @@ export const CanvasLayersPanel = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx index 4e628e6f46..4534cae881 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx @@ -11,7 +11,7 @@ export const CanvasLaunchpadPanel = memo(() => { return ( - Get started with Invoke. + Edit and refine on Canvas. diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx index 3999f0580f..2452296fe8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx @@ -16,7 +16,7 @@ export const GenerateLaunchpadPanel = memo(() => { return ( - Get started with Invoke. + Generate images from text prompts. diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx new file mode 100644 index 0000000000..6279cf5970 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/UpscalingLaunchpadPanel.tsx @@ -0,0 +1,13 @@ +import { Flex, Heading } from '@invoke-ai/ui-library'; +import { memo } from 'react'; + +export const UpscalingLaunchpadPanel = memo(() => { + return ( + + + Upscale and add detail. + + + ); +}); +UpscalingLaunchpadPanel.displayName = 'UpscalingLaunchpadPanel'; 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 a233a14f13..077b9ff187 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx @@ -1,4 +1,4 @@ -import { Divider, Flex, Heading } from '@invoke-ai/ui-library'; +import { Divider, Flex } from '@invoke-ai/ui-library'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings'; @@ -29,10 +29,6 @@ export const CanvasToolbar = memo(() => { return ( - - Canvas - - @@ -48,7 +44,6 @@ export const CanvasToolbar = memo(() => { - ); }); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index bfe72e5df0..13dd2be08c 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -2,6 +2,8 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { ALL_TABS } from 'features/ui/store/uiTypes'; import { merge } from 'lodash-es'; const baseDimensionConfig: NumericalParameterConfig = { @@ -225,3 +227,12 @@ export const selectIsClientSideUploadEnabled = createConfigSelector((config) => export const selectAllowPublishWorkflows = createConfigSelector((config) => config.allowPublishWorkflows); export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal); export const selectShouldShowCredits = createConfigSelector((config) => config.shouldShowCredits); +export const selectEnabledTabs = createConfigSelector((config) => { + const enabledTabs: TabName[] = []; + for (const tab of ALL_TABS) { + if (!config.disabledTabs.includes(tab)) { + enabledTabs.push(tab); + } + } + return enabledTabs; +}); diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index c66fdfb6c8..2e94ba0990 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -1,10 +1,14 @@ import 'dockview/dist/styles/dockview.css'; import 'features/ui/styles/dockview-theme-invoke.css'; -import { Flex } from '@invoke-ai/ui-library'; +import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { useDndMonitor } from 'features/dnd/useDndMonitor'; import { VerticalNavBar } from 'features/ui/components/VerticalNavBar'; -import { AutoLayout } from 'features/ui/layouts/AutoLayout'; +import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout'; +import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout'; +import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout'; +import { selectActiveTabIndex } from 'features/ui/store/uiSelectors'; import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice'; import type { CSSProperties } from 'react'; import { memo } from 'react'; @@ -16,6 +20,7 @@ const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!is export const AppContent = memo(() => { // const tab = useAppSelector(selectActiveTab); + const tabIndex = useAppSelector(selectActiveTabIndex); // const imperativePanelGroupRef = useRef(null); useDndMonitor(); @@ -93,10 +98,22 @@ export const AppContent = memo(() => { // }); return ( - - - - + + + + + + + + + + + + + + + + ); }); AppContent.displayName = 'AppContent'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx similarity index 96% rename from invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx rename to invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx index 26ff557bf5..a51aa59a10 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx @@ -22,7 +22,7 @@ const overlayScrollbarsStyles: CSSProperties = { width: '100%', }; -const ParametersPanelTextToImage = () => { +export const ParametersPanelCanvas = memo(() => { const isSDXL = useAppSelector(selectIsSDXL); const isCogview4 = useAppSelector(selectIsCogView4); const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen); @@ -55,6 +55,6 @@ const ParametersPanelTextToImage = () => { ); -}; +}); -export default memo(ParametersPanelTextToImage); +ParametersPanelCanvas.displayName = 'ParametersPanelCanvas'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx new file mode 100644 index 0000000000..de15d4f706 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx @@ -0,0 +1,58 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice'; +import { Prompts } from 'features/parameters/components/Prompts/Prompts'; +import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel'; +import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; +import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; +import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion'; +import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; +import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu'; +import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger'; +import { $isStylePresetsMenuOpen } from 'features/stylePresets/store/stylePresetSlice'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import type { CSSProperties } from 'react'; +import { memo } from 'react'; + +const overlayScrollbarsStyles: CSSProperties = { + height: '100%', + width: '100%', +}; + +export const ParametersPanelGenerate = memo(() => { + const isSDXL = useAppSelector(selectIsSDXL); + const isCogview4 = useAppSelector(selectIsCogView4); + const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen); + + const isApiModel = useIsApiModel(); + + return ( + + + + + {isStylePresetsMenuOpen && ( + + + + + + )} + + + + + + {isSDXL && } + {!isCogview4 && !isApiModel && } + + + + + + ); +}); + +ParametersPanelGenerate.displayName = 'ParametersPanelGenerate'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx index 830bcd5709..2903d765b5 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx @@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = { width: '100%', }; -const ParametersPanelUpscale = () => { +export const ParametersPanelUpscale = memo(() => { const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen); return ( @@ -44,6 +44,6 @@ const ParametersPanelUpscale = () => { ); -}; +}); -export default memo(ParametersPanelUpscale); +ParametersPanelUpscale.displayName = 'ParametersPanelUpscale'; diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index 8974a9c326..6bcc351618 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx @@ -1,5 +1,5 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { IconButton, Tooltip } from '@invoke-ai/ui-library'; +import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; @@ -26,13 +26,14 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React return ( { + return ( + + + + + + + ); +}); +CanvasTabLeftPanel.displayName = 'CanvasTabLeftPanel'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx index 1a2a592ddd..a2813fefa1 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -66,6 +66,7 @@ export const CanvasWorkspacePanel = memo(() => { alignItems="center" justifyContent="center" overflow="hidden" + p={2} > diff --git a/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx new file mode 100644 index 0000000000..598a51d142 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/GenerateTabLeftPanel.tsx @@ -0,0 +1,16 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import QueueControls from 'features/queue/components/QueueControls'; +import { ParametersPanelGenerate } from 'features/ui/components/ParametersPanels/ParametersPanelGenerate'; +import { memo } from 'react'; + +export const GenerateTabLeftPanel = memo(() => { + return ( + + + + + + + ); +}); +GenerateTabLeftPanel.displayName = 'GenerateTabLeftPanel'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx new file mode 100644 index 0000000000..ea5e006aba --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/UpscalingTabLeftPanel.tsx @@ -0,0 +1,16 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import QueueControls from 'features/queue/components/QueueControls'; +import { ParametersPanelUpscale } from 'features/ui/components/ParametersPanels/ParametersPanelUpscale'; +import { memo } from 'react'; + +export const UpscalingTabLeftPanel = memo(() => { + return ( + + + + + + + ); +}); +UpscalingTabLeftPanel.displayName = 'UpscalingTabLeftPanel'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx index 66c200db3f..ab88198660 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx @@ -1,225 +1,71 @@ -import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; -import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading'; -import { useAppSelector } from 'app/store/storeHooks'; import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, Orientation } from 'dockview'; -import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress'; -import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask'; -import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus'; -import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems'; -import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems'; -import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent'; -import { Filter } from 'features/controlLayers/components/Filters/Filter'; -import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD'; -import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent'; -import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject'; -import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context'; -import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel'; -import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList'; -import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; -import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; -import { Transform } from 'features/controlLayers/components/Transform/Transform'; -import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { CanvasLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; -import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; -import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; -import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; -import QueueControls from 'features/queue/components/QueueControls'; -import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage'; +import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel'; +import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; -import { components } from 'features/ui/layouts/components'; import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton'; import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice'; import { dockviewTheme } from 'features/ui/styles/theme'; -import { memo, useCallback, useState } from 'react'; -import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; +import { atom } from 'nanostores'; +import { memo, useCallback, useRef, useState } from 'react'; -const MenuContent = memo(() => { - return ( - - - - - - - ); -}); -MenuContent.displayName = 'MenuContent'; +import { CanvasTabLeftPanel } from './CanvasTabLeftPanel'; +import { CanvasWorkspacePanel } from './CanvasWorkspacePanel'; +import { useOnFirstVisible } from './use-on-first-visible'; -const canvasBgSx = { - position: 'relative', - w: 'full', - h: 'full', - borderRadius: 'base', - overflow: 'hidden', - bg: 'base.900', - '&[data-dynamic-grid="true"]': { - bg: 'base.850', - }, -}; - -export const CanvasWorkspacePanel = memo(() => { - const dynamicGrid = useAppSelector(selectDynamicGrid); - const showHUD = useAppSelector(selectShowHUD); - const canvasId = useAppSelector(selectCanvasSessionId); - - const renderMenu = useCallback(() => { - return ; - }, []); - - return ( - - - - - - renderMenu={renderMenu} withLongPress={false}> - {(ref) => ( - - - - - {showHUD && } - - - - - - - } colorScheme="base" /> - - - - - - )} - - {canvasId !== null && ( - - - - - - - - - - - - - )} - - - - - - - - - - - - ); -}); -CanvasWorkspacePanel.displayName = 'CanvasPanel'; - -const LayersPanelContent = memo(() => ( - - - -)); -LayersPanelContent.displayName = 'LayersPanelContent'; - -const ViewerPanelContent = memo(() => ( - - - - - -)); -ViewerPanelContent.displayName = 'ViewerPanelContent'; - -const ProgressPanelContent = memo(() => ( - - - -)); -ProgressPanelContent.displayName = 'ProgressPanelContent'; +const LAUNCHPAD_PANEL_ID = 'launchpad'; +const WORKSPACE_PANEL_ID = 'workspace'; +const VIEWER_PANEL_ID = 'viewer'; +const PROGRESS_PANEL_ID = 'progress'; const mainPanelComponents: IDockviewReactProps['components'] = { - canvasLaunchpad: GenerateLaunchpadPanel, - canvas: CanvasWorkspacePanel, - viewer: ViewerPanelContent, - progress: ProgressPanelContent, + [LAUNCHPAD_PANEL_ID]: CanvasLaunchpadPanel, + [WORKSPACE_PANEL_ID]: CanvasWorkspacePanel, + [VIEWER_PANEL_ID]: ImageViewerPanel, + [PROGRESS_PANEL_ID]: GenerationProgressPanel, }; const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { const { api } = event; api.addPanel({ - id: 'canvasLaunchpad', - component: 'canvasLaunchpad', - title: 'canvasLaunchpad', + id: LAUNCHPAD_PANEL_ID, + component: LAUNCHPAD_PANEL_ID, + title: 'Launchpad', }); api.addPanel({ - id: 'canvas', - component: 'canvas', + id: WORKSPACE_PANEL_ID, + component: WORKSPACE_PANEL_ID, title: 'Canvas', position: { direction: 'within', - referencePanel: 'canvasLaunchpad', + referencePanel: LAUNCHPAD_PANEL_ID, }, }); api.addPanel({ - id: 'viewer', - component: 'viewer', + id: VIEWER_PANEL_ID, + component: VIEWER_PANEL_ID, title: 'Image Viewer', position: { direction: 'within', - referencePanel: 'canvasLaunchpad', + referencePanel: LAUNCHPAD_PANEL_ID, }, }); api.addPanel({ - id: 'progress', - component: 'progress', + id: PROGRESS_PANEL_ID, + component: PROGRESS_PANEL_ID, title: 'Generation Progress', position: { direction: 'within', - referencePanel: 'canvasLaunchpad', + referencePanel: LAUNCHPAD_PANEL_ID, }, }); + api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); + const disposables = [ api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { @@ -238,102 +84,122 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { const MainPanel = memo(() => { return ( - - - + ); }); MainPanel.displayName = 'MainPanel'; -const Left = memo(() => { - return ( - - - - - - - ); -}); -Left.displayName = 'Left'; - -const Null = () => null; +const LEFT_PANEL_ID = 'left'; +const MAIN_PANEL_ID = 'main'; +const BOARDS_PANEL_ID = 'boards'; +const GALLERY_PANEL_ID = 'gallery'; +const LAYERS_PANEL_ID = 'layers'; export const canvasTabComponents: IGridviewReactProps['components'] = { - left: Left, - main: MainPanel, - boards: BoardsPanel, - gallery: GalleryPanel, - layers: LayersPanelContent, + [LEFT_PANEL_ID]: CanvasTabLeftPanel, + [MAIN_PANEL_ID]: MainPanel, + [BOARDS_PANEL_ID]: BoardsPanel, + [GALLERY_PANEL_ID]: GalleryPanel, + [LAYERS_PANEL_ID]: CanvasLayersPanel, }; -export const initializeCanvasTabLayout = (api: GridviewApi) => { - const main = api.addPanel({ - id: 'main', - component: 'main', - minimumWidth: 256, +export const initializeLayout = (api: GridviewApi) => { + api.addPanel({ + id: MAIN_PANEL_ID, + component: MAIN_PANEL_ID, }); - const left = api.addPanel({ - id: 'left', - component: 'left', + api.addPanel({ + id: LEFT_PANEL_ID, + component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, position: { direction: 'left', - referencePanel: 'main', + referencePanel: MAIN_PANEL_ID, }, }); api.addPanel({ - id: 'gallery', - component: 'gallery', + id: GALLERY_PANEL_ID, + component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: 232, position: { direction: 'right', - referencePanel: 'main', + referencePanel: MAIN_PANEL_ID, }, }); api.addPanel({ - id: 'layers', - component: 'layers', + id: LAYERS_PANEL_ID, + component: LAYERS_PANEL_ID, minimumHeight: 256, position: { direction: 'below', - referencePanel: 'gallery', + referencePanel: GALLERY_PANEL_ID, }, }); - const boards = api.addPanel({ - id: 'boards', - component: 'boards', + api.addPanel({ + id: BOARDS_PANEL_ID, + component: BOARDS_PANEL_ID, minimumHeight: 36, position: { direction: 'above', - referencePanel: 'gallery', + referencePanel: GALLERY_PANEL_ID, }, }); - left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); + api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); + api.getPanel(MAIN_PANEL_ID)?.api.setActive(); }; export const CanvasTabAutoLayout = memo(() => { - const [api, setApi] = useState(null); - const onReady = useCallback((event) => { - $isLayoutLoading.set(true); - setApi(event.api); - initializeCanvasTabLayout(event.api); - $isLayoutLoading.set(false); - }, []); + const ref = useRef(null); + const $api = useState(() => atom(null))[0]; + const onReady = useCallback( + (event) => { + $api.set(event.api); + initializeLayout(event.api); + }, + [$api] + ); + const resizeMainPanelOnFirstVisible = useCallback(() => { + const api = $api.get(); + if (!api) { + return; + } + const mainPanel = api.getPanel(MAIN_PANEL_ID); + if (!mainPanel) { + return; + } + if (mainPanel.width !== 0) { + return; + } + let count = 0; + const setSize = () => { + if (count++ > 50) { + return; + } + mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER }); + if (mainPanel.width === 0) { + requestAnimationFrame(setSize); + return; + } + }; + setSize(); + }, [$api]); + useOnFirstVisible(ref, resizeMainPanelOnFirstVisible); + return ( - + ( - - - - - -)); -ViewerPanelContent.displayName = 'ViewerPanelContent'; +import { GenerateTabLeftPanel } from './GenerateTabLeftPanel'; +import { useOnFirstVisible } from './use-on-first-visible'; -const ProgressPanelContent = memo(() => ( - - - -)); -ProgressPanelContent.displayName = 'ProgressPanelContent'; +const LAUNCHPAD_PANEL_ID = 'launchpad'; +const VIEWER_PANEL_ID = 'viewer'; +const PROGRESS_PANEL_ID = 'progress'; const mainPanelComponents: IDockviewReactProps['components'] = { - welcome: GenerateLaunchpadPanel, - viewer: ViewerPanelContent, - progress: ProgressPanelContent, + [LAUNCHPAD_PANEL_ID]: GenerateLaunchpadPanel, + [VIEWER_PANEL_ID]: ImageViewerPanel, + [PROGRESS_PANEL_ID]: GenerationProgressPanel, }; const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { const { api } = event; api.addPanel({ - id: 'welcome', - component: 'welcome', + id: LAUNCHPAD_PANEL_ID, + component: LAUNCHPAD_PANEL_ID, title: 'Launchpad', }); api.addPanel({ - id: 'viewer', - component: 'viewer', + id: VIEWER_PANEL_ID, + component: VIEWER_PANEL_ID, title: 'Image Viewer', position: { direction: 'within', - referencePanel: 'welcome', + referencePanel: LAUNCHPAD_PANEL_ID, }, }); api.addPanel({ - id: 'progress', - component: 'progress', + id: PROGRESS_PANEL_ID, + component: PROGRESS_PANEL_ID, title: 'Generation Progress', position: { direction: 'within', - referencePanel: 'welcome', + referencePanel: LAUNCHPAD_PANEL_ID, }, }); + api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); + const disposables = [ api.onWillShowOverlay((e) => { if (e.kind === 'header_space' || e.kind === 'tab') { @@ -82,90 +71,111 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { const MainPanel = memo(() => { return ( - - - + ); }); MainPanel.displayName = 'MainPanel'; -export const GenerateLeftPanel = memo(() => { - return ( - - - - - - - ); -}); -GenerateLeftPanel.displayName = 'GenerateLeftPanel'; +const LEFT_PANEL_ID = 'left'; +const MAIN_PANEL_ID = 'main'; +const BOARDS_PANEL_ID = 'boards'; +const GALLERY_PANEL_ID = 'gallery'; export const generateTabComponents: IGridviewReactProps['components'] = { - left: GenerateLeftPanel, - main: MainPanel, - boards: BoardsPanel, - gallery: GalleryPanel, + [LEFT_PANEL_ID]: GenerateTabLeftPanel, + [MAIN_PANEL_ID]: MainPanel, + [BOARDS_PANEL_ID]: BoardsPanel, + [GALLERY_PANEL_ID]: GalleryPanel, }; -export const initializeGenerateTabLayout = (api: GridviewApi) => { - const main = api.addPanel({ - id: 'main', - component: 'main', - minimumWidth: 256, +export const initializeLayout = (api: GridviewApi) => { + api.addPanel({ + id: MAIN_PANEL_ID, + component: MAIN_PANEL_ID, }); - const left = api.addPanel({ - id: 'left', - component: 'left', + api.addPanel({ + id: LEFT_PANEL_ID, + component: LEFT_PANEL_ID, minimumWidth: LEFT_PANEL_MIN_SIZE_PX, position: { direction: 'left', - referencePanel: 'main', + referencePanel: MAIN_PANEL_ID, }, }); api.addPanel({ - id: 'gallery', - component: 'gallery', + id: GALLERY_PANEL_ID, + component: GALLERY_PANEL_ID, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, minimumHeight: 232, position: { direction: 'right', - referencePanel: 'main', + referencePanel: MAIN_PANEL_ID, }, }); - const boards = api.addPanel({ - id: 'boards', - component: 'boards', + api.addPanel({ + id: BOARDS_PANEL_ID, + component: BOARDS_PANEL_ID, minimumHeight: 36, position: { direction: 'above', - referencePanel: 'gallery', + referencePanel: GALLERY_PANEL_ID, }, }); - left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); - boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); + api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); + api.getPanel(MAIN_PANEL_ID)?.api.setActive(); }; export const GenerateTabAutoLayout = memo(() => { - const [api, setApi] = useState(null); - const onReady = useCallback((event) => { - $isLayoutLoading.set(true); - setApi(event.api); - initializeGenerateTabLayout(event.api); - $isLayoutLoading.set(false); - }, []); + const ref = useRef(null); + const $api = useState(() => atom(null))[0]; + const onReady = useCallback( + (event) => { + $api.set(event.api); + initializeLayout(event.api); + }, + [$api] + ); + const resizeMainPanelOnFirstVisible = useCallback(() => { + const api = $api.get(); + if (!api) { + return; + } + const mainPanel = api.getPanel(MAIN_PANEL_ID); + if (!mainPanel) { + return; + } + if (mainPanel.width !== 0) { + return; + } + let count = 0; + const setSize = () => { + if (count++ > 50) { + return; + } + mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER }); + if (mainPanel.width === 0) { + requestAnimationFrame(setSize); + return; + } + }; + setSize(); + }, [$api]); + useOnFirstVisible(ref, resizeMainPanelOnFirstVisible); + return ( - + { + const { api } = event; + api.addPanel({ + id: LAUNCHPAD_PANEL_ID, + component: LAUNCHPAD_PANEL_ID, + title: 'Launchpad', + }); + api.addPanel({ + id: VIEWER_PANEL_ID, + component: VIEWER_PANEL_ID, + title: 'Image Viewer', + position: { + direction: 'within', + referencePanel: LAUNCHPAD_PANEL_ID, + }, + }); + api.addPanel({ + id: PROGRESS_PANEL_ID, + component: PROGRESS_PANEL_ID, + title: 'Generation Progress', + position: { + direction: 'within', + referencePanel: LAUNCHPAD_PANEL_ID, + }, + }); + + api.getPanel(LAUNCHPAD_PANEL_ID)?.api.setActive(); + + const disposables = [ + api.onWillShowOverlay((e) => { + if (e.kind === 'header_space' || e.kind === 'tab') { + return; + } + e.preventDefault(); + }), + ]; + + return () => { + disposables.forEach((disposable) => { + disposable.dispose(); + }); + }; +}; + +const MainPanel = memo(() => { + return ( + + ); +}); +MainPanel.displayName = 'MainPanel'; + +const LEFT_PANEL_ID = 'left'; +const MAIN_PANEL_ID = 'main'; +const BOARDS_PANEL_ID = 'boards'; +const GALLERY_PANEL_ID = 'gallery'; + +export const gridviewComponents: IGridviewReactProps['components'] = { + [LEFT_PANEL_ID]: UpscalingTabLeftPanel, + [MAIN_PANEL_ID]: MainPanel, + [BOARDS_PANEL_ID]: BoardsPanel, + [GALLERY_PANEL_ID]: GalleryPanel, +}; + +export const initializeLayout = (api: GridviewApi) => { + api.addPanel({ + id: MAIN_PANEL_ID, + component: MAIN_PANEL_ID, + // priority: LayoutPriority.High, + }); + api.addPanel({ + id: LEFT_PANEL_ID, + component: LEFT_PANEL_ID, + minimumWidth: LEFT_PANEL_MIN_SIZE_PX, + position: { + direction: 'left', + referencePanel: MAIN_PANEL_ID, + }, + // priority: LayoutPriority.High, + }); + api.addPanel({ + id: GALLERY_PANEL_ID, + component: GALLERY_PANEL_ID, + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'right', + referencePanel: MAIN_PANEL_ID, + }, + // priority: LayoutPriority.High, + }); + api.addPanel({ + id: BOARDS_PANEL_ID, + component: BOARDS_PANEL_ID, + minimumHeight: 36, + position: { + direction: 'above', + referencePanel: GALLERY_PANEL_ID, + }, + // priority: LayoutPriority.High, + }); + api.getPanel(LEFT_PANEL_ID)?.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX }); + api.getPanel(BOARDS_PANEL_ID)?.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX }); + api.getPanel(MAIN_PANEL_ID)?.api.setActive(); +}; + +export const UpscalingTabAutoLayout = memo(() => { + const ref = useRef(null); + const $api = useState(() => atom(null))[0]; + const onReady = useCallback( + (event) => { + $api.set(event.api); + initializeLayout(event.api); + }, + [$api] + ); + const resizeMainPanelOnFirstVisible = useCallback(() => { + const api = $api.get(); + if (!api) { + return; + } + const mainPanel = api.getPanel(MAIN_PANEL_ID); + if (!mainPanel) { + return; + } + if (mainPanel.width !== 0) { + return; + } + let count = 0; + const setSize = () => { + if (count++ > 50) { + return; + } + mainPanel.api.setSize({ width: Number.MAX_SAFE_INTEGER }); + if (mainPanel.width === 0) { + requestAnimationFrame(setSize); + return; + } + }; + setSize(); + }, [$api]); + useOnFirstVisible(ref, resizeMainPanelOnFirstVisible); + + return ( + + + + ); +}); +UpscalingTabAutoLayout.displayName = 'UpscalingTabAutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts b/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts new file mode 100644 index 0000000000..3ad3bfb0f3 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/use-did-render-tab.ts @@ -0,0 +1,25 @@ +import { addAppListener } from 'app/store/middleware/listenerMiddleware'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { useEffect } from 'react'; + +export const useOnFirstVisitToTab = (tab: TabName, cb: () => void) => { + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch( + addAppListener({ + predicate: (action) => { + if (!setActiveTab.match(action)) { + return false; + } + return action.payload === tab; + }, + effect: (_, api) => { + cb(); + api.unsubscribe(); + }, + }) + ); + }, [cb, dispatch, tab]); +}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts new file mode 100644 index 0000000000..1122b44cac --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/use-on-first-visible.ts @@ -0,0 +1,48 @@ +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +export const useOnFirstVisible = (elementRef: RefObject, callback: () => void): void => { + useEffect(() => { + const element = elementRef.current; + if (!element) { + return; + } + + // Find the parent element that has display: none + const findParentWithDisplay = (el: HTMLElement): HTMLElement | null => { + let parent = el.parentElement; + while (parent) { + const computedStyle = window.getComputedStyle(parent); + if (computedStyle.display === 'none') { + return parent; + } + parent = parent.parentElement; + } + return null; + }; + + const targetParent = findParentWithDisplay(element); + if (!targetParent) { + return; + } + + const observerCallback = () => { + if (window.getComputedStyle(targetParent).display === 'none') { + return; + } + observer.disconnect(); + callback(); + }; + const observer = new MutationObserver(observerCallback); + + observer.observe(targetParent, { + attributes: true, + attributeFilter: ['hidden', 'style', 'class'], + }); + + observerCallback(); + return () => { + observer.disconnect(); + }; + }, [elementRef, callback]); +}; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 4b11ca8d6a..381c046c36 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -1,7 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; +import { selectEnabledTabs } from 'features/system/store/configSlice'; import { selectUiSlice } from 'features/ui/store/uiSlice'; export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab); +export const selectActiveTabIndex = createSelector(selectActiveTab, selectEnabledTabs, (activeTab, enabledTabs) => { + return enabledTabs.indexOf(activeTab); +}); export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails); export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer); export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel); diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 6c0ab9023a..8ef49ac4c7 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -2,6 +2,7 @@ import { deepClone } from 'common/util/deepClone'; import { z } from 'zod'; const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']); +export const ALL_TABS = zTabName.options; export type TabName = z.infer; const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']); export type CanvasRightPanelTabName = z.infer;