Adds pin feature to options panel

This commit is contained in:
psychedelicious 2022-10-30 14:36:28 +11:00
parent dc556cb1a7
commit e58b7a7ef9
33 changed files with 951 additions and 687 deletions

517
frontend/dist/assets/index.2d90b6f0.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

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.64b87783.js"></script>
<link rel="stylesheet" href="./assets/index.352e4760.css">
<script type="module" crossorigin src="./assets/index.2d90b6f0.js"></script>
<link rel="stylesheet" href="./assets/index.8c67e6b9.css">
</head>
<body>

View File

@ -11,11 +11,7 @@
display: grid;
row-gap: 0.5rem;
padding: $app-padding;
grid-auto-rows: max-content;
grid-auto-rows: min-content auto;
width: $app-width;
height: $app-height;
}
.app-console {
z-index: 20;
}

View File

@ -6,16 +6,52 @@ import Loading from '../Loading';
import { useAppDispatch } from './store';
import { requestSystemConfig } from './socketio/actions';
import { keepGUIAlive } from './utils';
import InvokeTabs from '../features/tabs/InvokeTabs';
import InvokeTabs, { tabMap } from '../features/tabs/InvokeTabs';
import ImageUploader from '../common/components/ImageUploader';
import { RootState, useAppSelector } from '../app/store';
import ShowHideGalleryButton from '../features/tabs/ShowHideGalleryButton';
import ShowHideOptionsPanelButton from '../features/tabs/ShowHideOptionsPanelButton';
import { createSelector } from '@reduxjs/toolkit';
import { GalleryState } from '../features/gallery/gallerySlice';
import { OptionsState } from '../features/options/optionsSlice';
keepGUIAlive();
const appSelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options],
(gallery: GalleryState, options: OptionsState) => {
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
gallery;
const {
shouldShowOptionsPanel,
shouldHoldOptionsPanelOpen,
shouldPinOptionsPanel,
activeTab,
} = options;
return {
shouldShowGalleryButton: !(
shouldShowGallery ||
(shouldHoldGalleryOpen && !shouldPinGallery)
),
shouldShowOptionsPanelButton:
!(
shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
) && ['txt2img', 'img2img', 'inpainting'].includes(tabMap[activeTab]),
};
}
);
const App = () => {
const dispatch = useAppDispatch();
const [isReady, setIsReady] = useState<boolean>(false);
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
useAppSelector(appSelector);
useEffect(() => {
dispatch(requestSystemConfig());
setIsReady(true);
@ -32,6 +68,8 @@ const App = () => {
<div className="app-console">
<Console />
</div>
{shouldShowGalleryButton && <ShowHideGalleryButton />}
{shouldShowOptionsPanelButton && <ShowHideOptionsPanelButton />}
</ImageUploader>
</div>
) : (

View File

@ -1,4 +1,5 @@
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
import { MouseEvent } from 'react';
interface Props extends SelectProps {
label: string;
@ -21,7 +22,16 @@ const IAISelect = (props: Props) => {
...rest
} = props;
return (
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
<FormControl
isDisabled={isDisabled}
className={`invokeai__select ${styleClass}`}
onClick={(e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
>
<FormLabel
fontSize={fontSize}
marginBottom={1}

View File

@ -142,10 +142,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
return (
<ContextMenu.Root
onOpenChange={(open: boolean) => {
dispatch(setShouldHoldGalleryOpen(open));
dispatch(setShouldShowGallery(true));
}}
// onOpenChange={(open: boolean) => {
// dispatch(setShouldHoldGalleryOpen(open));
// dispatch(setShouldShowGallery(true));
// }}
>
<ContextMenu.Trigger>
<Box

View File

@ -1,24 +1,24 @@
@use '../../styles/Mixins/' as *;
.image-gallery-area-enter {
.image-gallery-wrapper-enter {
transform: translateX(150%);
}
.image-gallery-area-enter-active {
.image-gallery-wrapper-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.image-gallery-area-exit {
.image-gallery-wrapper-exit {
transform: translateX(0);
}
.image-gallery-area-exit-active {
.image-gallery-wrapper-exit-active {
transform: translateX(150%);
transition: all 120ms ease-out;
}
.image-gallery-area {
.image-gallery-wrapper {
z-index: 10;
&[data-pinned='false'] {
@ -29,6 +29,7 @@
.image-gallery-popup {
border-radius: 0;
box-shadow: 0 0 1rem var(--text-color-a3);
.image-gallery-container {
max-height: calc($app-height + 5rem);
}

View File

@ -96,8 +96,8 @@ export default function ImageGallery() {
const timeoutIdRef = useRef<number | null>(null);
const handleSetShouldPinGallery = () => {
dispatch(setNeedsCache(true));
dispatch(setShouldPinGallery(!shouldPinGallery));
dispatch(setNeedsCache(true));
};
const handleToggleGallery = () => {
@ -106,7 +106,7 @@ export default function ImageGallery() {
const handleOpenGallery = () => {
dispatch(setShouldShowGallery(true));
dispatch(setNeedsCache(true));
shouldPinGallery && dispatch(setNeedsCache(true));
};
const handleCloseGallery = () => {
@ -117,7 +117,7 @@ export default function ImageGallery() {
);
dispatch(setShouldShowGallery(false));
dispatch(setShouldHoldGalleryOpen(false));
dispatch(setNeedsCache(true));
shouldPinGallery && dispatch(setNeedsCache(true));
};
const handleClickLoadMore = () => {
@ -251,16 +251,20 @@ export default function ImageGallery() {
galleryContainerRef.current.scrollTop = galleryScrollPosition;
}, [galleryScrollPosition, shouldShowGallery]);
useEffect(() => {
setShouldShowButtons(galleryWidth >= 280);
}, [galleryWidth]);
return (
<CSSTransition
nodeRef={galleryRef}
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
unmountOnExit
timeout={200}
classNames="image-gallery-area"
classNames="image-gallery-wrapper"
>
<div
className="image-gallery-area"
className="image-gallery-wrapper"
data-pinned={shouldPinGallery}
ref={galleryRef}
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
@ -270,7 +274,6 @@ export default function ImageGallery() {
<Resizable
minWidth={galleryMinWidth}
maxWidth={galleryMaxWidth}
// maxHeight={'100%'}
className={'image-gallery-popup'}
handleStyles={{ left: { width: '15px' } }}
enable={{
@ -316,9 +319,9 @@ export default function ImageGallery() {
Number(galleryMaxWidth)
);
if (newWidth >= 320 && !shouldShowButtons) {
if (newWidth >= 280 && !shouldShowButtons) {
setShouldShowButtons(true);
} else if (newWidth < 320 && shouldShowButtons) {
} else if (newWidth < 280 && shouldShowButtons) {
setShouldShowButtons(false);
}
@ -448,15 +451,6 @@ export default function ImageGallery() {
icon={<BsPinAngleFill />}
data-selected={shouldPinGallery}
/>
<IAIIconButton
size={'sm'}
aria-label={'Close Gallery'}
tooltip={'Close Gallery (G)'}
onClick={handleCloseGallery}
className="image-gallery-icon-btn"
icon={<MdClear />}
/>
</div>
</div>
<div className="image-gallery-container" ref={galleryContainerRef}>

View File

@ -1,20 +0,0 @@
@use '../../styles/Mixins/' as *;
.show-hide-gallery-button {
position: absolute !important;
top: 50%;
right: -1rem;
transform: translate(0, -50%);
z-index: 10;
border-radius: 0.5rem 0 0 0.5rem !important;
padding: 0 0.5rem;
@include Button(
$btn-width: 1rem,
$btn-height: 12rem,
$icon-size: 20px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}

View File

@ -1,57 +0,0 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { MdPhotoLibrary } from 'react-icons/md';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import IAIIconButton from '../../common/components/IAIIconButton';
import { setShouldShowGallery } from '../gallery/gallerySlice';
import { selectNextImage, selectPrevImage } from './gallerySlice';
const ShowHideGalleryButton = () => {
const dispatch = useAppDispatch();
const { shouldPinGallery, shouldShowGallery } = useAppSelector(
(state: RootState) => state.gallery
);
const handleShowGalleryToggle = () => {
dispatch(setShouldShowGallery(!shouldShowGallery));
};
// useHotkeys(
// 'g',
// () => {
// handleShowGalleryToggle();
// },
// [shouldShowGallery]
// );
// useHotkeys(
// 'left',
// () => {
// dispatch(selectPrevImage());
// },
// []
// );
// useHotkeys(
// 'right',
// () => {
// dispatch(selectNextImage());
// },
// []
// );
return (
<IAIIconButton
tooltip="Show Gallery (G)"
tooltipPlacement="top"
aria-label="Show Gallery"
onClick={handleShowGalleryToggle}
styleClass="show-hide-gallery-button"
onMouseOver={!shouldPinGallery ? handleShowGalleryToggle : undefined}
>
<MdPhotoLibrary />
</IAIIconButton>
);
};
export default ShowHideGalleryButton;

View File

@ -42,6 +42,10 @@ export interface OptionsState {
activeTab: number;
shouldShowImageDetails: boolean;
showDualDisplay: boolean;
shouldShowOptionsPanel: boolean;
shouldPinOptionsPanel: boolean;
optionsPanelScrollPosition: number;
shouldHoldOptionsPanelOpen: boolean;
}
const initialOptionsState: OptionsState = {
@ -75,6 +79,10 @@ const initialOptionsState: OptionsState = {
activeTab: 0,
shouldShowImageDetails: false,
showDualDisplay: true,
shouldShowOptionsPanel: true,
shouldPinOptionsPanel: true,
optionsPanelScrollPosition: 0,
shouldHoldOptionsPanelOpen: false,
};
const initialState: OptionsState = initialOptionsState;
@ -324,6 +332,18 @@ export const optionsSlice = createSlice({
clearInitialImage: (state) => {
state.initialImage = undefined;
},
setShouldPinOptionsPanel: (state, action: PayloadAction<boolean>) => {
state.shouldPinOptionsPanel = action.payload;
},
setShouldShowOptionsPanel: (state, action: PayloadAction<boolean>) => {
state.shouldShowOptionsPanel = action.payload;
},
setOptionsPanelScrollPosition: (state, action: PayloadAction<number>) => {
state.optionsPanelScrollPosition = action.payload;
},
setShouldHoldOptionsPanelOpen: (state, action: PayloadAction<boolean>) => {
state.shouldHoldOptionsPanelOpen = action.payload;
},
},
});
@ -366,6 +386,10 @@ export const {
setShowDualDisplay,
setInitialImage,
clearInitialImage,
setShouldShowOptionsPanel,
setShouldPinOptionsPanel,
setOptionsPanelScrollPosition,
setShouldHoldOptionsPanelOpen,
} = optionsSlice.actions;
export default optionsSlice.reducer;

View File

@ -1,3 +1,10 @@
.console-resizable {
display: flex;
position: fixed;
left: 0;
bottom: 0;
}
.console {
width: 100vw;
display: flex;
@ -41,12 +48,13 @@
position: fixed !important;
left: 0.5rem;
bottom: 0.5rem;
z-index: 21;
&:hover {
background: var(--console-icon-button-bg-color-hover) !important;
}
&.error-seen {
&[data-error-seen='true'] {
background: var(--status-bad-color) !important;
&:hover {
background: var(--status-bad-color) !important;
@ -59,12 +67,13 @@
position: fixed !important;
left: 0.5rem;
bottom: 3rem;
z-index: 21;
&:hover {
background: var(--console-icon-button-bg-color-hover) !important;
}
&.autoscroll-enabled {
&[data-autoscroll-enabled='true'] {
background: var(--accent-color) !important;
&:hover {
background: var(--accent-color-hover) !important;

View File

@ -2,7 +2,7 @@ import { IconButton, Tooltip } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { errorSeen, setShouldShowLogViewer, SystemState } from './systemSlice';
import { useLayoutEffect, useRef, useState } from 'react';
import { UIEvent, useLayoutEffect, useRef, useState } from 'react';
import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
@ -75,6 +75,17 @@ const Console = () => {
[shouldShowLogViewer]
);
const handleOnScroll = () => {
if (!viewerRef.current) return;
if (
shouldAutoscroll &&
viewerRef.current.scrollTop <
viewerRef.current.scrollHeight - viewerRef.current.clientHeight
) {
setShouldAutoscroll(false);
}
};
return (
<>
{shouldShowLogViewer && (
@ -83,10 +94,16 @@ const Console = () => {
width: '100%',
height: 200,
}}
style={{ display: 'flex', position: 'fixed', left: 0, bottom: 0 }}
style={{
display: 'flex',
position: 'fixed',
left: 0,
bottom: 0,
zIndex: 20,
}}
maxHeight={'90vh'}
>
<div className="console" ref={viewerRef}>
<div className="console" ref={viewerRef} onScroll={handleOnScroll}>
{log.map((entry, i) => {
const { timestamp, message, level } = entry;
return (
@ -100,11 +117,13 @@ const Console = () => {
</Resizable>
)}
{shouldShowLogViewer && (
<Tooltip hasArrow label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}>
<Tooltip
hasArrow
label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}
>
<IconButton
className={`console-autoscroll-icon-button ${
shouldAutoscroll && 'autoscroll-enabled'
}`}
className={'console-autoscroll-icon-button'}
data-autoscroll-enabled={shouldAutoscroll}
size="sm"
aria-label="Toggle autoscroll"
variant={'solid'}
@ -113,16 +132,17 @@ const Console = () => {
/>
</Tooltip>
)}
<Tooltip hasArrow label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}>
<Tooltip
hasArrow
label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}
>
<IconButton
className={`console-toggle-icon-button ${
(hasError || !wasErrorSeen) && 'error-seen'
}`}
className={'console-toggle-icon-button'}
data-error-seen={hasError || !wasErrorSeen}
size="sm"
position={'fixed'}
variant={'solid'}
aria-label="Toggle Log Viewer"
// colorScheme={hasError || !wasErrorSeen ? 'red' : 'gray'}
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
onClick={handleClickLogViewerToggle}
/>

View File

@ -0,0 +1,30 @@
@use '../../styles/Mixins/' as *;
.floating-show-hide-button {
position: absolute !important;
top: 50%;
transform: translate(0, -50%);
z-index: 20;
padding: 0;
min-width: 2rem !important;
filter: drop-shadow(0 0 1rem var(--text-color-a3));
&.left {
left: 0;
border-radius: 0 0.5rem 0.5rem 0 !important;
}
&.right {
right: 0;
border-radius: 0.5rem 0 0 0.5rem !important;
}
@include Button(
$btn-width: 1rem,
$btn-height: 12rem,
$icon-size: 20px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}

View File

@ -8,15 +8,6 @@
height: 100%;
}
.image-to-image-panel {
display: grid;
row-gap: 1rem;
grid-auto-rows: max-content;
height: $app-content-height;
overflow-y: scroll;
@include HideScrollbar;
}
.image-to-image-strength-main-option {
display: grid;
grid-template-columns: none !important;

View File

@ -17,6 +17,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
import OptionsAccordion from '../../options/OptionsAccordion';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput';
import InvokeOptionsPanel from '../InvokeOptionsPanel';
export default function ImageToImagePanel() {
const showAdvancedOptions = useAppSelector(
@ -45,14 +46,14 @@ export default function ImageToImagePanel() {
options: <UpscaleOptions />,
},
other: {
header: <OutputHeader /> ,
header: <OutputHeader />,
feature: Feature.OTHER,
options: <OutputOptions />,
},
};
return (
<div className="image-to-image-panel">
<InvokeOptionsPanel>
<PromptInput />
<ProcessButtons />
<MainOptions />
@ -65,6 +66,6 @@ export default function ImageToImagePanel() {
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={imageToImageAccordions} />
) : null}
</div>
</InvokeOptionsPanel>
);
}

View File

@ -85,7 +85,7 @@
// Overrides
.inpainting-workarea-overrides {
.image-gallery-area {
.image-gallery-wrapper {
.chakra-popover__popper {
inset: 0 auto auto -75px !important;
}

View File

@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
import OptionsAccordion from '../../options/OptionsAccordion';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput';
import InvokeOptionsPanel from '../InvokeOptionsPanel';
export default function InpaintingPanel() {
const showAdvancedOptions = useAppSelector(
@ -45,7 +46,7 @@ export default function InpaintingPanel() {
};
return (
<div className="image-to-image-panel">
<InvokeOptionsPanel>
<PromptInput />
<ProcessButtons />
<MainOptions />
@ -58,6 +59,6 @@ export default function InpaintingPanel() {
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={imageToImageAccordions} />
) : null}
</div>
</InvokeOptionsPanel>
);
}

View File

@ -0,0 +1,51 @@
@use '../../styles/Mixins/' as *;
.options-panel-wrapper-enter {
transform: translateX(-150%);
}
.options-panel-wrapper-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.options-panel-wrapper-exit {
transform: translateX(0);
}
.options-panel-wrapper-exit-active {
transform: translateX(-150%);
transition: all 120ms ease-out;
}
.options-panel-wrapper {
z-index: 20;
background-color: var(--background-color);
grid-auto-rows: max-content;
height: $app-content-height;
width: $options-bar-max-width;
max-width: $options-bar-max-width;
flex-shrink: 0;
.options-panel {
display: grid;
row-gap: 1rem;
height: 100%;
overflow-y: scroll;
@include HideScrollbar;
}
&[data-pinned='false'] {
position: fixed;
top: 0;
left: 0;
box-shadow: 0 0 1rem var(--text-color-a3);
width: calc($options-bar-max-width + 2rem);
max-width: calc($options-bar-max-width + 2rem);
height: 100%;
.options-panel {
padding: 1rem;
}
}
}

View File

@ -0,0 +1,137 @@
import { createSelector } from '@reduxjs/toolkit';
import { MouseEvent, ReactNode, useEffect, useRef } from 'react';
import { BsPinAngleFill } from 'react-icons/bs';
import { CSSTransition } from 'react-transition-group';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import IAIIconButton from '../../common/components/IAIIconButton';
import {
OptionsState,
setOptionsPanelScrollPosition,
setShouldHoldOptionsPanelOpen,
setShouldPinOptionsPanel,
setShouldShowOptionsPanel,
} from '../options/optionsSlice';
import { setNeedsCache } from './Inpainting/inpaintingSlice';
type Props = { children: ReactNode };
const optionsPanelSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => {
const {
shouldShowOptionsPanel,
shouldHoldOptionsPanelOpen,
shouldPinOptionsPanel,
optionsPanelScrollPosition,
} = options;
return {
shouldShowOptionsPanel,
shouldHoldOptionsPanelOpen,
shouldPinOptionsPanel,
optionsPanelScrollPosition,
};
}
);
const InvokeOptionsPanel = (props: Props) => {
const dispatch = useAppDispatch();
const {
shouldShowOptionsPanel,
shouldHoldOptionsPanelOpen,
shouldPinOptionsPanel,
optionsPanelScrollPosition,
} = useAppSelector(optionsPanelSelector);
const optionsPanelRef = useRef<HTMLDivElement>(null);
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
const timeoutIdRef = useRef<number | null>(null);
const { children } = props;
const handleCloseOptionsPanel = () => {
dispatch(
setOptionsPanelScrollPosition(
optionsPanelContainerRef.current
? optionsPanelContainerRef.current.scrollTop
: 0
)
);
dispatch(setShouldShowOptionsPanel(false));
dispatch(setShouldHoldOptionsPanelOpen(false));
shouldPinOptionsPanel && dispatch(setNeedsCache(true));
};
const setCloseOptionsPanelTimer = () => {
timeoutIdRef.current = window.setTimeout(
() => handleCloseOptionsPanel(),
500
);
};
const cancelCloseOptionsPanelTimer = () => {
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
};
const handleClickPinOptionsPanel = () => {
dispatch(setShouldPinOptionsPanel(!shouldPinOptionsPanel));
dispatch(setNeedsCache(true));
};
// // set gallery scroll position
// useEffect(() => {
// if (!optionsPanelContainerRef.current) return;
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
return (
<CSSTransition
nodeRef={optionsPanelRef}
in={
shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
}
unmountOnExit
timeout={200}
classNames="options-panel-wrapper"
>
<div
className="options-panel-wrapper"
data-pinned={shouldPinOptionsPanel}
tabIndex={1}
ref={optionsPanelRef}
onMouseEnter={
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
}
onMouseOver={
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
}
>
<div
className="options-panel"
ref={optionsPanelContainerRef}
onMouseLeave={(e: MouseEvent<HTMLDivElement>) => {
if (e.target !== optionsPanelContainerRef.current) {
cancelCloseOptionsPanelTimer();
} else {
!shouldPinOptionsPanel && setCloseOptionsPanelTimer();
}
}}
>
<IAIIconButton
size={'sm'}
aria-label={'Pin Options Panel'}
tooltip={'Pin Options Panel (Shift+P)'}
onClick={handleClickPinOptionsPanel}
icon={<BsPinAngleFill />}
data-selected={shouldPinOptionsPanel}
/>
{children}
</div>
</div>
</CSSTransition>
);
};
export default InvokeOptionsPanel;

View File

@ -4,13 +4,15 @@
display: grid !important;
grid-template-columns: min-content auto;
column-gap: 0.5rem;
// height: 100%;
}
.app-tabs-list {
display: grid;
row-gap: 0.3rem;
grid-auto-rows: max-content;
grid-auto-rows: min-content;
color: var(--tab-list-text-inactive);
z-index: 15;
button {
font-size: 0.85rem;
@ -39,5 +41,6 @@
.app-tabs-panels {
.app-tabs-panel {
padding: 0;
height: 100%;
}
}

View File

@ -10,12 +10,6 @@
column-gap: 0.5rem;
height: 100%;
.workarea-options-panel {
width: $options-bar-max-width;
max-width: $options-bar-max-width;
flex-shrink: 0;
}
.workarea-split-view {
width: 100%;
display: grid;

View File

@ -1,7 +1,5 @@
import { ReactNode } from 'react';
import { RootState, useAppSelector } from '../../app/store';
import ImageGallery from '../gallery/ImageGallery';
import ShowHideGalleryButton from '../gallery/ShowHideGalleryButton';
type InvokeWorkareaProps = {
optionsPanel: ReactNode;
@ -12,9 +10,6 @@ type InvokeWorkareaProps = {
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const { optionsPanel, children, styleClass } = props;
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
useAppSelector((state: RootState) => state.gallery);
return (
<div
className={
@ -22,13 +17,10 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
}
>
<div className="workarea-main">
<div className="workarea-options-panel">{optionsPanel}</div>
{optionsPanel}
{children}
<ImageGallery />
</div>
{!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) && (
<ShowHideGalleryButton />
)}
</div>
);
};

View File

@ -0,0 +1,26 @@
import { MdPhotoLibrary } from 'react-icons/md';
import { useAppDispatch } from '../../app/store';
import IAIIconButton from '../../common/components/IAIIconButton';
import { setShouldShowGallery } from '../gallery/gallerySlice';
const ShowHideGalleryButton = () => {
const dispatch = useAppDispatch();
const handleShowGallery = () => {
dispatch(setShouldShowGallery(true));
};
return (
<IAIIconButton
tooltip="Show Gallery (G)"
tooltipPlacement="top"
aria-label="Show Gallery"
styleClass="floating-show-hide-button right"
onMouseOver={handleShowGallery}
>
<MdPhotoLibrary />
</IAIIconButton>
);
};
export default ShowHideGalleryButton;

View File

@ -0,0 +1,26 @@
import { IoMdOptions } from 'react-icons/io';
import { useAppDispatch } from '../../app/store';
import IAIIconButton from '../../common/components/IAIIconButton';
import { setShouldShowOptionsPanel } from '../options/optionsSlice';
const ShowHideOptionsPanelButton = () => {
const dispatch = useAppDispatch();
const handleShowOptionsPanel = () => {
dispatch(setShouldShowOptionsPanel(true));
};
return (
<IAIIconButton
tooltip="Show Options Panel (G)"
tooltipPlacement="top"
aria-label="Show Options Panel"
styleClass="floating-show-hide-button left"
onMouseOver={handleShowOptionsPanel}
>
<IoMdOptions />
</IAIIconButton>
);
};
export default ShowHideOptionsPanelButton;

View File

@ -4,12 +4,3 @@
padding: 1rem;
height: 100%;
}
.text-to-image-panel {
display: grid;
row-gap: 1rem;
grid-auto-rows: max-content;
height: $app-content-height;
overflow-y: scroll;
@include HideScrollbar;
}

View File

@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
import OptionsAccordion from '../../options/OptionsAccordion';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput';
import InvokeOptionsPanel from '../InvokeOptionsPanel';
export default function TextToImagePanel() {
const showAdvancedOptions = useAppSelector(
@ -50,7 +51,7 @@ export default function TextToImagePanel() {
};
return (
<div className="text-to-image-panel">
<InvokeOptionsPanel>
<PromptInput />
<ProcessButtons />
<MainOptions />
@ -58,6 +59,6 @@ export default function TextToImagePanel() {
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={textToImageAccordions} />
) : null}
</div>
</InvokeOptionsPanel>
);
}

View File

@ -25,6 +25,8 @@
--destructive-color: rgb(185, 55, 55);
--destructive-color-hover: rgb(255, 75, 75);
--text-color-a3: rgba(255, 255, 255, 0.3);
// Error status colors
--border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: rgb(210, 30, 10);

View File

@ -25,6 +25,8 @@
--destructive-color: rgb(237, 51, 51);
--destructive-color-hover: rgb(255, 55, 55);
--text-color-a3: rgba(0, 0, 0, 0.3);
// Error status colors
--border-color-invalid: rgb(255, 80, 50);
--box-shadow-color-invalid: none;

View File

@ -31,15 +31,16 @@
@use '../features/gallery/ImageGallery.scss';
@use '../features/gallery/HoverableImage.scss';
@use '../features/gallery/InvokePopover.scss';
@use '../features/gallery/ShowHideGalleryButton.scss';
@use '../features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss';
// Tabs
@use '../features/tabs/InvokeTabs.scss';
@use '../features/tabs/InvokeWorkarea.scss';
@use '../features/tabs/InvokeOptionsPanel.scss';
@use '../features/tabs/TextToImage/TextToImage.scss';
@use '../features/tabs/ImageToImage/ImageToImage.scss';
@use '../features/tabs/Inpainting/Inpainting.scss';
@use '../features/tabs/FloatingShowHideButton.scss';
// Component Shared
@use '../common/components/IAINumberInput.scss';