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

more splitview tests

上级 9afefc8b
......@@ -6,10 +6,12 @@
'use strict';
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 types = require('vs/base/common/types');
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';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
......@@ -33,33 +35,22 @@ export interface IView {
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`;
}
interface IViewItem {
view: IView;
size: number;
explicitSize: number;
container: HTMLElement;
disposable: IDisposable;
}
this.view.layout(this.size, this.orientation);
function layoutViewItem(item: IViewItem, orientation: Orientation): void {
if (orientation === Orientation.VERTICAL) {
item.container.style.height = `${item.size}px`;
} else {
item.container.style.width = `${item.size}px`;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
item.view.layout(item.size, orientation);
}
interface ISashItem {
......@@ -67,14 +58,27 @@ interface ISashItem {
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 {
private orientation: Orientation;
private el: HTMLElement;
// private size: number;
private size = 0;
// private viewElements: HTMLElement[];
private viewItems: ViewItem[] = [];
private viewItems: IViewItem[] = [];
private sashItems: ISashItem[] = [];
// private viewChangeListeners: IDisposable[];
// private viewFocusPreviousListeners: IDisposable[];
......@@ -86,7 +90,7 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// private sashesListeners: IDisposable[];
// private eventWrapper: (event: ISashEvent) => ISashEvent;
// private animationTimeout: number;
// private state: IState;
private sashDragState: ISashDragState;
// private _onFocus: Emitter<IView> = this._register(new Emitter<IView>());
// readonly onFocus: Event<IView> = this._onFocus.event;
......@@ -133,25 +137,27 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// this.addView(new VoidView(), 0);
}
private getContainerSize(): number {
return this.orientation === Orientation.VERTICAL
? dom.getContentHeight(this.container)
: dom.getContentWidth(this.container);
}
// 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 {
addView(view: IView, size: number, index = this.viewItems.length): 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]);
const item: IViewItem = { view, container, explicitSize: size, size, disposable: EmptyDisposable };
this.viewItems.splice(index, 0, item);
const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this);
// Disposable
item.disposable = combinedDisposable([onChangeDisposable, containerDisposable]);
// Render view
view.render(container, this.orientation);
......@@ -163,33 +169,33 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
}
// Add sash
if (this.viewItems.length <= 1) {
return;
if (this.viewItems.length > 1) {
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<IBaseSashEvent>(sash, 'start'), sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this);
const onChange = mapEvent(fromEventEmitter<IBaseSashEvent>(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);
}
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 });
// TODO: layout
// go through all viewitems, set their size to preferred size
// sum all sizes up
// run expandcollapse
const onStart = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'start'), sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this);
this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize));
const onChange = mapEvent(fromEventEmitter<IBaseSashEvent>(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);
const previousSize = this.size;
this.size = this.viewItems.reduce((r, i) => r + i.size, 0);
this.layout(previousSize);
}
removeView(index: number): void {
......@@ -199,63 +205,153 @@ export class SplitView implements IDisposable, IHorizontalSashLayoutProvider, IV
// Remove view
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
if (this.viewItems.length >= 1) {
const sashIndex = Math.max(index - 1, 0);
const sashItem = this.sashItems.splice(sashIndex, 1)[0];
sashItem.disposable.dispose();
}
// Remove sash
const sashIndex = Math.max(index - 1, 0);
const sashItem = this.sashItems.splice(sashIndex, 1)[0];
sashItem.disposable.dispose();
// Layout views
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, []);
}
layout(size?: number): void {
size = size || this.getContainerSize();
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, current }: ISashEvent): void {
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 {
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);
}
this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size);
}
// 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 onViewChange(item: IViewItem): void {
const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize);
if (size === item.size) {
return;
}
// this could maybe use the same code than the addView() does
// 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));
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;
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 layoutViews(): void {
this.viewItems.forEach(item => item.layout());
// 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');
// }
// 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.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());
// Update sashes enablement
......@@ -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 {
return this.getSashPosition(sash);
}
......
......@@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] {
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 {
private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>();
readonly onDidRender = this._onDidRender.event;
private _size = 0;
get size(): number { return this._size; }
private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>();
readonly onDidLayout = this._onDidLayout.event;
......@@ -39,6 +41,7 @@ class TestView implements IView {
}
layout(size: number, orientation: Orientation): void {
this._size = size;
this._onDidLayout.fire({ size, orientation });
}
......@@ -54,14 +57,16 @@ class TestView implements IView {
}
}
const TOTAL_SIZE = 200;
suite('Splitview', () => {
let container: HTMLElement;
setup(() => {
container = document.createElement('div');
container.style.position = 'absolute';
container.style.width = '200px';
container.style.height = '200px';
container.style.width = `${TOTAL_SIZE}px`;
container.style.height = `${TOTAL_SIZE}px`;
});
teardown(() => {
......@@ -75,12 +80,14 @@ suite('Splitview', () => {
});
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);
splitview.addView(view, 20);
splitview.addView(view, 20);
splitview.addView(view, 20);
splitview.addView(view1, 20);
splitview.addView(view2, 20);
splitview.addView(view3, 20);
let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 3, 'split view should have 3 views');
......@@ -113,6 +120,67 @@ suite('Splitview', () => {
assert.equal(sashQuery.length, 0, 'split view should have no sashes');
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();
});
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.
先完成此消息的编辑!
想要评论请 注册