/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./gridview'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/dom'; import { equals as arrayEquals, tail2 as tail } from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { clamp } from 'vs/base/common/numbers'; import { isUndefined } from 'vs/base/common/types'; export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; export { Orientation } from 'vs/base/browser/ui/sash/sash'; export interface IViewSize { readonly width: number; readonly height: number; } interface IRelativeBoundarySashes { readonly start?: Sash; readonly end?: Sash; readonly orthogonalStart?: Sash; readonly orthogonalEnd?: Sash; } export interface IBoundarySashes { readonly top?: Sash; readonly right?: Sash; readonly bottom?: Sash; readonly left?: Sash; } export interface IView { readonly element: HTMLElement; readonly minimumWidth: number; readonly maximumWidth: number; readonly minimumHeight: number; readonly maximumHeight: number; readonly onDidChange: Event; readonly priority?: LayoutPriority; readonly snap?: boolean; layout(width: number, height: number, top: number, left: number): void; setVisible?(visible: boolean): void; setBoundarySashes?(sashes: IBoundarySashes): void; } export interface ISerializableView extends IView { toJSON(): object; } export interface IViewDeserializer { fromJSON(json: any): T; } export interface ISerializedLeafNode { type: 'leaf'; data: any; size: number; visible?: boolean; } export interface ISerializedBranchNode { type: 'branch'; data: ISerializedNode[]; size: number; } export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode; export interface ISerializedGridView { root: ISerializedNode; orientation: Orientation; width: number; height: number; } export function orthogonal(orientation: Orientation): Orientation { return orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; } export interface Box { readonly top: number; readonly left: number; readonly width: number; readonly height: number; } export interface GridLeafNode { readonly view: IView; readonly box: Box; readonly cachedVisibleSize: number | undefined; } export interface GridBranchNode { readonly children: GridNode[]; readonly box: Box; } export type GridNode = GridLeafNode | GridBranchNode; export function isGridBranchNode(node: GridNode): node is GridBranchNode { return !!(node as any).children; } export interface IGridViewStyles extends ISplitViewStyles { } const defaultStyles: IGridViewStyles = { separatorBorder: Color.transparent }; export interface ILayoutController { readonly isLayoutEnabled: boolean; } export class LayoutController implements ILayoutController { constructor(public isLayoutEnabled: boolean) { } } export class MultiplexLayoutController implements ILayoutController { get isLayoutEnabled(): boolean { return this.layoutControllers.every(l => l.isLayoutEnabled); } constructor(private layoutControllers: ILayoutController[]) { } } export interface IGridViewOptions { readonly styles?: IGridViewStyles; readonly proportionalLayout?: boolean; // default true readonly layoutController?: ILayoutController; } interface ILayoutContext { readonly orthogonalSize: number; readonly absoluteOffset: number; readonly absoluteOrthogonalOffset: number; readonly absoluteSize: number; readonly absoluteOrthogonalSize: number; } function toAbsoluteBoundarySashes(sashes: IRelativeBoundarySashes, orientation: Orientation): IBoundarySashes { if (orientation === Orientation.HORIZONTAL) { return { left: sashes.start, right: sashes.end, top: sashes.orthogonalStart, bottom: sashes.orthogonalEnd }; } else { return { top: sashes.start, bottom: sashes.end, left: sashes.orthogonalStart, right: sashes.orthogonalEnd }; } } function fromAbsoluteBoundarySashes(sashes: IBoundarySashes, orientation: Orientation): IRelativeBoundarySashes { if (orientation === Orientation.HORIZONTAL) { return { start: sashes.left, end: sashes.right, orthogonalStart: sashes.top, orthogonalEnd: sashes.bottom }; } else { return { start: sashes.top, end: sashes.bottom, orthogonalStart: sashes.left, orthogonalEnd: sashes.right }; } } class BranchNode implements ISplitView, IDisposable { readonly element: HTMLElement; readonly children: Node[] = []; private splitview: SplitView; private _size: number; get size(): number { return this._size; } private _orthogonalSize: number; get orthogonalSize(): number { return this._orthogonalSize; } private absoluteOffset: number = 0; private absoluteOrthogonalOffset: number = 0; private absoluteOrthogonalSize: number = 0; private _styles: IGridViewStyles; get styles(): IGridViewStyles { return this._styles; } get width(): number { return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize; } get height(): number { return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size; } get top(): number { return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset; } get left(): number { return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset; } get minimumSize(): number { return this.children.length === 0 ? 0 : Math.max(...this.children.map(c => c.minimumOrthogonalSize)); } get maximumSize(): number { return Math.min(...this.children.map(c => c.maximumOrthogonalSize)); } get priority(): LayoutPriority { if (this.children.length === 0) { return LayoutPriority.Normal; } const priorities = this.children.map(c => typeof c.priority === 'undefined' ? LayoutPriority.Normal : c.priority); if (priorities.some(p => p === LayoutPriority.High)) { return LayoutPriority.High; } else if (priorities.some(p => p === LayoutPriority.Low)) { return LayoutPriority.Low; } return LayoutPriority.Normal; } get minimumOrthogonalSize(): number { return this.splitview.minimumSize; } get maximumOrthogonalSize(): number { return this.splitview.maximumSize; } get minimumWidth(): number { return this.orientation === Orientation.HORIZONTAL ? this.minimumOrthogonalSize : this.minimumSize; } get minimumHeight(): number { return this.orientation === Orientation.HORIZONTAL ? this.minimumSize : this.minimumOrthogonalSize; } get maximumWidth(): number { return this.orientation === Orientation.HORIZONTAL ? this.maximumOrthogonalSize : this.maximumSize; } get maximumHeight(): number { return this.orientation === Orientation.HORIZONTAL ? this.maximumSize : this.maximumOrthogonalSize; } private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; private _onDidScroll = new Emitter(); private onDidScrollDisposable: IDisposable = Disposable.None; readonly onDidScroll: Event = this._onDidScroll.event; private childrenChangeDisposable: IDisposable = Disposable.None; private readonly _onDidSashReset = new Emitter(); readonly onDidSashReset: Event = this._onDidSashReset.event; private splitviewSashResetDisposable: IDisposable = Disposable.None; private childrenSashResetDisposable: IDisposable = Disposable.None; private _boundarySashes: IRelativeBoundarySashes = {}; get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; } set boundarySashes(boundarySashes: IRelativeBoundarySashes) { this._boundarySashes = boundarySashes; this.splitview.orthogonalStartSash = boundarySashes.orthogonalStart; this.splitview.orthogonalEndSash = boundarySashes.orthogonalEnd; for (let index = 0; index < this.children.length; index++) { const child = this.children[index]; const first = index === 0; const last = index === this.children.length - 1; child.boundarySashes = { start: boundarySashes.orthogonalStart, end: boundarySashes.orthogonalEnd, orthogonalStart: first ? boundarySashes.start : child.boundarySashes.orthogonalStart, orthogonalEnd: last ? boundarySashes.end : child.boundarySashes.orthogonalEnd, }; } } private _edgeSnapping = false; get edgeSnapping(): boolean { return this._edgeSnapping; } set edgeSnapping(edgeSnapping: boolean) { if (this._edgeSnapping === edgeSnapping) { return; } this._edgeSnapping = edgeSnapping; for (const child of this.children) { if (child instanceof BranchNode) { child.edgeSnapping = edgeSnapping; } } this.updateSplitviewEdgeSnappingEnablement(); } constructor( readonly orientation: Orientation, readonly layoutController: ILayoutController, styles: IGridViewStyles, readonly proportionalLayout: boolean, size: number = 0, orthogonalSize: number = 0, edgeSnapping: boolean = false, childDescriptors?: INodeDescriptor[] ) { this._styles = styles; this._size = size; this._orthogonalSize = orthogonalSize; this.element = $('.monaco-grid-branch-node'); if (!childDescriptors) { // Normal behavior, we have no children yet, just set up the splitview this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout }); this.splitview.layout(size, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize }); } else { // Reconstruction behavior, we want to reconstruct a splitview const descriptor = { views: childDescriptors.map(childDescriptor => { return { view: childDescriptor.node, size: childDescriptor.node.size, visible: childDescriptor.node instanceof LeafNode && childDescriptor.visible !== undefined ? childDescriptor.visible : true }; }), size: this.orthogonalSize }; const options = { proportionalLayout, orientation, styles }; this.children = childDescriptors.map(c => c.node); this.splitview = new SplitView(this.element, { ...options, descriptor }); this.children.forEach((node, index) => { const first = index === 0; const last = index === this.children.length; node.boundarySashes = { start: this.boundarySashes.orthogonalStart, end: this.boundarySashes.orthogonalEnd, orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1], orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index], }; }); } const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]); this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset); this.updateChildrenEvents(); } style(styles: IGridViewStyles): void { this._styles = styles; this.splitview.style(styles); for (const child of this.children) { if (child instanceof BranchNode) { child.style(styles); } } } layout(size: number, offset: number, ctx: ILayoutContext | undefined): void { if (!this.layoutController.isLayoutEnabled) { return; } if (typeof ctx === 'undefined') { throw new Error('Invalid state'); } // branch nodes should flip the normal/orthogonal directions this._size = ctx.orthogonalSize; this._orthogonalSize = size; this.absoluteOffset = ctx.absoluteOffset + offset; this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset; this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize; this.splitview.layout(ctx.orthogonalSize, { orthogonalSize: size, absoluteOffset: this.absoluteOrthogonalOffset, absoluteOrthogonalOffset: this.absoluteOffset, absoluteSize: ctx.absoluteOrthogonalSize, absoluteOrthogonalSize: ctx.absoluteSize }); this.updateSplitviewEdgeSnappingEnablement(); } setVisible(visible: boolean): void { for (const child of this.children) { child.setVisible(visible); } } addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void { if (index < 0 || index > this.children.length) { throw new Error('Invalid index'); } this.splitview.addView(node, size, index, skipLayout); this._addChild(node, index); this.onDidChildrenChange(); } private _addChild(node: Node, index: number): void { const first = index === 0; const last = index === this.children.length; this.children.splice(index, 0, node); node.boundarySashes = { start: this.boundarySashes.orthogonalStart, end: this.boundarySashes.orthogonalEnd, orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1], orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index], }; if (!first) { this.children[index - 1].boundarySashes = { ...this.children[index - 1].boundarySashes, orthogonalEnd: this.splitview.sashes[index - 1] }; } if (!last) { this.children[index + 1].boundarySashes = { ...this.children[index + 1].boundarySashes, orthogonalStart: this.splitview.sashes[index] }; } } removeChild(index: number, sizing?: Sizing): void { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } this.splitview.removeView(index, sizing); this._removeChild(index); this.onDidChildrenChange(); } private _removeChild(index: number): Node { const first = index === 0; const last = index === this.children.length - 1; const [child] = this.children.splice(index, 1); if (!first) { this.children[index - 1].boundarySashes = { ...this.children[index - 1].boundarySashes, orthogonalEnd: this.splitview.sashes[index - 1] }; } if (!last) { // [0,1,2,3] (2) => [0,1,3] this.children[index].boundarySashes = { ...this.children[index].boundarySashes, orthogonalStart: this.splitview.sashes[Math.max(index - 1, 0)] }; } return child; } moveChild(from: number, to: number): void { if (from === to) { return; } if (from < 0 || from >= this.children.length) { throw new Error('Invalid from index'); } to = clamp(to, 0, this.children.length); if (from < to) { to--; } this.splitview.moveView(from, to); const child = this._removeChild(from); this._addChild(child, to); this.onDidChildrenChange(); } swapChildren(from: number, to: number): void { if (from === to) { return; } if (from < 0 || from >= this.children.length) { throw new Error('Invalid from index'); } to = clamp(to, 0, this.children.length); this.splitview.swapViews(from, to); // swap boundary sashes [this.children[from].boundarySashes, this.children[to].boundarySashes] = [this.children[from].boundarySashes, this.children[to].boundarySashes]; // swap children [this.children[from], this.children[to]] = [this.children[to], this.children[from]]; this.onDidChildrenChange(); } resizeChild(index: number, size: number): void { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } this.splitview.resizeView(index, size); } distributeViewSizes(recursive = false): void { this.splitview.distributeViewSizes(); if (recursive) { for (const child of this.children) { if (child instanceof BranchNode) { child.distributeViewSizes(true); } } } } getChildSize(index: number): number { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } return this.splitview.getViewSize(index); } isChildVisible(index: number): boolean { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } return this.splitview.isViewVisible(index); } setChildVisible(index: number, visible: boolean): void { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } if (this.splitview.isViewVisible(index) === visible) { return; } this.splitview.setViewVisible(index, visible); } getChildCachedVisibleSize(index: number): number | undefined { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } return this.splitview.getViewCachedVisibleSize(index); } private onDidChildrenChange(): void { this.updateChildrenEvents(); this._onDidChange.fire(undefined); } private updateChildrenEvents(): void { const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined); this.childrenChangeDisposable.dispose(); this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange); const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location]))); this.childrenSashResetDisposable.dispose(); this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset); const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll)); this.onDidScrollDisposable.dispose(); this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll); } trySet2x2(other: BranchNode): IDisposable { if (this.children.length !== 2 || other.children.length !== 2) { return Disposable.None; } if (this.getChildSize(0) !== other.getChildSize(0)) { return Disposable.None; } const [firstChild, secondChild] = this.children; const [otherFirstChild, otherSecondChild] = other.children; if (!(firstChild instanceof LeafNode) || !(secondChild instanceof LeafNode)) { return Disposable.None; } if (!(otherFirstChild instanceof LeafNode) || !(otherSecondChild instanceof LeafNode)) { return Disposable.None; } if (this.orientation === Orientation.VERTICAL) { secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = firstChild; firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = secondChild; otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = otherFirstChild; otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = otherSecondChild; } else { otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = firstChild; otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = secondChild; firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = otherFirstChild; secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = otherSecondChild; } const mySash = this.splitview.sashes[0]; const otherSash = other.splitview.sashes[0]; mySash.linkedSash = otherSash; otherSash.linkedSash = mySash; this._onDidChange.fire(undefined); other._onDidChange.fire(undefined); return toDisposable(() => { mySash.linkedSash = otherSash.linkedSash = undefined; firstChild.linkedHeightNode = firstChild.linkedWidthNode = undefined; secondChild.linkedHeightNode = secondChild.linkedWidthNode = undefined; otherFirstChild.linkedHeightNode = otherFirstChild.linkedWidthNode = undefined; otherSecondChild.linkedHeightNode = otherSecondChild.linkedWidthNode = undefined; }); } private updateSplitviewEdgeSnappingEnablement(): void { this.splitview.startSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset > 0; this.splitview.endSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize; } dispose(): void { for (const child of this.children) { child.dispose(); } this._onDidChange.dispose(); this._onDidSashReset.dispose(); this.splitviewSashResetDisposable.dispose(); this.childrenSashResetDisposable.dispose(); this.childrenChangeDisposable.dispose(); this.splitview.dispose(); } } /** * Creates a latched event that avoids being fired when the view * constraints do not change at all. */ function createLatchedOnDidChangeViewEvent(view: IView): Event { const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split(view.onDidChange, isUndefined); return Event.any( onDidSetViewSize, Event.map( Event.latch( Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])), arrayEquals ), _ => undefined ) ); } class LeafNode implements ISplitView, IDisposable { private _size: number = 0; get size(): number { return this._size; } private _orthogonalSize: number; get orthogonalSize(): number { return this._orthogonalSize; } private absoluteOffset: number = 0; private absoluteOrthogonalOffset: number = 0; readonly onDidScroll: Event = Event.None; readonly onDidSashReset: Event = Event.None; private _onDidLinkedWidthNodeChange = new Relay(); private _linkedWidthNode: LeafNode | undefined = undefined; get linkedWidthNode(): LeafNode | undefined { return this._linkedWidthNode; } set linkedWidthNode(node: LeafNode | undefined) { this._onDidLinkedWidthNodeChange.input = node ? node._onDidViewChange : Event.None; this._linkedWidthNode = node; this._onDidSetLinkedNode.fire(undefined); } private _onDidLinkedHeightNodeChange = new Relay(); private _linkedHeightNode: LeafNode | undefined = undefined; get linkedHeightNode(): LeafNode | undefined { return this._linkedHeightNode; } set linkedHeightNode(node: LeafNode | undefined) { this._onDidLinkedHeightNodeChange.input = node ? node._onDidViewChange : Event.None; this._linkedHeightNode = node; this._onDidSetLinkedNode.fire(undefined); } private readonly _onDidSetLinkedNode = new Emitter(); private _onDidViewChange: Event; readonly onDidChange: Event; constructor( readonly view: IView, readonly orientation: Orientation, readonly layoutController: ILayoutController, orthogonalSize: number, size: number = 0 ) { this._orthogonalSize = orthogonalSize; this._size = size; const onDidChange = createLatchedOnDidChangeViewEvent(view); this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height)); this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event); } get width(): number { return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size; } get height(): number { return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize; } get top(): number { return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset; } get left(): number { return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset; } get element(): HTMLElement { return this.view.element; } private get minimumWidth(): number { return this.linkedWidthNode ? Math.max(this.linkedWidthNode.view.minimumWidth, this.view.minimumWidth) : this.view.minimumWidth; } private get maximumWidth(): number { return this.linkedWidthNode ? Math.min(this.linkedWidthNode.view.maximumWidth, this.view.maximumWidth) : this.view.maximumWidth; } private get minimumHeight(): number { return this.linkedHeightNode ? Math.max(this.linkedHeightNode.view.minimumHeight, this.view.minimumHeight) : this.view.minimumHeight; } private get maximumHeight(): number { return this.linkedHeightNode ? Math.min(this.linkedHeightNode.view.maximumHeight, this.view.maximumHeight) : this.view.maximumHeight; } get minimumSize(): number { return this.orientation === Orientation.HORIZONTAL ? this.minimumHeight : this.minimumWidth; } get maximumSize(): number { return this.orientation === Orientation.HORIZONTAL ? this.maximumHeight : this.maximumWidth; } get priority(): LayoutPriority | undefined { return this.view.priority; } get snap(): boolean | undefined { return this.view.snap; } get minimumOrthogonalSize(): number { return this.orientation === Orientation.HORIZONTAL ? this.minimumWidth : this.minimumHeight; } get maximumOrthogonalSize(): number { return this.orientation === Orientation.HORIZONTAL ? this.maximumWidth : this.maximumHeight; } private _boundarySashes: IRelativeBoundarySashes = {}; get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; } set boundarySashes(boundarySashes: IRelativeBoundarySashes) { this._boundarySashes = boundarySashes; if (this.view.setBoundarySashes) { this.view.setBoundarySashes(toAbsoluteBoundarySashes(boundarySashes, this.orientation)); } } layout(size: number, offset: number, ctx: ILayoutContext | undefined): void { if (!this.layoutController.isLayoutEnabled) { return; } if (typeof ctx === 'undefined') { throw new Error('Invalid state'); } this._size = size; this._orthogonalSize = ctx.orthogonalSize; this.absoluteOffset = ctx.absoluteOffset + offset; this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset; this._layout(this.width, this.height, this.top, this.left); } private cachedWidth: number = 0; private cachedHeight: number = 0; private cachedTop: number = 0; private cachedLeft: number = 0; private _layout(width: number, height: number, top: number, left: number): void { if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) { return; } this.cachedWidth = width; this.cachedHeight = height; this.cachedTop = top; this.cachedLeft = left; this.view.layout(width, height, top, left); } setVisible(visible: boolean): void { if (this.view.setVisible) { this.view.setVisible(visible); } } dispose(): void { } } type Node = BranchNode | LeafNode; export interface INodeDescriptor { node: Node; visible?: boolean; } function flipNode(node: T, size: number, orthogonalSize: number): T { if (node instanceof BranchNode) { const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.proportionalLayout, size, orthogonalSize, node.edgeSnapping); let totalSize = 0; for (let i = node.children.length - 1; i >= 0; i--) { const child = node.children[i]; const childSize = child instanceof BranchNode ? child.orthogonalSize : child.size; let newSize = node.size === 0 ? 0 : Math.round((size * childSize) / node.size); totalSize += newSize; // The last view to add should adjust to rounding errors if (i === 0) { newSize += size - totalSize; } result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true); } return result as T; } else { return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), node.layoutController, orthogonalSize) as T; } } export class GridView implements IDisposable { readonly element: HTMLElement; private styles: IGridViewStyles; private proportionalLayout: boolean; private _root!: BranchNode; private onDidSashResetRelay = new Relay(); readonly onDidSashReset: Event = this.onDidSashResetRelay.event; private disposable2x2: IDisposable = Disposable.None; private get root(): BranchNode { return this._root; } private set root(root: BranchNode) { const oldRoot = this._root; if (oldRoot) { this.element.removeChild(oldRoot.element); oldRoot.dispose(); } this._root = root; this.element.appendChild(root.element); this.onDidSashResetRelay.input = root.onDidSashReset; this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO this._onDidScroll.input = root.onDidScroll; } get orientation(): Orientation { return this._root.orientation; } set orientation(orientation: Orientation) { if (this._root.orientation === orientation) { return; } const { size, orthogonalSize } = this._root; this.root = flipNode(this._root, orthogonalSize, size); this.root.layout(size, 0, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize }); this.boundarySashes = this.boundarySashes; } get width(): number { return this.root.width; } get height(): number { return this.root.height; } get minimumWidth(): number { return this.root.minimumWidth; } get minimumHeight(): number { return this.root.minimumHeight; } get maximumWidth(): number { return this.root.maximumHeight; } get maximumHeight(): number { return this.root.maximumHeight; } private _onDidScroll = new Relay(); readonly onDidScroll = this._onDidScroll.event; private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; private _boundarySashes: IBoundarySashes = {}; get boundarySashes(): IBoundarySashes { return this._boundarySashes; } set boundarySashes(boundarySashes: IBoundarySashes) { this._boundarySashes = boundarySashes; this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation); } set edgeSnapping(edgeSnapping: boolean) { this.root.edgeSnapping = edgeSnapping; } /** * The first layout controller makes sure layout only propagates * to the views after the very first call to gridview.layout() */ private firstLayoutController: LayoutController; private layoutController: LayoutController; constructor(options: IGridViewOptions = {}) { this.element = $('.monaco-grid-view'); this.styles = options.styles || defaultStyles; this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true; this.firstLayoutController = new LayoutController(false); this.layoutController = new MultiplexLayoutController([ this.firstLayoutController, ...(options.layoutController ? [options.layoutController] : []) ]); this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout); } getViewMap(map: Map, node?: Node): void { if (!node) { node = this.root; } if (node instanceof BranchNode) { node.children.forEach(child => this.getViewMap(map, child)); } else { map.set(node.view, node.element); } } style(styles: IGridViewStyles): void { this.styles = styles; this.root.style(styles); } layout(width: number, height: number): void { this.firstLayoutController.isLayoutEnabled = true; const [size, orthogonalSize] = this.root.orientation === Orientation.HORIZONTAL ? [height, width] : [width, height]; this.root.layout(size, 0, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize }); } addView(view: IView, size: number | Sizing, location: number[]): void { this.disposable2x2.dispose(); this.disposable2x2 = Disposable.None; const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); if (parent instanceof BranchNode) { const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize); parent.addChild(node, size, index); } else { const [, grandParent] = tail(pathToParent); const [, parentIndex] = tail(rest); let newSiblingSize: number | Sizing = 0; const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(parentIndex); if (typeof newSiblingCachedVisibleSize === 'number') { newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize); } grandParent.removeChild(parentIndex); const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping); grandParent.addChild(newParent, parent.size, parentIndex); const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size); newParent.addChild(newSibling, newSiblingSize, 0); if (typeof size !== 'number' && size.type === 'split') { size = Sizing.Split(0); } const node = new LeafNode(view, grandParent.orientation, this.layoutController, parent.size); newParent.addChild(node, size, index); } } removeView(location: number[], sizing?: Sizing): IView { this.disposable2x2.dispose(); this.disposable2x2 = Disposable.None; const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } const node = parent.children[index]; if (!(node instanceof LeafNode)) { throw new Error('Invalid location'); } parent.removeChild(index, sizing); if (parent.children.length === 0) { throw new Error('Invalid grid state'); } if (parent.children.length > 1) { return node.view; } if (pathToParent.length === 0) { // parent is root const sibling = parent.children[0]; if (sibling instanceof LeafNode) { return node.view; } // we must promote sibling to be the new root parent.removeChild(0); this.root = sibling; this.boundarySashes = this.boundarySashes; return node.view; } const [, grandParent] = tail(pathToParent); const [, parentIndex] = tail(rest); const sibling = parent.children[0]; const isSiblingVisible = parent.isChildVisible(0); parent.removeChild(0); const sizes = grandParent.children.map((_, i) => grandParent.getChildSize(i)); grandParent.removeChild(parentIndex, sizing); if (sibling instanceof BranchNode) { sizes.splice(parentIndex, 1, ...sibling.children.map(c => c.size)); for (let i = 0; i < sibling.children.length; i++) { const child = sibling.children[i]; grandParent.addChild(child, child.size, parentIndex + i); } } else { const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size); const sizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize); grandParent.addChild(newSibling, sizing, parentIndex); } for (let i = 0; i < sizes.length; i++) { grandParent.resizeChild(i, sizes[i]); } return node.view; } moveView(parentLocation: number[], from: number, to: number): void { const [, parent] = this.getNode(parentLocation); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } parent.moveChild(from, to); } swapViews(from: number[], to: number[]): void { const [fromRest, fromIndex] = tail(from); const [, fromParent] = this.getNode(fromRest); if (!(fromParent instanceof BranchNode)) { throw new Error('Invalid from location'); } const fromSize = fromParent.getChildSize(fromIndex); const fromNode = fromParent.children[fromIndex]; if (!(fromNode instanceof LeafNode)) { throw new Error('Invalid from location'); } const [toRest, toIndex] = tail(to); const [, toParent] = this.getNode(toRest); if (!(toParent instanceof BranchNode)) { throw new Error('Invalid to location'); } const toSize = toParent.getChildSize(toIndex); const toNode = toParent.children[toIndex]; if (!(toNode instanceof LeafNode)) { throw new Error('Invalid to location'); } if (fromParent === toParent) { fromParent.swapChildren(fromIndex, toIndex); } else { fromParent.removeChild(fromIndex); toParent.removeChild(toIndex); fromParent.addChild(toNode, fromSize, fromIndex); toParent.addChild(fromNode, toSize, toIndex); } } resizeView(location: number[], { width, height }: Partial): void { const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } if (!width && !height) { return; } const [parentSize, grandParentSize] = parent.orientation === Orientation.HORIZONTAL ? [width, height] : [height, width]; if (typeof grandParentSize === 'number' && pathToParent.length > 0) { const [, grandParent] = tail(pathToParent); const [, parentIndex] = tail(rest); grandParent.resizeChild(parentIndex, grandParentSize); } if (typeof parentSize === 'number') { parent.resizeChild(index, parentSize); } } getViewSize(location?: number[]): IViewSize { if (!location) { return { width: this.root.width, height: this.root.height }; } const [, node] = this.getNode(location); return { width: node.width, height: node.height }; } getViewCachedVisibleSize(location: number[]): number | undefined { const [rest, index] = tail(location); const [, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } return parent.getChildCachedVisibleSize(index); } maximizeViewSize(location: number[]): void { const [ancestors, node] = this.getNode(location); if (!(node instanceof LeafNode)) { throw new Error('Invalid location'); } for (let i = 0; i < ancestors.length; i++) { ancestors[i].resizeChild(location[i], Number.POSITIVE_INFINITY); } } distributeViewSizes(location?: number[]): void { if (!location) { this.root.distributeViewSizes(true); return; } const [, node] = this.getNode(location); if (!(node instanceof BranchNode)) { throw new Error('Invalid location'); } node.distributeViewSizes(); } isViewVisible(location: number[]): boolean { const [rest, index] = tail(location); const [, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid from location'); } return parent.isChildVisible(index); } setViewVisible(location: number[], visible: boolean): void { const [rest, index] = tail(location); const [, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid from location'); } parent.setChildVisible(index, visible); } getView(): GridBranchNode; getView(location?: number[]): GridNode; getView(location?: number[]): GridNode { const node = location ? this.getNode(location)[1] : this._root; return this._getViews(node, this.orientation); } static deserialize(json: ISerializedGridView, deserializer: IViewDeserializer, options: IGridViewOptions = {}): GridView { if (typeof json.orientation !== 'number') { throw new Error('Invalid JSON: \'orientation\' property must be a number.'); } else if (typeof json.width !== 'number') { throw new Error('Invalid JSON: \'width\' property must be a number.'); } else if (typeof json.height !== 'number') { throw new Error('Invalid JSON: \'height\' property must be a number.'); } else if (json.root?.type !== 'branch') { throw new Error('Invalid JSON: \'root\' property must have \'type\' value of branch.'); } const orientation = json.orientation; const height = json.height; const result = new GridView(options); result._deserialize(json.root as ISerializedBranchNode, orientation, deserializer, height); return result; } private _deserialize(root: ISerializedBranchNode, orientation: Orientation, deserializer: IViewDeserializer, orthogonalSize: number): void { this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize) as BranchNode; } private _deserializeNode(node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer, orthogonalSize: number): Node { let result: Node; if (node.type === 'branch') { const serializedChildren = node.data as ISerializedNode[]; const children = serializedChildren.map(serializedChild => { return { node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size), visible: (serializedChild as { visible?: boolean }).visible } as INodeDescriptor; }); result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children); } else { result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size); } return result; } private _getViews(node: Node, orientation: Orientation, cachedVisibleSize?: number): GridNode { const box = { top: node.top, left: node.left, width: node.width, height: node.height }; if (node instanceof LeafNode) { return { view: node.view, box, cachedVisibleSize }; } const children: GridNode[] = []; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const cachedVisibleSize = node.getChildCachedVisibleSize(i); children.push(this._getViews(child, orthogonal(orientation), cachedVisibleSize)); } return { children, box }; } private getNode(location: number[], node: Node = this.root, path: BranchNode[] = []): [BranchNode[], Node] { 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); } trySet2x2(): void { this.disposable2x2.dispose(); this.disposable2x2 = Disposable.None; if (this.root.children.length !== 2) { return; } const [first, second] = this.root.children; if (!(first instanceof BranchNode) || !(second instanceof BranchNode)) { return; } this.disposable2x2 = first.trySet2x2(second); } dispose(): void { this.onDidSashResetRelay.dispose(); this.root.dispose(); if (this.element && this.element.parentElement) { this.element.parentElement.removeChild(this.element); } } }