提交 5a2c401a 编写于 作者: J Joao Moreno

Merge remote-tracking branch 'joao/grid' into grid

/*---------------------------------------------------------------------------------------------
* 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 { Event, anyEvent, Emitter } from 'vs/base/common/event';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { SplitView, IView } from 'vs/base/browser/ui/splitview/splitview';
import { empty as EmptyDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { $, append } from 'vs/base/browser/dom';
export { IView } from 'vs/base/browser/ui/splitview/splitview';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
function orthogonal(orientation: Orientation): Orientation {
return orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
}
export class GridLeafNode<T extends IView> {
constructor(readonly view: T) { }
}
export class GridBranchNode<T extends IView> {
constructor(readonly children: GridNode<T>[]) { }
}
export type GridNode<T extends IView> = GridLeafNode<T> | GridBranchNode<T>;
export interface IGrid<T extends IView> {
layout(width: number, height: number): void;
addView(view: IView, size: number, location: number[]): void;
removeView(location: number[]): void;
moveView(from: number[], to: number[]): void;
resizeView(location: number[], size: number): void;
getViewSize(location: number[]): number;
getViews(): GridBranchNode<T>;
}
function tail<T>(arr: T[]): [T[], T] {
if (arr.length === 0) {
throw new Error('Invalid tail call');
}
return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
}
abstract class AbstractNode implements IView {
abstract minimumSize: number;
abstract maximumSize: number;
abstract onDidChange: Event<number>;
abstract render(container: HTMLElement, orientation: Orientation): void;
protected size: number | undefined;
protected orthogonalSize: number | undefined;
readonly orientation;
layout(size: number): void {
this.size = size;
}
orthogonalLayout(size: number): void {
this.orthogonalSize = size;
}
dispose(): void {
}
}
class BranchNode<T extends IView> extends AbstractNode {
readonly children: Node<T>[] = [];
private splitview: SplitView;
get minimumSize(): number {
let result = 0;
for (const child of this.children) {
if (!(child instanceof BranchNode)) {
continue;
}
for (const grandchild of child.children) {
result += grandchild.minimumSize;
}
}
return result;
}
get maximumSize(): number {
let result = 0;
for (const child of this.children) {
if (!(child instanceof BranchNode)) {
continue;
}
for (const grandchild of child.children) {
result += grandchild.maximumSize;
}
}
return result;
}
private _onDidChange = new Emitter<number | undefined>();
get onDidChange(): Event<number | undefined> { return this._onDidChange.event; }
private onDidChangeDisposable: IDisposable = EmptyDisposable;
constructor(readonly orientation: Orientation) {
super();
}
layout(size: number): void {
super.layout(size);
for (const child of this.children) {
child.orthogonalLayout(size);
}
}
orthogonalLayout(size: number): void {
super.orthogonalLayout(size);
this.splitview.layout(size);
}
render(container: HTMLElement): void {
this.splitview = new SplitView(container, { orientation: this.orientation });
this.layout(this.size);
this.orthogonalLayout(this.orthogonalSize);
}
addChild(node: Node<T>, size: number, index: number): void {
if (index < 0 || index > this.children.length) {
throw new Error('Invalid index');
}
this.splitview.addView(node, size, index);
this.children.splice(index, 0, node);
this.onDidChildrenChange();
}
removeChild(index: number): Node<T> {
if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index');
}
const child = this.children[index];
this.splitview.removeView(index);
this.children.splice(index, 1);
this.onDidChildrenChange();
return child;
}
resizeChild(index: number, size: number): void {
if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index');
}
this.splitview.resizeView(index, size);
}
getChildSize(index: number): number {
if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index');
}
return this.splitview.getViewSize(index);
}
private onDidChildrenChange(): void {
const onDidChildrenChange = anyEvent(...this.children.map(c => c.onDidChange));
this.onDidChangeDisposable.dispose();
this.onDidChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
}
dispose(): void {
for (const child of this.children) {
child.dispose();
}
this.onDidChangeDisposable.dispose();
this.splitview.dispose();
super.dispose();
}
}
class LeafNode<T extends IView> extends AbstractNode {
constructor(readonly view: T, readonly orientation: Orientation) {
super();
}
get minimumSize(): number { return this.view.minimumSize; }
get maximumSize(): number { return this.view.maximumSize; }
get onDidChange(): Event<number> { return this.view.onDidChange; }
render(container: HTMLElement, orientation: Orientation): void {
return this.view.render(container, orientation);
}
layout(size: number): void {
super.layout(size);
return this.view.layout(size, this.orientation);
}
}
type Node<T extends IView> = BranchNode<T> | LeafNode<T>;
export class GridView<T extends IView> implements IGrid<T>, IDisposable {
private root: BranchNode<T>;
constructor(container: HTMLElement) {
const el = append(container, $('.monaco-grid-view'));
this.root = new BranchNode(Orientation.VERTICAL);
this.root.render(el);
}
addView(view: T, size: number, location: number[]): void {
const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest);
const node = new LeafNode<T>(view, orthogonal(parent.orientation));
if (parent instanceof BranchNode) {
parent.addChild(node, size, index);
} else {
const [, grandParent] = tail(pathToParent);
const [, parentIndex] = tail(rest);
grandParent.removeChild(parentIndex);
const newParent = new BranchNode<T>(parent.orientation);
grandParent.addChild(newParent, 20, parentIndex);
newParent.addChild(parent, 20, 0);
newParent.addChild(node, size, index);
}
}
removeView(location: number[]): void {
const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
parent.removeChild(index);
if (parent.children.length === 0) {
throw new Error('Invalid grid state');
}
if (parent.children.length > 1) {
return;
}
const [, grandParent] = tail(pathToParent);
const [, parentIndex] = tail(rest);
const sibling = parent.removeChild(0);
grandParent.removeChild(parentIndex);
grandParent.addChild(sibling, 20, parentIndex);
}
layout(width: number, height: number): void {
this.root.layout(width);
this.root.orthogonalLayout(height);
}
moveView(from: number[], to: number[]): void {
throw new Error('Method not implemented.');
}
resizeView(location: number[], size: number): void {
const [rest, index] = tail(location);
const [, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
parent.resizeChild(index, size);
}
getViewSize(location: number[]): number {
const [rest, index] = tail(location);
const [, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
return parent.getChildSize(index);
}
getViews(): GridBranchNode<T> {
return this._getViews(this.root) as GridBranchNode<T>;
}
private _getViews(node: Node<T>): GridNode<T> {
if (node instanceof BranchNode) {
return new GridBranchNode(node.children.map(c => this._getViews(c)));
} else {
return new GridLeafNode(node.view);
}
}
private getNode(location: number[], node: Node<T> = this.root, path: BranchNode<T>[] = []): [BranchNode<T>[], Node<T>] {
if (location.length === 0) {
return [path, node];
}
if (!(node instanceof BranchNode)) {
throw new Error('Invalid location');
}
const [index, ...rest] = location;
if (index < 0 || index >= node.children.length) {
throw new Error('Invalid location');
}
const child = node.children[index];
path.push(node);
return this.getNode(rest, child, path);
}
dispose(): void {
this.root.dispose();
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 { Event, anyEvent } from 'vs/base/common/event';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { append, $ } from 'vs/base/browser/dom';
import { SplitView, IView } from 'vs/base/browser/ui/splitview/splitview';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
export class GridNode implements IView {
get minimumSize(): number {
let result = 0;
for (const child of this.children) {
for (const grandchild of child.children) {
result += grandchild.minimumSize;
}
}
return result === 0 ? 50 : result;
}
readonly maximumSize = Number.MAX_VALUE;
private _onDidChange: Event<number | undefined> = Event.None;
get onDidChange(): Event<number | undefined> {
return this._onDidChange;
}
protected orientation: Orientation | undefined;
protected size: number | undefined;
protected orthogonalSize: number | undefined;
private splitview: SplitView | undefined;
private children: GridNode[] = [];
private color: string | undefined;
constructor(private parent?: GridNode, orthogonalSize?: number, color?: string) {
this.orthogonalSize = orthogonalSize;
this.color = color || `hsl(${Math.round(Math.random() * 360)}, 72%, 72%)`;
}
render(container: HTMLElement): void {
container = append(container, $('.node'));
container.style.backgroundColor = this.color;
append(container, $('.action', { onclick: () => this.split(container, Orientation.HORIZONTAL) }, ''));
append(container, $('.action', { onclick: () => this.split(container, Orientation.VERTICAL) }, ''));
}
protected split(container: HTMLElement, orientation: Orientation): void {
if (this.parent && this.parent.orientation === orientation) {
const index = this.parent.children.indexOf(this);
this.parent.addChild(this.size / 2, this.orthogonalSize, index + 1);
} else {
this.branch(container, orientation);
}
}
protected branch(container: HTMLElement, orientation: Orientation): void {
this.orientation = orientation;
container.innerHTML = '';
this.splitview = new SplitView(container, { orientation });
this.layout(this.size);
this.orthogonalLayout(this.orthogonalSize);
this.addChild(this.orthogonalSize / 2, this.size, 0, this.color);
this.addChild(this.orthogonalSize / 2, this.size);
}
layout(size: number): void {
this.size = size;
for (const child of this.children) {
child.orthogonalLayout(size);
}
}
orthogonalLayout(size: number): void {
this.orthogonalSize = size;
if (this.splitview) {
this.splitview.layout(size);
}
}
private addChild(size: number, orthogonalSize: number, index?: number, color?: string): void {
const child = new GridNode(this, orthogonalSize, color);
this.splitview.addView(child, size, index);
if (typeof index === 'number') {
this.children.splice(index, 0, child);
} else {
this.children.push(child);
}
this._onDidChange = anyEvent(...this.children.map(c => c.onDidChange));
}
}
export class RootGridNode extends GridNode {
private width: number;
private height: number;
protected branch(container: HTMLElement, orientation: Orientation): void {
if (orientation === Orientation.VERTICAL) {
this.size = this.width;
this.orthogonalSize = this.height;
} else {
this.size = this.height;
this.orthogonalSize = this.width;
}
super.branch(container, orientation);
}
layoutBox(width: number, height: number): void {
if (this.orientation === Orientation.VERTICAL) {
this.layout(width);
this.orthogonalLayout(height);
} else if (this.orientation === Orientation.HORIZONTAL) {
this.layout(height);
this.orthogonalLayout(width);
} else {
this.width = width;
this.height = height;
}
}
}
export class Grid {
private root: RootGridNode;
constructor(container: HTMLElement) {
this.root = new RootGridNode();
this.root.render(container);
}
layout(width: number, height: number): void {
this.root.layoutBox(width, height);
}
}
/*---------------------------------------------------------------------------------------------
* 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 { clamp } from './numbers';
export interface ITreeNode<T> {
readonly element: T;
readonly children: ITreeNode<T>[];
}
function clone<T>(nodes: ITreeNode<T>[]): ITreeNode<T>[] {
return nodes.map(({ element, children }) => ({ element, children: clone(children) }));
}
export class Tree<T> {
private root: ITreeNode<T>[] = [];
splice(start: number[], deleteCount: number, nodes: ITreeNode<T>[] = []): ITreeNode<T>[] {
if (start.length === 0) {
throw new Error('invalid tree location');
}
const children = this.findChildren(start);
const index = start[start.length - 1];
return children.splice(index, deleteCount, ...clone(nodes));
}
getElement(location: number[]): T | undefined {
const node = this.findElement(location);
return node && node.element;
}
getNodes(): ITreeNode<T>[] {
return clone(this.root);
}
private findElement(location: number[]): ITreeNode<T> {
const children = this.findChildren(location);
const lastIndex = clamp(location[location.length - 1], 0, children.length);
return children[lastIndex];
}
private findChildren(location: number[], children: ITreeNode<T>[] = this.root): ITreeNode<T>[] {
if (location.length === 1) {
return children;
}
let [i, ...rest] = location;
i = clamp(i, 0, children.length);
return this.findChildren(rest, children[i].children);
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 { GridView, IView, Orientation, GridNode, GridBranchNode } from 'vs/base/browser/ui/grid/gridview';
class TestView implements IView {
private _onDidChange = new Emitter<number | undefined>();
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 _size = 0;
get size(): number { return this._size; }
private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>();
readonly onDidLayout = this._onDidLayout.event;
private _onDidFocus = new Emitter<void>();
readonly onDidFocus = this._onDidFocus.event;
constructor(
private _minimumSize: number,
private _maximumSize: number
) {
assert(_minimumSize <= _maximumSize, 'gridview 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._size = size;
this._onDidLayout.fire({ size, orientation });
}
focus(): void {
this._onDidFocus.fire();
}
dispose(): void {
this._onDidChange.dispose();
this._onDidRender.dispose();
this._onDidLayout.dispose();
this._onDidFocus.dispose();
}
}
function nodesToArrays(node: GridNode<any>): any {
if (node instanceof GridBranchNode) {
return node.children.map(nodesToArrays);
} else {
return node.view;
}
}
suite('GridView', function () {
let container: HTMLElement;
setup(function () {
container = document.createElement('div');
container.style.position = 'absolute';
container.style.width = `${200}px`;
container.style.height = `${200}px`;
});
teardown(function () {
container = null;
});
test('empty gridview is empty', function () {
const gridview = new GridView(container);
assert.deepEqual(gridview.getViews(), { children: [] });
gridview.dispose();
});
test('gridview addView', function () {
const gridview = new GridView(container);
const view = new TestView(20, 20);
assert.throws(() => gridview.addView(view, 200, []), 'empty location');
assert.throws(() => gridview.addView(view, 200, [1]), 'index overflow');
assert.throws(() => gridview.addView(view, 200, [0, 0]), 'hierarchy overflow');
const views = [
new TestView(20, 20),
new TestView(20, 20),
new TestView(20, 20)
];
gridview.addView(views[0], 200, [0]);
gridview.addView(views[1], 200, [1]);
gridview.addView(views[2], 200, [2]);
assert.deepEqual(nodesToArrays(gridview.getViews()), views);
gridview.dispose();
});
test('gridview addView nested', function () {
const gridview = new GridView(container);
const views = [
new TestView(20, 20),
[
new TestView(20, 20),
new TestView(20, 20)
]
];
gridview.addView(views[0] as IView, 200, [0]);
gridview.addView(views[1][0] as IView, 200, [1]);
gridview.addView(views[1][1] as IView, 200, [1, 1]);
assert.deepEqual(nodesToArrays(gridview.getViews()), views);
gridview.dispose();
});
test('gridview addView deep nested', function () {
const gridview = new GridView(container);
const view1 = new TestView(20, 20);
gridview.addView(view1 as IView, 200, [0]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1]);
const view2 = new TestView(20, 20);
gridview.addView(view2 as IView, 200, [1]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, view2]);
const view3 = new TestView(20, 20);
gridview.addView(view3 as IView, 200, [1, 0]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view3, view2]]);
const view4 = new TestView(20, 20);
gridview.addView(view4 as IView, 200, [1, 0, 0]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [[view4, view3], view2]]);
const view5 = new TestView(20, 20);
gridview.addView(view5 as IView, 200, [1, 0]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view4, view3], view2]]);
const view6 = new TestView(20, 20);
gridview.addView(view6 as IView, 200, [2]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view4, view3], view2], view6]);
const view7 = new TestView(20, 20);
gridview.addView(view7 as IView, 200, [1, 1]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, view7, [view4, view3], view2], view6]);
const view8 = new TestView(20, 20);
gridview.addView(view8 as IView, 200, [1, 1, 0]);
assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view8, view7], [view4, view3], view2], view6]);
gridview.dispose();
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 { Tree } from 'vs/base/common/tree';
suite('Base Tree', () => {
test('ctor', () => {
const tree = new Tree<number>();
assert(tree);
const nodes = tree.getNodes();
assert.equal(nodes.length, 0);
});
test('insert', () => {
const tree = new Tree<number>();
tree.splice([0], 0, [
{ element: 0, children: [] },
{ element: 1, children: [] },
{ element: 2, children: [] }
]);
const nodes = tree.getNodes();
assert.deepEqual(nodes.length, 3);
assert.deepEqual(nodes[0].element, 0);
assert.deepEqual(nodes[1].element, 1);
assert.deepEqual(nodes[2].element, 2);
});
test('deep insert', () => {
const tree = new Tree<number>();
tree.splice([0], 0, [
{
element: 0, children: [
{ element: 10, children: [] },
{ element: 11, children: [] },
{ element: 12, children: [] },
]
},
{ element: 1, children: [] },
{ element: 2, children: [] }
]);
const nodes = tree.getNodes();
assert.deepEqual(nodes.length, 3);
assert.deepEqual(nodes[0].element, 0);
assert.deepEqual(nodes[0].children.length, 3);
assert.deepEqual(nodes[0].children[0].element, 10);
assert.deepEqual(nodes[0].children[1].element, 11);
assert.deepEqual(nodes[0].children[2].element, 12);
assert.deepEqual(nodes[1].element, 1);
assert.deepEqual(nodes[2].element, 2);
});
test('delete', () => {
const tree = new Tree<number>();
tree.splice([0], 0, [
{ element: 0, children: [] },
{ element: 1, children: [] },
{ element: 2, children: [] }
]);
tree.splice([0], 3, []);
const nodes = tree.getNodes();
assert.equal(nodes.length, 0);
});
test('nested delete', () => {
const tree = new Tree<number>();
tree.splice([0], 0, [
{
element: 0, children: [
{ element: 10, children: [] },
{ element: 11, children: [] },
{ element: 12, children: [] },
]
},
{ element: 1, children: [] },
{ element: 2, children: [] }
]);
tree.splice([0, 1], 1, []);
const nodes = tree.getNodes();
assert.deepEqual(nodes.length, 3);
assert.deepEqual(nodes[0].element, 0);
assert.deepEqual(nodes[0].children.length, 2);
assert.deepEqual(nodes[0].children[0].element, 10);
assert.deepEqual(nodes[0].children[1].element, 12);
assert.deepEqual(nodes[1].element, 1);
assert.deepEqual(nodes[2].element, 2);
});
test('deep delete', () => {
const tree = new Tree<number>();
tree.splice([0], 0, [
{
element: 0, children: [
{ element: 10, children: [] },
{ element: 11, children: [] },
{ element: 12, children: [] },
]
},
{ element: 1, children: [] },
{ element: 2, children: [] }
]);
tree.splice([0], 1, []);
const nodes = tree.getNodes();
assert.deepEqual(nodes.length, 2);
assert.deepEqual(nodes[0].element, 1);
assert.deepEqual(nodes[1].element, 2);
});
});
......@@ -10,11 +10,23 @@
<script>
require.config({ baseUrl: '../out' });
require(['vs/base/browser/ui/splitview/grid'], ({ Grid }) => {
const grid = new Grid(document.body);
require(['vs/base/browser/ui/grid/gridview', 'vs/base/common/event'], ({ GridView }, { Event }) => {
const grid = new GridView(document.body);
const layout = () => grid.layout(document.body.clientWidth, document.body.clientHeight);
window.onresize = layout;
layout();
const view = {
minimumSize: 20,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None,
layout() { },
render() {
console.log('RENDER');
}
};
grid.addView(view, 200, [0]);
});
</script>
<style>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册