diff --git a/src/vs/base/browser/ui/splitview/splitview2.ts b/src/vs/base/browser/ui/splitview/splitview2.ts index e8ae8eeacdd..5c75416648d 100644 --- a/src/vs/base/browser/ui/splitview/splitview2.ts +++ b/src/vs/base/browser/ui/splitview/splitview2.ts @@ -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(); } 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 be73ee96d46..07fb5816104 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -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();