Reworks canvas toolbar

This commit is contained in:
psychedelicious 2022-11-20 09:35:22 +11:00 committed by blessedcoolant
parent 7f999e9dfc
commit 83d8e69219
11 changed files with 438 additions and 217 deletions

View File

@ -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<TooltipProps, 'children'>;
validValues:
| Array<number | string>
| 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 (
<FormControl
isDisabled={isDisabled}
className={`invokeai__select ${styleClass}`}
onClick={(e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
>
{label && (
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
>
{label}
</FormLabel>
)}
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
<Tooltip label={tooltip} {...tooltipProps}>
<FormControl
isDisabled={isDisabled}
className={`invokeai__select ${styleClass}`}
onClick={(e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
{opt.key}
</option>
);
})}
</Select>
</FormControl>
{label && (
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
>
{label}
</FormLabel>
)}
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
{opt.key}
</option>
);
})}
</Select>
</FormControl>
</Tooltip>
);
};

View File

@ -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>(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<HTMLInputElement>) => {
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 && (
<NumberInput
min={min}
max={max}
max={numberInputMax}
step={step}
value={localInputValue}
onChange={handleInputChange}

View File

@ -84,8 +84,8 @@ const IAICanvasBrushButtonPopover = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Brush (B)"
tooltip="Brush (B)"
aria-label="Brush Tool (B)"
tooltip="Brush Tool (B)"
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
onClick={handleSelectBrushTool}

View File

@ -77,8 +77,8 @@ const IAICanvasEraserButtonPopover = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Eraser (E)"
tooltip="Eraser (E)"
aria-label="Eraser Tool (E)"
tooltip="Eraser Tool (E)"
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isDisabled={isStaging}

View File

@ -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 (
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Select Mask Layer (Q)"
tooltip="Select Mask Layer (Q)"
data-alert={layer === 'mask'}
onClick={handleToggleMaskLayer}
icon={<FaMask />}
/>
}
>
<Flex direction={'column'} gap={'0.5rem'}>
<IAIButton onClick={handleClearMask} tooltip={'Clear Mask (Shift+C)'}>
Clear Mask
</IAIButton>
<IAICheckbox
label="Enable Mask (H)"
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}
/>
<IAICheckbox
label="Preserve Masked Area"
isChecked={shouldPreserveMaskedArea}
onChange={(e) =>
dispatch(setShouldPreserveMaskedArea(e.target.checked))
}
/>
<IAIColorPicker
color={maskColor}
onChange={(newColor) => dispatch(setMaskColor(newColor))}
/>
</Flex>
</IAIPopover>
);
};
export default IAICanvasMaskButtonPopover;

View File

@ -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 (
<>
<IAISelect
label={'Layer'}
tooltipProps={{ hasArrow: true, placement: 'top' }}
value={isMaskEnabled ? layer : 'base'}
isDisabled={!isMaskEnabled}
validValues={LAYER_NAMES_DICT}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setLayer(e.target.value as CanvasLayer))
}
/>
<IAIPopover
trigger="hover"
triggerComponent={
<ButtonGroup>
<IAIIconButton
aria-label="Inpainting Mask Options"
tooltip="Inpainting Mask Options"
data-alert={layer === 'mask'}
icon={<FaMask />}
/>
</ButtonGroup>
}
>
<Flex direction={'column'} gap={'0.5rem'}>
<IAICheckbox
label="Enable Mask (H)"
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}
/>
<IAICheckbox
label="Preserve Masked Area"
isChecked={shouldPreserveMaskedArea}
onChange={(e) =>
dispatch(setShouldPreserveMaskedArea(e.target.checked))
}
/>
<IAIColorPicker
style={{ paddingTop: '0.5rem', paddingBottom: '0.5rem' }}
color={maskColor}
onChange={(newColor) => dispatch(setMaskColor(newColor))}
/>
<IAIButton onClick={handleClearMask} tooltip={'Clear Mask (Shift+C)'}>
Clear Mask
</IAIButton>
</Flex>
</IAIPopover>
</>
);
};
export default IAICanvasMaskOptions;

View File

@ -60,8 +60,6 @@ const IAICanvasSettingsButtonPopover = () => {
trigger="hover"
triggerComponent={
<IAIIconButton
variant="link"
data-variant="link"
tooltip="Canvas Settings"
aria-label="Canvas Settings"
icon={<FaWrench />}

View File

@ -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 (
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Brush Tool (B)"
tooltip="Brush Tool (B)"
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
onClick={handleSelectBrushTool}
isDisabled={isStaging}
/>
<IAIIconButton
aria-label="Eraser Tool (E)"
tooltip="Eraser Tool (E)"
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isDisabled={isStaging}
onClick={() => dispatch(setTool('eraser'))}
/>
<IAIIconButton
aria-label="Move Tool (V)"
tooltip="Move Tool (V)"
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IAIPopover
trigger="hover"
triggerComponent={
<IAIIconButton
aria-label="Tool Options"
tooltip="Tool Options"
icon={<FaSlidersH />}
/>
}
>
<Flex
minWidth={'15rem'}
direction={'column'}
gap={'1rem'}
width={'100%'}
>
<Flex gap={'1rem'} justifyContent="space-between">
<IAISlider
label="Size"
value={brushSize}
withInput
onChange={(newSize) => dispatch(setBrushSize(newSize))}
sliderNumberInputProps={{ max: 500 }}
inputReadOnly={false}
/>
</Flex>
<IAIColorPicker
style={{
width: '100%',
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
}}
color={brushColor}
onChange={(newColor) => dispatch(setBrushColor(newColor))}
/>
</Flex>
</IAIPopover>
</ButtonGroup>
);
};
export default IAICanvasToolChooserOptions;

View File

@ -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 (
<div className="inpainting-settings">
<IAICanvasMaskButtonPopover />
<ButtonGroup isAttached>
<IAICanvasBrushButtonPopover />
<IAICanvasEraserButtonPopover />
<IAIIconButton
aria-label="Move (V)"
tooltip="Move (V)"
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
</ButtonGroup>
<IAICanvasMaskOptions />
<IAICanvasToolChooserOptions />
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Merge Visible (Shift+M)"
@ -234,9 +213,7 @@ const IAICanvasOutpaintingControls = () => {
<IAICanvasUndoButton />
<IAICanvasRedoButton />
</ButtonGroup>
<ButtonGroup isAttached>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Upload"
@ -250,12 +227,16 @@ const IAICanvasOutpaintingControls = () => {
onClick={handleResetCanvasView}
/>
<IAIIconButton
aria-label="Reset Canvas"
tooltip="Reset Canvas"
aria-label="Clear Canvas"
tooltip="Clear Canvas"
icon={<FaTrash />}
onClick={handleResetCanvas}
style={{ backgroundColor: 'var(--btn-delete-image)' }}
/>
</ButtonGroup>
<ButtonGroup isAttached>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
</div>
);
};

View File

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

View File

@ -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)' }}
/>
</DeleteImageModal>
</div>