import { computed, onBeforeUnmount, shallowRef, toValue, watch, type MaybeRefOrGetter } from 'vue' export interface ResizePanelWidthConstraints { minWidth: number maxWidth: number mainMinWidth?: number layoutGap?: number } export interface ResizePercentConstraints { minPercent: number maxPercent: number } function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)) } function availablePanelWidth( containerWidth: number, constraints: ResizePanelWidthConstraints, ): number { return Math.min( constraints.maxWidth, containerWidth - (constraints.mainMinWidth ?? 0) - (constraints.layoutGap ?? 0), ) } export function resizedLeadingPanelWidth( startWidth: number, deltaX: number, containerWidth: number, constraints: ResizePanelWidthConstraints, ): number { return Math.round(clamp( startWidth + deltaX, constraints.minWidth, Math.max(constraints.minWidth, availablePanelWidth(containerWidth, constraints)), )) } export function resizedTrailingPanelWidth( startWidth: number, deltaX: number, containerWidth: number, constraints: ResizePanelWidthConstraints, ): number { return resizedLeadingPanelWidth(startWidth, -deltaX, containerWidth, constraints) } export function resizedVerticalSplitPercent( startPercent: number, deltaY: number, containerHeight: number, constraints: ResizePercentConstraints, ): number { if (containerHeight <= 0) return startPercent return Math.round(clamp( startPercent + (deltaY / containerHeight) * 100, constraints.minPercent, constraints.maxPercent, )) } export interface ManualLayoutResizeContext { kind: TKind startValue: number deltaX: number deltaY: number containerRect: DOMRectReadOnly secondaryRect: DOMRectReadOnly | null } export interface ManualLayoutResizeTarget { cursor?: string getContainerElement: () => HTMLElement | null getSecondaryElement?: () => HTMLElement | null getStartValue: () => number onResize: (context: ManualLayoutResizeContext) => void } /** Coordinates pointer-driven resizing for one or more layout splitters. */ export function useManualLayoutResize(options: { enabled: MaybeRefOrGetter resolveTarget: (kind: TKind) => ManualLayoutResizeTarget | null }) { const activeResize = shallowRef<{ kind: TKind startClientX: number startClientY: number startValue: number containerRect: DOMRectReadOnly secondaryRect: DOMRectReadOnly | null cursor: string } | null>(null) const isResizing = computed(() => activeResize.value !== null) let previousBodyCursor = '' let previousBodyUserSelect = '' function stopResize(): void { if (!activeResize.value) return activeResize.value = null window.removeEventListener('pointermove', handlePointerMove) window.removeEventListener('pointerup', stopResize) window.removeEventListener('pointercancel', stopResize) if (typeof document !== 'undefined') { document.body.style.cursor = previousBodyCursor document.body.style.userSelect = previousBodyUserSelect } } function handlePointerMove(event: PointerEvent): void { const state = activeResize.value if (!state) return options.resolveTarget(state.kind)?.onResize({ kind: state.kind, startValue: state.startValue, deltaX: event.clientX - state.startClientX, deltaY: event.clientY - state.startClientY, containerRect: state.containerRect, secondaryRect: state.secondaryRect, }) } function startResize(kind: TKind, event: PointerEvent): void { if (!toValue(options.enabled) || event.button !== 0) return const target = options.resolveTarget(kind) const containerRect = target?.getContainerElement()?.getBoundingClientRect() if (!target || !containerRect) return event.preventDefault() if (event.currentTarget instanceof HTMLElement) { event.currentTarget.setPointerCapture(event.pointerId) } const secondary = target.getSecondaryElement?.() ?? null activeResize.value = { kind, startClientX: event.clientX, startClientY: event.clientY, startValue: target.getStartValue(), containerRect, secondaryRect: secondary?.getBoundingClientRect() ?? null, cursor: target.cursor ?? 'col-resize', } if (typeof document !== 'undefined') { previousBodyCursor = document.body.style.cursor previousBodyUserSelect = document.body.style.userSelect document.body.style.cursor = activeResize.value.cursor document.body.style.userSelect = 'none' } window.addEventListener('pointermove', handlePointerMove) window.addEventListener('pointerup', stopResize) window.addEventListener('pointercancel', stopResize) } function isResizingKind(kind: TKind): boolean { return activeResize.value?.kind === kind } watch(() => toValue(options.enabled), enabled => { if (!enabled) stopResize() }) onBeforeUnmount(stopResize) return { activeResize, isResizing, isResizingKind, startResize, stopResize } }