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: '^_' },
|
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||||
],
|
],
|
||||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
@ -52,6 +52,8 @@
|
|||||||
"i18next-http-backend": "^2.1.1",
|
"i18next-http-backend": "^2.1.1",
|
||||||
"konva": "^8.4.2",
|
"konva": "^8.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"overlayscrollbars": "^2.1.0",
|
||||||
|
"overlayscrollbars-react": "^0.5.0",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
|
@ -9,7 +9,7 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
|||||||
|
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
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';
|
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants';
|
||||||
|
|
||||||
keepGUIAlive();
|
keepGUIAlive();
|
||||||
@ -18,26 +18,32 @@ const App = () => {
|
|||||||
useToastWatcher();
|
useToastWatcher();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid w="100vw" h="100vh">
|
<>
|
||||||
<ImageUploader>
|
<Grid w="100vw" h="100vh">
|
||||||
<ProgressBar />
|
<ImageUploader>
|
||||||
<Grid
|
<ProgressBar />
|
||||||
gap={4}
|
<Grid
|
||||||
p={APP_PADDING}
|
gap={4}
|
||||||
gridAutoRows="min-content auto"
|
p={APP_PADDING}
|
||||||
w={APP_WIDTH}
|
gridAutoRows="min-content auto"
|
||||||
h={APP_HEIGHT}
|
w={APP_WIDTH}
|
||||||
>
|
h={APP_HEIGHT}
|
||||||
<SiteHeader />
|
>
|
||||||
<InvokeTabs />
|
<SiteHeader />
|
||||||
</Grid>
|
<InvokeTabs />
|
||||||
<Box>
|
</Grid>
|
||||||
<Console />
|
<Box>
|
||||||
</Box>
|
<Console />
|
||||||
</ImageUploader>
|
</Box>
|
||||||
<FloatingParametersPanelButtons />
|
</ImageUploader>
|
||||||
<FloatingGalleryButton />
|
<Portal>
|
||||||
</Grid>
|
<FloatingParametersPanelButtons />
|
||||||
|
</Portal>
|
||||||
|
<Portal>
|
||||||
|
<FloatingGalleryButton />
|
||||||
|
</Portal>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,6 +9,17 @@ import { greenTeaThemeColors } from 'theme/colors/greenTea';
|
|||||||
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
|
import { invokeAIThemeColors } from 'theme/colors/invokeAI';
|
||||||
import { lightThemeColors } from 'theme/colors/lightTheme';
|
import { lightThemeColors } from 'theme/colors/lightTheme';
|
||||||
import { oceanBlueColors } from 'theme/colors/oceanBlue';
|
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 = {
|
type ThemeLocaleProviderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -30,7 +30,7 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
|
|||||||
if (!shouldDisplayGuides) return null;
|
if (!shouldDisplayGuides) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover trigger="hover">
|
<Popover trigger="hover" isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Box>{children}</Box>
|
<Box>{children}</Box>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
@ -59,6 +59,11 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
|
|||||||
allowMultiple
|
allowMultiple
|
||||||
reduceMotion
|
reduceMotion
|
||||||
onChange={handleChangeAccordionState}
|
onChange={handleChangeAccordionState}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderAccordions()}
|
{renderAccordions()}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
@ -28,7 +28,6 @@ export const floatingSelector = createSelector(
|
|||||||
const {
|
const {
|
||||||
shouldPinParametersPanel,
|
shouldPinParametersPanel,
|
||||||
shouldShowParametersPanel,
|
shouldShowParametersPanel,
|
||||||
shouldHoldParametersPanelOpen,
|
|
||||||
shouldUseCanvasBetaLayout,
|
shouldUseCanvasBetaLayout,
|
||||||
} = ui;
|
} = ui;
|
||||||
|
|
||||||
@ -40,10 +39,7 @@ export const floatingSelector = createSelector(
|
|||||||
|
|
||||||
const shouldShowParametersPanelButton =
|
const shouldShowParametersPanelButton =
|
||||||
!canvasBetaLayoutCheck &&
|
!canvasBetaLayoutCheck &&
|
||||||
!(
|
!shouldPinParametersPanel &&
|
||||||
shouldShowParametersPanel ||
|
|
||||||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
|
|
||||||
) &&
|
|
||||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||||
|
|
||||||
const shouldShowGalleryButton =
|
const shouldShowGalleryButton =
|
||||||
@ -51,8 +47,7 @@ export const floatingSelector = createSelector(
|
|||||||
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
|
||||||
|
|
||||||
const shouldShowProcessButtons =
|
const shouldShowProcessButtons =
|
||||||
!canvasBetaLayoutCheck &&
|
!canvasBetaLayoutCheck && !shouldPinParametersPanel;
|
||||||
(!shouldPinParametersPanel || !shouldShowParametersPanel);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shouldPinParametersPanel,
|
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';
|
} from 'react-icons/md';
|
||||||
import { activeTabIndexSelector } from '../store/uiSelectors';
|
import { activeTabIndexSelector } from '../store/uiSelectors';
|
||||||
import { floatingSelector } from './FloatingParametersPanelButtons';
|
import { floatingSelector } from './FloatingParametersPanelButtons';
|
||||||
import ImageToImageWorkarea from './ImageToImage';
|
import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea';
|
||||||
import TextToImageWorkarea from './TextToImage';
|
import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea';
|
||||||
import UnifiedCanvasWorkarea from './UnifiedCanvas/UnifiedCanvasWorkarea';
|
import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea';
|
||||||
|
|
||||||
export interface InvokeTabInfo {
|
export interface InvokeTabInfo {
|
||||||
title: ReactElement;
|
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,
|
padding: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageToImageDisplay = () => {
|
const ImageToImageContent = () => {
|
||||||
const initialImage = useAppSelector(
|
const initialImage = useAppSelector(
|
||||||
(state: RootState) => state.generation.initialImage
|
(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 ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const imageToImageAccordions = {
|
const imageToImageAccordions = {
|
||||||
@ -69,13 +69,12 @@ export default function ImageToImagePanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvokeOptionsPanel>
|
<Flex flexDir="column" gap={2} position="relative">
|
||||||
<Flex flexDir="column" rowGap={2}>
|
<PromptInput />
|
||||||
<PromptInput />
|
<NegativePromptInput />
|
||||||
<NegativePromptInput />
|
|
||||||
</Flex>
|
|
||||||
<ProcessButtons />
|
<ProcessButtons />
|
||||||
<ParametersAccordion accordionInfo={imageToImageAccordions} />
|
<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 { Box, Flex } from '@chakra-ui/react';
|
||||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||||
|
|
||||||
const TextToImageDisplay = () => {
|
const TextToImageContent = () => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
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 ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const textToImageAccordions = {
|
const textToImageAccordions = {
|
||||||
@ -63,13 +63,12 @@ export default function TextToImagePanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvokeOptionsPanel>
|
<Flex flexDir="column" gap={2} position="relative">
|
||||||
<Flex flexDir="column" rowGap={2}>
|
<PromptInput />
|
||||||
<PromptInput />
|
<NegativePromptInput />
|
||||||
<NegativePromptInput />
|
|
||||||
</Flex>
|
|
||||||
<ProcessButtons />
|
<ProcessButtons />
|
||||||
<ParametersAccordion accordionInfo={textToImageAccordions} />
|
<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 dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { doesCanvasNeedScaling } = useAppSelector(selector);
|
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 dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { doesCanvasNeedScaling } = useAppSelector(selector);
|
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 ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
|
||||||
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
|
||||||
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
|
||||||
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const unifiedCanvasAccordions = {
|
const unifiedCanvasAccordions = {
|
||||||
@ -66,14 +66,12 @@ export default function UnifiedCanvasPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvokeOptionsPanel>
|
<Flex flexDir="column" gap={2} position="relative">
|
||||||
<Flex flexDir="column" rowGap={2}>
|
<PromptInput />
|
||||||
<PromptInput />
|
<NegativePromptInput />
|
||||||
<NegativePromptInput />
|
|
||||||
</Flex>
|
|
||||||
<ProcessButtons />
|
<ProcessButtons />
|
||||||
{/* <ParametersAccordion accordionInfo={unifiedCanvasImg2ImgAccordion} /> */}
|
|
||||||
<ParametersAccordion accordionInfo={unifiedCanvasAccordions} />
|
<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,
|
activeTab: 0,
|
||||||
currentTheme: 'dark',
|
currentTheme: 'dark',
|
||||||
parametersPanelScrollPosition: 0,
|
parametersPanelScrollPosition: 0,
|
||||||
shouldHoldParametersPanelOpen: false,
|
|
||||||
shouldPinParametersPanel: true,
|
shouldPinParametersPanel: true,
|
||||||
shouldShowParametersPanel: true,
|
shouldShowParametersPanel: true,
|
||||||
shouldShowImageDetails: false,
|
shouldShowImageDetails: false,
|
||||||
@ -41,16 +40,11 @@ export const uiSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
|
setShouldPinParametersPanel: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldPinParametersPanel = action.payload;
|
state.shouldPinParametersPanel = action.payload;
|
||||||
|
state.shouldShowParametersPanel = true;
|
||||||
},
|
},
|
||||||
setShouldShowParametersPanel: (state, action: PayloadAction<boolean>) => {
|
setShouldShowParametersPanel: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowParametersPanel = action.payload;
|
state.shouldShowParametersPanel = action.payload;
|
||||||
},
|
},
|
||||||
setShouldHoldParametersPanelOpen: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<boolean>
|
|
||||||
) => {
|
|
||||||
state.shouldHoldParametersPanelOpen = action.payload;
|
|
||||||
},
|
|
||||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldShowImageDetails = action.payload;
|
state.shouldShowImageDetails = action.payload;
|
||||||
},
|
},
|
||||||
@ -76,7 +70,6 @@ export const {
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
setCurrentTheme,
|
setCurrentTheme,
|
||||||
setParametersPanelScrollPosition,
|
setParametersPanelScrollPosition,
|
||||||
setShouldHoldParametersPanelOpen,
|
|
||||||
setShouldPinParametersPanel,
|
setShouldPinParametersPanel,
|
||||||
setShouldShowParametersPanel,
|
setShouldShowParametersPanel,
|
||||||
setShouldShowImageDetails,
|
setShouldShowImageDetails,
|
||||||
|
@ -4,7 +4,6 @@ export interface UIState {
|
|||||||
activeTab: number;
|
activeTab: number;
|
||||||
currentTheme: string;
|
currentTheme: string;
|
||||||
parametersPanelScrollPosition: number;
|
parametersPanelScrollPosition: number;
|
||||||
shouldHoldParametersPanelOpen: boolean;
|
|
||||||
shouldPinParametersPanel: boolean;
|
shouldPinParametersPanel: boolean;
|
||||||
shouldShowParametersPanel: boolean;
|
shouldShowParametersPanel: boolean;
|
||||||
shouldShowImageDetails: boolean;
|
shouldShowImageDetails: boolean;
|
||||||
|
@ -9,7 +9,6 @@ const { definePartsStyle, defineMultiStyleConfig } =
|
|||||||
|
|
||||||
const invokeAIContainer = defineStyle({
|
const invokeAIContainer = defineStyle({
|
||||||
border: 'none',
|
border: 'none',
|
||||||
pt: 2,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const invokeAIButton = defineStyle((props) => {
|
const invokeAIButton = defineStyle((props) => {
|
||||||
@ -40,7 +39,6 @@ const invokeAIPanel = defineStyle((props) => {
|
|||||||
bg: `${c}.800`,
|
bg: `${c}.800`,
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
borderTopRadius: 'none',
|
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 { numberInputTheme } from './components/numberInput';
|
||||||
import { popoverTheme } from './components/popover';
|
import { popoverTheme } from './components/popover';
|
||||||
import { progressTheme } from './components/progress';
|
import { progressTheme } from './components/progress';
|
||||||
import { scrollbar } from './components/scrollbar';
|
import { no_scrollbar, scrollbar } from './components/scrollbar';
|
||||||
import { selectTheme } from './components/select';
|
import { selectTheme } from './components/select';
|
||||||
import { sliderTheme } from './components/slider';
|
import { sliderTheme } from './components/slider';
|
||||||
import { switchTheme } from './components/switch';
|
import { switchTheme } from './components/switch';
|
||||||
@ -31,7 +31,7 @@ export const theme: ThemeOverride = {
|
|||||||
color: 'base.50',
|
color: 'base.50',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
...scrollbar,
|
...no_scrollbar,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
direction: 'ltr',
|
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_GALLERY_POPOVER_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 6rem))`;
|
||||||
export const APP_METADATA_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 4.4rem))`;
|
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
|
// do not touch ffs
|
||||||
export const APP_TEXT_TO_IMAGE_HEIGHT =
|
export const APP_TEXT_TO_IMAGE_HEIGHT =
|
||||||
'calc(100vh - 9.4375rem - 1.925rem - 1.15rem)';
|
'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"
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
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:
|
p-cancelable@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user