diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 420212fbd1..7d719a2f49 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -36,6 +36,7 @@ import { FaFont, FaImage } from 'react-icons/fa'; import ResizeHandle from './tabs/ResizeHandle'; import ImageTab from './tabs/ImageToImage/ImageToImageTab'; import AuxiliaryProgressIndicator from 'app/components/AuxiliaryProgressIndicator'; +import { useMinimumPanelSize } from '../hooks/useMinimumPanelSize'; export interface InvokeTabInfo { id: InvokeTabName; @@ -78,6 +79,9 @@ const enabledTabsSelector = createSelector( } ); +const MIN_GALLERY_WIDTH = 300; +const DEFAULT_GALLERY_PCT = 20; + const InvokeTabs = () => { const activeTab = useAppSelector(activeTabIndexSelector); const activeTabName = useAppSelector(activeTabNameSelector); @@ -150,6 +154,9 @@ const InvokeTabs = () => { [enabledTabs] ); + const { ref: galleryPanelRef, minSizePct: galleryMinSizePct } = + useMinimumPanelSize(MIN_GALLERY_WIDTH, DEFAULT_GALLERY_PCT, 'app'); + return ( { { <> diff --git a/invokeai/frontend/web/src/features/ui/hooks/useMinimumPanelSize.ts b/invokeai/frontend/web/src/features/ui/hooks/useMinimumPanelSize.ts new file mode 100644 index 0000000000..b382b27329 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/hooks/useMinimumPanelSize.ts @@ -0,0 +1,70 @@ +// adapted from https://github.com/bvaughn/react-resizable-panels/issues/141#issuecomment-1540048714 + +import { + RefObject, + useCallback, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { ImperativePanelHandle } from 'react-resizable-panels'; + +export const useMinimumPanelSize = ( + minSizePx: number, + defaultSizePct: number, + groupId: string, + orientation: 'horizontal' | 'vertical' = 'horizontal' +): { ref: RefObject; minSizePct: number } => { + const ref = useRef(null); + const [minSizePct, setMinSizePct] = useState(defaultSizePct); + + const handleWindowResize = useCallback(() => { + const size = ref.current?.getSize(); + + if (size !== undefined && size < minSizePct) { + ref.current?.resize(minSizePct); + } + }, [minSizePct]); + + useLayoutEffect(() => { + const panelGroup = document.querySelector( + `[data-panel-group-id="${groupId}"]` + ); + const resizeHandles = document.querySelectorAll( + '[data-panel-resize-handle-id]' + ); + + if (!panelGroup) { + return; + } + const observer = new ResizeObserver(() => { + let dim = + orientation === 'horizontal' + ? panelGroup.getBoundingClientRect().width + : panelGroup.getBoundingClientRect().height; + + resizeHandles.forEach((resizeHandle) => { + dim -= + orientation === 'horizontal' + ? resizeHandle.getBoundingClientRect().width + : resizeHandle.getBoundingClientRect().height; + }); + + // Minimum size in pixels is a percentage of the PanelGroup's width/height + setMinSizePct((minSizePx / dim) * 100); + }); + observer.observe(panelGroup); + resizeHandles.forEach((resizeHandle) => { + observer.observe(resizeHandle); + }); + + window.addEventListener('resize', handleWindowResize); + + return () => { + observer.disconnect(); + window.removeEventListener('resize', handleWindowResize); + }; + }, [groupId, handleWindowResize, minSizePct, minSizePx, orientation]); + + return { ref, minSizePct }; +};