splitview: fix layout code

This commit is contained in:
Joao Moreno
2017-09-19 11:34:26 +02:00
parent b6e9893b9b
commit 63cf6afdbf
2 changed files with 104 additions and 92 deletions

View File

@@ -36,7 +36,6 @@ interface ISashEvent {
interface IViewItem {
view: IView;
size: number;
explicitSize: number;
container: HTMLElement;
disposable: IDisposable;
layout(): void;
@@ -51,6 +50,8 @@ interface ISashDragState {
index: number;
start: number;
sizes: number[];
minDelta: number;
maxDelta: number;
}
export class SplitView implements IDisposable {
@@ -58,6 +59,7 @@ export class SplitView implements IDisposable {
private orientation: Orientation;
private el: HTMLElement;
private size = 0;
private contentSize = 0;
private viewItems: IViewItem[] = [];
private sashItems: ISashItem[] = [];
private sashDragState: ISashDragState;
@@ -105,8 +107,7 @@ export class SplitView implements IDisposable {
};
size = Math.round(size);
const explicitSize = size;
const item: IViewItem = { view, container, explicitSize, size, layout, disposable };
const item: IViewItem = { view, container, size, layout, disposable };
this.viewItems.splice(index, 0, item);
// Add sash
@@ -129,7 +130,7 @@ export class SplitView implements IDisposable {
}
view.render(container, this.orientation);
this.relayoutPreferredSizes();
this.relayout();
}
removeView(index: number): void {
@@ -148,7 +149,7 @@ export class SplitView implements IDisposable {
sashItem.disposable.dispose();
}
this.relayoutPreferredSizes();
this.relayout();
}
moveView(from: number, to: number): void {
@@ -169,54 +170,82 @@ export class SplitView implements IDisposable {
this.layoutViews();
}
private relayoutPreferredSizes(): void {
this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize));
this.relayout();
}
private relayout(): void {
const previousSize = this.size;
this.size = this.viewItems.reduce((r, i) => r + i.size, 0);
this.layout(previousSize);
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.resize(this.viewItems.length - 1, this.contentSize - contentSize);
}
layout(size: number): void {
this.resize(this.viewItems.length - 1, size - this.size);
this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0));
const previousSize = Math.max(this.size, this.contentSize);
this.size = size;
this.resize(this.viewItems.length - 1, size - previousSize);
}
private onSashStart({ sash, start }: ISashEvent): void {
const index = firstIndex(this.sashItems, item => item.sash === sash);
const sizes = this.viewItems.map(i => i.size);
this.sashDragState = { start, index, sizes };
const upIndexes = range(index, -1);
const collapseUp = upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0);
const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0);
const downIndexes = range(index + 1, this.viewItems.length);
const collapseDown = downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0);
const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0);
const minDelta = -Math.min(collapseUp, expandDown);
const maxDelta = Math.min(collapseDown, expandUp);
this.sashDragState = { start, index, sizes, minDelta, maxDelta };
}
private onSashChange({ sash, current }: ISashEvent): void {
const { index, start, sizes } = this.sashDragState;
const { index, start, sizes, minDelta, maxDelta } = this.sashDragState;
const delta = clamp(current - start, minDelta, maxDelta);
this.resize(index, current - start, sizes);
this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size);
this.resize(index, delta, sizes);
}
private onViewChange(item: IViewItem): void {
const index = this.viewItems.indexOf(item);
if (index < 0 || index >= this.viewItems.length - 1) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
const size = clamp(item.size, item.view.minimumSize, item.view.maximumSize);
this.resize(index, size - item.size);
item.size = size;
this.relayout();
}
resizeView(index: number, size: number): void {
if (index < 0 || index >= this.viewItems.length - 1) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
const item = this.viewItems[index];
size = Math.round(size);
this.resize(index, size - this.viewItems[index].size);
size = clamp(size, item.view.minimumSize, item.view.maximumSize);
let delta = size - item.size;
if (delta !== 0 && index < this.viewItems.length - 1) {
const downIndexes = range(index + 1, this.viewItems.length);
const collapseDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0);
const expandDown = downIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0);
const deltaDown = clamp(delta, -expandDown, collapseDown);
this.resize(index, deltaDown);
delta -= deltaDown;
}
if (delta !== 0 && index > 0) {
const upIndexes = range(index - 1, -1);
const collapseUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].size - this.viewItems[i].view.minimumSize), 0);
const expandUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - this.viewItems[i].size), 0);
const deltaUp = clamp(-delta, -collapseUp, expandUp);
this.resize(index - 1, deltaUp);
}
}
private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void {
@@ -228,17 +257,10 @@ export class SplitView implements IDisposable {
const upIndexes = range(index, -1);
const up = upIndexes.map(i => this.viewItems[i]);
const upSizes = upIndexes.map(i => sizes[i]);
const downIndexes = range(index + 1, this.viewItems.length);
const down = downIndexes.map(i => this.viewItems[i]);
const downSizes = downIndexes.map(i => sizes[i]);
delta = clamp(
delta,
-upIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0),
downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0)
);
for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) {
const item = up[i];
const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize);
@@ -258,6 +280,20 @@ export class SplitView implements IDisposable {
}
}
let contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let emptyDelta = this.size - contentSize;
for (let i = this.viewItems.length - 1; emptyDelta > 0 && i >= 0; i--) {
const item = this.viewItems[i];
const size = clamp(item.size + emptyDelta, item.view.minimumSize, item.view.maximumSize);
const viewDelta = size - item.size;
emptyDelta -= viewDelta;
item.size = size;
}
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.layoutViews();
}

View File

@@ -82,7 +82,7 @@ suite('Splitview', () => {
splitview.dispose();
});
test('has views as sashes as children', () => {
test('has views and sashes as children', () => {
const view1 = new TestView(20, 20);
const view2 = new TestView(20, 20);
const view3 = new TestView(20, 20);
@@ -92,34 +92,34 @@ suite('Splitview', () => {
splitview.addView(view2, 20);
splitview.addView(view3, 20);
let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
let viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view');
assert.equal(viewQuery.length, 3, 'split view should have 3 views');
let sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
let sashQuery = container.querySelectorAll('.monaco-split-view2 > .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');
viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view');
assert.equal(viewQuery.length, 2, 'split view should have 2 views');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
sashQuery = container.querySelectorAll('.monaco-split-view2 > .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');
viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view');
assert.equal(viewQuery.length, 1, 'split view should have 1 view');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
sashQuery = container.querySelectorAll('.monaco-split-view2 > .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');
viewQuery = container.querySelectorAll('.monaco-split-view2 > .split-view-view');
assert.equal(viewQuery.length, 0, 'split view should have no views');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
sashQuery = container.querySelectorAll('.monaco-split-view2 > .monaco-sash');
assert.equal(sashQuery.length, 0, 'split view should have no sashes');
splitview.dispose();
@@ -177,31 +177,6 @@ suite('Splitview', () => {
view.dispose();
});
test('respects preferred sizes with structural changes', () => {
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(200);
splitview.addView(view1, 20);
assert.equal(view1.size, 200, 'view1 is stretched');
splitview.addView(view2, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 200 - 20, 'view2 is stretched');
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 20, 'view2 size is restored');
assert.equal(view3.size, 160, 'view3 is stretched');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
test('can resize views', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
@@ -213,27 +188,27 @@ suite('Splitview', () => {
splitview.addView(view2, 20);
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is the default');
assert.equal(view2.size, 20, 'view2 size the the default');
assert.equal(view3.size, 160, 'view3 is stretched');
assert.equal(view1.size, 160, 'view1 is stretched');
assert.equal(view2.size, 20, 'view2 size is 20');
assert.equal(view3.size, 20, 'view3 size is 20');
splitview.resizeView(1, 40);
assert.equal(view1.size, 20, 'view1 is untouched');
assert.equal(view1.size, 140, 'view1 is collapsed');
assert.equal(view2.size, 40, 'view2 is stretched');
assert.equal(view3.size, 140, 'view3 is collapsed');
assert.equal(view3.size, 20, 'view3 stays the same');
splitview.resizeView(0, 70);
assert.equal(view1.size, 70, 'view1 is stretched');
assert.equal(view2.size, 20, 'view2 is collapsed');
assert.equal(view3.size, 110, 'view3 is collapsed');
assert.equal(view1.size, 70, 'view1 is collapsed');
assert.equal(view2.size, 110, 'view2 is expanded');
assert.equal(view3.size, 20, 'view3 stays the same');
assert.throws(() => splitview.resizeView(2, 20));
splitview.resizeView(2, 40);
assert.equal(view1.size, 70, 'view1 stays the same');
assert.equal(view2.size, 20, 'view2 stays the same');
assert.equal(view3.size, 110, 'view3 stays the same');
assert.equal(view2.size, 90, 'view2 is collapsed');
assert.equal(view3.size, 40, 'view3 is stretched');
splitview.dispose();
view3.dispose();
@@ -252,32 +227,33 @@ suite('Splitview', () => {
splitview.addView(view2, 20);
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 20, 'view2 size is restored');
assert.equal(view1.size, 160, 'view1 is stretched');
assert.equal(view2.size, 20, 'view2 size is 20');
assert.equal(view3.size, 20, 'view3 size is 20');
view1.maximumSize = 20;
assert.equal(view1.size, 20, 'view1 is collapsed');
assert.equal(view2.size, 20, 'view2 stays the same');
assert.equal(view3.size, 160, 'view3 is stretched');
view3.maximumSize = 20;
view3.maximumSize = 40;
assert.equal(view1.size, 20, 'view1 stays the same');
assert.equal(view2.size, 160, 'view2 is stretched');
assert.equal(view3.size, 20, 'view3 is collapsed');
assert.equal(view2.size, 140, 'view2 is stretched');
assert.equal(view3.size, 40, 'view3 is collapsed');
view2.maximumSize = 40;
view2.maximumSize = 200;
assert.equal(view1.size, 140, 'view1 is stretched');
assert.equal(view2.size, 40, 'view2 is collapsed');
assert.equal(view3.size, 20, 'view3 is collapsed');
view3.maximumSize = 200;
assert.equal(view1.size, 140, 'view1 stays the same');
assert.equal(view2.size, 40, 'view2 stays the same');
assert.equal(view3.size, 20, 'view3 stays the same');
assert.equal(view1.size, 20, 'view1 stays the same');
assert.equal(view2.size, 140, 'view2 stays the same');
assert.equal(view3.size, 40, 'view3 stays the same');
view3.maximumSize = Number.POSITIVE_INFINITY;
view3.minimumSize = 100;
assert.equal(view1.size, 80, 'view1 is collapsed');
assert.equal(view2.size, 20, 'view2 stays the same');
assert.equal(view1.size, 20, 'view1 is collapsed');
assert.equal(view2.size, 80, 'view2 is collapsed');
assert.equal(view3.size, 100, 'view3 is stretched');
splitview.dispose();