feat(ui): wip layouting

This commit is contained in:
psychedelicious 2023-05-08 00:55:31 +10:00
parent 3dc60254b9
commit 5365f42a04
25 changed files with 687 additions and 187 deletions

View File

@ -89,6 +89,7 @@
"react-konva": "^18.2.7", "react-konva": "^18.2.7",
"react-konva-utils": "^1.0.4", "react-konva-utils": "^1.0.4",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-resizable-panels": "^0.0.42",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-use": "^17.4.0", "react-use": "^17.4.0",

View File

@ -1,6 +1,6 @@
import ImageUploader from 'common/components/ImageUploader'; import ImageUploader from 'common/components/ImageUploader';
import ProgressBar from 'features/system/components/ProgressBar';
import SiteHeader from 'features/system/components/SiteHeader'; import SiteHeader from 'features/system/components/SiteHeader';
import ProgressBar from 'features/system/components/ProgressBar';
import InvokeTabs from 'features/ui/components/InvokeTabs'; import InvokeTabs from 'features/ui/components/InvokeTabs';
import useToastWatcher from 'features/system/hooks/useToastWatcher'; import useToastWatcher from 'features/system/hooks/useToastWatcher';
@ -28,6 +28,9 @@ import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger'; import { useLogger } from 'app/logging/useLogger';
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview'; import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import CreateParametersDrawer from 'features/ui/components/CreateParametersDrawer';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -84,11 +87,13 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
flexDir={{ base: 'column', xl: 'row' }} flexDir={{ base: 'column', xl: 'row' }}
> >
<InvokeTabs /> <InvokeTabs />
<ImageGalleryPanel />
</Flex> </Flex>
</Grid> </Grid>
</ImageUploader> </ImageUploader>
<ImageGalleryPanel />
<CreateParametersDrawer />
<AnimatePresence> <AnimatePresence>
{!isApplicationReady && !loadingOverridden && ( {!isApplicationReady && !loadingOverridden && (
<motion.div <motion.div

View File

@ -2,6 +2,12 @@ import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import {
toggleGalleryPanel,
toggleParametersPanel,
togglePinGalleryPanel,
togglePinParametersPanel,
} from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook'; import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
@ -36,4 +42,20 @@ export const useGlobalHotkeys = () => {
{ keyup: true, keydown: true }, { keyup: true, keydown: true },
[shift] [shift]
); );
useHotkeys('o', () => {
dispatch(toggleParametersPanel());
});
useHotkeys(['shift+o'], () => {
dispatch(togglePinParametersPanel());
});
useHotkeys('g', () => {
dispatch(toggleGalleryPanel());
});
useHotkeys(['shift+g'], () => {
dispatch(togglePinGalleryPanel());
});
}; };

View File

@ -73,7 +73,7 @@ const galleryPanelSelector = createSelector(
} }
); );
export const ImageGalleryPanel = () => { const ImageGalleryPanel = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const {
shouldPinGallery, shouldPinGallery,
@ -102,21 +102,21 @@ export const ImageGalleryPanel = () => {
const resolution = useResolution(); const resolution = useResolution();
useHotkeys( // useHotkeys(
'g', // 'g',
() => { // () => {
handleToggleGallery(); // handleToggleGallery();
}, // },
[shouldPinGallery] // [shouldPinGallery]
); // );
useHotkeys( // useHotkeys(
'shift+g', // 'shift+g',
() => { // () => {
handleSetShouldPinGallery(); // handleSetShouldPinGallery();
}, // },
[shouldPinGallery] // [shouldPinGallery]
); // );
useHotkeys( useHotkeys(
'esc', 'esc',
@ -162,55 +162,71 @@ export const ImageGalleryPanel = () => {
[galleryImageMinimumWidth] [galleryImageMinimumWidth]
); );
const calcGalleryMinHeight = () => { // const calcGalleryMinHeight = () => {
if (resolution === 'desktop') return; // if (resolution === 'desktop') return;
return 300; // return 300;
}; // };
const imageGalleryContent = () => { // const imageGalleryContent = () => {
return ( // return (
<Flex // <Flex
w="100vw" // w="100vw"
h={{ base: 300, xl: '100vh' }} // h={{ base: 300, xl: '100vh' }}
paddingRight={{ base: 8, xl: 0 }} // paddingRight={{ base: 8, xl: 0 }}
paddingBottom={{ base: 4, xl: 0 }} // paddingBottom={{ base: 4, xl: 0 }}
> // >
<ImageGalleryContent /> // <ImageGalleryContent />
</Flex> // </Flex>
); // );
}; // };
const resizableImageGalleryContent = () => { // const resizableImageGalleryContent = () => {
return ( // return (
<ResizableDrawer // <ResizableDrawer
direction="right" // direction="right"
isResizable={isResizable || !shouldPinGallery} // isResizable={isResizable || !shouldPinGallery}
isOpen={shouldShowGallery} // isOpen={shouldShowGallery}
onClose={handleCloseGallery} // onClose={handleCloseGallery}
isPinned={shouldPinGallery && !isLightboxOpen} // isPinned={shouldPinGallery && !isLightboxOpen}
minWidth={ // minWidth={
shouldPinGallery // shouldPinGallery
? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth // ? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth
: 200 // : 200
} // }
maxWidth={ // maxWidth={
shouldPinGallery // shouldPinGallery
? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth // ? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth
: undefined // : undefined
} // }
minHeight={calcGalleryMinHeight()} // minHeight={calcGalleryMinHeight()}
> // >
<ImageGalleryContent /> // <ImageGalleryContent />
</ResizableDrawer> // </ResizableDrawer>
); // );
}; // };
const renderImageGallery = () => { // const renderImageGallery = () => {
if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent(); // if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent();
return resizableImageGalleryContent(); // return resizableImageGalleryContent();
}; // };
return renderImageGallery(); if (shouldPinGallery) {
return null;
}
return (
<ResizableDrawer
direction="right"
isResizable={true}
isOpen={shouldShowGallery}
onClose={handleCloseGallery}
minWidth={200}
>
<ImageGalleryContent />
</ResizableDrawer>
);
// return renderImageGallery();
}; };
export default memo(ImageGalleryPanel); export default memo(ImageGalleryPanel);

View File

@ -10,7 +10,6 @@ import ConditioningInputFieldComponent from './fields/ConditioningInputFieldComp
import ModelInputFieldComponent from './fields/ModelInputFieldComponent'; import ModelInputFieldComponent from './fields/ModelInputFieldComponent';
import NumberInputFieldComponent from './fields/NumberInputFieldComponent'; import NumberInputFieldComponent from './fields/NumberInputFieldComponent';
import StringInputFieldComponent from './fields/StringInputFieldComponent'; import StringInputFieldComponent from './fields/StringInputFieldComponent';
import ItemInputFieldComponent from './fields/ItemInputFieldComponent';
import ColorInputFieldComponent from './fields/ColorInputFieldComponent'; import ColorInputFieldComponent from './fields/ColorInputFieldComponent';
import ItemInputFieldComponent from './fields/ItemInputFieldComponent'; import ItemInputFieldComponent from './fields/ItemInputFieldComponent';

View File

@ -20,7 +20,7 @@ const AnimatedImageToImagePanel = () => {
exit={{ opacity: 0, scale: 0, width: 0 }} exit={{ opacity: 0, scale: 0, width: 0 }}
transition={{ type: 'spring', bounce: 0, duration: 0.35 }} transition={{ type: 'spring', bounce: 0, duration: 0.35 }}
> >
<Box sx={{ h: 'full', w: 'full', pl: 4 }}> <Box sx={{ h: 'full', w: 'full' }}>
<ImageToImageSettings /> <ImageToImageSettings />
</Box> </Box>
</motion.div> </motion.div>

View File

@ -11,7 +11,7 @@ import {
CancelStrategy, CancelStrategy,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useCallback, memo } from 'react'; import { useCallback, memo, useMemo } from 'react';
import { import {
ButtonSpinner, ButtonSpinner,
ButtonGroup, ButtonGroup,
@ -102,39 +102,39 @@ const CancelButton = (
[isConnected, isProcessing, isCancelable] [isConnected, isProcessing, isCancelable]
); );
const cancelLabel = useMemo(() => {
if (isCancelScheduled) {
return t('parameters.cancel.isScheduled');
}
if (cancelType === 'immediate') {
return t('parameters.cancel.immediate');
}
return t('parameters.cancel.schedule');
}, [t, cancelType, isCancelScheduled]);
const cancelIcon = useMemo(() => {
if (isCancelScheduled) {
return <ButtonSpinner />;
}
if (cancelType === 'immediate') {
return <MdCancel />;
}
return <MdCancelScheduleSend />;
}, [cancelType, isCancelScheduled]);
return ( return (
<ButtonGroup isAttached width={btnGroupWidth}> <ButtonGroup isAttached width={btnGroupWidth}>
{cancelType === 'immediate' ? ( <IAIIconButton
<IAIIconButton icon={cancelIcon}
icon={<MdCancel />} tooltip={cancelLabel}
tooltip={t('parameters.cancel.immediate')} aria-label={cancelLabel}
aria-label={t('parameters.cancel.immediate')} isDisabled={!isConnected || !isProcessing || !isCancelable}
isDisabled={!isConnected || !isProcessing || !isCancelable} onClick={handleClickCancel}
onClick={handleClickCancel} colorScheme="error"
colorScheme="error" {...rest}
{...rest} />
/>
) : (
<IAIIconButton
icon={
isCancelScheduled ? <ButtonSpinner /> : <MdCancelScheduleSend />
}
tooltip={
isCancelScheduled
? t('parameters.cancel.isScheduled')
: t('parameters.cancel.schedule')
}
aria-label={
isCancelScheduled
? t('parameters.cancel.isScheduled')
: t('parameters.cancel.schedule')
}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
colorScheme="error"
{...rest}
/>
)}
<Menu closeOnSelect={false}> <Menu closeOnSelect={false}>
<MenuButton <MenuButton

View File

@ -0,0 +1,78 @@
import { isEqual } from 'lodash-es';
import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer';
import GenerateParameters from './tabs/Create/GenerateParameters';
import { createSelector } from '@reduxjs/toolkit';
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
setShouldShowParametersPanel,
toggleParametersPanel,
} from '../store/uiSlice';
import { memo } from 'react';
import { Flex } from '@chakra-ui/react';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import PinParametersPanelButton from './PinParametersPanelButton';
const selector = createSelector(
[uiSelector, activeTabNameSelector, lightboxSelector],
(ui, activeTabName, lightbox) => {
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
const { isLightboxOpen } = lightbox;
return {
shouldPinParametersPanel,
shouldShowParametersPanel,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const CreateParametersPanel = () => {
const dispatch = useAppDispatch();
const { shouldPinParametersPanel, shouldShowParametersPanel } =
useAppSelector(selector);
const handleClosePanel = () => {
dispatch(setShouldShowParametersPanel(false));
};
if (shouldPinParametersPanel) {
return null;
}
return (
<ResizableDrawer
direction="left"
isResizable={true}
isOpen={shouldShowParametersPanel}
onClose={handleClosePanel}
minWidth={500}
>
<Flex
flexDir="column"
position="relative"
h={{ base: 600, xl: 'full' }}
w={{ sm: 'full', lg: '100vw', xl: 'full' }}
paddingRight={{ base: 8, xl: 0 }}
>
<Flex
paddingTop={1.5}
paddingBottom={4}
justifyContent="space-between"
alignItems="center"
>
<InvokeAILogoComponent />
<PinParametersPanelButton />
</Flex>
<GenerateParameters />
</Flex>
</ResizableDrawer>
);
};
export default memo(CreateParametersPanel);

View File

@ -16,7 +16,7 @@ const floatingGalleryButtonSelector = createSelector(
return { return {
shouldPinGallery, shouldPinGallery,
shouldShowGalleryButton: !shouldPinGallery || !shouldShowGallery, shouldShowGalleryButton: !shouldShowGallery,
}; };
}, },
{ memoizeOptions: { resultEqualityCheck: isEqual } } { memoizeOptions: { resultEqualityCheck: isEqual } }

View File

@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector(
const shouldShowParametersPanelButton = const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck && !canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel) && !shouldShowParametersPanel &&
['generate', 'unifiedCanvas'].includes(activeTabName); ['generate', 'unifiedCanvas'].includes(activeTabName);
return { return {
@ -65,7 +65,11 @@ const FloatingParametersPanelButtons = () => {
shouldPinParametersPanel && dispatch(requestCanvasRescale()); shouldPinParametersPanel && dispatch(requestCanvasRescale());
}; };
return shouldShowParametersPanelButton ? ( if (!shouldShowParametersPanelButton) {
return null;
}
return (
<Flex <Flex
pos="absolute" pos="absolute"
transform="translate(0, -50%)" transform="translate(0, -50%)"
@ -92,7 +96,7 @@ const FloatingParametersPanelButtons = () => {
</> </>
)} )}
</Flex> </Flex>
) : null; );
}; };
export default memo(FloatingParametersPanelButtons); export default memo(FloatingParametersPanelButtons);

View File

@ -1,5 +1,7 @@
import { import {
Box,
ChakraProps, ChakraProps,
Flex,
Icon, Icon,
Tab, Tab,
TabList, TabList,
@ -14,7 +16,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice'; import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
import { memo, ReactNode, useMemo } from 'react'; import {
memo,
ReactNode,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { MdDeviceHub, MdGridOn } from 'react-icons/md'; import { MdDeviceHub, MdGridOn } from 'react-icons/md';
import { activeTabIndexSelector } from '../store/uiSelectors'; import { activeTabIndexSelector } from '../store/uiSelectors';
@ -23,33 +32,47 @@ import { useTranslation } from 'react-i18next';
import { ResourceKey } from 'i18next'; import { ResourceKey } from 'i18next';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import NodeEditor from 'features/nodes/components/NodeEditor'; import NodeEditor from 'features/nodes/components/NodeEditor';
import GenerateWorkspace from './tabs/Generate/GenerateWorkspace'; import GenerateWorkspace from './tabs/Create/GenerateWorkspace';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { BsLightningChargeFill } from 'react-icons/bs'; import { BsLightningChargeFill } from 'react-icons/bs';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel';
import Scrollable from './common/Scrollable';
import GenerateParameters from './tabs/Create/GenerateParameters';
import PinParametersPanelButton from './PinParametersPanelButton';
import ParametersSlide from './common/ParametersSlide';
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import CreateTabContent from './tabs/Create/GenerateContent';
import ParametersPanel from './ParametersPanel';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import CreateTab from './tabs/Create/CreateTab';
import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab';
import NodesTab from './tabs/Nodes/NodesTab';
export interface InvokeTabInfo { export interface InvokeTabInfo {
id: InvokeTabName; id: InvokeTabName;
icon: ReactNode; icon: ReactNode;
workarea: ReactNode; content: ReactNode;
} }
const tabs: InvokeTabInfo[] = [ const tabs: InvokeTabInfo[] = [
{ {
id: 'generate', id: 'generate',
icon: <Icon as={BsLightningChargeFill} sx={{ boxSize: 5 }} />, icon: <Icon as={BsLightningChargeFill} sx={{ boxSize: 5 }} />,
workarea: <GenerateWorkspace />, content: <CreateTab />,
}, },
{ {
id: 'unifiedCanvas', id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={{ boxSize: 6 }} />, icon: <Icon as={MdGridOn} sx={{ boxSize: 6 }} />,
workarea: <UnifiedCanvasWorkarea />, content: <UnifiedCanvasTab />,
}, },
{ {
id: 'nodes', id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={{ boxSize: 6 }} />, icon: <Icon as={MdDeviceHub} sx={{ boxSize: 6 }} />,
workarea: <NodeEditor />, content: <NodesTab />,
}, },
]; ];
@ -72,9 +95,12 @@ const InvokeTabs = () => {
(state: RootState) => state.lightbox.isLightboxOpen (state: RootState) => state.lightbox.isLightboxOpen
); );
const { shouldPinGallery, shouldPinParametersPanel } = useAppSelector( const {
(state: RootState) => state.ui shouldPinGallery,
); shouldPinParametersPanel,
shouldShowGallery,
shouldShowParametersPanel,
} = useAppSelector((state: RootState) => state.ui);
const { t } = useTranslation(); const { t } = useTranslation();
@ -133,9 +159,7 @@ const InvokeTabs = () => {
const tabPanels = useMemo( const tabPanels = useMemo(
() => () =>
enabledTabs.map((tab) => ( enabledTabs.map((tab) => <TabPanel key={tab.id}>{tab.content}</TabPanel>),
<TabPanel key={tab.id}>{tab.workarea}</TabPanel>
)),
[enabledTabs] [enabledTabs]
); );
@ -165,3 +189,17 @@ const InvokeTabs = () => {
}; };
export default memo(InvokeTabs); export default memo(InvokeTabs);
// <PanelGroup autoSaveId="example" direction="horizontal">
// <Panel defaultSize={25}>
// <SourcesExplorer />
// </Panel>
// <PanelResizeHandle />
// <Panel>
// <SourceViewer />
// </Panel>
// <PanelResizeHandle />
// <Panel defaultSize={25}>
// <Console />
// </Panel>
// </PanelGroup>;

View File

@ -0,0 +1,24 @@
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { PropsWithChildren, memo } from 'react';
const OverlayScrollable = (props: PropsWithChildren) => {
return (
<OverlayScrollbarsComponent
defer
style={{ height: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
overflow: { x: 'hidden' },
}}
>
{props.children}
</OverlayScrollbarsComponent>
);
};
export default memo(OverlayScrollable);

View File

@ -26,7 +26,6 @@ import {
type ResizableDrawerProps = ResizableProps & { type ResizableDrawerProps = ResizableProps & {
children: ReactNode; children: ReactNode;
isResizable: boolean; isResizable: boolean;
isPinned: boolean;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
direction?: SlideDirection; direction?: SlideDirection;
@ -51,7 +50,6 @@ const ChakraResizeable = chakra(Resizable, {
const ResizableDrawer = ({ const ResizableDrawer = ({
direction = 'left', direction = 'left',
isResizable, isResizable,
isPinned,
isOpen, isOpen,
onClose, onClose,
children, children,
@ -95,7 +93,7 @@ const ResizableDrawer = ({
handler: () => { handler: () => {
onClose(); onClose();
}, },
enabled: isOpen && !isPinned, enabled: isOpen,
}); });
const handleEnables = useMemo( const handleEnables = useMemo(
@ -107,30 +105,33 @@ const ResizableDrawer = ({
() => () =>
getMinMaxDimensions({ getMinMaxDimensions({
direction, direction,
minWidth: isResizable // minWidth: isResizable
? parseAndPadSize(minWidth, 18) // ? parseAndPadSize(minWidth, 18)
: parseAndPadSize(minWidth), // : parseAndPadSize(minWidth),
maxWidth: isResizable // maxWidth: isResizable
? parseAndPadSize(maxWidth, 18) // ? parseAndPadSize(maxWidth, 18)
: parseAndPadSize(maxWidth), // : parseAndPadSize(maxWidth),
minHeight: isResizable // minHeight: isResizable
? parseAndPadSize(minHeight, 18) // ? parseAndPadSize(minHeight, 18)
: parseAndPadSize(minHeight), // : parseAndPadSize(minHeight),
maxHeight: isResizable // maxHeight: isResizable
? parseAndPadSize(maxHeight, 18) // ? parseAndPadSize(maxHeight, 18)
: parseAndPadSize(maxHeight), // : parseAndPadSize(maxHeight),
minWidth,
maxWidth,
minHeight,
maxHeight,
}), }),
[minWidth, maxWidth, minHeight, maxHeight, direction, isResizable] [minWidth, maxWidth, minHeight, maxHeight, direction]
); );
const { containerStyles, handleStyles } = useMemo( const { containerStyles, handleStyles } = useMemo(
() => () =>
getStyles({ getStyles({
isPinned,
isResizable, isResizable,
direction, direction,
}), }),
[isPinned, isResizable, direction] [isResizable, direction]
); );
const slideDirection = useMemo( const slideDirection = useMemo(
@ -140,34 +141,37 @@ const ResizableDrawer = ({
useEffect(() => { useEffect(() => {
if (['left', 'right'].includes(direction)) { if (['left', 'right'].includes(direction)) {
setHeight(isPinned ? '100%' : '100vh'); setHeight('100vh');
// setHeight(isPinned ? '100%' : '100vh');
} }
if (['top', 'bottom'].includes(direction)) { if (['top', 'bottom'].includes(direction)) {
setWidth(isPinned ? '100%' : '100vw'); setWidth('100vw');
// setWidth(isPinned ? '100%' : '100vw');
} }
}, [isPinned, direction]); }, [direction]);
return ( return (
<Slide <Slide
direction={slideDirection} direction={slideDirection}
in={isOpen} in={isOpen}
unmountOnExit={isPinned} // unmountOnExit={isPinned}
motionProps={{ initial: isPinned }} motionProps={{ initial: false }}
{...(isPinned style={{ zIndex: 99, width: 'full' }}
? { // {...(isPinned
style: { // ? {
position: undefined, // style: {
left: undefined, // position: undefined,
right: undefined, // left: undefined,
top: undefined, // right: undefined,
bottom: undefined, // top: undefined,
width: undefined, // bottom: undefined,
}, // width: undefined,
} // },
: { // }
// transition: { enter: { duration: 0.15 }, exit: { duration: 0.15 } }, // : {
style: { zIndex: 99, width: 'full' }, // // transition: { enter: { duration: 0.15 }, exit: { duration: 0.15 } },
})} // style: { zIndex: 99, width: 'full' },
// })}
> >
<Box <Box
ref={outsideClickRef} ref={outsideClickRef}
@ -186,10 +190,10 @@ const ResizableDrawer = ({
{...minMaxDimensions} {...minMaxDimensions}
sx={{ sx={{
borderColor: 'base.800', borderColor: 'base.800',
p: isPinned ? 0 : 4, p: 4,
bg: 'base.900', bg: 'base.900',
height: 'full', height: 'full',
boxShadow: !isPinned ? '0 0 4rem 0 rgba(0, 0, 0, 0.8)' : '', boxShadow: '0 0 4rem 0 rgba(0, 0, 0, 0.8)',
...containerStyles, ...containerStyles,
...sx, ...sx,
}} }}

View File

@ -155,7 +155,6 @@ export const getAnimations = ({
}; };
export type GetResizableStylesProps = { export type GetResizableStylesProps = {
isPinned: boolean;
isResizable: boolean; isResizable: boolean;
direction: SlideDirection; direction: SlideDirection;
}; };
@ -168,10 +167,10 @@ const HANDLE_PADDING = '1rem';
const HANDLE_WIDTH_PINNED = '2px'; const HANDLE_WIDTH_PINNED = '2px';
const HANDLE_WIDTH_UNPINNED = '5px'; const HANDLE_WIDTH_UNPINNED = '5px';
const HANDLE_WIDTH = '5px';
// Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS // Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS
export const getStyles = ({ export const getStyles = ({
isPinned,
isResizable, isResizable,
direction, direction,
}: GetResizableStylesProps): { }: GetResizableStylesProps): {
@ -182,15 +181,13 @@ export const getStyles = ({
return { containerStyles: {}, handleStyles: {} }; return { containerStyles: {}, handleStyles: {} };
} }
const handleWidth = isPinned ? HANDLE_WIDTH_PINNED : HANDLE_WIDTH_UNPINNED;
// Calculate the positioning offset of the handle hitbox so it is centered over the handle // Calculate the positioning offset of the handle hitbox so it is centered over the handle
const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${handleWidth}) / -2)`; const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${HANDLE_WIDTH}) / -2)`;
if (direction === 'top') { if (direction === 'top') {
return { return {
containerStyles: { containerStyles: {
borderBottomWidth: handleWidth, borderBottomWidth: HANDLE_WIDTH,
paddingBottom: HANDLE_PADDING, paddingBottom: HANDLE_PADDING,
}, },
handleStyles: { handleStyles: {
@ -206,7 +203,7 @@ export const getStyles = ({
if (direction === 'left') { if (direction === 'left') {
return { return {
containerStyles: { containerStyles: {
borderInlineEndWidth: handleWidth, borderInlineEndWidth: HANDLE_WIDTH,
paddingInlineEnd: HANDLE_PADDING, paddingInlineEnd: HANDLE_PADDING,
}, },
handleStyles: { handleStyles: {
@ -222,7 +219,7 @@ export const getStyles = ({
if (direction === 'bottom') { if (direction === 'bottom') {
return { return {
containerStyles: { containerStyles: {
borderTopWidth: handleWidth, borderTopWidth: HANDLE_WIDTH,
paddingTop: HANDLE_PADDING, paddingTop: HANDLE_PADDING,
}, },
handleStyles: { handleStyles: {
@ -238,7 +235,7 @@ export const getStyles = ({
if (direction === 'right') { if (direction === 'right') {
return { return {
containerStyles: { containerStyles: {
borderInlineStartWidth: handleWidth, borderInlineStartWidth: HANDLE_WIDTH,
paddingInlineStart: HANDLE_PADDING, paddingInlineStart: HANDLE_PADDING,
}, },
handleStyles: { handleStyles: {

View File

@ -0,0 +1,96 @@
import { Portal, TabPanel } from '@chakra-ui/react';
import { memo } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import GenerateParameters from './GenerateParameters';
import PinParametersPanelButton from '../../PinParametersPanelButton';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import { createSelector } from '@reduxjs/toolkit';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import CreateTabContent from './GenerateContent';
import ResizeHandle from '../ResizeHandle';
import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel';
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
const selector = createSelector(uiSelector, (ui) => {
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
} = ui;
return {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
};
});
const CreateTab = () => {
const dispatch = useAppDispatch();
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
} = useAppSelector(selector);
return (
<PanelGroup
direction="horizontal"
style={{ height: '100%', width: '100%' }}
>
{shouldPinParametersPanel && shouldShowParametersPanel && (
<>
<Panel
order={0}
defaultSize={30}
minSize={20}
style={{ position: 'relative' }}
>
<GenerateParameters />
<PinParametersPanelButton
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
/>
</Panel>
<ResizeHandle />
</>
)}
{shouldPinParametersPanel && shouldShowParametersPanel && (
<>
<Panel
order={0}
defaultSize={30}
minSize={20}
style={{ position: 'relative' }}
>
<ImageToImageSettings />
</Panel>
<ResizeHandle />
</>
)}
<Panel
order={1}
minSize={30}
onResize={() => {
dispatch(requestCanvasRescale());
}}
>
<CreateTabContent />
</Panel>
{shouldPinGallery && shouldShowGallery && (
<>
<ResizeHandle />
<Panel order={2} defaultSize={10} minSize={10}>
<ImageGalleryContent />
</Panel>
</>
)}
</PanelGroup>
);
};
export default memo(CreateTab);

View File

@ -2,7 +2,7 @@ import { Box, Flex } from '@chakra-ui/react';
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview'; import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
const GenerateContent = () => { const CreateTabContent = () => {
return ( return (
<Box <Box
sx={{ sx={{
@ -20,4 +20,4 @@ const GenerateContent = () => {
); );
}; };
export default GenerateContent; export default CreateTabContent;

View File

@ -30,9 +30,15 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces
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 { findIndex } from 'lodash-es'; import { findIndex } from 'lodash-es';
import { memo, useMemo, useState } from 'react'; import {
OverlayScrollbarsComponent,
useOverlayScrollbars,
} from 'overlayscrollbars-react';
import { memo, useMemo, useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
import OverlayScrollable from '../../common/OverlayScrollable';
import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel';
const GenerateParameters = () => { const GenerateParameters = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -83,25 +89,35 @@ const GenerateParameters = () => {
); );
return ( return (
<Flex flexDir="column" gap={2}> <OverlayScrollable>
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<Flex <Flex
sx={{ sx={{
flexDirection: 'column',
gap: 2, gap: 2,
bg: 'base.800', flexDirection: 'column',
p: 4, h: 'full',
pb: 6, w: 'full',
borderRadius: 'base', position: 'absolute',
}} }}
> >
<MainSettings /> <PromptInput />
<NegativePromptInput />
<ProcessButtons />
<Flex
sx={{
flexDirection: 'column',
gap: 2,
bg: 'base.800',
p: 4,
pb: 6,
borderRadius: 'base',
}}
>
<MainSettings />
</Flex>
<ImageToImageToggle />
<ParametersAccordion accordionItems={generateAccordionItems} />
</Flex> </Flex>
<ImageToImageToggle /> </OverlayScrollable>
<ParametersAccordion accordionItems={generateAccordionItems} />
</Flex>
); );
}; };

View File

@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react'; import { memo } from 'react';
import GenerateContent from './GenerateContent'; import CreateTabContent from './GenerateContent';
import GenerateParameters from './GenerateParameters'; import GenerateParameters from './GenerateParameters';
import PinParametersPanelButton from '../../PinParametersPanelButton'; import PinParametersPanelButton from '../../PinParametersPanelButton';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
@ -14,6 +14,8 @@ const GenerateWorkspace = () => {
(state: RootState) => state.ui.shouldPinParametersPanel (state: RootState) => state.ui.shouldPinParametersPanel
); );
return <CreateTabContent />;
return ( return (
<Flex <Flex
flexDirection={{ base: 'column-reverse', xl: 'row' }} flexDirection={{ base: 'column-reverse', xl: 'row' }}
@ -45,7 +47,7 @@ const GenerateWorkspace = () => {
<GenerateParameters /> <GenerateParameters />
</ParametersSlide> </ParametersSlide>
)} )}
<GenerateContent /> <CreateTabContent />
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,64 @@
import { TabPanel } from '@chakra-ui/react';
import { memo } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import PinParametersPanelButton from '../../PinParametersPanelButton';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import { createSelector } from '@reduxjs/toolkit';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import ResizeHandle from '../ResizeHandle';
import NodeEditor from 'features/nodes/components/NodeEditor';
const selector = createSelector(uiSelector, (ui) => {
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
} = ui;
return {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
};
});
const NodesTab = () => {
const dispatch = useAppDispatch();
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
} = useAppSelector(selector);
return (
<PanelGroup
direction="horizontal"
style={{ height: '100%', width: '100%' }}
>
<Panel
order={1}
minSize={30}
onResize={() => {
dispatch(requestCanvasRescale());
}}
>
<NodeEditor />
</Panel>
{shouldPinGallery && shouldShowGallery && (
<>
<ResizeHandle />
<Panel order={2} defaultSize={10} minSize={10} collapsible={true}>
<ImageGalleryContent />
</Panel>
</>
)}
</PanelGroup>
);
};
export default memo(NodesTab);

View File

@ -0,0 +1,17 @@
import { Box, Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { PanelResizeHandle } from 'react-resizable-panels';
const ResizeHandle = () => {
return (
<PanelResizeHandle>
<Flex
sx={{ w: 6, h: 'full', justifyContent: 'center', alignItems: 'center' }}
>
<Box sx={{ w: 0.5, h: 'calc(100% - 1rem)', py: 4, bg: 'base.800' }} />
</Flex>
</PanelResizeHandle>
);
};
export default memo(ResizeHandle);

View File

@ -17,6 +17,7 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces
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 { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import OverlayScrollable from '../../common/OverlayScrollable';
export default function UnifiedCanvasParameters() { export default function UnifiedCanvasParameters() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -74,11 +75,21 @@ export default function UnifiedCanvasParameters() {
}; };
return ( return (
<Flex flexDir="column" gap={2} position="relative"> <OverlayScrollable>
<PromptInput /> <Flex
<NegativePromptInput /> sx={{
<ProcessButtons /> gap: 2,
<ParametersAccordion accordionItems={unifiedCanvasAccordions} /> flexDirection: 'column',
</Flex> h: 'full',
w: 'full',
position: 'absolute',
}}
>
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<ParametersAccordion accordionItems={unifiedCanvasAccordions} />
</Flex>
</OverlayScrollable>
); );
} }

View File

@ -0,0 +1,89 @@
import { TabPanel } from '@chakra-ui/react';
import { memo } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import PinParametersPanelButton from '../../PinParametersPanelButton';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import { createSelector } from '@reduxjs/toolkit';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import UnifiedCanvasContent from './UnifiedCanvasContent';
import ResizeHandle from '../ResizeHandle';
import UnifiedCanvasParameters from './UnifiedCanvasParameters';
import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta';
const selector = createSelector(uiSelector, (ui) => {
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
shouldUseCanvasBetaLayout,
} = ui;
return {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
shouldUseCanvasBetaLayout,
};
});
const UnifiedCanvasTab = () => {
const dispatch = useAppDispatch();
const {
shouldPinGallery,
shouldShowGallery,
shouldPinParametersPanel,
shouldShowParametersPanel,
shouldUseCanvasBetaLayout,
} = useAppSelector(selector);
return (
<PanelGroup
direction="horizontal"
style={{ height: '100%', width: '100%' }}
>
{shouldPinParametersPanel && shouldShowParametersPanel && (
<>
<Panel
order={0}
defaultSize={30}
minSize={20}
style={{ position: 'relative' }}
>
<UnifiedCanvasParameters />
<PinParametersPanelButton
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
/>
</Panel>
<ResizeHandle />
</>
)}
<Panel
order={1}
minSize={30}
onResize={() => {
dispatch(requestCanvasRescale());
}}
>
{shouldUseCanvasBetaLayout ? (
<UnifiedCanvasContentBeta />
) : (
<UnifiedCanvasContent />
)}
</Panel>
{shouldPinGallery && shouldShowGallery && (
<>
<ResizeHandle />
<Panel order={2} defaultSize={10} minSize={10}>
<ImageGalleryContent />
</Panel>
</>
)}
</PanelGroup>
);
};
export default memo(UnifiedCanvasTab);

View File

@ -18,6 +18,12 @@ const CanvasWorkspace = () => {
(state: RootState) => state.ui.shouldUseCanvasBetaLayout (state: RootState) => state.ui.shouldUseCanvasBetaLayout
); );
return shouldUseCanvasBetaLayout ? (
<UnifiedCanvasContentBeta />
) : (
<UnifiedCanvasContent />
);
return ( return (
<Flex <Flex
flexDirection={{ base: 'column-reverse', xl: 'row' }} flexDirection={{ base: 'column-reverse', xl: 'row' }}
@ -25,7 +31,7 @@ const CanvasWorkspace = () => {
h="full" h="full"
gap={4} gap={4}
> >
{shouldPinParametersPanel ? ( {/* {shouldPinParametersPanel ? (
<Box width="28rem" flexShrink={0} position="relative"> <Box width="28rem" flexShrink={0} position="relative">
<Scrollable> <Scrollable>
<UnifiedCanvasParameters /> <UnifiedCanvasParameters />
@ -38,7 +44,7 @@ const CanvasWorkspace = () => {
<ParametersSlide> <ParametersSlide>
<UnifiedCanvasParameters /> <UnifiedCanvasParameters />
</ParametersSlide> </ParametersSlide>
)} )} */}
{shouldUseCanvasBetaLayout ? ( {shouldUseCanvasBetaLayout ? (
<UnifiedCanvasContentBeta /> <UnifiedCanvasContentBeta />
) : ( ) : (

View File

@ -78,9 +78,15 @@ export const uiSlice = createSlice({
}, },
togglePinGalleryPanel: (state) => { togglePinGalleryPanel: (state) => {
state.shouldPinGallery = !state.shouldPinGallery; state.shouldPinGallery = !state.shouldPinGallery;
if (!state.shouldPinGallery) {
state.shouldShowGallery = true;
}
}, },
togglePinParametersPanel: (state) => { togglePinParametersPanel: (state) => {
state.shouldPinParametersPanel = !state.shouldPinParametersPanel; state.shouldPinParametersPanel = !state.shouldPinParametersPanel;
if (!state.shouldPinParametersPanel) {
state.shouldShowParametersPanel = true;
}
}, },
toggleParametersPanel: (state) => { toggleParametersPanel: (state) => {
state.shouldShowParametersPanel = !state.shouldShowParametersPanel; state.shouldShowParametersPanel = !state.shouldShowParametersPanel;

View File

@ -5579,6 +5579,11 @@ react-remove-scroll@^2.5.5:
use-callback-ref "^1.3.0" use-callback-ref "^1.3.0"
use-sidecar "^1.1.2" use-sidecar "^1.1.2"
react-resizable-panels@^0.0.42:
version "0.0.42"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-0.0.42.tgz#e1a5d7fde7be4d18f32d0e021a0b4edb28b9edfe"
integrity sha512-nOaN9DeUTsmKjN3MFGaLd35kngGyO29SHRLdBRafYR1SV2F/LbWbpVUKVPwL2fBBTnQe2/rqOQwT4k+3cKeK+w==
react-rnd@^10.4.1: react-rnd@^10.4.1:
version "10.4.1" version "10.4.1"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.4.1.tgz#9e1c3f244895d7862ef03be98b2a620848c3fba1" resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.4.1.tgz#9e1c3f244895d7862ef03be98b2a620848c3fba1"