diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx index 6097b15ac1..af2a5db6c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx @@ -50,7 +50,7 @@ import { newCanvasFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { DndImage } from 'features/dnd/DndImage'; import { newCanvasFromImage } from 'features/imageActions/actions'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi'; @@ -283,7 +283,9 @@ const GenerateWithStartingImageAndInpaintMask = memo(() => { GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask'; const SimpleActiveSession = memo(() => { - const dispatch = useAppDispatch(); + const { getState, dispatch } = useAppStore(); + const selectedImage = useAppSelector(selectSelectedImage); + const isStaging = useAppSelector(selectIsStaging); const onReset = useCallback(() => { @@ -302,13 +304,64 @@ const SimpleActiveSession = memo(() => { useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]); + const vary = useCallback(() => { + if (!selectedImage) { + return; + } + newCanvasFromImage({ + imageDTO: selectedImage.imageDTO, + type: 'raster_layer', + withResize: true, + getState, + dispatch, + }); + }, [dispatch, getState, selectedImage]); + + const useAsControl = useCallback(() => { + if (!selectedImage) { + return; + } + newCanvasFromImage({ + imageDTO: selectedImage.imageDTO, + type: 'control_layer', + withResize: true, + getState, + dispatch, + }); + }, [dispatch, getState, selectedImage]); + + const edit = useCallback(() => { + if (!selectedImage) { + return; + } + newCanvasFromImage({ + imageDTO: selectedImage.imageDTO, + type: 'raster_layer', + withInpaintMask: true, + getState, + dispatch, + }); + }, [dispatch, getState, selectedImage]); + return ( Simple Session (staging view) {isStaging && 'STAGING'} - + + + + + + + @@ -343,7 +396,11 @@ const SelectedImage = memo(() => { ); } - return No images; + return ( + + No images + + ); }); SelectedImage.displayName = 'SelectedImage'; @@ -361,6 +418,8 @@ const SessionImages = memo(() => { }); SessionImages.displayName = 'SessionImages'; +const getStagingImageId = (imageDTO: ImageDTO) => `staging-image-${imageDTO.image_name}`; + const sx = { objectFit: 'contain', maxW: 'full', @@ -381,8 +440,14 @@ const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: Image const onClick = useCallback(() => { dispatch(stagingAreaImageSelected({ index })); }, [dispatch, index]); + useEffect(() => { + if (selectedImageIndex === index) { + document.getElementById(getStagingImageId(imageDTO))?.scrollIntoView(); + } + }, [imageDTO, index, selectedImageIndex]); return ( { const store = useAppStore(); const [isDragging, setIsDragging] = useState(false); - const [element, ref] = useState(null); + const ref = useRef(null); const [dragPreviewState, setDragPreviewState] = useState(null); useEffect(() => { + const element = ref.current; if (!element) { return; } @@ -66,9 +67,9 @@ export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props }, }) ); - }, [imageDTO, element, store]); + }, [imageDTO, store]); - useImageContextMenu(imageDTO, element); + useImageContextMenu(imageDTO, ref); return ( <> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx index 530269c5fb..b8676a2ad6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx @@ -7,6 +7,7 @@ import MultipleSelectionMenuItems from 'features/gallery/components/ImageContext import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; import { selectSelectionCount } from 'features/gallery/store/gallerySelectors'; import { map } from 'nanostores'; +import type { RefObject } from 'react'; import { memo, useCallback, useEffect, useRef } from 'react'; import type { ImageDTO } from 'services/api/types'; @@ -43,7 +44,7 @@ const onClose = () => { * Map of elements to image DTOs. This is used to determine which image DTO to show the context menu for, depending on * the target of the context menu or long press event. */ -const elToImageMap = new Map(); +const elToImageMap = new Map(); /** * Given a target node, find the first registered parent element that contains the target node and return the imageDTO @@ -59,17 +60,20 @@ const getImageDTOFromMap = (target: Node): ImageDTO | undefined => { * @param imageDTO The image DTO to register the context menu for. * @param targetRef The ref of the target element that should trigger the context menu. */ -export const useImageContextMenu = (imageDTO: ImageDTO | undefined, targetRef: HTMLDivElement | null) => { +export const useImageContextMenu = (imageDTO: ImageDTO | undefined, ref: RefObject) => { useEffect(() => { - if (!targetRef || !imageDTO) { + if (!imageDTO) { + return; + } + const el = ref.current; + if (!el) { return; } - const el = targetRef; elToImageMap.set(el, imageDTO); return () => { elToImageMap.delete(el); }; - }, [imageDTO, targetRef]); + }, [imageDTO, ref]); }; /** 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 5721307fba..91e425c1f6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -19,7 +19,7 @@ import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/Sized import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { MouseEventHandler } from 'react'; -import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import type { ImageDTO } from 'services/api/types'; // This class name is used to calculate the number of images that fit in the gallery @@ -89,7 +89,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { const [dragPreviewState, setDragPreviewState] = useState< DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null >(null); - const [element, ref] = useState(null); + const ref = useRef(null); const dndId = useId(); const selectIsSelectedForCompare = useMemo( () => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name), @@ -111,6 +111,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { const isSelected = useAppSelector(selectIsSelected); useEffect(() => { + const element = ref.current; if (!element) { return; } @@ -175,7 +176,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { }, }) ); - }, [imageDTO, element, store, dndId]); + }, [imageDTO, store, dndId]); const [isHovered, setIsHovered] = useState(false); @@ -211,7 +212,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { const dataTestId = useMemo(() => getGalleryImageDataTestId(imageDTO.image_name), [imageDTO.image_name]); - useImageContextMenu(imageDTO, element); + useImageContextMenu(imageDTO, ref); return ( <>