feat(ui): revise app layout strategy, add interaction scopes for hotkeys

This commit is contained in:
psychedelicious
2024-08-18 23:37:49 +10:00
parent 50051ee147
commit 4c66a0dcd0
33 changed files with 807 additions and 613 deletions

View File

@@ -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} />

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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';

View File

@@ -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"

View File

@@ -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 (
<>