diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index b9bfadb8f3..9fb626c643 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -95,7 +95,7 @@ "react-icons": "^4.12.0", "react-konva": "^18.2.10", "react-redux": "^9.0.4", - "react-resizable-panels": "^1.0.6", + "react-resizable-panels": "^1.0.7", "react-select": "5.8.0", "react-textarea-autosize": "^8.5.3", "react-use": "^17.4.2", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index de7e3d3256..e1bd1a955b 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -135,8 +135,8 @@ dependencies: specifier: ^9.0.4 version: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1) react-resizable-panels: - specifier: ^1.0.6 - version: 1.0.6(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.0.7 + version: 1.0.7(react-dom@18.2.0)(react@18.2.0) react-select: specifier: 5.8.0 version: 5.8.0(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) @@ -11569,8 +11569,8 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.46)(react@18.2.0) dev: false - /react-resizable-panels@1.0.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-yZQiOP/uW2nTSdESDUBlBkQ1NQjUABpRKfBqonUQnNYSur7qRDy3W2wEEmrEyUY+W3opshpMiHf45zngIduJ0g==} + /react-resizable-panels@1.0.7(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg==} peerDependencies: react: ^16.14.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 diff --git a/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts b/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts index 3617c74c8d..95f618c7bd 100644 --- a/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts +++ b/invokeai/frontend/web/src/features/ui/hooks/usePanel.ts @@ -11,33 +11,100 @@ import { getResizeHandleElementsForGroup, } from 'react-resizable-panels'; +type Direction = 'horizontal' | 'vertical'; + export type UsePanelOptions = - | { minSize: number; unit: 'percentages' } | { + /** + * The minimum size of the panel as a percentage. + */ minSize: number; + /** + * The unit of the minSize + */ + unit: 'percentages'; + } + | { + /** + * The minimum size of the panel in pixels. + */ + minSize: number; + /** + * The unit of the minSize. + */ unit: 'pixels'; - fallbackMinSizePct: number; + /** + * The direction of the panel group. + * This is required to accurately calculate the available space for the panel, minus the space taken by the handles. + */ + panelGroupDirection: Direction; + /** + * A ref to the panel group. + */ panelGroupRef: RefObject; - panelGroupDirection: 'horizontal' | 'vertical'; }; -export const usePanel = (arg: UsePanelOptions) => { +export type UsePanelReturn = { + /** + * The ref to the panel handle. + */ + ref: RefObject; + /** + * The dynamically calculated minimum size of the panel. + */ + minSize: number; + /** + * Whether the panel is collapsed. + */ + isCollapsed: boolean; + /** + * The onCollapse callback. This is required to update the isCollapsed state. + * This should be passed to the panel as the onCollapse prop. Wrap it if additional logic is required. + */ + onCollapse: PanelOnCollapse; + /** + * The onExpand callback. This is required to update the isCollapsed state. + * This should be passed to the panel as the onExpand prop. Wrap it if additional logic is required. + */ + onExpand: PanelOnExpand; + /** + * Reset the panel to the minSize. If the panel is already at the minSize, collapse it. + * This can be provided to the `onDoubleClick` prop of the panel's nearest resize handle. + */ + reset: () => void; + /** + * Toggle the panel between collapsed and expanded. + */ + toggle: () => void; + /** + * Expand the panel. + */ + expand: () => void; + /** + * Collapse the panel. + */ + collapse: () => void; + /** + * Resize the panel to the given size in the same units as the minSize. + */ + resize: (size: number) => void; +}; + +export const usePanel = (arg: UsePanelOptions): UsePanelReturn => { const panelHandleRef = useRef(null); const [_minSize, _setMinSize] = useState( - arg.unit === 'percentages' ? arg.minSize : arg.fallbackMinSizePct + arg.unit === 'percentages' ? arg.minSize : 0 ); - // If the units are pixels, we need to calculate the min size as a percentage of the available space + // If the units are pixels, we need to calculate the min size as a percentage of the available space, + // then resize the panel if it is too small. useLayoutEffect(() => { if (arg.unit === 'percentages' || !arg.panelGroupRef.current) { return; } - const panelGroupElement = getPanelGroupElement( - arg.panelGroupRef.current.getId() - ); - const panelGroupHandleElements = getResizeHandleElementsForGroup( - arg.panelGroupRef.current.getId() - ); + const id = arg.panelGroupRef.current.getId(); + const panelGroupElement = getPanelGroupElement(id); + const panelGroupHandleElements = getResizeHandleElementsForGroup(id); if (!panelGroupElement) { return; } @@ -45,33 +112,23 @@ export const usePanel = (arg: UsePanelOptions) => { if (!panelHandleRef?.current) { return; } - // Calculate the available space for the panel, minus the space taken by the handles - let dim = - arg.panelGroupDirection === 'horizontal' - ? panelGroupElement.offsetWidth - : panelGroupElement.offsetHeight; - panelGroupHandleElements.forEach( - (el) => - (dim -= - arg.panelGroupDirection === 'horizontal' - ? el.offsetWidth - : el.offsetHeight) + const minSizePct = getSizeAsPercentage( + arg.minSize, + arg.panelGroupRef, + arg.panelGroupDirection ); - // Calculate the min size as a percentage of the available space - const minSize = (arg.minSize * 100) / dim; - // Must store this to avoid race conditions - const currentSize = panelHandleRef.current.getSize(); - // Resize if the current size is smaller than the new min size - happens when the window is resized smaller - if (currentSize < minSize) { - panelHandleRef.current.resize(minSize); - } + _setMinSize(minSizePct); - _setMinSize(minSize); + // Resize if the current size is smaller than the new min size - happens when the window is resized smaller + if (panelHandleRef.current.getSize() < minSizePct) { + panelHandleRef.current.resize(minSizePct); + } }); resizeObserver.observe(panelGroupElement); + panelGroupHandleElements.forEach((el) => resizeObserver.observe(el)); return () => { resizeObserver.disconnect(); @@ -106,15 +163,34 @@ export const usePanel = (arg: UsePanelOptions) => { panelHandleRef.current?.collapse(); }, []); + const resize = useCallback( + (size: number) => { + // If we are using percentages, we can just resize to the given size + if (arg.unit === 'percentages') { + panelHandleRef.current?.resize(size); + return; + } + + // If we are using pixels, we need to calculate the size as a percentage of the available space + const sizeAsPct = getSizeAsPercentage( + size, + arg.panelGroupRef, + arg.panelGroupDirection + ); + panelHandleRef.current?.resize(sizeAsPct); + }, + [arg] + ); + const reset = useCallback(() => { // 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) { + if (Math.abs((panelHandleRef.current?.getSize() ?? 0) - _minSize) < 0.01) { collapse(); - } else { - panelHandleRef.current?.resize(_minSize); + return; } + + // Otherwise, resize to the min size + panelHandleRef.current?.resize(_minSize); }, [_minSize, collapse]); return { @@ -127,5 +203,45 @@ export const usePanel = (arg: UsePanelOptions) => { toggle, expand, collapse, + resize, }; }; + +/** + * For a desired size in pixels, calculates the size of the panel as a percentage of the available space. + * @param sizeInPixels The desired size of the panel in pixels. + * @param panelGroupHandleRef The ref to the panel group handle. + * @param panelGroupDirection The direction of the panel group. + * @returns The size of the panel as a percentage. + */ +const getSizeAsPercentage = ( + sizeInPixels: number, + panelGroupHandleRef: RefObject, + panelGroupDirection: Direction +) => { + if (!panelGroupHandleRef.current) { + // No panel group handle ref, so we can't calculate the size + return 0; + } + const id = panelGroupHandleRef.current.getId(); + const panelGroupElement = getPanelGroupElement(id); + if (!panelGroupElement) { + // No panel group element, size is 0 + return 0; + } + + // The available space is the width/height of the panel group... + let availableSpace = + panelGroupDirection === 'horizontal' + ? panelGroupElement.offsetWidth + : panelGroupElement.offsetHeight; + + // ...minus the width/height of the resize handles + getResizeHandleElementsForGroup(id).forEach((el) => { + availableSpace -= + panelGroupDirection === 'horizontal' ? el.offsetWidth : el.offsetHeight; + }); + + // The final value is a percentage of the available space + return (sizeInPixels / availableSpace) * 100; +};