feat(ui): inpaint mask UI components

This commit is contained in:
psychedelicious 2024-06-21 12:52:39 +10:00
parent dd54d19f00
commit 2aad3f89c3
9 changed files with 156 additions and 2 deletions

View File

@ -7,6 +7,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { CA } from 'features/controlLayers/components/ControlAdapter/CA';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { IM } from 'features/controlLayers/components/InpaintMask/IM';
import { IPA } from 'features/controlLayers/components/IPAdapter/IPA';
import { Layer } from 'features/controlLayers/components/Layer/Layer';
import { RG } from 'features/controlLayers/components/RegionalGuidance/RG';
@ -34,6 +35,7 @@ export const ControlLayersPanelContent = memo(() => {
<AddLayerButton />
<DeleteAllLayersButton />
</Flex>
<IM />
{entityCount > 0 && (
<ScrollableContent>
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">

View File

@ -0,0 +1,26 @@
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader';
import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
export const IM = memo(() => {
const dispatch = useAppDispatch();
const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill));
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask');
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' }));
}, [dispatch]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect} selectedBorderColor={selectedBorderColor}>
<IMHeader onToggleVisibility={onToggle} />
{isOpen && <IMSettings />}
</CanvasEntityContainer>
);
});
IM.displayName = 'IM';

View File

@ -0,0 +1,28 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { imReset } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
export const IMActionsMenu = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onReset = useCallback(() => {
dispatch(imReset());
}, [dispatch]);
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={onReset} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
</MenuList>
</Menu>
);
});
IMActionsMenu.displayName = 'IMActionsMenu';

View File

@ -0,0 +1,36 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu';
import { imIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IMMaskFillColorPicker } from './IMMaskFillColorPicker';
type Props = {
onToggleVisibility: () => void;
};
export const IMHeader = memo(({ onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(imIsEnabledToggled());
}, [dispatch]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.inpaintMask')} />
<Spacer />
<IMMaskFillColorPicker />
<IMActionsMenu />
</CanvasEntityHeader>
);
});
IMHeader.displayName = 'IMHeader';

View File

@ -0,0 +1,46 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation';
import { imFillChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
export const IMMaskFillColorPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill);
const onChange = useCallback(
(fill: RgbColor) => {
dispatch(imFillChanged({ fill }));
},
[dispatch]
);
return (
<Popover isLazy>
<PopoverTrigger>
<Flex
as="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={fill} onChange={onChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</Popover>
);
});
IMMaskFillColorPicker.displayName = 'IMMaskFillColorPicker';

View File

@ -0,0 +1,8 @@
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { memo } from 'react';
export const IMSettings = memo(() => {
return <CanvasEntitySettings>PLACEHOLDER</CanvasEntitySettings>;
});
IMSettings.displayName = 'IMSettings';

View File

@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
caDeleted,
imReset,
ipaDeleted,
layerDeleted,
layerReset,
@ -85,9 +86,15 @@ export const ToolChooser: React.FC = () => {
if (type === 'regional_guidance') {
dispatch(rgReset({ id }));
}
if (type === 'inpaint_mask') {
dispatch(imReset());
}
}, [dispatch, selectedEntityIdentifier]);
const isResetEnabled = useMemo(
() => selectedEntityIdentifier?.type === 'layer' || selectedEntityIdentifier?.type === 'regional_guidance',
() =>
selectedEntityIdentifier?.type === 'layer' ||
selectedEntityIdentifier?.type === 'regional_guidance' ||
selectedEntityIdentifier?.type === 'inpaint_mask',
[selectedEntityIdentifier]
);
useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]);

View File

@ -319,6 +319,7 @@ export const {
imIsEnabledToggled,
imTranslated,
imBboxChanged,
imFillChanged,
imImageCacheChanged,
imBrushLineAdded,
imEraserLineAdded,

View File

@ -34,7 +34,7 @@ export const inpaintMaskReducers = {
state.inpaintMask.bbox = bbox;
state.inpaintMask.bboxNeedsUpdate = false;
},
inpaintMaskFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => {
imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => {
const { fill } = action.payload;
state.inpaintMask.fill = fill;
},