diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 96ce9888da..0cb811fec6 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react'; import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator'; import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator'; import type { StudioInitAction } from 'app/hooks/useStudioInitAction'; -import { $didStudioInit } from 'app/hooks/useStudioInitAction'; +import { $globalIsLoading } from 'app/store/nanostores/globalIsLoading'; import type { PartialAppConfig } from 'app/types/invokeai'; import Loading from 'common/components/Loading/Loading'; import { useClearStorage } from 'common/hooks/useClearStorage'; @@ -20,7 +20,7 @@ interface Props { } const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { - const didStudioInit = useStore($didStudioInit); + const globalIsLoading = useStore($globalIsLoading); const clearStorage = useClearStorage(); const handleReset = useCallback(() => { @@ -33,7 +33,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { - {!didStudioInit && } + {globalIsLoading && } diff --git a/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts b/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts new file mode 100644 index 0000000000..e875df4e52 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/globalIsLoading.ts @@ -0,0 +1,13 @@ +import { $didStudioInit } from 'app/hooks/useStudioInitAction'; +import { atom, computed } from 'nanostores'; +import { flushSync } from 'react-dom'; + +export const $isLayoutLoading = atom(false); +export const setIsLayoutLoading = (isLoading: boolean) => { + flushSync(() => { + $isLayoutLoading.set(isLoading); + }); +}; +export const $globalIsLoading = computed([$didStudioInit, $isLayoutLoading], (didStudioInit, isLayoutLoading) => { + return !didStudioInit || isLayoutLoading; +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx index 7a978ca905..fd21523c73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx @@ -26,7 +26,7 @@ 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 { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState'; import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; @@ -84,7 +84,7 @@ export const AdvancedSession = memo(({ id }: { id: string | null }) => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx index 6de28bbfa5..3b28be2799 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx @@ -1,24 +1,19 @@ -import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library'; +import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar'; +import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; import { ParamDenoisingStrength } from './ParamDenoisingStrength'; -const FOCUS_REGION_STYLES: SystemStyleObject = { - width: 'full', - height: 'full', -}; - -export const CanvasLayersPanelContent = memo(() => { +export const CanvasLayersPanel = memo(() => { const hasEntities = useAppSelector(selectHasEntities); return ( - + @@ -27,8 +22,8 @@ export const CanvasLayersPanelContent = memo(() => { {!hasEntities && } {hasEntities && } - + ); }); -CanvasLayersPanelContent.displayName = 'CanvasLayersPanelContent'; +CanvasLayersPanel.displayName = 'CanvasLayersPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx index 73d867cff2..16c6510417 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx @@ -6,7 +6,7 @@ import { InitialStateMainModelPicker } from 'features/controlLayers/components/S import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; -export const InitialState = memo(() => { +export const GenerateLaunchpadPanel = memo(() => { const dispatch = useAppDispatch(); const newCanvasSession = useCallback(() => { dispatch(setActiveTab('canvas')); @@ -43,4 +43,4 @@ export const InitialState = memo(() => { ); }); -InitialState.displayName = 'InitialState'; +GenerateLaunchpadPanel.displayName = 'InitialState'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx index 54b0be8e1a..5d5dd3b79a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx @@ -1,6 +1,6 @@ import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState'; import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; @@ -25,7 +25,7 @@ export const SimpleSession = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx index bb80d8e160..edf5ca25ec 100644 --- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx @@ -8,7 +8,7 @@ import { memo } from 'react'; const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 }; -export const BoardsListPanelContent = memo(() => { +export const BoardsPanel = memo(() => { const boardSearchDisclosure = useStore($boardSearchIsOpen); return ( @@ -23,4 +23,4 @@ export const BoardsListPanelContent = memo(() => { ); }); -BoardsListPanelContent.displayName = 'BoardsListPanelContent'; +BoardsPanel.displayName = 'BoardsPanel'; diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index 0fcfa7dd82..1679cc1fb1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -46,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10 const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView); const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm); -export const Gallery = memo(() => { +export const GalleryPanel = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const galleryView = useAppSelector(selectGalleryView); @@ -117,4 +117,4 @@ export const Gallery = memo(() => { ); }); -Gallery.displayName = 'Gallery'; +GalleryPanel.displayName = 'Gallery'; diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx index a5ff519384..6a05236929 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx @@ -17,8 +17,9 @@ export const GalleryTopBar = memo(() => { const dispatch = useAppDispatch(); const boardSearchText = useAppSelector(selectBoardSearchText); const boardSearchDisclosure = useBoardSearchDisclosure(); - const api = useAutoLayoutContext(); - const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256); + const $api = useAutoLayoutContext(); + const api = useStore($api); + const boardsPanel = useCollapsibleGridviewPanel(api, 'Boards', 'vertical', 256); const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed); const onClickBoardSearch = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx new file mode 100644 index 0000000000..bfcda19650 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/GenerationProgressPanel.tsx @@ -0,0 +1,10 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2'; +import { memo } from 'react'; + +export const GenerationProgressPanel = memo(() => ( + + + +)); +GenerationProgressPanel.displayName = 'GenerationProgressPanel'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx new file mode 100644 index 0000000000..0fd32cfa54 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerPanel.tsx @@ -0,0 +1,13 @@ +import { Divider, Flex } from '@invoke-ai/ui-library'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2'; +import { memo } from 'react'; + +export const ImageViewerPanel = memo(() => ( + + + + + +)); +ImageViewerPanel.displayName = 'ImageViewerPanel'; diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index 74bcdc6119..c66fdfb6c8 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -1,27 +1,13 @@ import 'dockview/dist/styles/dockview.css'; import 'features/ui/styles/dockview-theme-invoke.css'; -import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { Flex } from '@invoke-ai/ui-library'; import { useDndMonitor } from 'features/dnd/useDndMonitor'; -import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { VerticalNavBar } from 'features/ui/components/VerticalNavBar'; -import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; -import { usePanel } from 'features/ui/hooks/usePanel'; -import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout'; -import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { - $isLeftPanelOpen, - $isRightPanelOpen, - LEFT_PANEL_MIN_SIZE_PX, - RIGHT_PANEL_MIN_SIZE_PX, - selectWithLeftPanel, - selectWithRightPanel, -} from 'features/ui/store/uiSlice'; +import { AutoLayout } from 'features/ui/layouts/AutoLayout'; +import { $isLeftPanelOpen, $isRightPanelOpen } from 'features/ui/store/uiSlice'; import type { CSSProperties } from 'react'; -import { memo, useMemo, useRef } from 'react'; -import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; +import { memo } from 'react'; const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 }; @@ -29,97 +15,88 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed); export const AppContent = memo(() => { - const tab = useAppSelector(selectActiveTab); - const imperativePanelGroupRef = useRef(null); + // const tab = useAppSelector(selectActiveTab); + // const imperativePanelGroupRef = useRef(null); useDndMonitor(); - const withLeftPanel = useAppSelector(selectWithLeftPanel); - const leftPanelUsePanelOptions = useMemo( - () => ({ - id: 'left-panel', - minSizePx: LEFT_PANEL_MIN_SIZE_PX, - defaultSizePx: LEFT_PANEL_MIN_SIZE_PX, - imperativePanelGroupRef, - panelGroupDirection: 'horizontal', - onCollapse: onLeftPanelCollapse, - }), - [] - ); - const leftPanel = usePanel(leftPanelUsePanelOptions); - useRegisteredHotkeys({ - id: 'toggleLeftPanel', - category: 'app', - callback: leftPanel.toggle, - options: { enabled: withLeftPanel }, - dependencies: [leftPanel.toggle, withLeftPanel], - }); + // const withLeftPanel = useAppSelector(selectWithLeftPanel); + // const leftPanelUsePanelOptions = useMemo( + // () => ({ + // id: 'left-panel', + // minSizePx: LEFT_PANEL_MIN_SIZE_PX, + // defaultSizePx: LEFT_PANEL_MIN_SIZE_PX, + // imperativePanelGroupRef, + // panelGroupDirection: 'horizontal', + // onCollapse: onLeftPanelCollapse, + // }), + // [] + // ); + // const leftPanel = usePanel(leftPanelUsePanelOptions); + // useRegisteredHotkeys({ + // id: 'toggleLeftPanel', + // category: 'app', + // callback: leftPanel.toggle, + // options: { enabled: withLeftPanel }, + // dependencies: [leftPanel.toggle, withLeftPanel], + // }); - const withRightPanel = useAppSelector(selectWithRightPanel); - const rightPanelUsePanelOptions = useMemo( - () => ({ - id: 'right-panel', - minSizePx: RIGHT_PANEL_MIN_SIZE_PX, - defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX, - imperativePanelGroupRef, - panelGroupDirection: 'horizontal', - onCollapse: onRightPanelCollapse, - }), - [] - ); - const rightPanel = usePanel(rightPanelUsePanelOptions); - useRegisteredHotkeys({ - id: 'toggleRightPanel', - category: 'app', - callback: rightPanel.toggle, - options: { enabled: withRightPanel }, - dependencies: [rightPanel.toggle, withRightPanel], - }); + // const withRightPanel = useAppSelector(selectWithRightPanel); + // const rightPanelUsePanelOptions = useMemo( + // () => ({ + // id: 'right-panel', + // minSizePx: RIGHT_PANEL_MIN_SIZE_PX, + // defaultSizePx: RIGHT_PANEL_MIN_SIZE_PX, + // imperativePanelGroupRef, + // panelGroupDirection: 'horizontal', + // onCollapse: onRightPanelCollapse, + // }), + // [] + // ); + // const rightPanel = usePanel(rightPanelUsePanelOptions); + // useRegisteredHotkeys({ + // id: 'toggleRightPanel', + // category: 'app', + // callback: rightPanel.toggle, + // options: { enabled: withRightPanel }, + // dependencies: [rightPanel.toggle, withRightPanel], + // }); - useRegisteredHotkeys({ - id: 'resetPanelLayout', - category: 'app', - callback: () => { - leftPanel.reset(); - rightPanel.reset(); - }, - dependencies: [leftPanel.reset, rightPanel.reset], - }); - useRegisteredHotkeys({ - id: 'togglePanels', - category: 'app', - callback: () => { - if (leftPanel.isCollapsed || rightPanel.isCollapsed) { - leftPanel.expand(); - rightPanel.expand(); - } else { - leftPanel.collapse(); - rightPanel.collapse(); - } - }, - dependencies: [ - leftPanel.isCollapsed, - rightPanel.isCollapsed, - leftPanel.expand, - rightPanel.expand, - leftPanel.collapse, - rightPanel.collapse, - ], - }); + // useRegisteredHotkeys({ + // id: 'resetPanelLayout', + // category: 'app', + // callback: () => { + // leftPanel.reset(); + // rightPanel.reset(); + // }, + // dependencies: [leftPanel.reset, rightPanel.reset], + // }); + // useRegisteredHotkeys({ + // id: 'togglePanels', + // category: 'app', + // callback: () => { + // if (leftPanel.isCollapsed || rightPanel.isCollapsed) { + // leftPanel.expand(); + // rightPanel.expand(); + // } else { + // leftPanel.collapse(); + // rightPanel.collapse(); + // } + // }, + // dependencies: [ + // leftPanel.isCollapsed, + // rightPanel.isCollapsed, + // leftPanel.expand, + // rightPanel.expand, + // leftPanel.collapse, + // rightPanel.collapse, + // ], + // }); return ( - - - - - - - - - - - - - + + + + ); }); AppContent.displayName = 'AppContent'; diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx index 6625ce0175..04bf65154d 100644 --- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx @@ -2,10 +2,10 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { useDisclosure } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper'; -import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent'; +import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; -import { Gallery } from 'features/gallery/components/Gallery'; +import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; +import { GalleryPanel } from 'features/gallery/components/Gallery'; import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar'; import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors'; import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle'; @@ -71,18 +71,18 @@ export const RightPanelContent = memo(() => { - + - + {tab === 'canvas' && ( <> - + diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index 1a8850f309..8974a9c326 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, Tab, Tooltip } from '@invoke-ai/ui-library'; +import { IconButton, 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'; @@ -25,8 +25,7 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React return ( - { + return ( + api.getPanel('GenerateLaunchpad') || + api.addPanel({ + id: 'GenerateLaunchpad', + component: 'GenerateLaunchpad', + title: 'Launchpad', + }) + ); +}; + +const getCanvasLaunchpadPanel = (api: DockviewApi) => { + return ( + api.getPanel('CanvasLaunchpad') || + api.addPanel({ + id: 'CanvasLaunchpad', + component: 'CanvasLaunchpad', + title: 'Launchpad', + }) + ); +}; + +const getCanvasWorkspacePanel = (api: DockviewApi) => { + return ( + api.getPanel('CanvasWorkspace') || + api.addPanel({ + id: 'CanvasWorkspace', + component: 'CanvasWorkspace', + title: 'Canvas', + }) + ); +}; + +const getImageViewerPanel = (api: DockviewApi) => { + return ( + api.getPanel('ImageViewer') || + api.addPanel({ + id: 'ImageViewer', + component: 'ImageViewer', + title: 'Image Viewer', + }) + ); +}; + +const getGenerationProgressPanel = (api: DockviewApi) => { + return ( + api.getPanel('GenerationProgress') || + api.addPanel({ + id: 'GenerationProgress', + component: 'GenerationProgress', + title: 'Generation Progress', + }) + ); +}; + +const syncMainPanelLayout = (tab: TabName, api: DockviewApi) => { + if (tab === 'generate') { + const GenerateLaunchpad = getGenerateLaunchpadPanel(api); + const ImageViewer = getImageViewerPanel(api); + const GenerationProgress = getGenerationProgressPanel(api); + const panelsToKeep = [GenerateLaunchpad.id, ImageViewer.id, GenerationProgress.id]; + for (const panel of api.panels) { + if (!panelsToKeep.includes(panel.id)) { + api.removePanel(panel); + } + } + } else if (tab === 'canvas') { + const CanvasLaunchpad = getCanvasLaunchpadPanel(api); + const CanvasWorkspace = getCanvasWorkspacePanel(api); + const ImageViewer = getImageViewerPanel(api); + const GenerationProgress = getGenerationProgressPanel(api); + const panelsToKeep = [CanvasLaunchpad.id, CanvasWorkspace.id, ImageViewer.id, GenerationProgress.id]; + for (const panel of api.panels) { + if (!panelsToKeep.includes(panel.id)) { + api.removePanel(panel); + } + } + } +}; + +const MainPanel = memo(() => { + const tab = useAppSelector(selectActiveTab); + const [api, setApi] = useState(null); + const onReady = useCallback((event) => { + console.log('MainPanel onReady', event.api); + setApi(event.api); + }, []); + + useEffect(() => { + if (api) { + syncMainPanelLayout(tab, api); + } + }, [api, tab]); + + return ( + + + + ); +}); +MainPanel.displayName = 'MainPanel'; + +export const gridviewComponents: IGridviewReactProps['components'] = { + // Shared components + Gallery: GalleryPanel, + Boards: BoardsPanel, + Main: MainPanel, + GenerateLeft: GenerateLeftPanel, + CanvasLeft: GenerateLeftPanel, + CanvasLayers: CanvasLayersPanel, +}; + +const syncGridviewLayout = (tab: TabName, api: GridviewApi) => { + if (tab === 'generate') { + const MainPanel = + api.getPanel('Main') ?? + api.addPanel({ + id: 'Main', + component: 'Main', + }); + + const GenerateLeftPanel = + api.getPanel('GenerateLeft') ?? + api.addPanel({ + id: 'GenerateLeft', + component: 'GenerateLeft', + minimumWidth: LEFT_PANEL_MIN_SIZE_PX, + position: { + direction: 'left', + referencePanel: MainPanel.id, + }, + }); + + const GalleryPanel = + api.getPanel('Gallery') ?? + api.addPanel({ + id: 'Gallery', + component: 'Gallery', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'right', + referencePanel: MainPanel.id, + }, + }); + + const BoardsPanel = + api.getPanel('Boards') ?? + api.addPanel({ + id: 'Boards', + component: 'Boards', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 36, + position: { + direction: 'above', + referencePanel: GalleryPanel.id, + }, + }); + + const panelsToKeep = [MainPanel.id, GenerateLeftPanel.id, GalleryPanel.id, BoardsPanel.id]; + for (const panel of api.panels) { + if (!panelsToKeep.includes(panel.id)) { + api.removePanel(panel); + } + } + } else if (tab === 'canvas') { + const MainPanel = + api.getPanel('Main') ?? + api.addPanel({ + id: 'Main', + component: 'Main', + }); + + const CanvasLeftPanel = + api.getPanel('CanvasLeft') ?? + api.addPanel({ + id: 'CanvasLeft', + component: 'CanvasLeft', + minimumWidth: LEFT_PANEL_MIN_SIZE_PX, + position: { + direction: 'left', + referencePanel: MainPanel.id, + }, + }); + + const GalleryPanel = + api.getPanel('Gallery') ?? + api.addPanel({ + id: 'Gallery', + component: 'Gallery', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'right', + referencePanel: MainPanel.id, + }, + }); + + const BoardsPanel = + api.getPanel('Boards') ?? + api.addPanel({ + id: 'Boards', + component: 'Boards', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 36, + position: { + direction: 'above', + referencePanel: GalleryPanel.id, + }, + }); + + const CanvasLayersPanel = + api.getPanel('CanvasLayers') ?? + api.addPanel({ + id: 'CanvasLayers', + component: 'CanvasLayers', + minimumWidth: RIGHT_PANEL_MIN_SIZE_PX, + minimumHeight: 232, + position: { + direction: 'below', + referencePanel: GalleryPanel.id, + }, + }); + + const panelsToKeep = [MainPanel.id, CanvasLeftPanel.id, GalleryPanel.id, BoardsPanel.id, CanvasLayersPanel.id]; + for (const panel of api.panels) { + if (!panelsToKeep.includes(panel.id)) { + api.removePanel(panel); + } + } + } }; export const AutoLayout = memo(() => { const tab = useAppSelector(selectActiveTab); - const [api, setApi] = useState(null); - const syncLayout = useCallback((tab: TabName, api: GridviewApi) => { - if (tab === 'generate') { - initializeGenerateTabLayout(api); - } else if (tab === 'canvas') { - initializeCanvasTabLayout(api); - } - }, []); - const onReady = useCallback((event) => { - setApi(event.api); - }, []); + const $api = useState(() => atom(null))[0]; + const onReady = useCallback( + (event) => { + $api.set(event.api); + }, + [$api] + ); useEffect(() => { + const api = $api.get(); if (api) { - syncLayout(tab, api); + syncGridviewLayout(tab, api); } - }, [api, syncLayout, tab]); + }, [$api, tab]); return ( - + diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx new file mode 100644 index 0000000000..1a2a592ddd --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -0,0 +1,139 @@ +import { ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +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 { 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 { 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 { memo, useCallback } from 'react'; +import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; + +const MenuContent = memo(() => { + return ( + + + + + + + ); +}); +MenuContent.displayName = 'MenuContent'; + +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'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx index 77e469a462..0d4803adca 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx @@ -1,10 +1,9 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import type { IDockviewPanelHeaderProps } from 'dockview'; -import { useCallback, useEffect, useId, useRef } from 'react'; +import { useCallback, useRef } from 'react'; export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => { - const id = useId(); const ref = useRef(null); const setActive = useCallback(() => { if (!props.api.isActive) { @@ -13,18 +12,7 @@ export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => { }, [props.api]); useCallbackOnDragEnter(setActive, ref, 300); - - useEffect(() => { - const el = document.querySelector(`[data-id="${id}"]`); - if (!el) { - return; - } - const parentTab = el.closest('.dv-tab'); - if (!parentTab) { - return; - } - parentTab.setAttribute('draggable', 'false'); - }, [id]); + console.log(props.api.title); return ( diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx index d86ae4bee1..87ad53faf8 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx @@ -1,14 +1,18 @@ import type { GridviewApi } from 'dockview'; +import type { Atom } from 'nanostores'; import type { PropsWithChildren } from 'react'; import { createContext, useContext } from 'react'; -const AutoLayoutContext = createContext(null); +const AutoLayoutContext = createContext | null>(null); -export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => { - return {props.children}; +export const AutoLayoutProvider = (props: PropsWithChildren<{ $api: Atom }>) => { + return {props.children}; }; export const useAutoLayoutContext = () => { const api = useContext(AutoLayoutContext); + if (!api) { + throw new Error('useAutoLayoutContext must be used within an AutoLayoutProvider'); + } return api; }; 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 43ea4dc4e1..ed2cc14b8f 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,4 +1,5 @@ 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'; @@ -8,13 +9,13 @@ import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/compone import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems'; import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems'; import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; -import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent'; +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 { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState'; import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; @@ -22,14 +23,15 @@ 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 { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; -import { Gallery } from 'features/gallery/components/Gallery'; +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 { 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'; @@ -60,7 +62,7 @@ const canvasBgSx = { }, }; -export const CanvasPanel = memo(() => { +export const CanvasWorkspacePanel = memo(() => { const dynamicGrid = useAppSelector(selectDynamicGrid); const showHUD = useAppSelector(selectShowHUD); const canvasId = useAppSelector(selectCanvasSessionId); @@ -151,11 +153,11 @@ export const CanvasPanel = memo(() => { ); }); -CanvasPanel.displayName = 'CanvasPanel'; +CanvasWorkspacePanel.displayName = 'CanvasPanel'; const LayersPanelContent = memo(() => ( - + )); LayersPanelContent.displayName = 'LayersPanelContent'; @@ -177,8 +179,8 @@ const ProgressPanelContent = memo(() => ( ProgressPanelContent.displayName = 'ProgressPanelContent'; const mainPanelComponents: IDockviewReactProps['components'] = { - welcome: InitialState, - canvas: CanvasPanel, + canvasLaunchpad: GenerateLaunchpadPanel, + canvas: CanvasWorkspacePanel, viewer: ViewerPanelContent, progress: ProgressPanelContent, }; @@ -186,9 +188,9 @@ const mainPanelComponents: IDockviewReactProps['components'] = { const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { const { api } = event; api.addPanel({ - id: 'welcome', - component: 'welcome', - title: 'Launchpad', + id: 'canvasLaunchpad', + component: 'canvasLaunchpad', + title: 'canvasLaunchpad', }); api.addPanel({ id: 'canvas', @@ -196,7 +198,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { title: 'Canvas', position: { direction: 'within', - referencePanel: 'welcome', + referencePanel: 'canvasLaunchpad', }, }); api.addPanel({ @@ -205,7 +207,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { title: 'Image Viewer', position: { direction: 'within', - referencePanel: 'welcome', + referencePanel: 'canvasLaunchpad', }, }); api.addPanel({ @@ -214,7 +216,7 @@ const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => { title: 'Generation Progress', position: { direction: 'within', - referencePanel: 'welcome', + referencePanel: 'canvasLaunchpad', }, }); @@ -243,7 +245,7 @@ const MainPanel = memo(() => { disableFloatingGroups={true} dndEdges={false} defaultTabComponent={TabWithoutCloseButton} - components={mainPanelComponents} + components={components} onReady={onReadyMainPanel} theme={dockviewTheme} /> @@ -264,11 +266,13 @@ const Left = memo(() => { }); Left.displayName = 'Left'; +const Null = () => null; + export const canvasTabComponents: IGridviewReactProps['components'] = { left: Left, main: MainPanel, - boards: BoardsListPanelContent, - gallery: Gallery, + boards: BoardsPanel, + gallery: GalleryPanel, layers: LayersPanelContent, }; @@ -322,8 +326,10 @@ export const initializeCanvasTabLayout = (api: GridviewApi) => { 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); }, []); return ( diff --git a/invokeai/frontend/web/src/features/ui/layouts/components.ts b/invokeai/frontend/web/src/features/ui/layouts/components.ts new file mode 100644 index 0000000000..becb52fc3b --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/components.ts @@ -0,0 +1,29 @@ +import type { IDockviewReactProps, IGridviewReactProps } from 'dockview'; +import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent'; +import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState'; +import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; +import { GalleryPanel } from 'features/gallery/components/Gallery'; +import { GenerationProgressPanel } from 'features/gallery/components/ImageViewer/GenerationProgressPanel'; +import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; +import { CanvasWorkspacePanel } from 'features/ui/layouts/canvas-tab-auto-layout'; +import { GenerateLeftPanel } from 'features/ui/layouts/generate-tab-auto-layout'; + +export const components: IDockviewReactProps['components'] & IGridviewReactProps['components'] = { + // Shared components + Gallery: GalleryPanel, + Boards: BoardsPanel, + ImageViewer: ImageViewerPanel, + GenerationProgress: GenerationProgressPanel, + // Generate tab + GenerateLaunchpad: GenerateLaunchpadPanel, + GenerateLeft: GenerateLeftPanel, + // Upscaling tab + UpscalingLaunchpad: GenerateLaunchpadPanel, + // Workflows tab + WorkflowsLaunchpad: GenerateLaunchpadPanel, + // Canvas tab + CanvasLaunchpad: GenerateLaunchpadPanel, + CanvasLayers: CanvasLayersPanel, + CanvasWorkspace: CanvasWorkspacePanel, + CanvasLeft: GenerateLeftPanel, +}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx index f97a404652..26ade5fe55 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx @@ -1,9 +1,10 @@ import { Box, Divider, Flex } from '@invoke-ai/ui-library'; +import { $isLayoutLoading } from 'app/store/nanostores/globalIsLoading'; import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, Orientation } from 'dockview'; -import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState'; -import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; -import { Gallery } from 'features/gallery/components/Gallery'; +import { GenerateLaunchpadPanel } from 'features/controlLayers/components/SimpleSession/InitialState'; +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'; @@ -32,7 +33,7 @@ const ProgressPanelContent = memo(() => ( ProgressPanelContent.displayName = 'ProgressPanelContent'; const mainPanelComponents: IDockviewReactProps['components'] = { - welcome: InitialState, + welcome: GenerateLaunchpadPanel, viewer: ViewerPanelContent, progress: ProgressPanelContent, }; @@ -97,7 +98,7 @@ const MainPanel = memo(() => { }); MainPanel.displayName = 'MainPanel'; -const Left = memo(() => { +export const GenerateLeftPanel = memo(() => { return ( @@ -107,13 +108,13 @@ const Left = memo(() => { ); }); -Left.displayName = 'Left'; +GenerateLeftPanel.displayName = 'GenerateLeftPanel'; export const generateTabComponents: IGridviewReactProps['components'] = { - left: Left, + left: GenerateLeftPanel, main: MainPanel, - boards: BoardsListPanelContent, - gallery: Gallery, + boards: BoardsPanel, + gallery: GalleryPanel, }; export const initializeGenerateTabLayout = (api: GridviewApi) => { @@ -157,9 +158,10 @@ export const initializeGenerateTabLayout = (api: GridviewApi) => { export const GenerateTabAutoLayout = memo(() => { const [api, setApi] = useState(null); const onReady = useCallback((event) => { - console.log('GenerateTabAutoLayout onReady'); + $isLayoutLoading.set(true); setApi(event.api); initializeGenerateTabLayout(event.api); + $isLayoutLoading.set(false); }, []); return (