diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bd509719a3cce..a6564545a7b61 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -12,6 +12,24 @@ border-radius: 8px; } +/* Resize handles */ +.quick-input-widget-resize { + position: absolute; + top: 0; + bottom: 0; + width: 6px; + cursor: ew-resize; + z-index: 1; +} + +.quick-input-widget-resize.left { + left: -3px; +} + +.quick-input-widget-resize.right { + right: -3px; +} + .quick-input-titlebar { cursor: grab; display: flex; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8e5283ef9ad92..6550d715c1180 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -47,6 +47,7 @@ const VIEWSTATE_STORAGE_KEY = 'workbench.quickInput.viewState'; type QuickInputViewState = { readonly top?: number; readonly left?: number; + readonly width?: number; // custom width in pixels }; export class QuickInputController extends Disposable { @@ -80,6 +81,7 @@ export class QuickInputController extends Disposable { private viewState: QuickInputViewState | undefined; private dndController: QuickInputDragAndDropController | undefined; + private resizeController: QuickInputResizeController | undefined; private readonly inQuickInputContext: IContextKey; private readonly quickInputTypeContext: IContextKey; @@ -400,6 +402,33 @@ export class QuickInputController extends Disposable { } })); + // Resize support + this.resizeController = this._register(new QuickInputResizeController( + this._container, + container, + this.viewState?.width + )); + + // Resize update layout + this._register(autorun(reader => { + const resizeState = this.resizeController?.resizeViewState.read(reader); + if (!resizeState) { + return; + } + + this.viewState = { + ...this.viewState, + width: resizeState.width, + }; + + this.updateLayout(); + + // Save width and position + if (resizeState.done) { + this.saveViewState(this.viewState); + } + })); + this.ui = { container, styleSheet, @@ -862,10 +891,18 @@ export class QuickInputController extends Disposable { this.updateLayout(); } + private getDefaultWidth(): number { + return Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + } + private updateLayout() { if (this.ui && this.isVisible()) { const style = this.ui.container.style; - let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + const defaultWidth = this.getDefaultWidth(); + const customWidth = this.viewState?.width; + let width = customWidth !== undefined + ? Math.max(QuickInputResizeController.MIN_WIDTH, Math.min(customWidth, this.dimension!.width - 20)) + : defaultWidth; style.width = width + 'px'; let listHeight = this.dimension && this.dimension.height * 0.4; @@ -978,7 +1015,7 @@ export class QuickInputController extends Disposable { private loadViewState(): QuickInputViewState | undefined { try { const data = JSON.parse(this.storageService.get(VIEWSTATE_STORAGE_KEY, StorageScope.APPLICATION, '{}')); - if (data.top !== undefined || data.left !== undefined) { + if (data.top !== undefined || data.left !== undefined || data.width !== undefined) { return data; } } catch { } @@ -1211,3 +1248,87 @@ class QuickInputDragAndDropController extends Disposable { return Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); } } + +class QuickInputResizeController extends Disposable { + static readonly MIN_WIDTH = 300; + static readonly MAX_WIDTH = 900; + + readonly resizeViewState = observableValue<{ width?: number; done: boolean } | undefined>(this, undefined); + + private readonly _leftHandle: HTMLElement; + private readonly _rightHandle: HTMLElement; + + constructor( + private _container: HTMLElement, + private readonly _quickInputContainer: HTMLElement, + initialWidth: number | undefined + ) { + super(); + + // Create resize handles + this._leftHandle = dom.append(this._quickInputContainer, $('.quick-input-widget-resize.left')); + this._rightHandle = dom.append(this._quickInputContainer, $('.quick-input-widget-resize.right')); + + this._registerMouseListeners(); + + if (initialWidth !== undefined) { + this.resizeViewState.set({ width: initialWidth, done: true }, undefined); + } + } + + private _registerMouseListeners(): void { + // Left handle + this._registerHandleListeners(this._leftHandle, 'left'); + + // Right handle + this._registerHandleListeners(this._rightHandle, 'right'); + + } + + private _registerHandleListeners(handle: HTMLElement, side: 'left' | 'right'): void { + this._register(dom.addDisposableGenericMouseDownListener(handle, (e: MouseEvent) => { + const activeWindow = dom.getWindow(this._container); + const originEvent = new StandardMouseEvent(activeWindow, e); + originEvent.preventDefault(); + originEvent.stopPropagation(); + + const startX = originEvent.browserEvent.clientX; + const startWidth = this._quickInputContainer.getBoundingClientRect().width; + const containerWidth = this._container.clientWidth; + + let isResizing = false; + const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { + const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); + mouseMoveEvent.preventDefault(); + + if (!isResizing) { + isResizing = true; + } + + const deltaX = mouseMoveEvent.browserEvent.clientX - startX; + let newWidth: number; + + if (side === 'right') { + newWidth = startWidth + deltaX * 2; + } else { + newWidth = startWidth - deltaX * 2; + } + + newWidth = Math.max(QuickInputResizeController.MIN_WIDTH, Math.min(newWidth, QuickInputResizeController.MAX_WIDTH)); + newWidth = Math.min(newWidth, containerWidth - 20); + + this.resizeViewState.set({ width: newWidth, done: false }, undefined); + }); + + const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + if (isResizing) { + const state = this.resizeViewState.get(); + this.resizeViewState.set({ width: state?.width, done: true }, undefined); + } + + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + } +}