From 09050b6aef914a52d382b72c163c4031fe2b45d9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 14 Sep 2017 15:43:46 +0200 Subject: [PATCH] splitview: initial tests --- .../base/browser/ui/splitview/splitview2.ts | 325 ++++++++++++++++++ .../browser/ui/splitview/splitview.test.ts | 118 +++++++ 2 files changed, 443 insertions(+) create mode 100644 src/vs/base/browser/ui/splitview/splitview2.ts create mode 100644 src/vs/base/test/browser/ui/splitview/splitview.test.ts diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts new file mode 100644 index 00000000000..a65ad774a40 --- /dev/null +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -0,0 +1,325 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./splitview'; +import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; +import types = require('vs/base/common/types'); +import dom = require('vs/base/browser/dom'); +import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; +export { Orientation } from 'vs/base/browser/ui/sash/sash'; + +interface ISashEvent { + sash: Sash; + start: number; + current: number; +} + +export interface IOptions { + orientation?: Orientation; // default Orientation.VERTICAL + canChangeOrderByDragAndDrop?: boolean; +} + +export interface IView { + readonly minimumSize: number; + readonly maximumSize: number; + readonly onDidChange: Event; + render(container: HTMLElement, orientation: Orientation): void; + layout(size: number, orientation: Orientation): void; + focus(): void; +} + +class ViewItem { + + public explicitSize: number; + + constructor( + readonly view: IView, + readonly container: HTMLElement, + public size: number, + private orientation: Orientation, + private disposables: IDisposable[] + ) { + + } + + layout(): void { + if (this.orientation === Orientation.VERTICAL) { + this.container.style.height = `${this.size}px`; + } else { + this.container.style.width = `${this.size}px`; + } + + this.view.layout(this.size, this.orientation); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + +interface ISashItem { + sash: Sash; + disposable: IDisposable; +} + +export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { + + private orientation: Orientation; + + private el: HTMLElement; + // private size: number; + // private viewElements: HTMLElement[]; + private viewItems: ViewItem[] = []; + private sashItems: ISashItem[] = []; + // private viewChangeListeners: IDisposable[]; + // private viewFocusPreviousListeners: IDisposable[]; + // private viewFocusNextListeners: IDisposable[]; + // private viewFocusListeners: IDisposable[]; + // private viewDnDListeners: IDisposable[][]; + // private sashOrientation: Orientation; + // private sashes: Sash[]; + // private sashesListeners: IDisposable[]; + // private eventWrapper: (event: ISashEvent) => ISashEvent; + // private animationTimeout: number; + // private state: IState; + + // private _onFocus: Emitter = this._register(new Emitter()); + // readonly onFocus: Event = this._onFocus.event; + + // private _onDidOrderChange: Emitter = this._register(new Emitter()); + // readonly onDidOrderChange: Event = this._onDidOrderChange.event; + + get length(): number { + return this.viewItems.length; + } + + constructor(private container: HTMLElement, options?: IOptions) { + options = options || {}; + this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; + + this.el = document.createElement('div'); + dom.addClass(this.el, 'monaco-split-view'); + dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal'); + container.appendChild(this.el); + + // this.size = null; + // this.viewElements = []; + // this.views = []; + // this.viewChangeListeners = []; + // this.viewFocusPreviousListeners = []; + // this.viewFocusNextListeners = []; + // this.viewFocusListeners = []; + // this.viewDnDListeners = []; + // this.sashes = []; + // this.sashesListeners = []; + // this.animationTimeout = null; + + // this.sashOrientation = this.orientation === Orientation.VERTICAL + // ? Orientation.HORIZONTAL + // : Orientation.VERTICAL; + + // if (this.orientation === Orientation.VERTICAL) { + // this.eventWrapper = e => { return { start: e.startY, current: e.currentY }; }; + // } else { + // this.eventWrapper = e => { return { start: e.startX, current: e.currentX }; }; + // } + + // The void space exists to handle the case where all other views are fixed size + // this.addView(new VoidView(), 0); + } + + private getContainerSize(): number { + return this.orientation === Orientation.VERTICAL + ? dom.getContentHeight(this.container) + : dom.getContentWidth(this.container); + } + + addView(view: IView, size: number, index = this.viewItems.length - 1): void { + // Create view container + const container = document.createElement('div'); + dom.addClass(container, 'split-view-view'); + const containerDisposable = toDisposable(() => this.el.removeChild(container)); + + // List to change events + const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this); + + // Create item + const item = new ViewItem(view, container, size, this.orientation, [onChangeDisposable, containerDisposable]); + this.viewItems.splice(index, 0, item); + + // Render view + view.render(container, this.orientation); + + // Attach view + if (this.viewItems.length === 1) { + this.el.appendChild(container); + } else { + this.el.insertBefore(container, this.el.children.item(index)); + } + + // Add sash + if (this.viewItems.length <= 1) { + return; + } + + const orientation = this.orientation === Orientation.VERTICAL + ? Orientation.HORIZONTAL + : Orientation.VERTICAL; + + const sash = new Sash(this.el, this, { orientation }); + const sashEventMapper = this.orientation === Orientation.VERTICAL + ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) + : (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX }); + + const onStart = mapEvent(fromEventEmitter(sash, 'start'), sashEventMapper); + const onStartDisposable = onStart(this.onSashStart, this); + + const onChange = mapEvent(fromEventEmitter(sash, 'change'), sashEventMapper); + const onSashChangeDisposable = onChange(this.onSashChange, this); + + const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); + + const sashItem: ISashItem = { + sash, + disposable + }; + + this.sashItems.splice(index - 1, 0, sashItem); + } + + removeView(index: number): void { + if (index < 0 || index >= this.viewItems.length) { + return; + } + + // Remove view + const viewItem = this.viewItems.splice(index, 1)[0]; + viewItem.dispose(); + + if (this.viewItems.length < 1) { + return; + } + + // Remove sash + const sashIndex = Math.max(index - 1, 0); + const sashItem = this.sashItems.splice(sashIndex, 1)[0]; + sashItem.disposable.dispose(); + } + + layout(size?: number): void { + size = size || this.getContainerSize(); + } + + private onSashStart({ sash, start, current }: ISashEvent): void { + + } + + private onSashChange({ sash, start, current }: ISashEvent): void { + + } + + // Main algorithm + // private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void { + // let totalCollapse = collapse; + // let totalExpand = totalCollapse; + + // collapseIndexes.forEach(i => { + // let collapse = Math.min(collapses[i], totalCollapse); + // totalCollapse -= collapse; + // this.views[i].size -= collapse; + // }); + + // expandIndexes.forEach(i => { + // let expand = Math.min(expands[i], totalExpand); + // totalExpand -= expand; + // this.views[i].size += expand; + // }); + // } + + private getLastFlexibleViewIndex(exceptIndex: number = null): number { + // for (let i = this.views.length - 1; i >= 0; i--) { + // if (exceptIndex === i) { + // continue; + // } + // if (this.views[i].sizing === ViewSizing.Flexible) { + // return i; + // } + // } + + return -1; + } + + private layoutViews(): void { + this.viewItems.forEach(item => item.layout()); + this.sashItems.forEach(item => item.sash.layout()); + + // Update sashes enablement + // let previous = false; + // let collapsesDown = this.views.map(v => previous = (v.size - v.minimumSize > 0) || previous); + + // previous = false; + // let expandsDown = this.views.map(v => previous = (v.maximumSize - v.size > 0) || previous); + + // let reverseViews = this.views.slice().reverse(); + // previous = false; + // let collapsesUp = reverseViews.map(v => previous = (v.size - v.minimumSize > 0) || previous).reverse(); + + // previous = false; + // let expandsUp = reverseViews.map(v => previous = (v.maximumSize - v.size > 0) || previous).reverse(); + + // this.sashes.forEach((s, i) => { + // if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) { + // s.enable(); + // } else { + // s.disable(); + // } + // }); + } + + private onViewChange(view: ViewItem): void { + } + + // private setupAnimation(): void { + // if (types.isNumber(this.animationTimeout)) { + // window.clearTimeout(this.animationTimeout); + // } + + // dom.addClass(this.el, 'animated'); + // this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200); + // } + + // private clearAnimation(): void { + // this.animationTimeout = null; + // dom.removeClass(this.el, 'animated'); + // } + + getVerticalSashLeft(sash: Sash): number { + return this.getSashPosition(sash); + } + + getHorizontalSashTop(sash: Sash): number { + return this.getSashPosition(sash); + } + + private getSashPosition(sash: Sash): number { + let position = 0; + + for (let i = 0; i < this.sashItems.length; i++) { + position += this.viewItems[i].size; + + if (this.sashItems[i].sash === sash) { + return position; + } + } + + throw new Error('Sash not found'); + } + + dispose(): void { + } +} diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts new file mode 100644 index 00000000000..94dbb16fe87 --- /dev/null +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from 'vs/base/common/event'; +import { SplitView, IView, Orientation } from 'vs/base/browser/ui/splitview/splitview2'; + +class TestView implements IView { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + get minimumSize(): number { return this._minimumSize; } + set minimumSize(size: number) { this._minimumSize = size; this._onDidChange.fire(); } + + get maximumSize(): number { return this._maximumSize; } + set maximumSize(size: number) { this._maximumSize = size; this._onDidChange.fire(); } + + private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>(); + readonly onDidRender = this._onDidRender.event; + + private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); + readonly onDidLayout = this._onDidLayout.event; + + private _onDidFocus = new Emitter(); + readonly onDidFocus = this._onDidFocus.event; + + constructor( + private _minimumSize: number, + private _maximumSize: number + ) { + assert(_minimumSize <= _maximumSize, 'splitview view minimum size must be <= maximum size'); + } + + render(container: HTMLElement, orientation: Orientation): void { + this._onDidRender.fire({ container, orientation }); + } + + layout(size: number, orientation: Orientation): void { + this._onDidLayout.fire({ size, orientation }); + } + + focus(): void { + this._onDidFocus.fire(); + } + + dispose(): void { + this._onDidChange.dispose(); + this._onDidRender.dispose(); + this._onDidLayout.dispose(); + this._onDidFocus.dispose(); + } +} + +suite('Splitview', () => { + let container: HTMLElement; + + setup(() => { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = '200px'; + container.style.height = '200px'; + }); + + teardown(() => { + container = null; + }); + + test('empty splitview has empty DOM', () => { + const splitview = new SplitView(container); + assert.equal(container.firstElementChild.childElementCount, 0, 'split view should be empty'); + splitview.dispose(); + }); + + test('splitview has views as sashes as children', () => { + const view = new TestView(20, 20); + const splitview = new SplitView(container); + + splitview.addView(view, 20); + splitview.addView(view, 20); + splitview.addView(view, 20); + + let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 3, 'split view should have 3 views'); + + let sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 2, 'split view should have 2 sashes'); + + splitview.removeView(2); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 2, 'split view should have 2 views'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 1, 'split view should have 1 sash'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 1, 'split view should have 1 view'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.removeView(0); + + viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); + assert.equal(viewQuery.length, 0, 'split view should have no views'); + + sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash'); + assert.equal(sashQuery.length, 0, 'split view should have no sashes'); + + splitview.dispose(); + view.dispose(); + }); +}); \ No newline at end of file -- GitLab