提交 b34aba90 编写于 作者: J Joao Moreno

more splitview tests

上级 9afefc8b
...@@ -6,10 +6,12 @@ ...@@ -6,10 +6,12 @@
'use strict'; 'use strict';
import 'vs/css!./splitview'; import 'vs/css!./splitview';
import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IDisposable, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle';
import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event'; import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event';
import types = require('vs/base/common/types'); import types = require('vs/base/common/types');
import dom = require('vs/base/browser/dom'); import dom = require('vs/base/browser/dom');
import { clamp } from 'vs/base/common/numbers';
import { range, firstIndex, weave } from 'vs/base/common/arrays';
import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; import { Sash, IVerticalSashLayoutProvider, IHorizontalSashLayoutProvider, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash';
export { Orientation } from 'vs/base/browser/ui/sash/sash'; export { Orientation } from 'vs/base/browser/ui/sash/sash';
...@@ -33,33 +35,22 @@ export interface IView { ...@@ -33,33 +35,22 @@ export interface IView {
focus(): void; focus(): void;
} }
class ViewItem { interface IViewItem {
view: IView;
public explicitSize: number; size: number;
explicitSize: number;
constructor( container: HTMLElement;
readonly view: IView, disposable: IDisposable;
readonly container: HTMLElement, }
public size: number,
private orientation: Orientation,
private disposables: IDisposable[]
) {
}
layout(): void { function layoutViewItem(item: IViewItem, orientation: Orientation): void {
if (this.orientation === Orientation.VERTICAL) { if (orientation === Orientation.VERTICAL) {
this.container.style.height = `${this.size}px`; item.container.style.height = `${item.size}px`;
} else { } else {
this.container.style.width = `${this.size}px`; item.container.style.width = `${item.size}px`;
} }
this.view.layout(this.size, this.orientation); item.view.layout(item.size, orientation);
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
} }
interface ISashItem { interface ISashItem {
...@@ -67,14 +58,27 @@ interface ISashItem { ...@@ -67,14 +58,27 @@ interface ISashItem {
disposable: IDisposable; disposable: IDisposable;
} }
interface ISashDragState {
start: number;
sizes: number[];
up: number[];
down: number[];
maxUp: number;
maxDown: number;
collapses: number[];
expands: number[];
}
const sum = (a: number[]) => a.reduce((a, b) => a + b, 0);
export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider { export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider {
private orientation: Orientation; private orientation: Orientation;
private el: HTMLElement; private el: HTMLElement;
// private size: number; private size = 0;
// private viewElements: HTMLElement[]; // private viewElements: HTMLElement[];
private viewItems: ViewItem[] = []; private viewItems: IViewItem[] = [];
private sashItems: ISashItem[] = []; private sashItems: ISashItem[] = [];
// private viewChangeListeners: IDisposable[]; // private viewChangeListeners: IDisposable[];
// private viewFocusPreviousListeners: IDisposable[]; // private viewFocusPreviousListeners: IDisposable[];
...@@ -86,7 +90,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -86,7 +90,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// private sashesListeners: IDisposable[]; // private sashesListeners: IDisposable[];
// private eventWrapper: (event: ISashEvent) => ISashEvent; // private eventWrapper: (event: ISashEvent) => ISashEvent;
// private animationTimeout: number; // private animationTimeout: number;
// private state: IState; private sashDragState: ISashDragState;
// private _onFocus: Emitter<IView> = this._register(new Emitter<IView>()); // private _onFocus: Emitter<IView> = this._register(new Emitter<IView>());
// readonly onFocus: Event<IView> = this._onFocus.event; // readonly onFocus: Event<IView> = this._onFocus.event;
...@@ -133,25 +137,27 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -133,25 +137,27 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// this.addView(new VoidView(), 0); // this.addView(new VoidView(), 0);
} }
private getContainerSize(): number { // private getContainerSize(): number {
return this.orientation === Orientation.VERTICAL // return this.orientation === Orientation.VERTICAL
? dom.getContentHeight(this.container) // ? dom.getContentHeight(this.container)
: dom.getContentWidth(this.container); // : dom.getContentWidth(this.container);
} // }
addView(view: IView, size: number, index = this.viewItems.length - 1): void { addView(view: IView, size: number, index = this.viewItems.length): void {
// Create view container // Create view container
const container = document.createElement('div'); const container = document.createElement('div');
dom.addClass(container, 'split-view-view'); dom.addClass(container, 'split-view-view');
const containerDisposable = toDisposable(() => this.el.removeChild(container)); const containerDisposable = toDisposable(() => this.el.removeChild(container));
// List to change events
const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this);
// Create item // Create item
const item = new ViewItem(view, container, size, this.orientation, [onChangeDisposable, containerDisposable]); const item: IViewItem = { view, container, explicitSize: size, size, disposable: EmptyDisposable };
this.viewItems.splice(index, 0, item); this.viewItems.splice(index, 0, item);
const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this);
// Disposable
item.disposable = combinedDisposable([onChangeDisposable, containerDisposable]);
// Render view // Render view
view.render(container, this.orientation); view.render(container, this.orientation);
...@@ -163,14 +169,8 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -163,14 +169,8 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
} }
// Add sash // Add sash
if (this.viewItems.length <= 1) { if (this.viewItems.length > 1) {
return; const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
}
const orientation = this.orientation === Orientation.VERTICAL
? Orientation.HORIZONTAL
: Orientation.VERTICAL;
const sash = new Sash(this.el, this, { orientation }); const sash = new Sash(this.el, this, { orientation });
const sashEventMapper = this.orientation === Orientation.VERTICAL const sashEventMapper = this.orientation === Orientation.VERTICAL
? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY }) ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY })
...@@ -178,20 +178,26 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -178,20 +178,26 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
const onStart = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'start'), sashEventMapper); const onStart = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'start'), sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this); const onStartDisposable = onStart(this.onSashStart, this);
const onChange = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'change'), sashEventMapper); const onChange = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'change'), sashEventMapper);
const onSashChangeDisposable = onChange(this.onSashChange, this); const onSashChangeDisposable = onChange(this.onSashChange, this);
const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]); const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]);
const sashItem: ISashItem = { sash, disposable };
const sashItem: ISashItem = {
sash,
disposable
};
this.sashItems.splice(index - 1, 0, sashItem); this.sashItems.splice(index - 1, 0, sashItem);
} }
// TODO: layout
// go through all viewitems, set their size to preferred size
// sum all sizes up
// run expandcollapse
this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize));
const previousSize = this.size;
this.size = this.viewItems.reduce((r, i) => r + i.size, 0);
this.layout(previousSize);
}
removeView(index: number): void { removeView(index: number): void {
if (index < 0 || index >= this.viewItems.length) { if (index < 0 || index >= this.viewItems.length) {
return; return;
...@@ -199,63 +205,153 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -199,63 +205,153 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// Remove view // Remove view
const viewItem = this.viewItems.splice(index, 1)[0]; const viewItem = this.viewItems.splice(index, 1)[0];
viewItem.dispose(); const collapse = viewItem.size;
viewItem.disposable.dispose();
if (this.viewItems.length < 1) {
return;
}
// Remove sash // Remove sash
if (this.viewItems.length >= 1) {
const sashIndex = Math.max(index - 1, 0); const sashIndex = Math.max(index - 1, 0);
const sashItem = this.sashItems.splice(sashIndex, 1)[0]; const sashItem = this.sashItems.splice(sashIndex, 1)[0];
sashItem.disposable.dispose(); sashItem.disposable.dispose();
} }
layout(size?: number): void { // Layout views
size = size || this.getContainerSize(); const up = range(index - 1, -1);
const down = range(index, this.viewItems.length);
const indexes = weave(up, down);
const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0));
this.expandCollapse(collapse, collapses, [], indexes, []);
} }
private onSashStart({ sash, start, current }: ISashEvent): void { layout(size: number): void {
// size = size || this.getContainerSize();
// size = Math.max(size, this.viewItems.reduce((t, i) => t + i.view.minimumSize, 0));
if (this.size === size) {
return;
}
const indexes = range(this.viewItems.length - 1, -1);
const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0));
const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0));
const diff = Math.abs(this.size - size);
if (size < this.size) {
this.expandCollapse(Math.min(diff, sum(collapses)), collapses, expands, indexes, []);
} else if (size > this.size) {
this.expandCollapse(Math.min(diff, sum(expands)), collapses, expands, [], indexes);
}
this.size = size;
}
private onSashStart({ sash, start }: ISashEvent): void {
const i = firstIndex(this.sashItems, item => item.sash === sash);
const sizes = this.viewItems.map(i => i.size);
const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0));
const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0));
const up = range(i, -1);
const down = range(i + 1, this.viewItems.length);
const collapsesUp = up.map(i => collapses[i]);
const collapsesDown = down.map(i => collapses[i]);
const expandsUp = up.map(i => expands[i]);
const expandsDown = down.map(i => expands[i]);
const maxUp = Math.min(sum(collapsesUp), sum(expandsDown));
const maxDown = Math.min(sum(expandsUp), sum(collapsesDown));
this.sashDragState = { start, sizes, up, down, maxUp, maxDown, collapses, expands };
} }
private onSashChange({ sash, start, current }: ISashEvent): void { private onSashChange({ sash, start, current }: ISashEvent): void {
const diff = current - this.sashDragState.start;
if (diff < 0) {
this.expandCollapse(Math.min(-diff, this.sashDragState.maxUp), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.up, this.sashDragState.down);
} else {
this.expandCollapse(Math.min(diff, this.sashDragState.maxDown), this.sashDragState.collapses, this.sashDragState.expands, this.sashDragState.down, this.sashDragState.up);
} }
// Main algorithm this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size);
// 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 => { private onViewChange(item: IViewItem): void {
// let expand = Math.min(expands[i], totalExpand); const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize);
// totalExpand -= expand;
// this.views[i].size += expand;
// });
// }
private getLastFlexibleViewIndex(exceptIndex: number = null): number { if (size === item.size) {
// for (let i = this.views.length - 1; i >= 0; i--) { return;
// if (exceptIndex === i) { }
// continue;
// } // this could maybe use the same code than the addView() does
// if (this.views[i].sizing === ViewSizing.Flexible) {
// return i; // this.setupAnimation();
const index = this.viewItems.indexOf(item);
const diff = Math.abs(size - item.size);
const up = range(index - 1, -1);
const down = range(index + 1, this.viewItems.length);
const downUp = down.concat(up);
const collapses = this.viewItems.map(i => Math.max(i.size - i.view.minimumSize, 0));
const expands = this.viewItems.map(i => Math.max(i.view.maximumSize - i.size, 0));
let collapse: number, collapseIndexes: number[], expandIndexes: number[];
if (size < item.size) {
collapse = Math.min(downUp.reduce((t, i) => t + expands[i], 0), diff);
collapseIndexes = [index];
expandIndexes = downUp;
} else {
collapse = Math.min(downUp.reduce((t, i) => t + collapses[i], 0), diff);
collapseIndexes = downUp;
expandIndexes = [index];
}
this.expandCollapse(collapse, collapses, expands, collapseIndexes, expandIndexes);
// this.layoutViews();
}
// private setupAnimation(): void {
// if (types.isNumber(this.animationTimeout)) {
// window.clearTimeout(this.animationTimeout);
// } // }
// dom.addClass(this.el, 'animated');
// this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200);
// } // }
return -1; // private clearAnimation(): void {
} // this.animationTimeout = null;
// dom.removeClass(this.el, 'animated');
// }
private layoutViews(): void { // Main algorithm
this.viewItems.forEach(item => item.layout()); 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.viewItems[i].size -= collapse;
});
expandIndexes.forEach(i => {
let expand = Math.min(expands[i], totalExpand);
totalExpand -= expand;
this.viewItems[i].size += expand;
});
this.viewItems.forEach(item => layoutViewItem(item, this.orientation));
this.sashItems.forEach(item => item.sash.layout()); this.sashItems.forEach(item => item.sash.layout());
// Update sashes enablement // Update sashes enablement
...@@ -281,23 +377,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV ...@@ -281,23 +377,6 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// }); // });
} }
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 { getVerticalSashLeft(sash: Sash): number {
return this.getSashPosition(sash); return this.getSashPosition(sash);
} }
......
...@@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] { ...@@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] {
return result; return result;
} }
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
\ No newline at end of file
...@@ -21,6 +21,8 @@ class TestView implements IView { ...@@ -21,6 +21,8 @@ class TestView implements IView {
private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>(); private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>();
readonly onDidRender = this._onDidRender.event; readonly onDidRender = this._onDidRender.event;
private _size = 0;
get size(): number { return this._size; }
private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>(); private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>();
readonly onDidLayout = this._onDidLayout.event; readonly onDidLayout = this._onDidLayout.event;
...@@ -39,6 +41,7 @@ class TestView implements IView { ...@@ -39,6 +41,7 @@ class TestView implements IView {
} }
layout(size: number, orientation: Orientation): void { layout(size: number, orientation: Orientation): void {
this._size = size;
this._onDidLayout.fire({ size, orientation }); this._onDidLayout.fire({ size, orientation });
} }
...@@ -54,14 +57,16 @@ class TestView implements IView { ...@@ -54,14 +57,16 @@ class TestView implements IView {
} }
} }
const TOTAL_SIZE = 200;
suite('Splitview', () => { suite('Splitview', () => {
let container: HTMLElement; let container: HTMLElement;
setup(() => { setup(() => {
container = document.createElement('div'); container = document.createElement('div');
container.style.position = 'absolute'; container.style.position = 'absolute';
container.style.width = '200px'; container.style.width = `${TOTAL_SIZE}px`;
container.style.height = '200px'; container.style.height = `${TOTAL_SIZE}px`;
}); });
teardown(() => { teardown(() => {
...@@ -75,12 +80,14 @@ suite('Splitview', () => { ...@@ -75,12 +80,14 @@ suite('Splitview', () => {
}); });
test('splitview has views as sashes as children', () => { test('splitview has views as sashes as children', () => {
const view = new TestView(20, 20); const view1 = new TestView(20, 20);
const view2 = new TestView(20, 20);
const view3 = new TestView(20, 20);
const splitview = new SplitView(container); const splitview = new SplitView(container);
splitview.addView(view, 20); splitview.addView(view1, 20);
splitview.addView(view, 20); splitview.addView(view2, 20);
splitview.addView(view, 20); splitview.addView(view3, 20);
let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view'); let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 3, 'split view should have 3 views'); assert.equal(viewQuery.length, 3, 'split view should have 3 views');
...@@ -113,6 +120,67 @@ suite('Splitview', () => { ...@@ -113,6 +120,67 @@ suite('Splitview', () => {
assert.equal(sashQuery.length, 0, 'split view should have no sashes'); assert.equal(sashQuery.length, 0, 'split view should have no sashes');
splitview.dispose(); splitview.dispose();
view1.dispose();
view2.dispose();
view3.dispose();
});
test('splitview calls view methods on addView and removeView', () => {
const view = new TestView(20, 20);
const splitview = new SplitView(container);
let didLayout = false;
const layoutDisposable = view.onDidLayout(() => didLayout = true);
let didRender = false;
const renderDisposable = view.onDidRender(() => didRender = true);
splitview.addView(view, 20);
assert.equal(view.size, 20, 'view has right size');
assert(didLayout, 'layout was called');
assert(didLayout, 'render was called');
splitview.dispose();
layoutDisposable.dispose();
renderDisposable.dispose();
view.dispose(); view.dispose();
}); });
test('splitview stretches view to viewport', () => {
const view = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(TOTAL_SIZE);
splitview.addView(view, 20);
assert.equal(view.size, TOTAL_SIZE, 'view was stretched');
splitview.dispose();
view.dispose();
});
test('splitview respects preferred sizes', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
const view3 = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(TOTAL_SIZE);
splitview.addView(view1, 20);
assert.equal(view1.size, TOTAL_SIZE, 'view1 was stretched');
splitview.addView(view2, 20);
assert.equal(view1.size, 20, 'view1 size was restored');
assert.equal(view2.size, TOTAL_SIZE - 20, 'view2 was stretched');
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size was restored');
assert.equal(view2.size, 20, 'view2 size was restored');
assert.equal(view3.size, TOTAL_SIZE - 20 * 2, 'view3 was stretched');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
}); });
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册