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" />
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="./assets/index.adcf8963.js"></script>
<link rel="stylesheet" href="./assets/index.28b80602.css">
<script type="module" crossorigin src="./assets/index.bf9dd1fc.js"></script>
<link rel="stylesheet" href="./assets/index.f9f4c989.css">
</head>
<body>

View File

@ -21,6 +21,27 @@
&[data-variant='link'] {
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'] {
@ -38,13 +59,17 @@
fill: var(--text-color);
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
background-color: var(--btn-grey);
svg {
fill: var(--text-color);
}
}
}
}
@keyframes pulseColor {
0% {
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 {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
align-items: center;
.invokeai__number-input-form-label {
@ -11,6 +10,7 @@
margin-bottom: 0;
flex-grow: 2;
white-space: nowrap;
padding-right: 1rem;
&[data-focus] + .invokeai__number-input-root {
outline: none;

View File

@ -123,13 +123,15 @@ const IAINumberInput = (props: Props) => {
}
{...formControlProps}
>
<FormLabel
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
{...formLabelProps}
>
{label}
</FormLabel>
{label && (
<FormLabel
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
{...formLabelProps}
>
{label}
</FormLabel>
)}
<NumberInput
className="invokeai__number-input-root"
value={valueAsString}
@ -145,19 +147,18 @@ const IAINumberInput = (props: Props) => {
textAlign={textAlign}
{...numberInputFieldProps}
/>
<div
className="invokeai__number-input-stepper"
style={showStepper ? { display: 'block' } : { display: 'none' }}
>
<NumberIncrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
<NumberDecrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
</div>
{showStepper && (
<div className="invokeai__number-input-stepper">
<NumberIncrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
<NumberDecrementStepper
{...numberInputStepperProps}
className="invokeai__number-input-stepper-button"
/>
</div>
)}
</NumberInput>
</FormControl>
</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,
FaTrash,
} from 'react-icons/fa';
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
import {
setImageToInpaint,
setNeedsCache,
} from '../tabs/Inpainting/inpaintingSlice';
import { GalleryState } from './gallerySlice';
import { activeTabNameSelector } from '../options/optionsSelectors';
import IAIPopover from '../../common/components/IAIPopover';
@ -95,7 +98,6 @@ const CurrentImageButtons = () => {
facetoolStrength,
shouldDisableToolbarButtons,
shouldShowImageDetails,
activeTabName,
currentImage,
} = useAppSelector(systemSelector);
@ -108,7 +110,7 @@ const CurrentImageButtons = () => {
const handleClickUseAsInitialImage = () => {
if (!currentImage) return;
dispatch(setInitialImage(currentImage));
dispatch(setActiveTab(1));
dispatch(setActiveTab('img2img'));
};
const handleCopyImageLink = () => {
@ -308,9 +310,10 @@ const CurrentImageButtons = () => {
if (!currentImage) return;
dispatch(setImageToInpaint(currentImage));
if (activeTabName !== 'inpainting') {
dispatch(setActiveTab('inpainting'));
}
dispatch(setActiveTab('inpainting'));
dispatch(setNeedsCache(true));
toast({
title: 'Sent to Inpainting',
status: 'success',
@ -461,7 +464,7 @@ const CurrentImageButtons = () => {
icon={<FaTrash />}
tooltip="Delete Image"
aria-label="Delete Image"
isDisabled={Boolean(currentImage) || !isConnected || isProcessing}
isDisabled={!currentImage || !isConnected || isProcessing}
/>
</DeleteImageModal>
</ButtonGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@
left: 0;
z-index: 2;
margin: 0.5rem;
pointer-events: none;
button {
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 { BiHide, BiShow } from 'react-icons/bi';
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 { RootState, useAppSelector } from '../../../app/store';
import { InpaintingState } from './inpaintingSlice';
import { MouseEvent, useRef, useState } from 'react';
const InpaintingCanvasStatusIcons = () => {
const {
@ -46,17 +47,48 @@ const InpaintingCanvasStatusIcons = () => {
isBoundingBoxTooSmall,
} = 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 (
<div className="inpainting-alerts">
<ButtonGroup isAttached>
<IconButton
aria-label="Show/HideMask"
size="xs"
variant={'ghost'}
fontSize={'1rem'}
data-selected={!shouldShowMask}
icon={shouldShowMask ? <BiShow /> : <BiHide />}
/>
<div
className="inpainting-alerts"
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
onMouseLeave={handleMouseOut}
onBlur={handleMouseOut}
>
<ButtonGroup
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
aria-label="Invert Mask"
variant={'ghost'}

View File

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

View File

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

View File

@ -62,7 +62,7 @@ export default function InpaintingMaskClear() {
<IAIIconButton
aria-label="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}
isDisabled={isMaskEmpty || !shouldShowMask}
/>

View File

@ -75,12 +75,10 @@ export default function InpaintingMaskColorPicker() {
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'}

View File

@ -51,9 +51,9 @@ export default function InpaintingMaskVisibilityControl() {
);
return (
<IAIIconButton
aria-label="Hide/Show Mask (H)"
tooltip="Hide/Show Mask (H)"
data-selected={!shouldShowMask}
aria-label="Hide Mask (H)"
tooltip="Hide Mask (H)"
data-alert={!shouldShowMask}
icon={shouldShowMask ? <BiShow size={22} /> : <BiHide size={22} />}
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 { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import { activeTabNameSelector } from '../../../options/optionsSelectors';
import { OptionsState } from '../../../options/optionsSlice';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import { activeTabNameSelector } from '../../options/optionsSelectors';
import { OptionsState } from '../../options/optionsSlice';
import {
InpaintingState,
setIsSpacebarHeld,
setShouldLockBoundingBox,
toggleShouldLockBoundingBox,
toggleTool,
} from '../inpaintingSlice';
} from './inpaintingSlice';
const keyboardEventManagerSelector = createSelector(
[

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,18 +10,23 @@
column-gap: 1rem;
height: 100%;
.workarea-children-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.workarea-split-view {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
// height: $app-content-height;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
}
.workarea-single-view {
width: 100%;
// height: $app-content-height;
height: 100%;
background-color: var(--background-color-secondary);
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 { VscSplitHorizontal } from 'react-icons/vsc';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
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 = {
optionsPanel: ReactNode;
@ -8,7 +22,9 @@ type InvokeWorkareaProps = {
};
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const dispatch = useAppDispatch();
const { optionsPanel, children, styleClass } = props;
const { showDualDisplay, activeTabName } = useAppSelector(workareaSelector);
return (
<div
@ -18,7 +34,20 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
>
<div className="workarea-main">
{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 />
</div>
</div>

View File

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

View File

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