diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 405bf43c21..9e771eb48e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -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(() => { + {entityCount > 0 && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx new file mode 100644 index 0000000000..3c0fb3b75b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -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 ( + + + {isOpen && } + + ); +}); + +IM.displayName = 'IM'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx new file mode 100644 index 0000000000..14462abc73 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx @@ -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 ( + + + + }> + {t('accessibility.reset')} + + + + ); +}); + +IMActionsMenu.displayName = 'IMActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx new file mode 100644 index 0000000000..1102e056f3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx @@ -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 ( + + + + + + + + ); +}); + +IMHeader.displayName = 'IMHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx new file mode 100644 index 0000000000..144012143d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx @@ -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 ( + + + + + + + + + + + ); +}); + +IMMaskFillColorPicker.displayName = 'IMMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx new file mode 100644 index 0000000000..3c1b7296d9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx @@ -0,0 +1,8 @@ +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { memo } from 'react'; + +export const IMSettings = memo(() => { + return PLACEHOLDER; +}); + +IMSettings.displayName = 'IMSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 863a56c1bd..688be5f043 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9f282f9734..20bbeade0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -319,6 +319,7 @@ export const { imIsEnabledToggled, imTranslated, imBboxChanged, + imFillChanged, imImageCacheChanged, imBrushLineAdded, imEraserLineAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index bff4a8a9bb..881028b0d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -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; },