diff --git a/frontend/src/common/components/IAISelect.tsx b/frontend/src/common/components/IAISelect.tsx index 138ba2da30..f4f74f5c83 100644 --- a/frontend/src/common/components/IAISelect.tsx +++ b/frontend/src/common/components/IAISelect.tsx @@ -1,9 +1,18 @@ -import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react'; +import { + FormControl, + FormLabel, + Select, + SelectProps, + Tooltip, + TooltipProps, +} from '@chakra-ui/react'; import { MouseEvent } from 'react'; type IAISelectProps = SelectProps & { label?: string; styleClass?: string; + tooltip?: string; + tooltipProps?: Omit; validValues: | Array | Array<{ key: string; value: string | number }>; @@ -16,57 +25,61 @@ const IAISelect = (props: IAISelectProps) => { label, isDisabled, validValues, + tooltip, + tooltipProps, size = 'sm', fontSize = 'md', styleClass, ...rest } = props; return ( - ) => { - e.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - e.nativeEvent.stopPropagation(); - e.nativeEvent.cancelBubble = true; - }} - > - {label && ( - - {label} - - )} - - - + {label && ( + + {label} + + )} + + + + ); }; diff --git a/frontend/src/common/components/IAISlider.tsx b/frontend/src/common/components/IAISlider.tsx index 6d9a0cb8e1..7dbbdea68f 100644 --- a/frontend/src/common/components/IAISlider.tsx +++ b/frontend/src/common/components/IAISlider.tsx @@ -23,7 +23,7 @@ import { Tooltip, TooltipProps, } from '@chakra-ui/react'; -import React, { FocusEvent, useEffect, useState } from 'react'; +import React, { FocusEvent, useEffect, useMemo, useState } from 'react'; import { BiReset } from 'react-icons/bi'; import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import _ from 'lodash'; @@ -101,6 +101,11 @@ export default function IAISlider(props: IAIFullSliderProps) { const [localInputValue, setLocalInputValue] = useState(String(value)); + const numberInputMax = useMemo( + () => (sliderNumberInputProps?.max ? sliderNumberInputProps.max : max), + [max, sliderNumberInputProps?.max] + ); + useEffect(() => { if (String(value) !== localInputValue && localInputValue !== '') { setLocalInputValue(String(value)); @@ -108,10 +113,11 @@ export default function IAISlider(props: IAIFullSliderProps) { }, [value, localInputValue, setLocalInputValue]); const handleInputBlur = (e: FocusEvent) => { + console.log(numberInputMax); const clamped = _.clamp( isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value), min, - max + numberInputMax ); setLocalInputValue(String(clamped)); onChange(clamped); @@ -202,7 +208,7 @@ export default function IAISlider(props: IAIFullSliderProps) { {withInput && ( { trigger="hover" triggerComponent={ } data-selected={tool === 'brush' && !isStaging} onClick={handleSelectBrushTool} diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx index f4afab8ca8..643c2afb26 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx @@ -77,8 +77,8 @@ const IAICanvasEraserButtonPopover = () => { trigger="hover" triggerComponent={ } data-selected={tool === 'eraser' && !isStaging} isDisabled={isStaging} diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskButtonPopover.tsx deleted file mode 100644 index b862908bc1..0000000000 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskButtonPopover.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { - clearMask, - setIsMaskEnabled, - setLayer, - setMaskColor, - setShouldPreserveMaskedArea, -} from 'features/canvas/store/canvasSlice'; -import { useAppDispatch, useAppSelector } from 'app/store'; -import _ from 'lodash'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { FaMask } from 'react-icons/fa'; -import IAIPopover from 'common/components/IAIPopover'; -import IAICheckbox from 'common/components/IAICheckbox'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import IAIButton from 'common/components/IAIButton'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; -import { useHotkeys } from 'react-hotkeys-hook'; - -export const selector = createSelector( - [canvasSelector], - (canvas) => { - const { maskColor, layer, isMaskEnabled, shouldPreserveMaskedArea } = - canvas; - - return { - layer, - maskColor, - isMaskEnabled, - shouldPreserveMaskedArea, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: _.isEqual, - }, - } -); -const IAICanvasMaskButtonPopover = () => { - const dispatch = useAppDispatch(); - const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } = - useAppSelector(selector); - - useHotkeys( - ['q'], - () => { - handleToggleMaskLayer(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [layer] - ); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [isMaskEnabled] - ); - - const handleToggleMaskLayer = () => { - dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); - }; - - const handleClearMask = () => dispatch(clearMask()); - - const handleToggleEnableMask = () => - dispatch(setIsMaskEnabled(!isMaskEnabled)); - - return ( - } - /> - } - > - - - Clear Mask - - - - dispatch(setShouldPreserveMaskedArea(e.target.checked)) - } - /> - dispatch(setMaskColor(newColor))} - /> - - - ); -}; - -export default IAICanvasMaskButtonPopover; diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx new file mode 100644 index 0000000000..b0090b4fb1 --- /dev/null +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx @@ -0,0 +1,153 @@ +import { Box, ButtonGroup, Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { + clearMask, + setIsMaskEnabled, + setLayer, + setMaskColor, + setShouldPreserveMaskedArea, +} from 'features/canvas/store/canvasSlice'; +import { useAppDispatch, useAppSelector } from 'app/store'; +import _ from 'lodash'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { FaMask } from 'react-icons/fa'; +import IAIPopover from 'common/components/IAIPopover'; +import IAICheckbox from 'common/components/IAICheckbox'; +import IAIColorPicker from 'common/components/IAIColorPicker'; +import IAIButton from 'common/components/IAIButton'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { useHotkeys } from 'react-hotkeys-hook'; +import IAISelect from 'common/components/IAISelect'; +import { + CanvasLayer, + LAYER_NAMES_DICT, +} from 'features/canvas/store/canvasTypes'; +import { ChangeEvent } from 'react'; +import { + rgbaColorToRgbString, + rgbaColorToString, +} from 'features/canvas/util/colorToString'; + +export const selector = createSelector( + [canvasSelector], + (canvas) => { + const { maskColor, layer, isMaskEnabled, shouldPreserveMaskedArea } = + canvas; + + return { + layer, + maskColor, + maskColorString: rgbaColorToString(maskColor), + isMaskEnabled, + shouldPreserveMaskedArea, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: _.isEqual, + }, + } +); +const IAICanvasMaskOptions = () => { + const dispatch = useAppDispatch(); + const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } = + useAppSelector(selector); + + useHotkeys( + ['q'], + () => { + handleToggleMaskLayer(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [layer] + ); + + useHotkeys( + ['shift+c'], + () => { + handleClearMask(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [] + ); + + useHotkeys( + ['h'], + () => { + handleToggleEnableMask(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [isMaskEnabled] + ); + + const handleToggleMaskLayer = () => { + dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); + }; + + const handleClearMask = () => dispatch(clearMask()); + + const handleToggleEnableMask = () => + dispatch(setIsMaskEnabled(!isMaskEnabled)); + + return ( + <> + ) => + dispatch(setLayer(e.target.value as CanvasLayer)) + } + /> + + } + /> + + } + > + + + + dispatch(setShouldPreserveMaskedArea(e.target.checked)) + } + /> + dispatch(setMaskColor(newColor))} + /> + + Clear Mask + + + + + ); +}; + +export default IAICanvasMaskOptions; diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx index 0c6624ec29..cf78acfe4e 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx @@ -60,8 +60,6 @@ const IAICanvasSettingsButtonPopover = () => { trigger="hover" triggerComponent={ } diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx new file mode 100644 index 0000000000..df1f35e51b --- /dev/null +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx @@ -0,0 +1,191 @@ +import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { + resetCanvas, + resetCanvasView, + resizeAndScaleCanvas, + setBrushColor, + setBrushSize, + setTool, +} from 'features/canvas/store/canvasSlice'; +import { useAppDispatch, useAppSelector } from 'app/store'; +import _ from 'lodash'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { + FaArrowsAlt, + FaEraser, + FaPaintBrush, + FaSlidersH, +} from 'react-icons/fa'; +import { + canvasSelector, + isStagingSelector, +} from 'features/canvas/store/canvasSelectors'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover'; +import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover'; +import { useHotkeys } from 'react-hotkeys-hook'; +import IAIPopover from 'common/components/IAIPopover'; +import IAISlider from 'common/components/IAISlider'; +import IAIColorPicker from 'common/components/IAIColorPicker'; +import { rgbaColorToString } from 'features/canvas/util/colorToString'; + +export const selector = createSelector( + [canvasSelector, isStagingSelector, systemSelector], + (canvas, isStaging, system) => { + const { isProcessing } = system; + const { tool, brushColor, brushSize } = canvas; + + return { + tool, + isStaging, + isProcessing, + brushColor, + brushColorString: rgbaColorToString(brushColor), + brushSize, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: _.isEqual, + }, + } +); + +const IAICanvasToolChooserOptions = () => { + const dispatch = useAppDispatch(); + const { tool, brushColor, brushSize, brushColorString, isStaging } = + useAppSelector(selector); + + useHotkeys( + ['v'], + () => { + handleSelectMoveTool(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [] + ); + + useHotkeys( + ['b'], + () => { + handleSelectBrushTool(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [] + ); + + useHotkeys( + ['e'], + () => { + handleSelectEraserTool(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [tool] + ); + + useHotkeys( + ['['], + () => { + dispatch(setBrushSize(Math.max(brushSize - 5, 5))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [brushSize] + ); + + useHotkeys( + [']'], + () => { + dispatch(setBrushSize(Math.min(brushSize + 5, 500))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [brushSize] + ); + + const handleSelectBrushTool = () => dispatch(setTool('brush')); + const handleSelectEraserTool = () => dispatch(setTool('eraser')); + const handleSelectMoveTool = () => dispatch(setTool('move')); + + return ( + + } + data-selected={tool === 'brush' && !isStaging} + onClick={handleSelectBrushTool} + isDisabled={isStaging} + /> + } + data-selected={tool === 'eraser' && !isStaging} + isDisabled={isStaging} + onClick={() => dispatch(setTool('eraser'))} + /> + } + data-selected={tool === 'move' || isStaging} + onClick={handleSelectMoveTool} + /> + + } + /> + } + > + + + dispatch(setBrushSize(newSize))} + sliderNumberInputProps={{ max: 500 }} + inputReadOnly={false} + /> + + dispatch(setBrushColor(newColor))} + /> + + + + ); +}; + +export default IAICanvasToolChooserOptions; diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 7f154faadb..19e88263ac 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -16,6 +16,7 @@ import { FaDownload, FaLayerGroup, FaSave, + FaSlidersH, FaTrash, FaUpload, } from 'react-icons/fa'; @@ -24,7 +25,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton'; import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover'; import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover'; -import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover'; +import IAICanvasMaskOptions from './IAICanvasMaskOptions'; import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas'; import { canvasSelector, @@ -33,6 +34,7 @@ import { import { useHotkeys } from 'react-hotkeys-hook'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; import { systemSelector } from 'features/system/store/systemSelectors'; +import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; export const selector = createSelector( [canvasSelector, isStagingSelector, systemSelector], @@ -58,18 +60,6 @@ const IAICanvasOutpaintingControls = () => { const { tool, isStaging, isProcessing } = useAppSelector(selector); const canvasBaseLayer = getCanvasBaseLayer(); - useHotkeys( - ['v'], - () => { - handleSelectMoveTool(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [] - ); - useHotkeys( ['r'], () => { @@ -130,8 +120,6 @@ const IAICanvasOutpaintingControls = () => { [canvasBaseLayer, isProcessing] ); - const handleSelectMoveTool = () => dispatch(setTool('move')); - const handleResetCanvasView = () => { const canvasBaseLayer = getCanvasBaseLayer(); if (!canvasBaseLayer) return; @@ -188,18 +176,9 @@ const IAICanvasOutpaintingControls = () => { return (
- - - - - } - data-selected={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - + + + { - - - + { onClick={handleResetCanvasView} /> } onClick={handleResetCanvas} + style={{ backgroundColor: 'var(--btn-delete-image)' }} /> + + +
); }; diff --git a/frontend/src/features/canvas/store/canvasTypes.ts b/frontend/src/features/canvas/store/canvasTypes.ts index 48d51a5f49..47ef56414c 100644 --- a/frontend/src/features/canvas/store/canvasTypes.ts +++ b/frontend/src/features/canvas/store/canvasTypes.ts @@ -1,8 +1,15 @@ import * as InvokeAI from 'app/invokeai'; -import { IRect, Vector2d } from 'konva/lib/types'; +import { Vector2d } from 'konva/lib/types'; import { RgbaColor } from 'react-colorful'; -export type CanvasLayer = 'base' | 'mask'; +export const LAYER_NAMES_DICT = [ + { key: 'Base', value: 'base' }, + { key: 'Mask', value: 'mask' }, +]; + +export const LAYER_NAMES = ['base', 'mask'] as const; + +export type CanvasLayer = typeof LAYER_NAMES[number]; export type CanvasDrawingTool = 'brush' | 'eraser'; diff --git a/frontend/src/features/gallery/components/CurrentImageButtons.tsx b/frontend/src/features/gallery/components/CurrentImageButtons.tsx index de861536ed..a7c6c410ed 100644 --- a/frontend/src/features/gallery/components/CurrentImageButtons.tsx +++ b/frontend/src/features/gallery/components/CurrentImageButtons.tsx @@ -488,7 +488,7 @@ const CurrentImageButtons = () => { tooltip="Delete Image" aria-label="Delete Image" isDisabled={!currentImage || !isConnected || isProcessing} - className="delete-image-btn" + style={{ backgroundColor: 'var(--btn-delete-image)' }} />