This commit is contained in:
psychedelicious 2023-12-30 21:15:25 +11:00 committed by Kent Keirsey
parent 89b7082bc0
commit ee2529f3fd
10 changed files with 236 additions and 149 deletions

View File

@ -63,7 +63,7 @@ const BoardsList = (props: Props) => {
<Grid <Grid
className="list-container" className="list-container"
data-testid="boards-list" data-testid="boards-list"
gridTemplateColumns="repeat(auto-fill, minmax(108px, 1fr))" gridTemplateColumns="repeat(auto-fill, minmax(90px, 1fr))"
maxH={346} maxH={346}
> >
<GridItem p={1.5} data-testid="no-board"> <GridItem p={1.5} data-testid="no-board">

View File

@ -12,7 +12,7 @@ export const initialGalleryState: GalleryState = {
shouldAutoSwitch: true, shouldAutoSwitch: true,
autoAssignBoardOnClick: true, autoAssignBoardOnClick: true,
autoAddBoardId: 'none', autoAddBoardId: 'none',
galleryImageMinimumWidth: 96, galleryImageMinimumWidth: 90,
selectedBoardId: 'none', selectedBoardId: 'none',
galleryView: 'images', galleryView: 'images',
boardSearchText: '', boardSearchText: '',

View File

@ -39,7 +39,7 @@ const NodeEditorPanelGroup = () => {
<WorkflowPanel /> <WorkflowPanel />
</Panel> </Panel>
<ResizeHandle <ResizeHandle
direction="vertical" orientation="horizontal"
onDoubleClick={handleDoubleClickHandle} onDoubleClick={handleDoubleClickHandle}
/> />
<Panel id="inspector" collapsible minSize={25}> <Panel id="inspector" collapsible minSize={25}>

View File

@ -29,7 +29,7 @@ const QueueControls = () => {
{isPauseEnabled && <PauseProcessorButton asIconButton />} */} {isPauseEnabled && <PauseProcessorButton asIconButton />} */}
<ClearQueueButton asIconButton /> <ClearQueueButton asIconButton />
</InvButtonGroup> </InvButtonGroup>
<Flex h={1} w="full"> <Flex h={2} w="full">
<ProgressBar /> <ProgressBar />
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,4 +1,4 @@
import { Flex, Spacer } from '@chakra-ui/react'; import { Spacer } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -13,6 +13,7 @@ import {
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { InvokeTabName } from 'features/ui/store/tabMap';
@ -22,15 +23,15 @@ import {
} from 'features/ui/store/uiSelectors'; } from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react'; import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCube, FaFont, FaImage } from 'react-icons/fa'; import { FaCube, FaFont, FaImage } from 'react-icons/fa';
import { FaCircleNodes, FaList } from 'react-icons/fa6'; import { FaCircleNodes, FaList } from 'react-icons/fa6';
import { MdGridOn } from 'react-icons/md'; import { MdGridOn } from 'react-icons/md';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
import FloatingGalleryButton from './FloatingGalleryButton';
import ParametersPanel from './ParametersPanel'; import ParametersPanel from './ParametersPanel';
import ImageTab from './tabs/ImageToImageTab'; import ImageTab from './tabs/ImageToImageTab';
import ModelManagerTab from './tabs/ModelManagerTab'; import ModelManagerTab from './tabs/ModelManagerTab';
@ -98,8 +99,24 @@ const enabledTabsSelector = createMemoizedSelector(
export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue'];
export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue'];
const panelStyles: CSSProperties = { height: '100%', width: '100%' }; const panelStyles: CSSProperties = { height: '100%', width: '100%' };
const GALLERY_MIN_SIZE_PX = 310;
const GALLERY_MIN_SIZE_PCT = 20; const GALLERY_MIN_SIZE_PCT = 20;
const OPTIONS_PANEL_WIDTH = '434px'; const OPTIONS_PANEL_MIN_SIZE_PX = 430;
const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
const optionsPanelUsePanelOptions: UsePanelOptions = {
unit: 'pixels',
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
fallbackMinSizePct: OPTIONS_PANEL_MIN_SIZE_PCT,
panelGroupID: 'app',
};
const galleryPanelUsePanelOptions: UsePanelOptions = {
unit: 'pixels',
minSize: GALLERY_MIN_SIZE_PX,
fallbackMinSizePct: GALLERY_MIN_SIZE_PCT,
panelGroupID: 'app',
};
const InvokeTabs = () => { const InvokeTabs = () => {
const activeTabIndex = useAppSelector(activeTabIndexSelector); const activeTabIndex = useAppSelector(activeTabIndexSelector);
@ -107,7 +124,7 @@ const InvokeTabs = () => {
const enabledTabs = useAppSelector(enabledTabsSelector); const enabledTabs = useAppSelector(enabledTabsSelector);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const panelGroupHandleRef = useRef<ImperativePanelGroupHandle>(null);
const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => { const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => {
if (e.target instanceof HTMLElement) { if (e.target instanceof HTMLElement) {
e.target.blur(); e.target.blur();
@ -153,6 +170,20 @@ const InvokeTabs = () => {
[dispatch, enabledTabs] [dispatch, enabledTabs]
); );
const panelStorage = usePanelStorage();
const {
ref: optionsPanelRef,
minSize: optionsPanelMinSize,
isCollapsed: isOptionsPanelCollapsed,
onCollapse: onCollapseOptionsPanel,
onExpand: onExpandOptionsPanel,
reset: resetOptionsPanel,
expand: expandOptionsPanel,
collapse: collapseOptionsPanel,
toggle: toggleOptionsPanel,
} = usePanel(optionsPanelUsePanelOptions);
const { const {
ref: galleryPanelRef, ref: galleryPanelRef,
minSize: galleryPanelMinSize, minSize: galleryPanelMinSize,
@ -161,12 +192,25 @@ const InvokeTabs = () => {
onExpand: onExpandGalleryPanel, onExpand: onExpandGalleryPanel,
reset: resetGalleryPanel, reset: resetGalleryPanel,
expand: expandGalleryPanel, expand: expandGalleryPanel,
collapse: collapseGalleryPanel,
toggle: toggleGalleryPanel, toggle: toggleGalleryPanel,
} = usePanel(GALLERY_MIN_SIZE_PCT); } = usePanel(galleryPanelUsePanelOptions);
useHotkeys('g', toggleGalleryPanel, []); useHotkeys('g', toggleGalleryPanel, []);
useHotkeys('t', toggleOptionsPanel, []);
const panelStorage = usePanelStorage(); useHotkeys(
'f',
() => {
if (isOptionsPanelCollapsed || isGalleryPanelCollapsed) {
expandOptionsPanel();
expandGalleryPanel();
} else {
collapseOptionsPanel();
collapseGalleryPanel();
}
},
[isOptionsPanelCollapsed, isGalleryPanelCollapsed]
);
return ( return (
<InvTabs <InvTabs
@ -183,35 +227,52 @@ const InvokeTabs = () => {
<Spacer /> <Spacer />
</InvTabList> </InvTabList>
<PanelGroup <PanelGroup
ref={panelGroupHandleRef}
id="app" id="app"
autoSaveId="app" autoSaveId="app"
direction="horizontal" direction="horizontal"
style={panelStyles} style={panelStyles}
storage={panelStorage} storage={panelStorage}
> >
<Panel id="main" order={0} minSize={50}>
<Flex w="full" h="full" gap={4}>
{!NO_SIDE_PANEL_TABS.includes(activeTabName) && ( {!NO_SIDE_PANEL_TABS.includes(activeTabName) && (
<Flex h="full" w={OPTIONS_PANEL_WIDTH} flexShrink={0}> <>
<Panel
id="options"
ref={optionsPanelRef}
order={0}
defaultSize={optionsPanelMinSize}
minSize={optionsPanelMinSize}
onCollapse={onCollapseOptionsPanel}
onExpand={onExpandOptionsPanel}
collapsible
>
{activeTabName === 'nodes' ? ( {activeTabName === 'nodes' ? (
<NodeEditorPanelGroup /> <NodeEditorPanelGroup />
) : ( ) : (
<ParametersPanel /> <ParametersPanel />
)} )}
</Flex> </Panel>
<ResizeHandle
onDoubleClick={resetOptionsPanel}
orientation="vertical"
/>
</>
)} )}
<Panel id="main" order={1} minSize={20}>
<InvTabPanels w="full" h="full"> <InvTabPanels w="full" h="full">
{tabPanels} {tabPanels}
</InvTabPanels> </InvTabPanels>
</Flex>
</Panel> </Panel>
{!NO_GALLERY_TABS.includes(activeTabName) && ( {!NO_GALLERY_TABS.includes(activeTabName) && (
<> <>
<ResizeHandle onDoubleClick={resetGalleryPanel} /> <ResizeHandle
onDoubleClick={resetGalleryPanel}
orientation="vertical"
/>
<Panel <Panel
id="gallery" id="gallery"
ref={galleryPanelRef} ref={galleryPanelRef}
order={1} order={2}
defaultSize={galleryPanelMinSize} defaultSize={galleryPanelMinSize}
minSize={galleryPanelMinSize} minSize={galleryPanelMinSize}
onCollapse={onCollapseGalleryPanel} onCollapse={onCollapseGalleryPanel}
@ -220,10 +281,6 @@ const InvokeTabs = () => {
> >
<ImageGalleryContent /> <ImageGalleryContent />
</Panel> </Panel>
<FloatingGalleryButton
isGalleryCollapsed={isGalleryPanelCollapsed}
expandGallery={expandGalleryPanel}
/>
</> </>
)} )}
</PanelGroup> </PanelGroup>

View File

@ -46,7 +46,10 @@ const ImageToImageTab = () => {
> >
<InitialImageDisplay /> <InitialImageDisplay />
</Panel> </Panel>
<ResizeHandle onDoubleClick={handleDoubleClickHandle} /> <ResizeHandle
orientation="vertical"
onDoubleClick={handleDoubleClickHandle}
/>
<Panel <Panel
id="imageTab.content.selectedImage" id="imageTab.content.selectedImage"
order={1} order={1}

View File

@ -1,103 +1,84 @@
import type { FlexProps, SystemStyleObject } from '@chakra-ui/react'; import { Box, defineStyleConfig, Flex, useStyleConfig } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react'; import { memo } from 'react';
import type { CSSProperties } from 'react'; import type { PanelResizeHandleProps } from 'react-resizable-panels';
import { memo, useMemo } from 'react';
import { PanelResizeHandle } from 'react-resizable-panels'; import { PanelResizeHandle } from 'react-resizable-panels';
type ResizeHandleProps = Omit<FlexProps, 'direction'> & { type ResizeHandleProps = PanelResizeHandleProps & {
direction?: 'horizontal' | 'vertical'; orientation: 'horizontal' | 'vertical';
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right';
isCollapsed?: boolean;
}; };
const ResizeHandle = (props: ResizeHandleProps) => { const ResizeHandle = (props: ResizeHandleProps) => {
const { const { orientation, ...rest } = props;
direction = 'horizontal', const styles = useStyleConfig('ResizeHandle', { orientation });
collapsedDirection,
isCollapsed = false,
...rest
} = props;
const resizeHandleStyles = useMemo<CSSProperties>(() => {
if (direction === 'horizontal') {
return {
visibility: isCollapsed ? 'hidden' : 'visible',
width: isCollapsed ? 0 : 'auto',
};
}
return {
visibility: isCollapsed ? 'hidden' : 'visible',
width: isCollapsed ? 0 : 'auto',
};
}, [direction, isCollapsed]);
const resizeHandleWrapperStyles = useMemo<SystemStyleObject>(() => {
if (direction === 'horizontal') {
return {
w: collapsedDirection ? 2.5 : 4,
h: 'full',
justifyContent: collapsedDirection
? collapsedDirection === 'left'
? 'flex-start'
: 'flex-end'
: 'center',
alignItems: 'center',
div: {
bg: 'base.850',
},
_hover: {
div: { bg: 'base.700' },
},
};
}
return {
w: 'full',
h: collapsedDirection ? 2.5 : 4,
alignItems: collapsedDirection
? collapsedDirection === 'top'
? 'flex-start'
: 'flex-end'
: 'center',
justifyContent: 'center',
div: {
bg: 'base.850',
},
_hover: {
div: { bg: 'base.700' },
},
};
}, [collapsedDirection, direction]);
const resizeInnerStyles = useMemo<SystemStyleObject>(() => {
if (direction === 'horizontal') {
return {
w: 1,
h: 'calc(100% - 1rem)',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: 'normal',
};
}
return {
h: 1,
w: 'calc(100% - 1rem)',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: 'normal',
};
}, [direction]);
return ( return (
<PanelResizeHandle style={resizeHandleStyles}> <PanelResizeHandle {...rest}>
<Flex <Flex __css={styles} data-orientation={orientation}>
className="resize-handle-horizontal" <Box className="resize-handle-inner" data-orientation={orientation} />
sx={resizeHandleWrapperStyles} <Box
{...rest} className="resize-handle-drag-handle"
> data-orientation={orientation}
<Box sx={resizeInnerStyles} /> />
</Flex> </Flex>
</PanelResizeHandle> </PanelResizeHandle>
); );
}; };
export default memo(ResizeHandle); export default memo(ResizeHandle);
export const resizeHandleTheme = defineStyleConfig({
// The styles all Cards have in common
baseStyle: () => ({
display: 'flex',
pos: 'relative',
'&[data-orientation="horizontal"]': {
w: 'full',
h: 5,
},
'&[data-orientation="vertical"]': { w: 5, h: 'full' },
alignItems: 'center',
justifyContent: 'center',
div: {
bg: 'base.800',
},
_hover: {
div: { bg: 'base.700' },
},
_active: {
div: { bg: 'base.600' },
},
transitionProperty: 'common',
transitionDuration: 'normal',
'.resize-handle-inner': {
'&[data-orientation="horizontal"]': {
w: 'calc(100% - 1rem)',
h: '2px',
},
'&[data-orientation="vertical"]': {
w: '2px',
h: 'calc(100% - 1rem)',
},
borderRadius: 'base',
transitionProperty: 'inherit',
transitionDuration: 'inherit',
},
'.resize-handle-drag-handle': {
pos: 'absolute',
borderRadius: '2px',
transitionProperty: 'inherit',
transitionDuration: 'inherit',
'&[data-orientation="horizontal"]': {
w: '20px',
h: '6px',
insetInlineStart: '50%',
transform: 'translate(-50%, 0)',
},
'&[data-orientation="vertical"]': {
w: '6px',
h: '20px',
insetBlockStart: '50%',
transform: 'translate(0, -50%)',
},
},
}),
});

View File

@ -1,16 +1,64 @@
import { useCallback, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import type { import type {
ImperativePanelHandle, ImperativePanelHandle,
PanelOnCollapse, PanelOnCollapse,
PanelOnExpand, PanelOnExpand,
} from 'react-resizable-panels'; } from 'react-resizable-panels';
export const usePanel = (minSize: number) => { export type UsePanelOptions =
const ref = useRef<ImperativePanelHandle>(null); | { minSize: number; unit: 'percentages' }
| {
minSize: number;
unit: 'pixels';
fallbackMinSizePct: number;
panelGroupID: string;
};
export const usePanel = (arg: UsePanelOptions) => {
const panelHandleRef = useRef<ImperativePanelHandle>(null);
const newMinSizeRef = useRef<number>(0);
const currentSizeRef = useRef<number>(0);
const [_minSize, _setMinSize] = useState<number>(
arg.unit === 'percentages' ? arg.minSize : arg.fallbackMinSizePct
);
useLayoutEffect(() => {
if (arg.unit === 'percentages') {
return;
}
const panelGroupElement = document.querySelector(
`[data-panel-group][data-panel-group-id="${arg.panelGroupID}"]`
);
if (!panelGroupElement) {
return;
}
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (!panelHandleRef?.current) {
return;
}
newMinSizeRef.current = (arg.minSize * 100) / entry.contentRect.width;
currentSizeRef.current =
panelHandleRef.current.getSize() ?? arg.fallbackMinSizePct;
if (currentSizeRef.current < newMinSizeRef.current) {
panelHandleRef.current.resize(newMinSizeRef.current);
}
_setMinSize(newMinSizeRef.current);
}
});
resizeObserver.observe(panelGroupElement);
// _setMinSize(
// (arg.minSize * 100) / panelGroupElement.getBoundingClientRect().width
// );
return () => {
resizeObserver.disconnect();
};
}, [arg]);
const [isCollapsed, setIsCollapsed] = useState(() => const [isCollapsed, setIsCollapsed] = useState(() =>
Boolean(ref.current?.isCollapsed()) Boolean(panelHandleRef.current?.isCollapsed())
); );
const onCollapse = useCallback<PanelOnCollapse>(() => { const onCollapse = useCallback<PanelOnCollapse>(() => {
@ -22,38 +70,35 @@ export const usePanel = (minSize: number) => {
}, []); }, []);
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (ref.current?.isCollapsed()) { if (panelHandleRef.current?.isCollapsed()) {
flushSync(() => { panelHandleRef.current?.expand();
ref.current?.expand();
});
} else { } else {
flushSync(() => { panelHandleRef.current?.collapse();
ref.current?.collapse();
});
} }
}, []); }, []);
const expand = useCallback(() => { const expand = useCallback(() => {
flushSync(() => { panelHandleRef.current?.expand();
ref.current?.expand();
});
}, []); }, []);
const collapse = useCallback(() => { const collapse = useCallback(() => {
flushSync(() => { panelHandleRef.current?.collapse();
ref.current?.collapse();
});
}, []); }, []);
const reset = useCallback(() => { const reset = useCallback(() => {
flushSync(() => { // If the panel is really super close to the min size, collapse it
ref.current?.resize(minSize); const shouldCollapse =
}); Math.abs((panelHandleRef.current?.getSize() ?? 0) - _minSize) < 0.01;
}, [minSize]); if (shouldCollapse) {
collapse();
} else {
panelHandleRef.current?.resize(_minSize);
}
}, [_minSize, collapse]);
return { return {
ref, ref: panelHandleRef,
minSize, minSize: _minSize,
isCollapsed, isCollapsed,
onCollapse, onCollapse,
onExpand, onExpand,

View File

@ -6,5 +6,4 @@ import type { UIState } from './uiTypes';
export const uiPersistDenylist: (keyof UIState)[] = [ export const uiPersistDenylist: (keyof UIState)[] = [
'shouldShowImageDetails', 'shouldShowImageDetails',
'globalMenuCloseTrigger', 'globalMenuCloseTrigger',
'panels',
]; ];

View File

@ -24,6 +24,7 @@ import { tabsTheme } from 'common/components/InvTabs/theme';
import { textTheme } from 'common/components/InvText/theme'; import { textTheme } from 'common/components/InvText/theme';
import { textareaTheme } from 'common/components/InvTextarea/theme'; import { textareaTheme } from 'common/components/InvTextarea/theme';
import { tooltipTheme } from 'common/components/InvTooltip/theme'; import { tooltipTheme } from 'common/components/InvTooltip/theme';
import { resizeHandleTheme } from 'features/ui/components/tabs/ResizeHandle';
import { import {
InvokeAIColors, InvokeAIColors,
@ -117,6 +118,7 @@ export const theme: ThemeOverride = {
Textarea: textareaTheme, Textarea: textareaTheme,
Tooltip: tooltipTheme, Tooltip: tooltipTheme,
FormError: formErrorTheme, FormError: formErrorTheme,
ResizeHandle: resizeHandleTheme,
}, },
space: space, space: space,
sizes: space, sizes: space,