Refactor canvas buttons + more

This commit is contained in:
psychedelicious 2022-11-03 00:53:53 +11:00 committed by Lincoln Stein
parent 3feb7d8922
commit 6173e3e9ca
37 changed files with 884 additions and 672 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

517
frontend/dist/assets/index.bf9dd1fc.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,8 @@
<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.adcf8963.js"></script> <script type="module" crossorigin src="./assets/index.bf9dd1fc.js"></script>
<link rel="stylesheet" href="./assets/index.28b80602.css"> <link rel="stylesheet" href="./assets/index.f9f4c989.css">
</head> </head>
<body> <body>

View File

@ -21,6 +21,27 @@
&[data-variant='link'] { &[data-variant='link'] {
background: none !important; background: none !important;
&:hover {
background: none !important;
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
&:hover {
border-color: var(--accent-color-hover);
}
}
&[data-alert='true'] {
animation-name: pulseColor;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
&:hover {
animation: none;
background-color: var(--accent-color-hover);
}
} }
&[data-as-checkbox='true'] { &[data-as-checkbox='true'] {
@ -38,13 +59,17 @@
fill: var(--text-color); fill: var(--text-color);
} }
} }
}
&[data-selected='true'] { }
border-color: var(--accent-color);
background-color: var(--btn-grey); @keyframes pulseColor {
svg { 0% {
fill: var(--text-color); background-color: var(--accent-color);
} }
} 50% {
background-color: var(--accent-color-dim);
}
100% {
background-color: var(--accent-color);
} }
} }

View File

@ -1,7 +1,6 @@
.invokeai__number-input-form-control { .invokeai__number-input-form-control {
display: grid; display: grid;
grid-template-columns: max-content auto; grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center; align-items: center;
.invokeai__number-input-form-label { .invokeai__number-input-form-label {
@ -11,6 +10,7 @@
margin-bottom: 0; margin-bottom: 0;
flex-grow: 2; flex-grow: 2;
white-space: nowrap; white-space: nowrap;
padding-right: 1rem;
&[data-focus] + .invokeai__number-input-root { &[data-focus] + .invokeai__number-input-root {
outline: none; outline: none;

View File

@ -123,13 +123,15 @@ const IAINumberInput = (props: Props) => {
} }
{...formControlProps} {...formControlProps}
> >
<FormLabel {label && (
className="invokeai__number-input-form-label" <FormLabel
style={{ display: label ? 'block' : 'none' }} className="invokeai__number-input-form-label"
{...formLabelProps} style={{ display: label ? 'block' : 'none' }}
> {...formLabelProps}
{label} >
</FormLabel> {label}
</FormLabel>
)}
<NumberInput <NumberInput
className="invokeai__number-input-root" className="invokeai__number-input-root"
value={valueAsString} value={valueAsString}
@ -145,19 +147,18 @@ const IAINumberInput = (props: Props) => {
textAlign={textAlign} textAlign={textAlign}
{...numberInputFieldProps} {...numberInputFieldProps}
/> />
<div {showStepper && (
className="invokeai__number-input-stepper" <div className="invokeai__number-input-stepper">
style={showStepper ? { display: 'block' } : { display: 'none' }} <NumberIncrementStepper
> {...numberInputStepperProps}
<NumberIncrementStepper className="invokeai__number-input-stepper-button"
{...numberInputStepperProps} />
className="invokeai__number-input-stepper-button" <NumberDecrementStepper
/> {...numberInputStepperProps}
<NumberDecrementStepper className="invokeai__number-input-stepper-button"
{...numberInputStepperProps} />
className="invokeai__number-input-stepper-button" </div>
/> )}
</div>
</NumberInput> </NumberInput>
</FormControl> </FormControl>
</Tooltip> </Tooltip>

View File

@ -0,0 +1,19 @@
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa';
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
import IAIIconButton from './IAIIconButton';
const ImageUploaderIconButton = () => {
const openImageUploader = useContext(ImageUploaderTriggerContext);
return (
<IAIIconButton
aria-label="Upload Image"
tooltip="Upload Image"
icon={<FaUpload />}
onClick={openImageUploader || undefined}
/>
);
};
export default ImageUploaderIconButton;

View File

@ -34,7 +34,10 @@ import {
FaShareAlt, FaShareAlt,
FaTrash, FaTrash,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice'; import {
setImageToInpaint,
setNeedsCache,
} from '../tabs/Inpainting/inpaintingSlice';
import { GalleryState } from './gallerySlice'; import { GalleryState } from './gallerySlice';
import { activeTabNameSelector } from '../options/optionsSelectors'; import { activeTabNameSelector } from '../options/optionsSelectors';
import IAIPopover from '../../common/components/IAIPopover'; import IAIPopover from '../../common/components/IAIPopover';
@ -95,7 +98,6 @@ const CurrentImageButtons = () => {
facetoolStrength, facetoolStrength,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
shouldShowImageDetails, shouldShowImageDetails,
activeTabName,
currentImage, currentImage,
} = useAppSelector(systemSelector); } = useAppSelector(systemSelector);
@ -108,7 +110,7 @@ const CurrentImageButtons = () => {
const handleClickUseAsInitialImage = () => { const handleClickUseAsInitialImage = () => {
if (!currentImage) return; if (!currentImage) return;
dispatch(setInitialImage(currentImage)); dispatch(setInitialImage(currentImage));
dispatch(setActiveTab(1)); dispatch(setActiveTab('img2img'));
}; };
const handleCopyImageLink = () => { const handleCopyImageLink = () => {
@ -308,9 +310,10 @@ const CurrentImageButtons = () => {
if (!currentImage) return; if (!currentImage) return;
dispatch(setImageToInpaint(currentImage)); dispatch(setImageToInpaint(currentImage));
if (activeTabName !== 'inpainting') {
dispatch(setActiveTab('inpainting')); dispatch(setActiveTab('inpainting'));
} dispatch(setNeedsCache(true));
toast({ toast({
title: 'Sent to Inpainting', title: 'Sent to Inpainting',
status: 'success', status: 'success',
@ -461,7 +464,7 @@ const CurrentImageButtons = () => {
icon={<FaTrash />} icon={<FaTrash />}
tooltip="Delete Image" tooltip="Delete Image"
aria-label="Delete Image" aria-label="Delete Image"
isDisabled={Boolean(currentImage) || !isConnected || isProcessing} isDisabled={!currentImage || !isConnected || isProcessing}
/> />
</DeleteImageModal> </DeleteImageModal>
</ButtonGroup> </ButtonGroup>

View File

@ -38,6 +38,7 @@
justify-content: space-between; justify-content: space-between;
z-index: 1; z-index: 1;
height: 100%; height: 100%;
width: 100%;
pointer-events: none; pointer-events: none;
} }

View File

@ -2,8 +2,15 @@ import { Link, useColorMode } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { FaSun, FaMoon, FaGithub, FaDiscord, FaBug } from 'react-icons/fa'; import {
import { MdKeyboard, MdSettings } from 'react-icons/md'; FaSun,
FaMoon,
FaGithub,
FaDiscord,
FaBug,
FaKeyboard,
FaWrench,
} from 'react-icons/fa';
import InvokeAILogo from '../../assets/images/logo.png'; import InvokeAILogo from '../../assets/images/logo.png';
import IAIIconButton from '../../common/components/IAIIconButton'; import IAIIconButton from '../../common/components/IAIIconButton';
@ -27,11 +34,6 @@ const SiteHeader = () => {
[colorMode, toggleColorMode] [colorMode, toggleColorMode]
); );
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
// Make FaMoon and FaSun icon apparent size consistent
const colorModeIconFontSize = colorMode == 'light' ? 18 : 20;
return ( return (
<div className="site-header"> <div className="site-header">
<div className="site-header-left-side"> <div className="site-header-left-side">
@ -48,10 +50,11 @@ const SiteHeader = () => {
<IAIIconButton <IAIIconButton
aria-label="Hotkeys" aria-label="Hotkeys"
tooltip="Hotkeys" tooltip="Hotkeys"
fontSize={24}
size={'sm'} size={'sm'}
variant="link" variant="link"
icon={<MdKeyboard />} data-variant="link"
fontSize={20}
icon={<FaKeyboard />}
/> />
</HotkeysModal> </HotkeysModal>
@ -60,16 +63,18 @@ const SiteHeader = () => {
tooltip="Dark Mode" tooltip="Dark Mode"
onClick={toggleColorMode} onClick={toggleColorMode}
variant="link" variant="link"
data-variant="link"
fontSize={20}
size={'sm'} size={'sm'}
fontSize={colorModeIconFontSize} icon={colorMode === 'light' ? <FaMoon /> : <FaSun />}
icon={colorModeIcon}
/> />
<IAIIconButton <IAIIconButton
aria-label="Report Bug" aria-label="Report Bug"
tooltip="Report Bug" tooltip="Report Bug"
variant="link" variant="link"
fontSize={19} data-variant="link"
fontSize={20}
size={'sm'} size={'sm'}
icon={ icon={
<Link isExternal href="http://github.com/invoke-ai/InvokeAI/issues"> <Link isExternal href="http://github.com/invoke-ai/InvokeAI/issues">
@ -82,6 +87,7 @@ const SiteHeader = () => {
aria-label="Link to Github Repo" aria-label="Link to Github Repo"
tooltip="Github" tooltip="Github"
variant="link" variant="link"
data-variant="link"
fontSize={20} fontSize={20}
size={'sm'} size={'sm'}
icon={ icon={
@ -95,6 +101,7 @@ const SiteHeader = () => {
aria-label="Link to Discord Server" aria-label="Link to Discord Server"
tooltip="Discord" tooltip="Discord"
variant="link" variant="link"
data-variant="link"
fontSize={20} fontSize={20}
size={'sm'} size={'sm'}
icon={ icon={
@ -109,9 +116,10 @@ const SiteHeader = () => {
aria-label="Settings" aria-label="Settings"
tooltip="Settings" tooltip="Settings"
variant="link" variant="link"
fontSize={24} data-variant="link"
fontSize={20}
size={'sm'} size={'sm'}
icon={<MdSettings />} icon={<FaWrench />}
/> />
</SettingsModal> </SettingsModal>
</div> </div>

View File

@ -20,7 +20,7 @@
.init-image-preview-header { .init-image-preview-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
width: 100%; width: 100%;
h2 { h2 {

View File

@ -1,7 +1,7 @@
import { IconButton, Image, useToast } from '@chakra-ui/react'; import { Image, useToast } from '@chakra-ui/react';
import React, { SyntheticEvent } from 'react'; import { SyntheticEvent } from 'react';
import { MdClear } from 'react-icons/md';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store'; import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
import { clearInitialImage } from '../../options/optionsSlice'; import { clearInitialImage } from '../../options/optionsSlice';
export default function InitImagePreview() { export default function InitImagePreview() {
@ -13,10 +13,10 @@ export default function InitImagePreview() {
const toast = useToast(); const toast = useToast();
const handleClickResetInitialImage = (e: SyntheticEvent) => { // const handleClickResetInitialImage = (e: SyntheticEvent) => {
e.stopPropagation(); // e.stopPropagation();
dispatch(clearInitialImage()); // dispatch(clearInitialImage());
}; // };
const alertMissingInitImage = () => { const alertMissingInitImage = () => {
toast({ toast({
@ -31,13 +31,7 @@ export default function InitImagePreview() {
return ( return (
<> <>
<div className="init-image-preview-header"> <div className="init-image-preview-header">
<h2>Initial Image</h2> <ImageUploaderIconButton />
<IconButton
isDisabled={!initialImage}
aria-label={'Reset Initial Image'}
onClick={handleClickResetInitialImage}
icon={<MdClear />}
/>
</div> </div>
{initialImage && ( {initialImage && (
<div className="init-image-preview"> <div className="init-image-preview">

View File

@ -11,7 +11,7 @@
.inpainting-settings { .inpainting-settings {
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 1rem; column-gap: 0.5rem;
.inpainting-buttons-group { .inpainting-buttons-group {
display: flex; display: flex;
@ -29,10 +29,10 @@
margin-left: 1rem !important; margin-left: 1rem !important;
} }
.inpainting-slider-numberinput { .inpainting-brush-options {
display: flex; display: flex;
column-gap: 1rem;
align-items: center; align-items: center;
column-gap: 1rem;
} }
} }

View File

@ -33,9 +33,8 @@ import InpaintingBoundingBoxPreview, {
InpaintingBoundingBoxPreviewOverlay, InpaintingBoundingBoxPreviewOverlay,
} from './components/InpaintingBoundingBoxPreview'; } from './components/InpaintingBoundingBoxPreview';
import { KonvaEventObject } from 'konva/lib/Node'; import { KonvaEventObject } from 'konva/lib/Node';
import KeyboardEventManager from './components/KeyboardEventManager'; import KeyboardEventManager from './KeyboardEventManager';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import InpaintingCanvasStatusIcons from './InpaintingCanvasStatusIcons';
// Use a closure allow other components to use these things... not ideal... // Use a closure allow other components to use these things... not ideal...
export let stageRef: MutableRefObject<StageType | null>; export let stageRef: MutableRefObject<StageType | null>;
@ -57,7 +56,6 @@ const InpaintingCanvas = () => {
shouldShowBoundingBox, shouldShowBoundingBox,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
isDrawing, isDrawing,
isMouseOverBoundingBox,
isModifyingBoundingBox, isModifyingBoundingBox,
stageCursor, stageCursor,
} = useAppSelector(inpaintingCanvasSelector); } = useAppSelector(inpaintingCanvasSelector);
@ -236,10 +234,8 @@ const InpaintingCanvas = () => {
); );
return ( return (
<div className="inpainting-canvas-container" tabIndex={1}> <div className="inpainting-canvas-container">
<div className="inpainting-canvas-wrapper"> <div className="inpainting-canvas-wrapper">
<InpaintingCanvasStatusIcons />
{canvasBgImage && ( {canvasBgImage && (
<Stage <Stage
width={Math.floor(canvasBgImage.width * stageScale)} width={Math.floor(canvasBgImage.width * stageScale)}
@ -274,9 +270,7 @@ const InpaintingCanvas = () => {
> >
<InpaintingCanvasLines /> <InpaintingCanvasLines />
{!isMouseOverBoundingBox && !isModifyingBoundingBox && ( <InpaintingCanvasBrushPreview />
<InpaintingCanvasBrushPreview />
)}
{shouldInvertMask && ( {shouldInvertMask && (
<KonvaImage <KonvaImage
@ -299,9 +293,7 @@ const InpaintingCanvas = () => {
)} )}
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />} {shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
{!isMouseOverBoundingBox && !isModifyingBoundingBox && ( <InpaintingCanvasBrushPreviewOutline />
<InpaintingCanvasBrushPreviewOutline />
)}
</Layer> </Layer>
</> </>
)} )}

View File

@ -4,7 +4,6 @@
left: 0; left: 0;
z-index: 2; z-index: 2;
margin: 0.5rem; margin: 0.5rem;
pointer-events: none;
button { button {
background-color: var(--inpainting-alerts-bg); background-color: var(--inpainting-alerts-bg);

View File

@ -27,7 +27,7 @@ const inpaintingCanvasStatusIconsSelector = createSelector(
} }
); );
import { ButtonGroup, IconButton } from '@chakra-ui/react'; import { ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { BiHide, BiShow } from 'react-icons/bi'; import { BiHide, BiShow } from 'react-icons/bi';
import { GiResize } from 'react-icons/gi'; import { GiResize } from 'react-icons/gi';
@ -36,6 +36,7 @@ import { FaLock, FaUnlock } from 'react-icons/fa';
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md'; import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
import { RootState, useAppSelector } from '../../../app/store'; import { RootState, useAppSelector } from '../../../app/store';
import { InpaintingState } from './inpaintingSlice'; import { InpaintingState } from './inpaintingSlice';
import { MouseEvent, useRef, useState } from 'react';
const InpaintingCanvasStatusIcons = () => { const InpaintingCanvasStatusIcons = () => {
const { const {
@ -46,17 +47,48 @@ const InpaintingCanvasStatusIcons = () => {
isBoundingBoxTooSmall, isBoundingBoxTooSmall,
} = useAppSelector(inpaintingCanvasStatusIconsSelector); } = useAppSelector(inpaintingCanvasStatusIconsSelector);
const [shouldAcceptPointerEvents, setShouldAcceptPointerEvents] =
useState<boolean>(false);
const timeoutRef = useRef<number>(0);
const handleMouseOver = () => {
if (!shouldAcceptPointerEvents) {
timeoutRef.current = window.setTimeout(
() => setShouldAcceptPointerEvents(true),
1000
);
}
};
const handleMouseOut = () => {
if (!shouldAcceptPointerEvents) {
setShouldAcceptPointerEvents(false);
window.clearTimeout(timeoutRef.current);
}
};
return ( return (
<div className="inpainting-alerts"> <div
<ButtonGroup isAttached> className="inpainting-alerts"
<IconButton onMouseOver={handleMouseOver}
aria-label="Show/HideMask" onMouseOut={handleMouseOut}
size="xs" onMouseLeave={handleMouseOut}
variant={'ghost'} onBlur={handleMouseOut}
fontSize={'1rem'} >
data-selected={!shouldShowMask} <ButtonGroup
icon={shouldShowMask ? <BiShow /> : <BiHide />} isAttached
/> pointerEvents={shouldAcceptPointerEvents ? 'auto' : 'none'}
>
<Tooltip label="Mask Hidden">
<IconButton
aria-label="Show/HideMask"
size="xs"
variant={'ghost'}
fontSize={'1rem'}
data-selected={!shouldShowMask}
icon={shouldShowMask ? <BiShow /> : <BiHide />}
/>
</Tooltip>
<IconButton <IconButton
aria-label="Invert Mask" aria-label="Invert Mask"
variant={'ghost'} variant={'ghost'}

View File

@ -1,30 +1,38 @@
import InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl'; import InpaintingBrushControl from './InpaintingControls/InpaintingBrushControl';
import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl'; import InpaintingEraserControl from './InpaintingControls/InpaintingEraserControl';
import InpaintingMaskControl from './InpaintingControls/InpaintingMaskControl';
import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl'; import InpaintingUndoControl from './InpaintingControls/InpaintingUndoControl';
import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl'; import InpaintingRedoControl from './InpaintingControls/InpaintingRedoControl';
import InpaintingClearImageControl from './InpaintingControls/InpaintingClearImageControl'; import { ButtonGroup } from '@chakra-ui/react';
import InpaintingSplitLayoutControl from './InpaintingControls/InpaintingSplitLayoutControl'; import InpaintingMaskClear from './InpaintingControls/InpaintingMaskControls/InpaintingMaskClear';
import InpaintingMaskVisibilityControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskVisibilityControl';
import InpaintingMaskInvertControl from './InpaintingControls/InpaintingMaskControls/InpaintingMaskInvertControl';
import InpaintingLockBoundingBoxControl from './InpaintingControls/InpaintingLockBoundingBoxControl';
import InpaintingShowHideBoundingBoxControl from './InpaintingControls/InpaintingShowHideBoundingBoxControl';
import ImageUploaderIconButton from '../../../common/components/ImageUploaderIconButton';
const InpaintingControls = () => { const InpaintingControls = () => {
return ( return (
<div className="inpainting-settings"> <div className="inpainting-settings">
<div className="inpainting-buttons-group"> <ButtonGroup isAttached={true}>
<InpaintingBrushControl /> <InpaintingBrushControl />
<InpaintingEraserControl /> <InpaintingEraserControl />
</div> </ButtonGroup>
<div className="inpainting-buttons-group">
<InpaintingMaskControl /> <ButtonGroup isAttached={true}>
</div> <InpaintingMaskVisibilityControl />
<div className="inpainting-buttons-group"> <InpaintingMaskInvertControl />
<InpaintingLockBoundingBoxControl />
<InpaintingShowHideBoundingBoxControl />
</ButtonGroup>
<ButtonGroup isAttached={true}>
<InpaintingUndoControl /> <InpaintingUndoControl />
<InpaintingRedoControl /> <InpaintingRedoControl />
</div> <InpaintingMaskClear />
</ButtonGroup>
<div className="inpainting-buttons-group"> <ButtonGroup isAttached={true}>
<InpaintingClearImageControl /> <ImageUploaderIconButton />
</div> </ButtonGroup>
<InpaintingSplitLayoutControl />
</div> </div>
); );
}; };

View File

@ -21,6 +21,7 @@ import {
} from '../inpaintingSlice'; } from '../inpaintingSlice';
import _ from 'lodash'; import _ from 'lodash';
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
const inpaintingBrushSelector = createSelector( const inpaintingBrushSelector = createSelector(
[(state: RootState) => state.inpainting, activeTabNameSelector], [(state: RootState) => state.inpainting, activeTabNameSelector],
@ -123,7 +124,7 @@ export default function InpaintingBrushControl() {
/> />
} }
> >
<div className="inpainting-slider-numberinput"> <div className="inpainting-brush-options">
<IAISlider <IAISlider
label="Brush Size" label="Brush Size"
value={brushSize} value={brushSize}
@ -142,6 +143,7 @@ export default function InpaintingBrushControl() {
max={999} max={999}
isDisabled={!shouldShowMask} isDisabled={!shouldShowMask}
/> />
<InpaintingMaskColorPicker />
</div> </div>
</IAIPopover> </IAIPopover>
); );

View File

@ -0,0 +1,29 @@
import { FaLock, FaUnlock } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { setShouldLockBoundingBox } from '../inpaintingSlice';
const InpaintingLockBoundingBoxControl = () => {
const dispatch = useAppDispatch();
const shouldLockBoundingBox = useAppSelector(
(state: RootState) => state.inpainting.shouldLockBoundingBox
);
return (
<IAIIconButton
aria-label="Lock Inpainting Box"
tooltip="Lock Inpainting Box"
icon={shouldLockBoundingBox ? <FaLock /> : <FaUnlock />}
data-selected={shouldLockBoundingBox}
onClick={() => {
dispatch(setShouldLockBoundingBox(!shouldLockBoundingBox));
}}
/>
);
};
export default InpaintingLockBoundingBoxControl;

View File

@ -7,7 +7,6 @@ import IAIPopover from '../../../../common/components/IAIPopover';
import InpaintingMaskVisibilityControl from './InpaintingMaskControls/InpaintingMaskVisibilityControl'; import InpaintingMaskVisibilityControl from './InpaintingMaskControls/InpaintingMaskVisibilityControl';
import InpaintingMaskInvertControl from './InpaintingMaskControls/InpaintingMaskInvertControl'; import InpaintingMaskInvertControl from './InpaintingMaskControls/InpaintingMaskInvertControl';
import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker'; import InpaintingMaskColorPicker from './InpaintingMaskControls/InpaintingMaskColorPicker';
import InpaintingMaskClear from './InpaintingMaskControls/InpaintingMaskClear';
export default function InpaintingMaskControl() { export default function InpaintingMaskControl() {
const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false); const [maskOptionsOpen, setMaskOptionsOpen] = useState<boolean>(false);
@ -34,7 +33,6 @@ export default function InpaintingMaskControl() {
<InpaintingMaskColorPicker /> <InpaintingMaskColorPicker />
</div> </div>
</IAIPopover> </IAIPopover>
<InpaintingMaskClear />
</> </>
); );
} }

View File

@ -62,7 +62,7 @@ export default function InpaintingMaskClear() {
<IAIIconButton <IAIIconButton
aria-label="Clear Mask (Shift+C)" aria-label="Clear Mask (Shift+C)"
tooltip="Clear Mask (Shift+C)" tooltip="Clear Mask (Shift+C)"
icon={<FaPlus size={18} style={{ transform: 'rotate(45deg)' }} />} icon={<FaPlus size={20} style={{ transform: 'rotate(45deg)' }} />}
onClick={handleClearMask} onClick={handleClearMask}
isDisabled={isMaskEmpty || !shouldShowMask} isDisabled={isMaskEmpty || !shouldShowMask}
/> />

View File

@ -75,12 +75,10 @@ export default function InpaintingMaskColorPicker() {
return ( return (
<IAIPopover <IAIPopover
trigger="hover" trigger="hover"
placement="right"
styleClass="inpainting-color-picker" styleClass="inpainting-color-picker"
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
aria-label="Mask Color" aria-label="Mask Color"
tooltip="Mask Color"
icon={<FaPalette />} icon={<FaPalette />}
isDisabled={!shouldShowMask} isDisabled={!shouldShowMask}
cursor={'pointer'} cursor={'pointer'}

View File

@ -51,9 +51,9 @@ export default function InpaintingMaskVisibilityControl() {
); );
return ( return (
<IAIIconButton <IAIIconButton
aria-label="Hide/Show Mask (H)" aria-label="Hide Mask (H)"
tooltip="Hide/Show Mask (H)" tooltip="Hide Mask (H)"
data-selected={!shouldShowMask} data-alert={!shouldShowMask}
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />} icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
onClick={handleToggleShouldShowMask} onClick={handleToggleShouldShowMask}
/> />

View File

@ -0,0 +1,29 @@
import { FaVectorSquare } from 'react-icons/fa';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import IAIIconButton from '../../../../common/components/IAIIconButton';
import { setShouldShowBoundingBox } from '../inpaintingSlice';
const InpaintingShowHideBoundingBoxControl = () => {
const dispatch = useAppDispatch();
const shouldShowBoundingBox = useAppSelector(
(state: RootState) => state.inpainting.shouldShowBoundingBox
);
return (
<IAIIconButton
aria-label="Hide Inpainting Box"
tooltip="Hide Inpainting Box"
icon={<FaVectorSquare />}
data-alert={!shouldShowBoundingBox}
onClick={() => {
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
}}
/>
);
};
export default InpaintingShowHideBoundingBoxControl;

View File

@ -2,20 +2,16 @@ import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
RootState, import { activeTabNameSelector } from '../../options/optionsSelectors';
useAppDispatch, import { OptionsState } from '../../options/optionsSlice';
useAppSelector,
} from '../../../../app/store';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
import { OptionsState } from '../../../options/optionsSlice';
import { import {
InpaintingState, InpaintingState,
setIsSpacebarHeld, setIsSpacebarHeld,
setShouldLockBoundingBox, setShouldLockBoundingBox,
toggleShouldLockBoundingBox, toggleShouldLockBoundingBox,
toggleTool, toggleTool,
} from '../inpaintingSlice'; } from './inpaintingSlice';
const keyboardEventManagerSelector = createSelector( const keyboardEventManagerSelector = createSelector(
[ [

View File

@ -35,6 +35,7 @@ const Cacher = () => {
isDrawing, isDrawing,
isTransformingBoundingBox, isTransformingBoundingBox,
isMovingBoundingBox, isMovingBoundingBox,
shouldShowBoundingBox,
} = useAppSelector((state: RootState) => state.inpainting); } = useAppSelector((state: RootState) => state.inpainting);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -60,6 +61,7 @@ const Cacher = () => {
imageToInpaint, imageToInpaint,
shouldShowBrush, shouldShowBrush,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
shouldShowBoundingBox,
shouldLockBoundingBox, shouldLockBoundingBox,
stageScale, stageScale,
pastLines, pastLines,

View File

@ -280,6 +280,7 @@ const InpaintingBoundingBoxPreview = () => {
const handleStartedTransforming = (e: KonvaEventObject<MouseEvent>) => { const handleStartedTransforming = (e: KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true; e.cancelBubble = true;
e.evt.stopImmediatePropagation(); e.evt.stopImmediatePropagation();
console.log("Started transform")
dispatch(setIsTransformingBoundingBox(true)); dispatch(setIsTransformingBoundingBox(true));
}; };

View File

@ -11,22 +11,28 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
const { const {
cursorPosition, cursorPosition,
canvasDimensions: { width, height }, canvasDimensions: { width, height },
shouldShowBrushPreview,
brushSize, brushSize,
maskColor, maskColor,
tool, tool,
shouldShowBrush, shouldShowBrush,
isMovingBoundingBox,
isTransformingBoundingBox,
} = inpainting; } = inpainting;
return { return {
cursorPosition, cursorPosition,
width, width,
height, height,
shouldShowBrushPreview,
brushSize, brushSize,
maskColorString: rgbaColorToRgbString(maskColor), maskColorString: rgbaColorToRgbString(maskColor),
tool, tool,
shouldShowBrush, shouldShowBrush,
shouldDrawBrushPreview:
!(
isMovingBoundingBox ||
isTransformingBoundingBox ||
!cursorPosition
) && shouldShowBrush,
}; };
}, },
{ {
@ -40,12 +46,17 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
* Draws a black circle around the canvas brush preview. * Draws a black circle around the canvas brush preview.
*/ */
const InpaintingCanvasBrushPreview = () => { const InpaintingCanvasBrushPreview = () => {
const { cursorPosition, width, height, brushSize, maskColorString, tool } = const {
useAppSelector(inpaintingCanvasBrushPreviewSelector); cursorPosition,
width,
height,
brushSize,
maskColorString,
tool,
shouldDrawBrushPreview,
} = useAppSelector(inpaintingCanvasBrushPreviewSelector);
if (!cursorPosition) { if (!shouldDrawBrushPreview) return null;
return null;
}
return ( return (
<Circle <Circle

View File

@ -4,26 +4,34 @@ import { Circle } from 'react-konva';
import { RootState, useAppSelector } from '../../../../app/store'; import { RootState, useAppSelector } from '../../../../app/store';
import { InpaintingState } from '../inpaintingSlice'; import { InpaintingState } from '../inpaintingSlice';
const inpaintingCanvasBrushPreviewSelector = createSelector( const inpaintingCanvasBrushPrevieOutlineSelector = createSelector(
(state: RootState) => state.inpainting, (state: RootState) => state.inpainting,
(inpainting: InpaintingState) => { (inpainting: InpaintingState) => {
const { const {
cursorPosition, cursorPosition,
canvasDimensions: { width, height }, canvasDimensions: { width, height },
shouldShowBrushPreview,
brushSize, brushSize,
stageScale, tool,
shouldShowBrush, shouldShowBrush,
isMovingBoundingBox,
isTransformingBoundingBox,
stageScale,
} = inpainting; } = inpainting;
return { return {
cursorPosition, cursorPosition,
width, width,
height, height,
shouldShowBrushPreview,
brushSize, brushSize,
tool,
strokeWidth: 1 / stageScale, // scale stroke thickness strokeWidth: 1 / stageScale, // scale stroke thickness
shouldShowBrush, radius: 1 / stageScale, // scale stroke thickness
shouldDrawBrushPreview:
!(
isMovingBoundingBox ||
isTransformingBoundingBox ||
!cursorPosition
) && shouldShowBrush,
}; };
}, },
{ {
@ -41,15 +49,13 @@ const InpaintingCanvasBrushPreviewOutline = () => {
cursorPosition, cursorPosition,
width, width,
height, height,
shouldShowBrushPreview,
brushSize, brushSize,
shouldDrawBrushPreview,
strokeWidth, strokeWidth,
shouldShowBrush, radius,
} = useAppSelector(inpaintingCanvasBrushPreviewSelector); } = useAppSelector(inpaintingCanvasBrushPrevieOutlineSelector);
if (!shouldShowBrush || !(cursorPosition || shouldShowBrushPreview))
return null;
if (!shouldDrawBrushPreview) return null;
return ( return (
<> <>
<Circle <Circle
@ -64,7 +70,7 @@ const InpaintingCanvasBrushPreviewOutline = () => {
<Circle <Circle
x={cursorPosition ? cursorPosition.x : width / 2} x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2} y={cursorPosition ? cursorPosition.y : height / 2}
radius={1} radius={radius}
fill={'rgba(0,0,0,1)'} fill={'rgba(0,0,0,1)'}
listening={false} listening={false}
/> />

View File

@ -112,12 +112,6 @@ const InvokeOptionsPanel = (props: Props) => {
dispatch(setNeedsCache(true)); dispatch(setNeedsCache(true));
}; };
// // set gallery scroll position
// useEffect(() => {
// if (!optionsPanelContainerRef.current) return;
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
return ( return (
<CSSTransition <CSSTransition
nodeRef={optionsPanelRef} nodeRef={optionsPanelRef}

View File

@ -15,6 +15,7 @@ import TextToImageIcon from '../../common/icons/TextToImageIcon';
import { setActiveTab } from '../options/optionsSlice'; import { setActiveTab } from '../options/optionsSlice';
import ImageToImageWorkarea from './ImageToImage'; import ImageToImageWorkarea from './ImageToImage';
import InpaintingWorkarea from './Inpainting'; import InpaintingWorkarea from './Inpainting';
import { setNeedsCache } from './Inpainting/inpaintingSlice';
import TextToImageWorkarea from './TextToImage'; import TextToImageWorkarea from './TextToImage';
export const tabDict = { export const tabDict = {
@ -73,6 +74,7 @@ export default function InvokeTabs() {
useHotkeys('3', () => { useHotkeys('3', () => {
dispatch(setActiveTab(2)); dispatch(setActiveTab(2));
dispatch(setNeedsCache(true));
}); });
useHotkeys('4', () => { useHotkeys('4', () => {
@ -125,6 +127,7 @@ export default function InvokeTabs() {
index={activeTab} index={activeTab}
onChange={(index: number) => { onChange={(index: number) => {
dispatch(setActiveTab(index)); dispatch(setActiveTab(index));
dispatch(setNeedsCache(true));
}} }}
> >
<div className="app-tabs-list">{renderTabs()}</div> <div className="app-tabs-list">{renderTabs()}</div>

View File

@ -10,18 +10,23 @@
column-gap: 1rem; column-gap: 1rem;
height: 100%; height: 100%;
.workarea-children-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.workarea-split-view { .workarea-split-view {
width: 100%; width: 100%;
height: 100%;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
// height: $app-content-height;
background-color: var(--background-color-secondary); background-color: var(--background-color-secondary);
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.workarea-single-view { .workarea-single-view {
width: 100%; width: 100%;
// height: $app-content-height; height: 100%;
background-color: var(--background-color-secondary); background-color: var(--background-color-secondary);
border-radius: 0.5rem; border-radius: 0.5rem;
} }
@ -45,3 +50,22 @@
} }
} }
} }
.workarea-split-button {
position: absolute;
cursor: pointer;
padding: 0.5rem;
top: 0;
right: 0;
z-index: 20;
&[data-selected='true'] {
top: 0;
right: 0;
svg {
opacity: 1;
}
}
svg {
opacity: 0.5;
}
}

View File

@ -1,5 +1,19 @@
import { Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { VscSplitHorizontal } from 'react-icons/vsc';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import ImageGallery from '../gallery/ImageGallery'; import ImageGallery from '../gallery/ImageGallery';
import { activeTabNameSelector } from '../options/optionsSelectors';
import { OptionsState, setShowDualDisplay } from '../options/optionsSlice';
const workareaSelector = createSelector(
[(state: RootState) => state.options, activeTabNameSelector],
(options: OptionsState, activeTabName) => {
const { showDualDisplay, shouldPinOptionsPanel } = options;
return { showDualDisplay, shouldPinOptionsPanel, activeTabName };
}
);
type InvokeWorkareaProps = { type InvokeWorkareaProps = {
optionsPanel: ReactNode; optionsPanel: ReactNode;
@ -8,7 +22,9 @@ type InvokeWorkareaProps = {
}; };
const InvokeWorkarea = (props: InvokeWorkareaProps) => { const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const dispatch = useAppDispatch();
const { optionsPanel, children, styleClass } = props; const { optionsPanel, children, styleClass } = props;
const { showDualDisplay, activeTabName } = useAppSelector(workareaSelector);
return ( return (
<div <div
@ -18,7 +34,20 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
> >
<div className="workarea-main"> <div className="workarea-main">
{optionsPanel} {optionsPanel}
{children} <div className="workarea-children-wrapper">
{children}
{activeTabName === 'inpainting' && (
<Tooltip label="Toggle Split View">
<div
className="workarea-split-button"
data-selected={showDualDisplay}
onClick={() => dispatch(setShowDualDisplay(!showDualDisplay))}
>
<VscSplitHorizontal />
</div>
</Tooltip>
)}
</div>
<ImageGallery /> <ImageGallery />
</div> </div>
</div> </div>

View File

@ -19,12 +19,16 @@
--invalid: rgb(255, 75, 75); --invalid: rgb(255, 75, 75);
--invalid-secondary: rgb(120, 5, 5); --invalid-secondary: rgb(120, 5, 5);
--accent-color-dim: rgb(57, 25, 153);
--accent-color: rgb(80, 40, 200); --accent-color: rgb(80, 40, 200);
--accent-color-hover: rgb(104, 60, 230); --accent-color-hover: rgb(104, 60, 230);
--destructive-color: rgb(185, 55, 55); --destructive-color: rgb(185, 55, 55);
--destructive-color-hover: rgb(255, 75, 75); --destructive-color-hover: rgb(255, 75, 75);
--warning-color: rgb(200, 88, 40);
--warning-color-hover: rgb(230, 117, 60);
// Error status colors // Error status colors
--border-color-invalid: rgb(255, 80, 50); --border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: rgb(210, 30, 10); --box-shadow-color-invalid: rgb(210, 30, 10);

View File

@ -19,12 +19,16 @@
--invalid: rgb(255, 75, 75); --invalid: rgb(255, 75, 75);
--invalid-secondary: rgb(120, 5, 5); --invalid-secondary: rgb(120, 5, 5);
--accent-color-dim: rgb(186, 146, 0);
--accent-color: rgb(235, 185, 5); --accent-color: rgb(235, 185, 5);
--accent-color-hover: rgb(255, 200, 0); --accent-color-hover: rgb(255, 200, 0);
--destructive-color: rgb(237, 51, 51); --destructive-color: rgb(237, 51, 51);
--destructive-color-hover: rgb(255, 55, 55); --destructive-color-hover: rgb(255, 55, 55);
--warning-color: rgb(224, 142, 42);
--warning-color-hover: rgb(255, 167, 60);
// Error status colors // Error status colors
--border-color-invalid: rgb(255, 80, 50); --border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: none; --box-shadow-color-invalid: none;