feat(ui): allow send-image-to-canvas to work when canvas is uninitialized

Add `useCanvasIsBusySafe()` hook. This is like `useCanvasIsBusy()`, but when the canvas is not initialized, it gracefully falls back to false instead of raising.

Because app tabs are lazy-loaded, the canvas is not initialized until the user visits that tab. If the page loads up on the workflows tab, the canvas will be uninitialized until the user clicks on it.

This graceful fallback behaviour allows actions like sending an image to canvas to work even when the canvas is not yet initialized. These actions are exposed in the image context menu, and previously were hidden when the canvas was not initialized. We can now show these actions and use them even when the canvas is uninitialized.

- Add `useCanvasIsBusySafe()` hook
- Use the new hook in the image context menu for send to canvas actions
- Do not use `<CanvasManagerProviderGate />` in the image context menu (this was hiding the actions when canvas was uninitialized)
This commit is contained in:
psychedelicious
2025-04-09 17:07:26 +10:00
parent f655a85154
commit a23d90187b
4 changed files with 33 additions and 17 deletions

View File

@@ -1,16 +1,35 @@
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
/**
* Returns a boolena indicating whether the canvas is busy:
* - While staging
* - While an entity is transforming
* - While an entity is filtering
* - While the canvas is doing some other long-running operation, like rasterizing a layer
*
* This hook will throw an error if the canvas manager is not initialized.
*/
export const useCanvasIsBusy = () => {
const canvasManager = useCanvasManager();
/**
* Whether the canvas is busy:
* - While staging
* - While an entity is transforming
* - While an entity is filtering
* - While the canvas is doing some other long-running operation, like rasterizing a layer
*/
const isBusy = useStore(canvasManager.$isBusy);
return isBusy;
};
/**
* Returns a boolena indicating whether the canvas is busy:
* - While staging
* - While an entity is transforming
* - While an entity is filtering
* - While the canvas is doing some other long-running operation, like rasterizing a layer
*
* This hook will fall back to false if the canvas manager is not initialized.
*/
export const useCanvasIsBusySafe = () => {
const canvasManager = useCanvasManagerSafe();
const isBusy = useStore(canvasManager?.$isBusy ?? $false);
return isBusy;
};

View File

@@ -2,7 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsSD3 } from 'features/controlLayers/store/paramsSlice';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
@@ -19,7 +19,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const store = useAppStore();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const isSD3 = useAppSelector(selectIsSD3);
const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {

View File

@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsFLUX, selectIsSD3 } from 'features/controlLayers/store/paramsSlice';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
@@ -21,7 +21,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
const store = useAppStore();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isBusy = useCanvasIsBusySafe();
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);

View File

@@ -1,6 +1,5 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
@@ -38,10 +37,8 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
<ImageMenuItemMetadataRecallActions />
<MenuDivider />
<ImageMenuItemSendToUpscale />
<CanvasManagerProviderGate>
<ImageMenuItemNewCanvasFromImageSubMenu />
<ImageMenuItemNewLayerFromImageSubMenu />
</CanvasManagerProviderGate>
<ImageMenuItemNewCanvasFromImageSubMenu />
<ImageMenuItemNewLayerFromImageSubMenu />
<MenuDivider />
<ImageMenuItemChangeBoard />
<ImageMenuItemStarUnstar />