mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-15 08:15:34 -05:00
feat(ui): revise app layout strategy, add interaction scopes for hotkeys
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { AddLayerFromImageDropData } from 'features/dnd/types';
|
||||
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
||||
@@ -9,6 +10,12 @@ const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
||||
};
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const isImageViewerOpen = useIsImageViewerOpen();
|
||||
|
||||
if (isImageViewerOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $shift, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@@ -9,6 +10,7 @@ import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
export const CanvasResetViewButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useStore($canvasManager);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
|
||||
const resetZoom = useCallback(() => {
|
||||
if (!canvasManager) {
|
||||
@@ -32,8 +34,8 @@ export const CanvasResetViewButton = memo(() => {
|
||||
}
|
||||
}, [resetView, resetZoom]);
|
||||
|
||||
useHotkeys('r', resetView);
|
||||
useHotkeys('shift+r', resetZoom);
|
||||
useHotkeys('r', resetView, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||
useHotkeys('shift+r', resetZoom, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||
|
||||
const meta: Meta<typeof ControlLayersEditor> = {
|
||||
const meta: Meta<typeof CanvasEditor> = {
|
||||
title: 'Feature/ControlLayers',
|
||||
tags: ['autodocs'],
|
||||
component: ControlLayersEditor,
|
||||
component: CanvasEditor,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ControlLayersEditor>;
|
||||
type Story = StoryObj<typeof CanvasEditor>;
|
||||
|
||||
const Component = () => {
|
||||
return (
|
||||
<Flex w={1500} h={1500}>
|
||||
<ControlLayersEditor />
|
||||
<CanvasEditor />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
export const CanvasEditor = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('canvas', ref);
|
||||
|
||||
export const ControlLayersEditor = memo(() => {
|
||||
return (
|
||||
<Flex
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
layerStyle="first"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
width="100%"
|
||||
height="full"
|
||||
width="full"
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -22,12 +31,9 @@ export const ControlLayersEditor = memo(() => {
|
||||
<Flex position="absolute" bottom={2} gap={2} align="center" justify="center">
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
{/* <Flex position="absolute" top={0} right={0} bottom={0} left={0} align="center" justify="center">
|
||||
<CanvasResizer />
|
||||
</Flex> */}
|
||||
<CanvasDropArea />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
ControlLayersEditor.displayName = 'ControlLayersEditor';
|
||||
CanvasEditor.displayName = 'CanvasEditor';
|
||||
|
||||
@@ -80,7 +80,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full">
|
||||
<Flex position="relative" w="full" h="full" bg={canvasBackgroundStyle === 'checkerboard' ? 'base.900' : 'base.850'}>
|
||||
{canvasBackgroundStyle === 'checkerboard' && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import {
|
||||
$shouldShowStagedImage,
|
||||
sessionNextStagedImageSelected,
|
||||
@@ -37,13 +38,13 @@ StagingAreaToolbar.displayName = 'StagingAreaToolbar';
|
||||
|
||||
export const StagingAreaToolbarContent = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const stagingArea = useAppSelector((s) => s.canvasV2.session);
|
||||
const session = useAppSelector((s) => s.canvasV2.session);
|
||||
const shouldShowStagedImage = useStore($shouldShowStagedImage);
|
||||
const images = useMemo(() => stagingArea.stagedImages, [stagingArea]);
|
||||
const images = useMemo(() => session.stagedImages, [session]);
|
||||
const selectedImage = useMemo(() => {
|
||||
return images[stagingArea.selectedStagedImageIndex] ?? null;
|
||||
}, [images, stagingArea.selectedStagedImageIndex]);
|
||||
|
||||
return images[session.selectedStagedImageIndex] ?? null;
|
||||
}, [images, session.selectedStagedImageIndex]);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
// const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -60,8 +61,8 @@ export const StagingAreaToolbarContent = memo(() => {
|
||||
if (!selectedImage) {
|
||||
return;
|
||||
}
|
||||
dispatch(sessionStagingAreaImageAccepted({ index: stagingArea.selectedStagedImageIndex }));
|
||||
}, [dispatch, selectedImage, stagingArea.selectedStagedImageIndex]);
|
||||
dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex }));
|
||||
}, [dispatch, selectedImage, session.selectedStagedImageIndex]);
|
||||
|
||||
const onDiscardOne = useCallback(() => {
|
||||
if (!selectedImage) {
|
||||
@@ -70,9 +71,9 @@ export const StagingAreaToolbarContent = memo(() => {
|
||||
if (images.length === 1) {
|
||||
dispatch(sessionStagingAreaReset());
|
||||
} else {
|
||||
dispatch(sessionStagedImageDiscarded({ index: stagingArea.selectedStagedImageIndex }));
|
||||
dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex }));
|
||||
}
|
||||
}, [selectedImage, images.length, dispatch, stagingArea.selectedStagedImageIndex]);
|
||||
}, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]);
|
||||
|
||||
const onDiscardAll = useCallback(() => {
|
||||
dispatch(sessionStagingAreaReset());
|
||||
@@ -95,25 +96,43 @@ export const StagingAreaToolbarContent = memo(() => {
|
||||
]
|
||||
);
|
||||
|
||||
useHotkeys(['left'], onPrev, {
|
||||
preventDefault: true,
|
||||
});
|
||||
useHotkeys(
|
||||
['left'],
|
||||
onPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
useHotkeys(['right'], onNext, {
|
||||
preventDefault: true,
|
||||
});
|
||||
useHotkeys(
|
||||
['right'],
|
||||
onNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
useHotkeys(['enter'], onAccept, {
|
||||
preventDefault: true,
|
||||
});
|
||||
useHotkeys(
|
||||
['enter'],
|
||||
onAccept,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
const counterText = useMemo(() => {
|
||||
if (images.length > 0) {
|
||||
return `${(stagingArea.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
|
||||
return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
|
||||
} else {
|
||||
return `0 of 0`;
|
||||
}
|
||||
}, [images.length, stagingArea.selectedStagedImageIndex]);
|
||||
}, [images.length, session.selectedStagedImageIndex]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user