diff --git a/.vscode/settings.json b/.vscode/settings.json index a6cd70e725d..d03741307f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -86,6 +86,7 @@ "splitview", "table", "list", - "git" + "git", + "sash" ] } diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index aa9bcfffc0a..d0e68ce6bd9 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -13,29 +13,39 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecy import { isMacintosh } from 'vs/base/common/platform'; import 'vs/css!./sash'; +/** + * Allow the sashes to be visible at runtime. + * @remark Use for development purposes only. + */ let DEBUG = false; // DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this -export interface ISashLayoutProvider { } - -export interface IVerticalSashLayoutProvider extends ISashLayoutProvider { +/** + * A vertical sash layout provider provides position and height for a sash. + */ +export interface IVerticalSashLayoutProvider { getVerticalSashLeft(sash: Sash): number; getVerticalSashTop?(sash: Sash): number; getVerticalSashHeight?(sash: Sash): number; } -export interface IHorizontalSashLayoutProvider extends ISashLayoutProvider { +/** + * A vertical sash layout provider provides position and width for a sash. + */ +export interface IHorizontalSashLayoutProvider { getHorizontalSashTop(sash: Sash): number; getHorizontalSashLeft?(sash: Sash): number; getHorizontalSashWidth?(sash: Sash): number; } +type ISashLayoutProvider = IVerticalSashLayoutProvider | IHorizontalSashLayoutProvider; + export interface ISashEvent { - startX: number; - currentX: number; - startY: number; - currentY: number; - altKey: boolean; + readonly startX: number; + readonly currentX: number; + readonly startY: number; + readonly currentY: number; + readonly altKey: boolean; } export enum OrthogonalEdge { @@ -46,10 +56,41 @@ export enum OrthogonalEdge { } export interface ISashOptions { + + /** + * Whether a sash is horizontal or vertical. + */ readonly orientation: Orientation; - readonly orthogonalStartSash?: Sash; - readonly orthogonalEndSash?: Sash; + + /** + * The width or height of a vertical or horizontal sash, respectively. + */ readonly size?: number; + + /** + * A reference to another sash, perpendicular to this one, which + * aligns at the start of this one. A corner sash will be created + * automatically at that location. + * + * The start of a horizontal sash is its left-most position. + * The start of a vertical sash is its top-most position. + */ + readonly orthogonalStartSash?: Sash; + + /** + * A reference to another sash, perpendicular to this one, which + * aligns at the end of this one. A corner sash will be created + * automatically at that location. + * + * The end of a horizontal sash is its right-most position. + * The end of a vertical sash is its bottom-most position. + */ + readonly orthogonalEndSash?: Sash; + + /** + * Provides a hint as to what mouse cursor to use whenever the user + * hovers over a corner sash provided by this and an orthogonal sash. + */ readonly orthogonalEdge?: OrthogonalEdge; } @@ -67,9 +108,31 @@ export const enum Orientation { } export const enum SashState { + + /** + * Disable any UI interaction. + */ Disabled, - Minimum, - Maximum, + + /** + * Allow dragging down or to the right, depending on the sash orientation. + * + * Some OSs allow customizing the mouse cursor differently whenever + * some resizable component can't be any smaller, but can be larger. + */ + AtMinimum, + + /** + * Allow dragging up or to the left, depending on the sash orientation. + * + * Some OSs allow customizing the mouse cursor differently whenever + * some resizable component can't be any larger, but can be smaller. + */ + AtMaximum, + + /** + * Enable dragging. + */ Enabled } @@ -159,52 +222,101 @@ class OrthogonalPointerEventFactory implements IPointerEventFactory { } } +/** + * The {@link Sash} is the UI component which allows the user to resize other + * components. It's usually an invisible horizontal or vertical line which, when + * hovered, becomes highlighted and can be dragged along the perpendicular dimension + * to its direction. + * + * Features: + * - Touch event handling + * - Corner sash support + * - Hover with different mouse cursor support + * - Configurable hover size + * - Linked sash support, for 2x2 corner sashes + */ export class Sash extends Disposable { private el: HTMLElement; private layoutProvider: ISashLayoutProvider; - private orientation!: Orientation; + private orientation: Orientation; private size: number; private hoverDelay = globalHoverDelay; private hoverDelayer = this._register(new Delayer(this.hoverDelay)); private _state: SashState = SashState.Enabled; + private readonly onDidEnablementChange = this._register(new Emitter()); + private readonly _onDidStart = this._register(new Emitter()); + private readonly _onDidChange = this._register(new Emitter()); + private readonly _onDidReset = this._register(new Emitter()); + private readonly _onDidEnd = this._register(new Emitter()); + private readonly orthogonalStartSashDisposables = this._register(new DisposableStore()); + private _orthogonalStartSash: Sash | undefined; + private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore()); + private _orthogonalStartDragHandle: HTMLElement | undefined; + private readonly orthogonalEndSashDisposables = this._register(new DisposableStore()); + private _orthogonalEndSash: Sash | undefined; + private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore()); + private _orthogonalEndDragHandle: HTMLElement | undefined; + get state(): SashState { return this._state; } + get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; } + get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; } + + /** + * The state of a sash defines whether it can be interacted with by the user + * as well as what mouse cursor to use, when hovered. + */ set state(state: SashState) { if (this._state === state) { return; } this.el.classList.toggle('disabled', state === SashState.Disabled); - this.el.classList.toggle('minimum', state === SashState.Minimum); - this.el.classList.toggle('maximum', state === SashState.Maximum); + this.el.classList.toggle('minimum', state === SashState.AtMinimum); + this.el.classList.toggle('maximum', state === SashState.AtMaximum); this._state = state; - this._onDidEnablementChange.fire(state); + this.onDidEnablementChange.fire(state); } - private readonly _onDidEnablementChange = this._register(new Emitter()); - readonly onDidEnablementChange: Event = this._onDidEnablementChange.event; - - private readonly _onDidStart = this._register(new Emitter()); + /** + * An event which fires whenever the user starts dragging this sash. + */ readonly onDidStart: Event = this._onDidStart.event; - private readonly _onDidChange = this._register(new Emitter()); + /** + * An event which fires whenever the user moves the mouse while + * dragging this sash. + */ readonly onDidChange: Event = this._onDidChange.event; - private readonly _onDidReset = this._register(new Emitter()); + /** + * An event which fires whenever the user double clicks this sash. + */ readonly onDidReset: Event = this._onDidReset.event; - private readonly _onDidEnd = this._register(new Emitter()); + /** + * An event which fires whenever the user stops dragging this sash. + */ readonly onDidEnd: Event = this._onDidEnd.event; + /** + * A linked sash will be forwarded the same user interactions and events + * so it moves exactly the same way as this sash. + * + * Useful in 2x2 grids. Not meant for widespread usage. + */ linkedSash: Sash | undefined = undefined; - private readonly orthogonalStartSashDisposables = this._register(new DisposableStore()); - private _orthogonalStartSash: Sash | undefined; - private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore()); - private _orthogonalStartDragHandle: HTMLElement | undefined; - get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; } + /** + * A reference to another sash, perpendicular to this one, which + * aligns at the start of this one. A corner sash will be created + * automatically at that location. + * + * The start of a horizontal sash is its left-most position. + * The start of a vertical sash is its top-most position. + */ set orthogonalStartSash(sash: Sash | undefined) { this.orthogonalStartDragHandleDisposables.clear(); this.orthogonalStartSashDisposables.clear(); @@ -223,18 +335,22 @@ export class Sash extends Disposable { } }; - this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(onChange, this)); + this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this)); onChange(sash.state); } this._orthogonalStartSash = sash; } - private readonly orthogonalEndSashDisposables = this._register(new DisposableStore()); - private _orthogonalEndSash: Sash | undefined; - private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore()); - private _orthogonalEndDragHandle: HTMLElement | undefined; - get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; } + /** + * A reference to another sash, perpendicular to this one, which + * aligns at the end of this one. A corner sash will be created + * automatically at that location. + * + * The end of a horizontal sash is its right-most position. + * The end of a vertical sash is its bottom-most position. + */ + set orthogonalEndSash(sash: Sash | undefined) { this.orthogonalEndDragHandleDisposables.clear(); this.orthogonalEndSashDisposables.clear(); @@ -253,15 +369,30 @@ export class Sash extends Disposable { } }; - this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(onChange, this)); + this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this)); onChange(sash.state); } this._orthogonalEndSash = sash; } - constructor(container: HTMLElement, layoutProvider: IVerticalSashLayoutProvider, options: ISashOptions); - constructor(container: HTMLElement, layoutProvider: IHorizontalSashLayoutProvider, options: ISashOptions); + /** + * Create a new vertical sash. + * + * @param container A DOM node to append the sash to. + * @param verticalLayoutProvider A vertical layout provider. + * @param options The options. + */ + constructor(container: HTMLElement, verticalLayoutProvider: IVerticalSashLayoutProvider, options: IVerticalSashOptions); + + /** + * Create a new horizontal sash. + * + * @param container A DOM node to append the sash to. + * @param horizontalLayoutProvider A horizontal layout provider. + * @param options The options. + */ + constructor(container: HTMLElement, horizontalLayoutProvider: IHorizontalSashLayoutProvider, options: IHorizontalSashOptions); constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) { super(); @@ -381,17 +512,17 @@ export class Sash extends Disposable { if (isMultisashResize) { cursor = 'all-scroll'; } else if (this.orientation === Orientation.HORIZONTAL) { - if (this.state === SashState.Minimum) { + if (this.state === SashState.AtMinimum) { cursor = 's-resize'; - } else if (this.state === SashState.Maximum) { + } else if (this.state === SashState.AtMaximum) { cursor = 'n-resize'; } else { cursor = isMacintosh ? 'row-resize' : 'ns-resize'; } } else { - if (this.state === SashState.Minimum) { + if (this.state === SashState.AtMinimum) { cursor = 'e-resize'; - } else if (this.state === SashState.Maximum) { + } else if (this.state === SashState.AtMaximum) { cursor = 'w-resize'; } else { cursor = isMacintosh ? 'col-resize' : 'ew-resize'; @@ -406,7 +537,7 @@ export class Sash extends Disposable { updateStyle(); if (!isMultisashResize) { - this.onDidEnablementChange(updateStyle, null, disposables); + this.onDidEnablementChange.event(updateStyle, null, disposables); } const onPointerMove = (e: PointerEvent) => { @@ -472,10 +603,19 @@ export class Sash extends Disposable { } } + /** + * Forcefully stop any user interactions with this sash. + * Useful when hiding a parent component, while the user is still + * interacting with the sash. + */ clearSashHoverState(): void { Sash.onMouseLeave(this); } + /** + * Layout the sash. The sash will size and position itself + * based on its provided {@link ISashLayoutProvider layout provider}. + */ layout(): void { if (this.orientation === Orientation.VERTICAL) { const verticalProvider = (this.layoutProvider); diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 31fb4c9da79..7ef513d2e9d 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -964,16 +964,16 @@ export class SplitView extends Disposable { const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible; if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) { - sash.state = SashState.Minimum; + sash.state = SashState.AtMinimum; } else if (snappedAfter && collapsesDown[index] && (position < this.contentSize || this.endSnappingEnabled)) { - sash.state = SashState.Maximum; + sash.state = SashState.AtMaximum; } else { sash.state = SashState.Disabled; } } else if (min && !max) { - sash.state = SashState.Minimum; + sash.state = SashState.AtMinimum; } else if (!min && max) { - sash.state = SashState.Maximum; + sash.state = SashState.AtMaximum; } else { sash.state = SashState.Enabled; } diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 2e9cf651117..be85ad23c15 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -297,12 +297,12 @@ suite('Splitview', () => { assert.strictEqual(sashes[1].state, SashState.Disabled, 'second sash is disabled'); view1.maximumSize = 300; - assert.strictEqual(sashes[0].state, SashState.Minimum, 'first sash is enabled'); - assert.strictEqual(sashes[1].state, SashState.Minimum, 'second sash is enabled'); + assert.strictEqual(sashes[0].state, SashState.AtMinimum, 'first sash is enabled'); + assert.strictEqual(sashes[1].state, SashState.AtMinimum, 'second sash is enabled'); view2.maximumSize = 200; - assert.strictEqual(sashes[0].state, SashState.Minimum, 'first sash is enabled'); - assert.strictEqual(sashes[1].state, SashState.Minimum, 'second sash is enabled'); + assert.strictEqual(sashes[0].state, SashState.AtMinimum, 'first sash is enabled'); + assert.strictEqual(sashes[1].state, SashState.AtMinimum, 'second sash is enabled'); splitview.resizeView(0, 40); assert.strictEqual(sashes[0].state, SashState.Enabled, 'first sash is enabled');