mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip resizable pinnable drawer
fix(ui): remove old scrollbar css fix(ui): make guidepopover lazy feat(ui): wip resizable drawer feat(ui): wip resizable drawer feat(ui): add scroll-linked shadow feat(ui): organize files Align Scrollbar next to content Move resizable drawer underneath the progress bar Add InvokeLogo to unpinned & align Adds Invoke Logo to Unpinned Parameters panel and aligns to make it feel seamless.
This commit is contained in:
parent
1aaad9336f
commit
d81088dff7
@ -35,6 +35,7 @@ module.exports = {
|
||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||
],
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -52,6 +52,8 @@
|
||||
"i18next-http-backend": "^2.1.1",
|
||||
"konva": "^8.4.2",
|
||||
"lodash": "^4.17.21",
|
||||
"overlayscrollbars": "^2.1.0",
|
||||
"overlayscrollbars-react": "^0.5.0",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
|
@ -9,7 +9,7 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
||||
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||
import { Box, Grid } from '@chakra-ui/react';
|
||||
import { Box, Grid, Portal } from '@chakra-ui/react';
|
||||
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants';
|
||||
|
||||
keepGUIAlive();
|
||||
@ -18,26 +18,32 @@ const App = () => {
|
||||
useToastWatcher();
|
||||
|
||||
return (
|
||||
<Grid w="100vw" h="100vh">
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
p={APP_PADDING}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
</Grid>
|
||||
<Box>
|
||||
<Console />
|
||||
</Box>
|
||||
</ImageUploader>
|
||||
<FloatingParametersPanelButtons />
|
||||
<FloatingGalleryButton />
|
||||
</Grid>
|
||||
<>
|
||||
<Grid w="100vw" h="100vh">
|
||||
<ImageUploader>
|
||||
<ProgressBar />
|
||||
<Grid
|
||||
gap={4}
|
||||
p={APP_PADDING}
|
||||
gridAutoRows="min-content auto"
|
||||
w={APP_WIDTH}
|
||||
h={APP_HEIGHT}
|
||||
>
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
</Grid>
|
||||
<Box>
|
||||
<Console />
|
||||
</Box>
|
||||
</ImageUploader>
|
||||
<Portal>
|
||||
<FloatingParametersPanelButtons />
|
||||
</Portal>
|
||||
<Portal>
|
||||
<FloatingGalleryButton />
|
||||
</Portal>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,17 @@ import { greenTeaThemeColors } from 'theme/colors/greenTea';
|
||||
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
|
||||
import { lightThemeColors } from 'theme/colors/lightTheme';
|
||||
import { oceanBlueColors } from 'theme/colors/oceanBlue';
|
||||
import '@fontsource/inter/100.css';
|
||||
import '@fontsource/inter/200.css';
|
||||
import '@fontsource/inter/300.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import '@fontsource/inter/800.css';
|
||||
import '@fontsource/inter/900.css';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import 'theme/overlayscrollbar.css';
|
||||
|
||||
type ThemeLocaleProviderProps = {
|
||||
children: ReactNode;
|
||||
|
@ -30,7 +30,7 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
||||
if (!shouldDisplayGuides) return null;
|
||||
|
||||
return (
|
||||
<Popover trigger="hover">
|
||||
<Popover trigger="hover" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
</PopoverTrigger>
|
||||
|
@ -59,6 +59,11 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={handleChangeAccordionState}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{renderAccordions()}
|
||||
</Accordion>
|
||||
|
@ -28,7 +28,6 @@ export const floatingSelector = createSelector(
|
||||
const {
|
||||
shouldPinParametersPanel,
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldUseCanvasBetaLayout,
|
||||
} = ui;
|
||||
|
||||
@ -40,10 +39,7 @@ export const floatingSelector = createSelector(
|
||||
|
||||
const shouldShowParametersPanelButton =
|
||||
!canvasBetaLayoutCheck &&
|
||||
!(
|
||||
shouldShowParametersPanel ||
|
||||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
|
||||
) &&
|
||||
!shouldPinParametersPanel &&
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
const shouldShowGalleryButton =
|
||||
@ -51,8 +47,7 @@ export const floatingSelector = createSelector(
|
||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
const shouldShowProcessButtons =
|
||||
!canvasBetaLayoutCheck &&
|
||||
(!shouldPinParametersPanel || !shouldShowParametersPanel);
|
||||
!canvasBetaLayoutCheck && !shouldPinParametersPanel;
|
||||
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
|
||||
export default function InitialImageOverlay() {
|
||||
const initialImage = useAppSelector(
|
||||
(state: RootState) => state.generation.initialImage
|
||||
);
|
||||
|
||||
return initialImage ? (
|
||||
<Image
|
||||
fit="contain"
|
||||
src={typeof initialImage === 'string' ? initialImage : initialImage.url}
|
||||
rounded="md"
|
||||
className="checkerboard"
|
||||
/>
|
||||
) : null;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import ImageToImageDisplay from './ImageToImageDisplay';
|
||||
import ImageToImagePanel from './ImageToImagePanel';
|
||||
|
||||
export default function ImageToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<ImageToImagePanel />}>
|
||||
<ImageToImageDisplay />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
.ltr-parameters-panel-transition-enter {
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.ltr-parameters-panel-transition-exit-active {
|
||||
transform: translateX(-150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-enter {
|
||||
transform: translateX(150%);
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.rtl-parameters-panel-transition-exit-active {
|
||||
transform: translateX(150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
import { Box, Flex, Tooltip, Icon, useTheme } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import {
|
||||
setShouldHoldParametersPanelOpen,
|
||||
setShouldPinParametersPanel,
|
||||
setShouldShowParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
|
||||
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { setParametersPanelScrollPosition } from 'features/ui/store/uiSlice';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { uiSelector } from '../store/uiSelectors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
APP_CONTENT_HEIGHT,
|
||||
OPTIONS_BAR_MAX_WIDTH,
|
||||
PROGRESS_BAR_THICKNESS,
|
||||
} from 'theme/util/constants';
|
||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||
|
||||
import './InvokeParametersPanel.css';
|
||||
import { no_scrollbar } from 'theme/components/scrollbar';
|
||||
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const optionsPanelSelector = createSelector(
|
||||
uiSelector,
|
||||
(ui) => {
|
||||
const {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
parametersPanelScrollPosition,
|
||||
} = ui;
|
||||
|
||||
return {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
parametersPanelScrollPosition,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const InvokeOptionsPanel = (props: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { direction } = useTheme();
|
||||
|
||||
const {
|
||||
shouldShowParametersPanel,
|
||||
shouldHoldParametersPanelOpen,
|
||||
shouldPinParametersPanel,
|
||||
} = useAppSelector(optionsPanelSelector);
|
||||
|
||||
const optionsPanelRef = useRef<HTMLDivElement>(null);
|
||||
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Hotkeys
|
||||
useHotkeys(
|
||||
'o',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
|
||||
shouldPinParametersPanel &&
|
||||
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
|
||||
},
|
||||
[shouldShowParametersPanel, shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinParametersPanel,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
handleClickPinOptionsPanel();
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
const handleCloseOptionsPanel = useCallback(() => {
|
||||
if (shouldPinParametersPanel) return;
|
||||
dispatch(
|
||||
setParametersPanelScrollPosition(
|
||||
optionsPanelContainerRef.current
|
||||
? optionsPanelContainerRef.current.scrollTop
|
||||
: 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
dispatch(setShouldHoldParametersPanelOpen(false));
|
||||
}, [dispatch, shouldPinParametersPanel]);
|
||||
|
||||
const setCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current = window.setTimeout(
|
||||
() => handleCloseOptionsPanel(),
|
||||
500
|
||||
);
|
||||
};
|
||||
|
||||
const cancelCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
};
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (
|
||||
optionsPanelRef.current &&
|
||||
!optionsPanelRef.current.contains(e.target as Node)
|
||||
) {
|
||||
handleCloseOptionsPanel();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleCloseOptionsPanel]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={optionsPanelRef}
|
||||
in={
|
||||
shouldShowParametersPanel ||
|
||||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
|
||||
}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames={`${direction}-parameters-panel-transition`}
|
||||
>
|
||||
<Box
|
||||
className={`${direction}-parameters-panel-transition`}
|
||||
tabIndex={1}
|
||||
ref={optionsPanelRef}
|
||||
onMouseEnter={
|
||||
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
onMouseOver={
|
||||
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
sx={{
|
||||
borderInlineEndWidth: !shouldPinParametersPanel ? 5 : 0,
|
||||
borderInlineEndStyle: 'solid',
|
||||
bg: 'base.900',
|
||||
borderColor: 'base.700',
|
||||
height: APP_CONTENT_HEIGHT,
|
||||
width: OPTIONS_BAR_MAX_WIDTH,
|
||||
maxWidth: OPTIONS_BAR_MAX_WIDTH,
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
...no_scrollbar,
|
||||
...(!shouldPinParametersPanel && {
|
||||
zIndex: 20,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
width: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
|
||||
maxWidth: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
|
||||
height: '100%',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box sx={{ margin: !shouldPinParametersPanel && 4 }}>
|
||||
<Flex
|
||||
ref={optionsPanelContainerRef}
|
||||
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target !== optionsPanelContainerRef.current) {
|
||||
cancelCloseOptionsPanelTimer();
|
||||
} else {
|
||||
!shouldPinParametersPanel && setCloseOptionsPanelTimer();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t('common.pinOptionsPanel')}>
|
||||
<Box
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
padding: 2,
|
||||
top: 4,
|
||||
insetInlineEnd: 4,
|
||||
zIndex: 20,
|
||||
...(shouldPinParametersPanel && {
|
||||
top: 0,
|
||||
insetInlineEnd: 0,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{ opacity: 0.2 }}
|
||||
as={shouldPinParametersPanel ? BsPinAngleFill : BsPinAngle}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{!shouldPinParametersPanel && (
|
||||
<Box sx={{ pt: PROGRESS_BAR_THICKNESS, pb: 2 }}>
|
||||
<InvokeAILogoComponent />
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeOptionsPanel;
|
@ -36,9 +36,9 @@ import {
|
||||
} from 'react-icons/md';
|
||||
import { activeTabIndexSelector } from '../store/uiSelectors';
|
||||
import { floatingSelector } from './FloatingParametersPanelButtons';
|
||||
import ImageToImageWorkarea from './ImageToImage';
|
||||
import TextToImageWorkarea from './TextToImage';
|
||||
import UnifiedCanvasWorkarea from './UnifiedCanvas/UnifiedCanvasWorkarea';
|
||||
import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea';
|
||||
import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea';
|
||||
import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea';
|
||||
|
||||
export interface InvokeTabInfo {
|
||||
title: ReactElement;
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import ImageGallery from 'features/gallery/components/ImageGallery';
|
||||
import { setInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { DragEvent, ReactNode } from 'react';
|
||||
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const workareaSelector = createSelector(
|
||||
[uiSelector, lightboxSelector, activeTabNameSelector],
|
||||
(ui, lightbox, activeTabName) => {
|
||||
const { shouldPinParametersPanel } = ui;
|
||||
const { isLightboxOpen } = lightbox;
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
isLightboxOpen,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type InvokeWorkareaProps = BoxProps & {
|
||||
optionsPanel: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { optionsPanel, children, ...rest } = props;
|
||||
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
|
||||
|
||||
const getImageByUuid = useGetImageByUuid();
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
const uuid = e.dataTransfer.getData('invokeai/imageUuid');
|
||||
const image = getImageByUuid(uuid);
|
||||
if (!image) return;
|
||||
if (activeTabName === 'img2img') {
|
||||
dispatch(setInitialImage(image));
|
||||
} else if (activeTabName === 'unifiedCanvas') {
|
||||
dispatch(setInitialCanvasImage(image));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box {...rest} pos="relative" w="100%" h="100%">
|
||||
<Flex gap={4} h="100%">
|
||||
{optionsPanel}
|
||||
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
|
||||
{children}
|
||||
</Box>
|
||||
{!isLightboxOpen && <ImageGallery />}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeWorkarea;
|
@ -1,11 +0,0 @@
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import TextToImageDisplay from './TextToImageDisplay';
|
||||
import TextToImagePanel from './TextToImagePanel';
|
||||
|
||||
export default function TextToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
|
||||
<TextToImageDisplay />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
|
||||
import UnifiedCanvasDisplayBeta from './UnifiedCanvasBeta/UnifiedCanvasDisplayBeta';
|
||||
import UnifiedCanvasDisplay from './UnifiedCanvasDisplay';
|
||||
import UnifiedCanvasPanel from './UnifiedCanvasPanel';
|
||||
|
||||
export default function UnifiedCanvasWorkarea() {
|
||||
const shouldUseCanvasBetaLayout = useAppSelector(
|
||||
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
|
||||
);
|
||||
return (
|
||||
<InvokeWorkarea optionsPanel={<UnifiedCanvasPanel />}>
|
||||
{shouldUseCanvasBetaLayout ? (
|
||||
<UnifiedCanvasDisplayBeta />
|
||||
) : (
|
||||
<UnifiedCanvasDisplay />
|
||||
)}
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import ImageGallery from 'features/gallery/components/ImageGallery';
|
||||
import { setInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import {
|
||||
activeTabNameSelector,
|
||||
uiSelector,
|
||||
} from 'features/ui/store/uiSelectors';
|
||||
import { DragEvent, ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
setDoesCanvasNeedScaling,
|
||||
setInitialCanvasImage,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
|
||||
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
APP_CONTENT_HEIGHT,
|
||||
PARAMETERS_PANEL_WIDTH,
|
||||
} from 'theme/util/constants';
|
||||
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
|
||||
import {
|
||||
setShouldPinParametersPanel,
|
||||
setShouldShowParametersPanel,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||
|
||||
const workareaSelector = createSelector(
|
||||
[uiSelector, lightboxSelector, activeTabNameSelector],
|
||||
(ui, lightbox, activeTabName) => {
|
||||
const { shouldPinParametersPanel } = ui;
|
||||
const { isLightboxOpen } = lightbox;
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
isLightboxOpen,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type InvokeWorkareaProps = BoxProps & {
|
||||
parametersPanel: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
||||
const { parametersPanel, children, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
|
||||
const { shouldPinParametersPanel, shouldShowParametersPanel } =
|
||||
useAppSelector(uiSelector);
|
||||
|
||||
const getImageByUuid = useGetImageByUuid();
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
const uuid = e.dataTransfer.getData('invokeai/imageUuid');
|
||||
const image = getImageByUuid(uuid);
|
||||
if (!image) return;
|
||||
if (activeTabName === 'img2img') {
|
||||
dispatch(setInitialImage(image));
|
||||
} else if (activeTabName === 'unifiedCanvas') {
|
||||
dispatch(setInitialCanvasImage(image));
|
||||
}
|
||||
};
|
||||
|
||||
const closeParametersPanel = () => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'o',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
|
||||
shouldPinParametersPanel &&
|
||||
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
|
||||
},
|
||||
[shouldShowParametersPanel, shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
dispatch(setShouldShowParametersPanel(false));
|
||||
},
|
||||
{
|
||||
enabled: () => !shouldPinParametersPanel,
|
||||
preventDefault: true,
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
},
|
||||
[shouldPinParametersPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex {...rest} pos="relative" h={APP_CONTENT_HEIGHT} gap={4}>
|
||||
<ResizableDrawer
|
||||
direction="left"
|
||||
isResizable={true}
|
||||
shouldAllowResize={!shouldPinParametersPanel}
|
||||
isOpen={shouldShowParametersPanel || shouldPinParametersPanel}
|
||||
onClose={closeParametersPanel}
|
||||
isPinned={shouldPinParametersPanel}
|
||||
handleWidth={5}
|
||||
handleInteractWidth={'15px'}
|
||||
sx={{
|
||||
borderColor: 'base.700',
|
||||
p: shouldPinParametersPanel ? 0 : 4,
|
||||
bg: 'base.900',
|
||||
}}
|
||||
initialWidth={PARAMETERS_PANEL_WIDTH}
|
||||
minWidth={PARAMETERS_PANEL_WIDTH}
|
||||
pinnedWidth={PARAMETERS_PANEL_WIDTH}
|
||||
pinnedHeight={APP_CONTENT_HEIGHT}
|
||||
>
|
||||
<Flex
|
||||
flexDir="column"
|
||||
rowGap={4}
|
||||
paddingTop={!shouldPinParametersPanel ? 1.5 : 0}
|
||||
>
|
||||
{!shouldPinParametersPanel && <InvokeAILogoComponent />}
|
||||
{parametersPanel}
|
||||
</Flex>
|
||||
</ResizableDrawer>
|
||||
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
|
||||
{children}
|
||||
</Box>
|
||||
{!isLightboxOpen && <ImageGallery />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeWorkarea;
|
@ -0,0 +1,44 @@
|
||||
import { Box, Icon, Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIIconButton, {
|
||||
IAIIconButtonProps,
|
||||
} from 'common/components/IAIIconButton';
|
||||
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { setShouldPinParametersPanel } from '../../store/uiSlice';
|
||||
|
||||
const PinParametersPanelButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldPinParametersPanel = useAppSelector(
|
||||
(state) => state.ui.shouldPinParametersPanel
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
|
||||
dispatch(setDoesCanvasNeedScaling(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={t('common.pinOptionsPanel')}>
|
||||
<IAIIconButton
|
||||
aria-label={t('common.pinOptionsPanel')}
|
||||
opacity={0.2}
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
variant="unstyled"
|
||||
size="sm"
|
||||
padding={2}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
insetInlineEnd: 1,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinParametersPanelButton;
|
@ -0,0 +1,217 @@
|
||||
import {
|
||||
Box,
|
||||
chakra,
|
||||
ChakraProps,
|
||||
Slide,
|
||||
useOutsideClick,
|
||||
useTheme,
|
||||
SlideDirection,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Resizable,
|
||||
ResizableProps,
|
||||
ResizeCallback,
|
||||
ResizeStartCallback,
|
||||
} from 're-resizable';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { LangDirection } from './types';
|
||||
import {
|
||||
getDefaultSize,
|
||||
getHandleEnables,
|
||||
getHandleStyles,
|
||||
getMinMaxDimensions,
|
||||
getResizableStyles,
|
||||
} from './util';
|
||||
import Scrollable from '../Scrollable';
|
||||
|
||||
type ResizableDrawerProps = ResizableProps & {
|
||||
children: ReactNode;
|
||||
isResizable: boolean;
|
||||
isPinned: boolean;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
direction?: SlideDirection;
|
||||
initialWidth?: string | number;
|
||||
minWidth?: string | number;
|
||||
maxWidth?: string | number;
|
||||
initialHeight?: string | number;
|
||||
minHeight?: string | number;
|
||||
maxHeight?: string | number;
|
||||
shouldAllowResize?: boolean;
|
||||
onResizeStart?: ResizeStartCallback;
|
||||
onResizeStop?: ResizeCallback;
|
||||
onResize?: ResizeCallback;
|
||||
handleWidth?: number;
|
||||
handleInteractWidth?: string | number;
|
||||
sx?: ChakraProps['sx'];
|
||||
pinnedWidth: number;
|
||||
pinnedHeight: string | number;
|
||||
};
|
||||
|
||||
const ChakraResizeable = chakra(Resizable, {
|
||||
shouldForwardProp: (prop) => !['sx'].includes(prop),
|
||||
});
|
||||
|
||||
const ResizableDrawer = ({
|
||||
direction = 'left',
|
||||
isResizable,
|
||||
isPinned,
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
initialWidth = undefined,
|
||||
minWidth = undefined,
|
||||
maxWidth = undefined,
|
||||
initialHeight = undefined,
|
||||
minHeight = undefined,
|
||||
maxHeight = undefined,
|
||||
shouldAllowResize,
|
||||
onResizeStart,
|
||||
onResizeStop,
|
||||
onResize,
|
||||
handleWidth = 5,
|
||||
handleInteractWidth = '15px',
|
||||
pinnedWidth,
|
||||
pinnedHeight,
|
||||
sx = {},
|
||||
}: ResizableDrawerProps) => {
|
||||
const langDirection = useTheme().direction as LangDirection;
|
||||
|
||||
const outsideClickRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick({
|
||||
ref: outsideClickRef,
|
||||
handler: () => {
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const [width, setWidth] = useState<number | string>(0);
|
||||
const [height, setHeight] = useState<number | string>(0);
|
||||
|
||||
const handleEnables = useMemo(
|
||||
() =>
|
||||
isResizable && shouldAllowResize
|
||||
? getHandleEnables({ direction, langDirection })
|
||||
: {},
|
||||
[isResizable, shouldAllowResize, langDirection, direction]
|
||||
);
|
||||
|
||||
const handleStyles = useMemo(
|
||||
() =>
|
||||
getHandleStyles({
|
||||
handleEnables,
|
||||
handleStyle: {
|
||||
width: handleInteractWidth,
|
||||
},
|
||||
}),
|
||||
[handleEnables, handleInteractWidth]
|
||||
);
|
||||
|
||||
const minMaxDimensions = useMemo(
|
||||
() =>
|
||||
getMinMaxDimensions({
|
||||
direction,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
}),
|
||||
[minWidth, maxWidth, minHeight, maxHeight, direction]
|
||||
);
|
||||
|
||||
const resizableStyles = useMemo(
|
||||
() => getResizableStyles({ isPinned, direction, sx, handleWidth }),
|
||||
[sx, handleWidth, direction, isPinned]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { width, height } = getDefaultSize({
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
direction,
|
||||
});
|
||||
|
||||
setWidth(width);
|
||||
setHeight(height);
|
||||
}, [initialWidth, initialHeight, direction, langDirection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (['left', 'right'].includes(direction)) {
|
||||
setHeight(isPinned ? '100%' : '100vh');
|
||||
}
|
||||
if (['top', 'bottom'].includes(direction)) {
|
||||
setWidth(isPinned ? '100%' : '100vw');
|
||||
}
|
||||
}, [isPinned, direction]);
|
||||
|
||||
return (
|
||||
<Slide
|
||||
direction={direction}
|
||||
in={isOpen}
|
||||
motionProps={{ initial: false }}
|
||||
{...(isPinned
|
||||
? {
|
||||
style: {
|
||||
position: undefined,
|
||||
left: undefined,
|
||||
top: undefined,
|
||||
bottom: undefined,
|
||||
width: undefined,
|
||||
},
|
||||
}
|
||||
: {
|
||||
transition: { enter: { duration: 0.2 }, exit: { duration: 0.2 } },
|
||||
style: { zIndex: 98 },
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
ref={outsideClickRef}
|
||||
sx={{
|
||||
width: ['left', 'right'].includes(direction) ? 'min-content' : 'full',
|
||||
height: ['left', 'right'].includes(direction)
|
||||
? '100%'
|
||||
: 'min-content',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<ChakraResizeable
|
||||
size={{
|
||||
width: isPinned ? '100%' : width,
|
||||
height: isPinned ? '100%' : height,
|
||||
}}
|
||||
enable={handleEnables}
|
||||
handleStyles={handleStyles}
|
||||
{...minMaxDimensions}
|
||||
sx={{ ...resizableStyles, height: 'full' }}
|
||||
onResizeStart={(event, direction, elementRef) => {
|
||||
onResizeStart && onResizeStart(event, direction, elementRef);
|
||||
}}
|
||||
onResize={(event, direction, elementRef, delta) => {
|
||||
onResize && onResize(event, direction, elementRef, delta);
|
||||
}}
|
||||
onResizeStop={(event, direction, elementRef, delta) => {
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
setWidth(Number(width) + delta.width);
|
||||
}
|
||||
if (direction === 'top' || direction === 'bottom') {
|
||||
setHeight(Number(height) + delta.height);
|
||||
}
|
||||
onResizeStop && onResizeStop(event, direction, elementRef, delta);
|
||||
}}
|
||||
>
|
||||
<Scrollable>{children}</Scrollable>
|
||||
</ChakraResizeable>
|
||||
</Box>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResizableDrawer;
|
@ -0,0 +1,2 @@
|
||||
export type Placement = 'top' | 'right' | 'bottom' | 'left';
|
||||
export type LangDirection = 'ltr' | 'rtl' | undefined;
|
@ -0,0 +1,211 @@
|
||||
import { ChakraProps, SlideDirection } from '@chakra-ui/react';
|
||||
import { AnimationProps } from 'framer-motion';
|
||||
import { Enable } from 're-resizable';
|
||||
import React from 'react';
|
||||
import { LangDirection } from './types';
|
||||
|
||||
export type GetHandleEnablesOptions = {
|
||||
direction: SlideDirection;
|
||||
langDirection: LangDirection;
|
||||
};
|
||||
|
||||
// Determine handles to enable, taking into account language direction
|
||||
export const getHandleEnables = ({
|
||||
direction,
|
||||
langDirection,
|
||||
}: GetHandleEnablesOptions) => {
|
||||
const top = direction === 'bottom';
|
||||
|
||||
const right =
|
||||
(langDirection !== 'rtl' && direction === 'left') ||
|
||||
(langDirection === 'rtl' && direction === 'right');
|
||||
|
||||
const bottom = direction === 'top';
|
||||
|
||||
const left =
|
||||
(langDirection !== 'rtl' && direction === 'right') ||
|
||||
(langDirection === 'rtl' && direction === 'left');
|
||||
|
||||
return { top, right, bottom, left };
|
||||
};
|
||||
|
||||
export type GetDefaultSizeOptions = {
|
||||
initialWidth?: string | number;
|
||||
initialHeight?: string | number;
|
||||
direction: SlideDirection;
|
||||
};
|
||||
|
||||
// Get default sizes based on direction and initial values
|
||||
export const getDefaultSize = ({
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
direction,
|
||||
}: GetDefaultSizeOptions) => {
|
||||
const width =
|
||||
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
|
||||
|
||||
const height =
|
||||
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export type GetMinMaxDimensionsOptions = {
|
||||
direction: SlideDirection;
|
||||
minWidth?: string | number;
|
||||
maxWidth?: string | number;
|
||||
minHeight?: string | number;
|
||||
maxHeight?: string | number;
|
||||
};
|
||||
|
||||
// Get the min/max width/height based on direction and provided values
|
||||
export const getMinMaxDimensions = ({
|
||||
direction,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
}: GetMinMaxDimensionsOptions) => {
|
||||
const minW =
|
||||
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
|
||||
|
||||
const maxW =
|
||||
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
|
||||
|
||||
const minH =
|
||||
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
|
||||
|
||||
const maxH =
|
||||
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
|
||||
|
||||
return { minWidth: minW, maxWidth: maxW, minHeight: minH, maxHeight: maxH };
|
||||
};
|
||||
|
||||
export type GetHandleStylesOptions = {
|
||||
handleEnables: Enable;
|
||||
handleStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
// Get handle styles, the enables already have language direction factored in so
|
||||
// that does not need to be handled here
|
||||
export const getHandleStyles = ({
|
||||
handleEnables,
|
||||
handleStyle,
|
||||
}: GetHandleStylesOptions) => {
|
||||
if (!handleStyle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const top = handleEnables.top ? handleStyle : {};
|
||||
const right = handleEnables.right ? handleStyle : {};
|
||||
const bottom = handleEnables.bottom ? handleStyle : {};
|
||||
const left = handleEnables.left ? handleStyle : {};
|
||||
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
};
|
||||
};
|
||||
|
||||
export type GetAnimationsOptions = {
|
||||
direction: SlideDirection;
|
||||
langDirection: LangDirection;
|
||||
};
|
||||
|
||||
// Get the framer-motion animation props, taking into account language direction
|
||||
export const getAnimations = ({
|
||||
direction,
|
||||
langDirection,
|
||||
}: GetAnimationsOptions): AnimationProps => {
|
||||
const baseAnimation = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
// chakra consumes the transition prop, which, for it, is a string.
|
||||
// however we know the transition prop will make it to framer motion,
|
||||
// which wants it as an object. cast as string to satisfy TS.
|
||||
transition: { duration: 0.2, ease: 'easeInOut' },
|
||||
};
|
||||
|
||||
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
|
||||
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { y: -999 },
|
||||
animate: { y: 0 },
|
||||
exit: { y: -999 },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { x: 999 * langDirectionFactor },
|
||||
animate: { x: 0 },
|
||||
exit: { x: 999 * langDirectionFactor },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'bottom') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { y: 999 },
|
||||
animate: { y: 0 },
|
||||
exit: { y: 999 },
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
return {
|
||||
...baseAnimation,
|
||||
initial: { x: -999 * langDirectionFactor },
|
||||
animate: { x: 0 },
|
||||
exit: { x: -999 * langDirectionFactor },
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export type GetResizableStylesProps = {
|
||||
sx: ChakraProps['sx'];
|
||||
direction: SlideDirection;
|
||||
handleWidth: number;
|
||||
isPinned: boolean;
|
||||
};
|
||||
|
||||
export const getResizableStyles = ({
|
||||
isPinned, // TODO add borderRadius for pinned?
|
||||
sx,
|
||||
direction,
|
||||
handleWidth,
|
||||
}: GetResizableStylesProps): ChakraProps['sx'] => {
|
||||
if (isPinned) {
|
||||
return sx;
|
||||
}
|
||||
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
borderBottomWidth: handleWidth,
|
||||
...sx,
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
return { borderInlineStartWidth: handleWidth, ...sx };
|
||||
}
|
||||
|
||||
if (direction === 'bottom') {
|
||||
return {
|
||||
borderTopWidth: handleWidth,
|
||||
...sx,
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
return { borderInlineEndWidth: handleWidth, ...sx };
|
||||
}
|
||||
};
|
@ -0,0 +1,117 @@
|
||||
import { Box, ChakraProps } from '@chakra-ui/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
type ScrollableProps = {
|
||||
children: ReactNode;
|
||||
containerProps?: ChakraProps;
|
||||
};
|
||||
|
||||
const Scrollable = ({
|
||||
children,
|
||||
containerProps = {
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
flexShrink: 0,
|
||||
},
|
||||
}: ScrollableProps) => {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const topShadowRef = useRef<HTMLDivElement>(null);
|
||||
const bottomShadowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [initialize, _instance] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
events: {
|
||||
initialized(instance) {
|
||||
if (!topShadowRef.current || !bottomShadowRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, offsetHeight } =
|
||||
instance.elements().content;
|
||||
|
||||
const scrollPercentage = scrollTop / (scrollHeight - offsetHeight);
|
||||
|
||||
topShadowRef.current.style.opacity = String(scrollPercentage * 5);
|
||||
|
||||
bottomShadowRef.current.style.opacity = String(
|
||||
(1 - scrollPercentage) * 5
|
||||
);
|
||||
},
|
||||
scroll: (_instance, event) => {
|
||||
if (
|
||||
!topShadowRef.current ||
|
||||
!bottomShadowRef.current ||
|
||||
!scrollableRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, offsetHeight } =
|
||||
event.target as HTMLDivElement;
|
||||
|
||||
const scrollPercentage = scrollTop / (scrollHeight - offsetHeight);
|
||||
|
||||
topShadowRef.current.style.opacity = String(scrollPercentage * 5);
|
||||
|
||||
bottomShadowRef.current.style.opacity = String(
|
||||
(1 - scrollPercentage) * 5
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!scrollableRef.current ||
|
||||
!topShadowRef.current ||
|
||||
!bottomShadowRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
topShadowRef.current.style.opacity = '0';
|
||||
|
||||
bottomShadowRef.current.style.opacity = '0';
|
||||
|
||||
initialize(scrollableRef.current);
|
||||
}, [initialize]);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<Box ref={scrollableRef} {...containerProps} overflowY="scroll">
|
||||
<Box paddingInlineEnd={5}>{children}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
ref={bottomShadowRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
boxShadow:
|
||||
'inset 0 -3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
|
||||
width: 'full',
|
||||
height: 24,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
></Box>
|
||||
<Box
|
||||
ref={topShadowRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
boxShadow:
|
||||
'inset 0 3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
|
||||
width: 'full',
|
||||
height: 24,
|
||||
top: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scrollable;
|
@ -14,7 +14,7 @@ const workareaSplitViewStyle: ChakraProps['sx'] = {
|
||||
padding: 4,
|
||||
};
|
||||
|
||||
const ImageToImageDisplay = () => {
|
||||
const ImageToImageContent = () => {
|
||||
const initialImage = useAppSelector(
|
||||
(state: RootState) => state.generation.initialImage
|
||||
);
|
||||
@ -47,4 +47,4 @@ const ImageToImageDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageDisplay;
|
||||
export default ImageToImageContent;
|
@ -15,11 +15,11 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
|
||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ImageToImageSettings from './ImageToImageSettings';
|
||||
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
|
||||
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
|
||||
|
||||
export default function ImageToImagePanel() {
|
||||
export default function ImageToImageParameters() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const imageToImageAccordions = {
|
||||
@ -69,13 +69,12 @@ export default function ImageToImagePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<InvokeOptionsPanel>
|
||||
<Flex flexDir="column" rowGap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} position="relative">
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
<ProcessButtons />
|
||||
<ParametersAccordion accordionInfo={imageToImageAccordions} />
|
||||
</InvokeOptionsPanel>
|
||||
<PinParametersPanelButton />
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
|
||||
import ImageToImageContent from './ImageToImageContent';
|
||||
import ImageToImageParameters from './ImageToImageParameters';
|
||||
|
||||
export default function ImageToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea parametersPanel={<ImageToImageParameters />}>
|
||||
<ImageToImageContent />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||
|
||||
const TextToImageDisplay = () => {
|
||||
const TextToImageContent = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -18,4 +18,4 @@ const TextToImageDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TextToImageDisplay;
|
||||
export default TextToImageContent;
|
@ -15,10 +15,10 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
|
||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
|
||||
|
||||
export default function TextToImagePanel() {
|
||||
export default function TextToImageParameters() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const textToImageAccordions = {
|
||||
@ -63,13 +63,12 @@ export default function TextToImagePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<InvokeOptionsPanel>
|
||||
<Flex flexDir="column" rowGap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} position="relative">
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
<ProcessButtons />
|
||||
<ParametersAccordion accordionInfo={textToImageAccordions} />
|
||||
</InvokeOptionsPanel>
|
||||
<PinParametersPanelButton />
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
|
||||
import TextToImageContent from './TextToImageContent';
|
||||
import TextToImageParameters from './TextToImageParameters';
|
||||
|
||||
export default function TextToImageWorkarea() {
|
||||
return (
|
||||
<InvokeWorkarea parametersPanel={<TextToImageParameters />}>
|
||||
<TextToImageContent />
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -27,7 +27,7 @@ const selector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const UnifiedCanvasDisplayBeta = () => {
|
||||
const UnifiedCanvasContentBeta = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { doesCanvasNeedScaling } = useAppSelector(selector);
|
||||
@ -70,4 +70,4 @@ const UnifiedCanvasDisplayBeta = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedCanvasDisplayBeta;
|
||||
export default UnifiedCanvasContentBeta;
|
@ -26,7 +26,7 @@ const selector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const UnifiedCanvasDisplay = () => {
|
||||
const UnifiedCanvasContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { doesCanvasNeedScaling } = useAppSelector(selector);
|
||||
@ -80,4 +80,4 @@ const UnifiedCanvasDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedCanvasDisplay;
|
||||
export default UnifiedCanvasContent;
|
@ -15,10 +15,10 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
|
||||
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PinParametersPanelButton from 'features/ui/components/common/PinParametersPanelButton';
|
||||
|
||||
export default function UnifiedCanvasPanel() {
|
||||
export default function UnifiedCanvasParameters() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const unifiedCanvasAccordions = {
|
||||
@ -66,14 +66,12 @@ export default function UnifiedCanvasPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<InvokeOptionsPanel>
|
||||
<Flex flexDir="column" rowGap={2}>
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} position="relative">
|
||||
<PromptInput />
|
||||
<NegativePromptInput />
|
||||
<ProcessButtons />
|
||||
{/* <ParametersAccordion accordionInfo={unifiedCanvasImg2ImgAccordion} /> */}
|
||||
<ParametersAccordion accordionInfo={unifiedCanvasAccordions} />
|
||||
</InvokeOptionsPanel>
|
||||
<PinParametersPanelButton />
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import InvokeWorkarea from 'features/ui/components/common/InvokeWorkarea';
|
||||
import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta';
|
||||
import UnifiedCanvasContent from './UnifiedCanvasContent';
|
||||
import UnifiedCanvasParameters from './UnifiedCanvasParameters';
|
||||
|
||||
export default function UnifiedCanvasWorkarea() {
|
||||
const shouldUseCanvasBetaLayout = useAppSelector(
|
||||
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
|
||||
);
|
||||
return (
|
||||
<InvokeWorkarea parametersPanel={<UnifiedCanvasParameters />}>
|
||||
{shouldUseCanvasBetaLayout ? (
|
||||
<UnifiedCanvasContentBeta />
|
||||
) : (
|
||||
<UnifiedCanvasContent />
|
||||
)}
|
||||
</InvokeWorkarea>
|
||||
);
|
||||
}
|
@ -7,7 +7,6 @@ const initialtabsState: UIState = {
|
||||
activeTab: 0,
|
||||
currentTheme: 'dark',
|
||||
parametersPanelScrollPosition: 0,
|
||||
shouldHoldParametersPanelOpen: false,
|
||||
shouldPinParametersPanel: true,
|
||||
shouldShowParametersPanel: true,
|
||||
shouldShowImageDetails: false,
|
||||
@ -41,16 +40,11 @@ export const uiSlice = createSlice({
|
||||
},
|
||||
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldPinParametersPanel = action.payload;
|
||||
state.shouldShowParametersPanel = true;
|
||||
},
|
||||
setShouldShowParametersPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowParametersPanel = action.payload;
|
||||
},
|
||||
setShouldHoldParametersPanelOpen: (
|
||||
state,
|
||||
action: PayloadAction<boolean>
|
||||
) => {
|
||||
state.shouldHoldParametersPanelOpen = action.payload;
|
||||
},
|
||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowImageDetails = action.payload;
|
||||
},
|
||||
@ -76,7 +70,6 @@ export const {
|
||||
setActiveTab,
|
||||
setCurrentTheme,
|
||||
setParametersPanelScrollPosition,
|
||||
setShouldHoldParametersPanelOpen,
|
||||
setShouldPinParametersPanel,
|
||||
setShouldShowParametersPanel,
|
||||
setShouldShowImageDetails,
|
||||
|
@ -4,7 +4,6 @@ export interface UIState {
|
||||
activeTab: number;
|
||||
currentTheme: string;
|
||||
parametersPanelScrollPosition: number;
|
||||
shouldHoldParametersPanelOpen: boolean;
|
||||
shouldPinParametersPanel: boolean;
|
||||
shouldShowParametersPanel: boolean;
|
||||
shouldShowImageDetails: boolean;
|
||||
|
@ -9,7 +9,6 @@ const { definePartsStyle, defineMultiStyleConfig } =
|
||||
|
||||
const invokeAIContainer = defineStyle({
|
||||
border: 'none',
|
||||
pt: 2,
|
||||
});
|
||||
|
||||
const invokeAIButton = defineStyle((props) => {
|
||||
@ -40,7 +39,6 @@ const invokeAIPanel = defineStyle((props) => {
|
||||
bg: `${c}.800`,
|
||||
borderRadius: 'base',
|
||||
borderTopRadius: 'none',
|
||||
p: 4,
|
||||
};
|
||||
});
|
||||
|
||||
|
25
invokeai/frontend/web/src/theme/overlayscrollbar.css
Normal file
25
invokeai/frontend/web/src/theme/overlayscrollbar.css
Normal file
@ -0,0 +1,25 @@
|
||||
.os-scrollbar {
|
||||
/* --os-size: 0; */
|
||||
/* --os-padding-perpendicular: 0; */
|
||||
/* --os-padding-axis: 0; */
|
||||
/* --os-track-border-radius: 0; */
|
||||
/* --os-track-bg: none; */
|
||||
/* --os-track-bg-hover: none; */
|
||||
/* --os-track-bg-active: none; */
|
||||
/* --os-track-border: none; */
|
||||
/* --os-track-border-hover: none; */
|
||||
/* --os-track-border-active: none; */
|
||||
/* --os-handle-border-radius: 0; */
|
||||
--os-handle-bg: var(--invokeai-colors-accent-600);
|
||||
--os-handle-bg-hover: var(--invokeai-colors-accent-550);
|
||||
--os-handle-bg-active: var(--invokeai-colors-accent-500);
|
||||
/* --os-handle-border: none; */
|
||||
/* --os-handle-border-hover: none; */
|
||||
/* --os-handle-border-active: none; */
|
||||
/* --os-handle-min-size: 33px; */
|
||||
/* --os-handle-max-size: none; */
|
||||
/* --os-handle-perpendicular-size: 100%; */
|
||||
/* --os-handle-perpendicular-size-hover: 100%; */
|
||||
/* --os-handle-perpendicular-size-active: 100%; */
|
||||
/* --os-handle-interactive-area-offset: 0; */
|
||||
}
|
@ -12,7 +12,7 @@ import { modalTheme } from './components/modal';
|
||||
import { numberInputTheme } from './components/numberInput';
|
||||
import { popoverTheme } from './components/popover';
|
||||
import { progressTheme } from './components/progress';
|
||||
import { scrollbar } from './components/scrollbar';
|
||||
import { no_scrollbar, scrollbar } from './components/scrollbar';
|
||||
import { selectTheme } from './components/select';
|
||||
import { sliderTheme } from './components/slider';
|
||||
import { switchTheme } from './components/switch';
|
||||
@ -31,7 +31,7 @@ export const theme: ThemeOverride = {
|
||||
color: 'base.50',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
...scrollbar,
|
||||
...no_scrollbar,
|
||||
}),
|
||||
},
|
||||
direction: 'ltr',
|
||||
|
@ -11,6 +11,9 @@ export const APP_GALLERY_HEIGHT = 'calc(100vw - 0.3rem + 5rem)';
|
||||
export const APP_GALLERY_POPOVER_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 6rem))`;
|
||||
export const APP_METADATA_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 4.4rem))`;
|
||||
|
||||
// this is in pixels
|
||||
export const PARAMETERS_PANEL_WIDTH = 384;
|
||||
|
||||
// do not touch ffs
|
||||
export const APP_TEXT_TO_IMAGE_HEIGHT =
|
||||
'calc(100vh - 9.4375rem - 1.925rem - 1.15rem)';
|
||||
|
@ -4204,6 +4204,16 @@ os-tmpdir@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
||||
|
||||
overlayscrollbars-react@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz#0272bdc6304c7228a58d30e5b678e97fd5c5d8dd"
|
||||
integrity sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==
|
||||
|
||||
overlayscrollbars@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-2.1.0.tgz#d647034ef388980e0e5e092f7429c501215330a1"
|
||||
integrity sha512-L6p4o4aWse5pDstRnJjZaos+al+bkuAgzGIlWwlsxRSgW6+7Kvrp+kAzlWoTZ1bgB4CJj+8u5bjdq8XHEhWjrw==
|
||||
|
||||
p-cancelable@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||
|
Loading…
Reference in New Issue
Block a user