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
className="list-container"
data-testid="boards-list"
gridTemplateColumns="repeat(auto-fill, minmax(108px, 1fr))"
gridTemplateColumns="repeat(auto-fill, minmax(90px, 1fr))"
maxH={346}
>
<GridItem p={1.5} data-testid="no-board">

View File

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

View File

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

View File

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

View File

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

View File

@ -1,103 +1,84 @@
import type { FlexProps, SystemStyleObject } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { Box, defineStyleConfig, Flex, useStyleConfig } from '@chakra-ui/react';
import { memo } from 'react';
import type { PanelResizeHandleProps } from 'react-resizable-panels';
import { PanelResizeHandle } from 'react-resizable-panels';
type ResizeHandleProps = Omit<FlexProps, 'direction'> & {
direction?: 'horizontal' | 'vertical';
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right';
isCollapsed?: boolean;
type ResizeHandleProps = PanelResizeHandleProps & {
orientation: 'horizontal' | 'vertical';
};
const ResizeHandle = (props: ResizeHandleProps) => {
const {
direction = 'horizontal',
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]);
const { orientation, ...rest } = props;
const styles = useStyleConfig('ResizeHandle', { orientation });
return (
<PanelResizeHandle style={resizeHandleStyles}>
<Flex
className="resize-handle-horizontal"
sx={resizeHandleWrapperStyles}
{...rest}
>
<Box sx={resizeInnerStyles} />
<PanelResizeHandle {...rest}>
<Flex __css={styles} data-orientation={orientation}>
<Box className="resize-handle-inner" data-orientation={orientation} />
<Box
className="resize-handle-drag-handle"
data-orientation={orientation}
/>
</Flex>
</PanelResizeHandle>
);
};
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 { flushSync } from 'react-dom';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import type {
ImperativePanelHandle,
PanelOnCollapse,
PanelOnExpand,
} from 'react-resizable-panels';
export const usePanel = (minSize: number) => {
const ref = useRef<ImperativePanelHandle>(null);
export type UsePanelOptions =
| { 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(() =>
Boolean(ref.current?.isCollapsed())
Boolean(panelHandleRef.current?.isCollapsed())
);
const onCollapse = useCallback<PanelOnCollapse>(() => {
@ -22,38 +70,35 @@ export const usePanel = (minSize: number) => {
}, []);
const toggle = useCallback(() => {
if (ref.current?.isCollapsed()) {
flushSync(() => {
ref.current?.expand();
});
if (panelHandleRef.current?.isCollapsed()) {
panelHandleRef.current?.expand();
} else {
flushSync(() => {
ref.current?.collapse();
});
panelHandleRef.current?.collapse();
}
}, []);
const expand = useCallback(() => {
flushSync(() => {
ref.current?.expand();
});
panelHandleRef.current?.expand();
}, []);
const collapse = useCallback(() => {
flushSync(() => {
ref.current?.collapse();
});
panelHandleRef.current?.collapse();
}, []);
const reset = useCallback(() => {
flushSync(() => {
ref.current?.resize(minSize);
});
}, [minSize]);
// If the panel is really super close to the min size, collapse it
const shouldCollapse =
Math.abs((panelHandleRef.current?.getSize() ?? 0) - _minSize) < 0.01;
if (shouldCollapse) {
collapse();
} else {
panelHandleRef.current?.resize(_minSize);
}
}, [_minSize, collapse]);
return {
ref,
minSize,
ref: panelHandleRef,
minSize: _minSize,
isCollapsed,
onCollapse,
onExpand,

View File

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

View File

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