Inpainting Controls Code Spitting and Performance

Codesplit the entirety of the inpainting controls. Created new selectors for each and every component to ensure there are no unnecessary re-renders. App feels a lot smoother.
This commit is contained in:
blessedcoolant 2022-11-02 11:36:28 +13:00 committed by Lincoln Stein
parent 868e4b2db8
commit db46e12f2b
14 changed files with 818 additions and 463 deletions

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI - A Stable Diffusion Toolkit</title> <title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" /> <link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="./assets/index.9ad2f384.js"></script> <script type="module" crossorigin src="./assets/index.ae6d2a5e.js"></script>
<link rel="stylesheet" href="./assets/index.14c578ee.css"> <link rel="stylesheet" href="./assets/index.14c578ee.css">
</head> </head>

View File

@ -1,442 +1,53 @@
import { useToast } from '@chakra-ui/react'; import InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl';
import { useHotkeys } from 'react-hotkeys-hook'; import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl';
import { import InpaintingMaskControl from './InpaintingControls/InpaintingMaskControl';
FaEraser, import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl';
FaMask, import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl';
FaPaintBrush, import InpaintingClearImageControl from './InpaintingControls/InpaintingClearImageControl';
FaPalette, import InpaintingSplitLayoutControl from './InpaintingControls/InpaintingSplitLayoutControl';
FaPlus,
FaRedo,
FaTrash,
FaUndo,
} from 'react-icons/fa';
import { BiHide, BiShow } from 'react-icons/bi';
import { VscSplitHorizontal } from 'react-icons/vsc';
import { useAppDispatch, useAppSelector } from '../../../app/store';
import IAIIconButton from '../../../common/components/IAIIconButton';
import {
clearMask,
redo,
setMaskColor,
setBrushSize,
setShouldShowBrushPreview,
setTool,
undo,
setShouldShowMask,
setShouldInvertMask,
setNeedsCache,
toggleShouldLockBoundingBox,
clearImageToInpaint,
} from './inpaintingSlice';
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
import IAISlider from '../../../common/components/IAISlider';
import IAINumberInput from '../../../common/components/IAINumberInput';
import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
import IAIPopover from '../../../common/components/IAIPopover';
import IAIColorPicker from '../../../common/components/IAIColorPicker';
import { RgbaColor } from 'react-colorful';
import { setShowDualDisplay } from '../../options/optionsSlice';
import { useState } from 'react';
const InpaintingControls = () => { const InpaintingControls = () => {
const { // const { shouldShowMask, activeTabName } = useAppSelector(
tool, // inpaintingControlsSelector
brushSize, // );
maskColor,
shouldInvertMask,
shouldShowMask,
canUndo,
canRedo,
isMaskEmpty,
activeTabName,
showDualDisplay,
} = useAppSelector(inpaintingControlsSelector);
const dispatch = useAppDispatch(); // const dispatch = useAppDispatch();
const toast = useToast();
// Button State Controllers
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
/** /**
* Hotkeys * Hotkeys
*/ */
// Decrease brush size
useHotkeys(
'[',
(e: KeyboardEvent) => {
e.preventDefault();
if (brushSize - 5 > 0) {
handleChangeBrushSize(brushSize - 5);
} else {
handleChangeBrushSize(1);
}
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, brushSize]
);
// Increase brush size
useHotkeys(
']',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeBrushSize(brushSize + 5);
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, brushSize]
);
// Decrease mask opacity
useHotkeys(
'shift+[',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeMaskColor({
...maskColor,
a: Math.max(maskColor.a - 0.05, 0),
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, maskColor.a]
);
// Increase mask opacity
useHotkeys(
'shift+]',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeMaskColor({
...maskColor,
a: Math.min(maskColor.a + 0.05, 100),
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, maskColor.a]
);
// Set tool to eraser
useHotkeys(
'e',
(e: KeyboardEvent) => {
e.preventDefault();
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
handleSelectEraserTool();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask]
);
// Set tool to brush
useHotkeys(
'b',
(e: KeyboardEvent) => {
e.preventDefault();
handleSelectBrushTool();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask]
);
// Toggle lock bounding box // Toggle lock bounding box
useHotkeys( // useHotkeys(
'shift+q', // 'shift+q',
(e: KeyboardEvent) => { // (e: KeyboardEvent) => {
e.preventDefault(); // e.preventDefault();
dispatch(toggleShouldLockBoundingBox()); // dispatch(toggleShouldLockBoundingBox());
}, // },
{ // {
enabled: activeTabName === 'inpainting' && shouldShowMask, // enabled: activeTabName === 'inpainting' && shouldShowMask,
}, // },
[activeTabName, shouldShowMask] // [activeTabName, shouldShowMask]
); // );
// Undo
useHotkeys(
'cmd+z, control+z',
(e: KeyboardEvent) => {
e.preventDefault();
handleUndo();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
},
[activeTabName, shouldShowMask, canUndo]
);
// Redo
useHotkeys(
'cmd+shift+z, control+shift+z, control+y, cmd+y',
(e: KeyboardEvent) => {
e.preventDefault();
handleRedo();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
},
[activeTabName, shouldShowMask, canRedo]
);
// Show/hide mask
useHotkeys(
'h',
(e: KeyboardEvent) => {
e.preventDefault();
handleToggleShouldShowMask();
},
{
enabled: activeTabName === 'inpainting',
},
[activeTabName, shouldShowMask]
);
// Invert mask
useHotkeys(
'shift+m',
(e: KeyboardEvent) => {
e.preventDefault();
handleToggleShouldInvertMask();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldInvertMask, shouldShowMask]
);
// Clear mask
useHotkeys(
'shift+c',
(e: KeyboardEvent) => {
e.preventDefault();
handleClearMask();
toast({
title: 'Mask Cleared',
status: 'success',
duration: 2500,
isClosable: true,
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
},
[activeTabName, isMaskEmpty, shouldShowMask]
);
// Toggle split view
useHotkeys(
'shift+j',
() => {
handleDualDisplay();
},
[showDualDisplay]
);
const handleClearMask = () => {
dispatch(clearMask());
};
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
const handleSelectBrushTool = () => dispatch(setTool('brush'));
const handleChangeBrushSize = (v: number) => {
dispatch(setShouldShowBrushPreview(true));
dispatch(setBrushSize(v));
};
const handleToggleShouldShowMask = () =>
dispatch(setShouldShowMask(!shouldShowMask));
const handleToggleShouldInvertMask = () =>
dispatch(setShouldInvertMask(!shouldInvertMask));
const handleShowBrushPreview = () => {
dispatch(setShouldShowBrushPreview(true));
};
const handleHideBrushPreview = () => {
dispatch(setShouldShowBrushPreview(false));
};
const handleChangeMaskColor = (newColor: RgbaColor) => {
dispatch(setMaskColor(newColor));
};
const handleUndo = () => dispatch(undo());
const handleRedo = () => dispatch(redo());
const handleDualDisplay = () => {
dispatch(setShowDualDisplay(!showDualDisplay));
dispatch(setNeedsCache(true));
};
const handleClearImage = () => {
dispatch(clearImageToInpaint());
};
return ( return (
<div className="inpainting-settings"> <div className="inpainting-settings">
<div className="inpainting-buttons-group"> <div className="inpainting-buttons-group">
<IAIPopover <InpaintingBrushControl />
trigger="hover" <InpaintingEraserControl />
onOpen={handleShowBrushPreview}
onClose={handleHideBrushPreview}
triggerComponent={
<IAIIconButton
aria-label="Brush (B)"
tooltip="Brush (B)"
icon={<FaPaintBrush />}
onClick={handleSelectBrushTool}
data-selected={tool === 'brush'}
isDisabled={!shouldShowMask}
/>
}
>
<div className="inpainting-slider-numberinput">
<IAISlider
label="Brush Size"
value={brushSize}
onChange={handleChangeBrushSize}
min={1}
max={200}
width="100px"
focusThumbOnChange={false}
isDisabled={!shouldShowMask}
/>
<IAINumberInput
value={brushSize}
onChange={handleChangeBrushSize}
width={'80px'}
min={1}
max={999}
isDisabled={!shouldShowMask}
/>
</div>
</IAIPopover>
<IAIIconButton
aria-label="Eraser (E)"
tooltip="Eraser (E)"
icon={<FaEraser />}
onClick={handleSelectEraserTool}
data-selected={tool === 'eraser'}
isDisabled={!shouldShowMask}
/>
</div> </div>
<div className="inpainting-buttons-group"> <div className="inpainting-buttons-group">
<IAIPopover <InpaintingMaskControl />
trigger="hover"
onOpen={() => setMaskOptionsOpen(true)}
onClose={() => setMaskOptionsOpen(false)}
triggerComponent={
<IAIIconButton
aria-label="Mask Options"
tooltip="Mask Options"
icon={<FaMask />}
cursor={'pointer'}
data-selected={maskOptionsOpen}
/>
}
>
<div className="inpainting-button-dropdown">
<IAIIconButton
aria-label="Hide/Show Mask (H)"
tooltip="Hide/Show Mask (H)"
data-selected={!shouldShowMask}
icon={
shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />
}
onClick={handleToggleShouldShowMask}
/>
<IAIIconButton
tooltip="Invert Mask Display (Shift+M)"
aria-label="Invert Mask Display (Shift+M)"
data-selected={shouldInvertMask}
icon={
shouldInvertMask ? (
<MdInvertColors size={22} />
) : (
<MdInvertColorsOff size={22} />
)
}
onClick={handleToggleShouldInvertMask}
isDisabled={!shouldShowMask}
/>
<IAIPopover
trigger="hover"
placement="right"
styleClass="inpainting-color-picker"
triggerComponent={
<IAIIconButton
aria-label="Mask Color"
tooltip="Mask Color"
icon={<FaPalette />}
isDisabled={!shouldShowMask}
cursor={'pointer'}
/>
}
>
<IAIColorPicker
color={maskColor}
onChange={handleChangeMaskColor}
/>
</IAIPopover>
</div>
</IAIPopover>
<IAIIconButton
aria-label="Clear Mask (Shift+C)"
tooltip="Clear Mask (Shift+C)"
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
onClick={handleClearMask}
isDisabled={isMaskEmpty || !shouldShowMask}
/>
</div> </div>
<div className="inpainting-buttons-group"> <div className="inpainting-buttons-group">
<IAIIconButton <InpaintingUndoControl />
aria-label="Undo" <InpaintingRedoControl />
tooltip="Undo"
icon={<FaUndo />}
onClick={handleUndo}
isDisabled={!canUndo || !shouldShowMask}
/>
<IAIIconButton
aria-label="Redo"
tooltip="Redo"
icon={<FaRedo />}
onClick={handleRedo}
isDisabled={!canRedo || !shouldShowMask}
/>
</div> </div>
<div className="inpainting-buttons-group"> <div className="inpainting-buttons-group">
<IAIIconButton <InpaintingClearImageControl />
aria-label="Clear Image"
tooltip="Clear Image"
icon={<FaTrash size={16} />}
onClick={handleClearImage}
/>
</div> </div>
<IAIIconButton <InpaintingSplitLayoutControl />
aria-label="Split Layout (Shift+J)"
tooltip="Split Layout (Shift+J)"
icon={<VscSplitHorizontal />}
data-selected={showDualDisplay}
onClick={handleDualDisplay}
/>
</div> </div>
); );
}; };

View File

@ -0,0 +1,148 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaPaintBrush } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import IAINumberInput from '../../../../common/components/IAINumberInput';
import IAIPopover from '../../../../common/components/IAIPopover';
import IAISlider from '../../../../common/components/IAISlider';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
import {
InpaintingState,
setBrushSize,
setShouldShowBrushPreview,
setTool,
} from '../inpaintingSlice';
import _ from 'lodash';
const inpaintingBrushSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { tool, brushSize, shouldShowMask } = inpainting;
return {
tool,
brushSize,
shouldShowMask,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingBrushControl() {
const dispatch = useAppDispatch();
const { tool, brushSize, shouldShowMask, activeTabName } = useAppSelector(
inpaintingBrushSelector
);
const handleSelectBrushTool = () => dispatch(setTool('brush'));
const handleShowBrushPreview = () => {
dispatch(setShouldShowBrushPreview(true));
};
const handleHideBrushPreview = () => {
dispatch(setShouldShowBrushPreview(false));
};
const handleChangeBrushSize = (v: number) => {
dispatch(setShouldShowBrushPreview(true));
dispatch(setBrushSize(v));
};
// Hotkeys
// Decrease brush size
useHotkeys(
'[',
(e: KeyboardEvent) => {
e.preventDefault();
if (brushSize - 5 > 0) {
handleChangeBrushSize(brushSize - 5);
} else {
handleChangeBrushSize(1);
}
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, brushSize]
);
// Increase brush size
useHotkeys(
']',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeBrushSize(brushSize + 5);
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, brushSize]
);
// Set tool to brush
useHotkeys(
'b',
(e: KeyboardEvent) => {
e.preventDefault();
handleSelectBrushTool();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask]
);
return (
<IAIPopover
trigger="hover"
onOpen={handleShowBrushPreview}
onClose={handleHideBrushPreview}
triggerComponent={
<IAIIconButton
aria-label="Brush (B)"
tooltip="Brush (B)"
icon={<FaPaintBrush />}
onClick={handleSelectBrushTool}
data-selected={tool === 'brush'}
isDisabled={!shouldShowMask}
/>
}
>
<div className="inpainting-slider-numberinput">
<IAISlider
label="Brush Size"
value={brushSize}
onChange={handleChangeBrushSize}
min={1}
max={200}
width="100px"
focusThumbOnChange={false}
isDisabled={!shouldShowMask}
/>
<IAINumberInput
value={brushSize}
onChange={handleChangeBrushSize}
width={'80px'}
min={1}
max={999}
isDisabled={!shouldShowMask}
/>
</div>
</IAIPopover>
);
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { FaTrash } from 'react-icons/fa';
import { useAppDispatch } from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { clearImageToInpaint } from '../inpaintingSlice';
export default function InpaintingClearImageControl() {
const dispatch = useAppDispatch();
const handleClearImage = () => {
dispatch(clearImageToInpaint());
};
return (
<IAIIconButton
aria-label="Clear Image"
tooltip="Clear Image"
icon={<FaTrash size={16} />}
onClick={handleClearImage}
/>
);
}

View File

@ -0,0 +1,67 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaEraser } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { InpaintingState, setTool } from '../inpaintingSlice';
import _ from 'lodash';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
const inpaintingEraserSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { tool, shouldShowMask } = inpainting;
return {
tool,
shouldShowMask,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingEraserControl() {
const { tool, shouldShowMask, activeTabName } = useAppSelector(
inpaintingEraserSelector
);
const dispatch = useAppDispatch();
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
// Hotkeys
// Set tool to eraser
useHotkeys(
'e',
(e: KeyboardEvent) => {
e.preventDefault();
if (activeTabName !== 'inpainting' || !shouldShowMask) return;
handleSelectEraserTool();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask]
);
return (
<IAIIconButton
aria-label="Eraser (E)"
tooltip="Eraser (E)"
icon={<FaEraser />}
onClick={handleSelectEraserTool}
data-selected={tool === 'eraser'}
isDisabled={!shouldShowMask}
/>
);
}

View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import { FaMask } from 'react-icons/fa';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import IAIPopover from '../../../../common/components/IAIPopover';
import InpaintingMaskVisibilityControl from './InpaintingMaskControls/InpaintingMaskVisibilityControl';
import InpaintingMaskInvertControl from './InpaintingMaskControls/InpaintingMaskInvertControl';
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
import InpaintingMaskClear from './InpaintingMaskControls/InpaintingMaskClear';
export default function InpaintingMaskControl() {
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
return (
<>
<IAIPopover
trigger="hover"
onOpen={() => setMaskOptionsOpen(true)}
onClose={() => setMaskOptionsOpen(false)}
triggerComponent={
<IAIIconButton
aria-label="Mask Options"
tooltip="Mask Options"
icon={<FaMask />}
cursor={'pointer'}
data-selected={maskOptionsOpen}
/>
}
>
<div className="inpainting-button-dropdown">
<InpaintingMaskVisibilityControl />
<InpaintingMaskInvertControl />
<InpaintingMaskColorPicker />
</div>
</IAIPopover>
<InpaintingMaskClear />
</>
);
}

View File

@ -0,0 +1,70 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { FaPlus } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../../app/store';
import IAIIconButton from '../../../../../common/components/IAIIconButton';
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
import { clearMask, InpaintingState } from '../../inpaintingSlice';
import _ from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook';
import { useToast } from '@chakra-ui/react';
const inpaintingMaskClearSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { shouldShowMask, lines } = inpainting;
return { shouldShowMask, activeTabName, isMaskEmpty: lines.length === 0 };
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingMaskClear() {
const { shouldShowMask, activeTabName, isMaskEmpty } = useAppSelector(
inpaintingMaskClearSelector
);
const dispatch = useAppDispatch();
const toast = useToast();
const handleClearMask = () => {
dispatch(clearMask());
};
// Clear mask
useHotkeys(
'shift+c',
(e: KeyboardEvent) => {
e.preventDefault();
handleClearMask();
toast({
title: 'Mask Cleared',
status: 'success',
duration: 2500,
isClosable: true,
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && !isMaskEmpty,
},
[activeTabName, isMaskEmpty, shouldShowMask]
);
return (
<IAIIconButton
aria-label="Clear Mask (Shift+C)"
tooltip="Clear Mask (Shift+C)"
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />}
onClick={handleClearMask}
isDisabled={isMaskEmpty || !shouldShowMask}
/>
);
}

View File

@ -0,0 +1,93 @@
import React from 'react';
import { RgbaColor } from 'react-colorful';
import { FaPalette } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../../app/store';
import IAIColorPicker from '../../../../../common/components/IAIColorPicker';
import IAIIconButton from '../../../../../common/components/IAIIconButton';
import IAIPopover from '../../../../../common/components/IAIPopover';
import { InpaintingState, setMaskColor } from '../../inpaintingSlice';
import _ from 'lodash';
import { createSelector } from '@reduxjs/toolkit';
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
const inpaintingMaskColorPickerSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { shouldShowMask, maskColor } = inpainting;
return { shouldShowMask, maskColor, activeTabName };
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingMaskColorPicker() {
const { shouldShowMask, maskColor, activeTabName } = useAppSelector(
inpaintingMaskColorPickerSelector
);
const dispatch = useAppDispatch();
const handleChangeMaskColor = (newColor: RgbaColor) => {
dispatch(setMaskColor(newColor));
};
// Hotkeys
// Decrease mask opacity
useHotkeys(
'shift+[',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeMaskColor({
...maskColor,
a: Math.max(maskColor.a - 0.05, 0),
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, maskColor.a]
);
// Increase mask opacity
useHotkeys(
'shift+]',
(e: KeyboardEvent) => {
e.preventDefault();
handleChangeMaskColor({
...maskColor,
a: Math.min(maskColor.a + 0.05, 100),
});
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldShowMask, maskColor.a]
);
return (
<IAIPopover
trigger="hover"
placement="right"
styleClass="inpainting-color-picker"
triggerComponent={
<IAIIconButton
aria-label="Mask Color"
tooltip="Mask Color"
icon={<FaPalette />}
isDisabled={!shouldShowMask}
cursor={'pointer'}
/>
}
>
<IAIColorPicker color={maskColor} onChange={handleChangeMaskColor} />
</IAIPopover>
);
}

View File

@ -0,0 +1,68 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../../app/store';
import IAIIconButton from '../../../../../common/components/IAIIconButton';
import { InpaintingState, setShouldInvertMask } from '../../inpaintingSlice';
import _ from 'lodash';
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
const inpaintingMaskInvertSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { shouldShowMask, shouldInvertMask } = inpainting;
return { shouldInvertMask, shouldShowMask, activeTabName };
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingMaskInvertControl() {
const { shouldInvertMask, shouldShowMask, activeTabName } = useAppSelector(
inpaintingMaskInvertSelector
);
const dispatch = useAppDispatch();
const handleToggleShouldInvertMask = () =>
dispatch(setShouldInvertMask(!shouldInvertMask));
// Invert mask
useHotkeys(
'shift+m',
(e: KeyboardEvent) => {
e.preventDefault();
handleToggleShouldInvertMask();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask,
},
[activeTabName, shouldInvertMask, shouldShowMask]
);
return (
<IAIIconButton
tooltip="Invert Mask Display (Shift+M)"
aria-label="Invert Mask Display (Shift+M)"
data-selected={shouldInvertMask}
icon={
shouldInvertMask ? (
<MdInvertColors size={22} />
) : (
<MdInvertColorsOff size={22} />
)
}
onClick={handleToggleShouldInvertMask}
isDisabled={!shouldShowMask}
/>
);
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BiHide, BiShow } from 'react-icons/bi';
import { createSelector } from 'reselect';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../../app/store';
import IAIIconButton from '../../../../../common/components/IAIIconButton';
import { activeTabNameSelector } from '../../../../options/optionsSelectors';
import { InpaintingState, setShouldShowMask } from '../../inpaintingSlice';
import _ from 'lodash';
const inpaintingMaskVisibilitySelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { shouldShowMask } = inpainting;
return { shouldShowMask, activeTabName };
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingMaskVisibilityControl() {
const dispatch = useAppDispatch();
const { shouldShowMask, activeTabName } = useAppSelector(
inpaintingMaskVisibilitySelector
);
const handleToggleShouldShowMask = () =>
dispatch(setShouldShowMask(!shouldShowMask));
// Hotkeys
// Show/hide mask
useHotkeys(
'h',
(e: KeyboardEvent) => {
e.preventDefault();
handleToggleShouldShowMask();
},
{
enabled: activeTabName === 'inpainting',
},
[activeTabName, shouldShowMask]
);
return (
<IAIIconButton
aria-label="Hide/Show Mask (H)"
tooltip="Hide/Show Mask (H)"
data-selected={!shouldShowMask}
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
onClick={handleToggleShouldShowMask}
/>
);
}

View File

@ -0,0 +1,66 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaRedo } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
import { InpaintingState, redo } from '../inpaintingSlice';
import _ from 'lodash';
const inpaintingRedoSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { futureLines, shouldShowMask } = inpainting;
return {
canRedo: futureLines.length > 0,
shouldShowMask,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingRedoControl() {
const dispatch = useAppDispatch();
const { canRedo, shouldShowMask, activeTabName } = useAppSelector(
inpaintingRedoSelector
);
const handleRedo = () => dispatch(redo());
// Hotkeys
// Redo
useHotkeys(
'cmd+shift+z, control+shift+z, control+y, cmd+y',
(e: KeyboardEvent) => {
e.preventDefault();
handleRedo();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && canRedo,
},
[activeTabName, shouldShowMask, canRedo]
);
return (
<IAIIconButton
aria-label="Redo"
tooltip="Redo"
icon={<FaRedo />}
onClick={handleRedo}
isDisabled={!canRedo || !shouldShowMask}
/>
);
}

View File

@ -0,0 +1,43 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { VscSplitHorizontal } from 'react-icons/vsc';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { setShowDualDisplay } from '../../../options/optionsSlice';
import { setNeedsCache } from '../inpaintingSlice';
export default function InpaintingSplitLayoutControl() {
const dispatch = useAppDispatch();
const showDualDisplay = useAppSelector(
(state: RootState) => state.options.showDualDisplay
);
const handleDualDisplay = () => {
dispatch(setShowDualDisplay(!showDualDisplay));
dispatch(setNeedsCache(true));
};
// Hotkeys
// Toggle split view
useHotkeys(
'shift+j',
() => {
handleDualDisplay();
},
[showDualDisplay]
);
return (
<IAIIconButton
aria-label="Split Layout (Shift+J)"
tooltip="Split Layout (Shift+J)"
icon={<VscSplitHorizontal />}
data-selected={showDualDisplay}
onClick={handleDualDisplay}
/>
);
}

View File

@ -0,0 +1,66 @@
import { createSelector } from '@reduxjs/toolkit';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaUndo } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { InpaintingState, undo } from '../inpaintingSlice';
import _ from 'lodash';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
const inpaintingUndoSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector],
(inpainting: InpaintingState, activeTabName) => {
const { pastLines, shouldShowMask } = inpainting;
return {
canUndo: pastLines.length > 0,
shouldShowMask,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function InpaintingUndoControl() {
const dispatch = useAppDispatch();
const { canUndo, shouldShowMask, activeTabName } = useAppSelector(
inpaintingUndoSelector
);
const handleUndo = () => dispatch(undo());
// Hotkeys
// Undo
useHotkeys(
'cmd+z, control+z',
(e: KeyboardEvent) => {
e.preventDefault();
handleUndo();
},
{
enabled: activeTabName === 'inpainting' && shouldShowMask && canUndo,
},
[activeTabName, shouldShowMask, canUndo]
);
return (
<IAIIconButton
aria-label="Undo"
tooltip="Undo"
icon={<FaUndo />}
onClick={handleUndo}
isDisabled={!canUndo || !shouldShowMask}
/>
);
}