sussudio
Version:
An unofficial VS Code Internal API
1,147 lines (1,146 loc) • 47.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $ } from "../../dom.mjs";
import { Sizing, SplitView } from "../splitview/splitview.mjs";
import { equals as arrayEquals, tail2 as tail } from "../../../common/arrays.mjs";
import { Color } from "../../../common/color.mjs";
import { Emitter, Event, Relay } from "../../../common/event.mjs";
import { Disposable, DisposableStore, toDisposable } from "../../../common/lifecycle.mjs";
import { rot } from "../../../common/numbers.mjs";
import { isUndefined } from "../../../common/types.mjs";
import "../../../../css!./gridview.mjs";
export { Orientation } from "../sash/sash.mjs";
export { LayoutPriority, Sizing } from "../splitview/splitview.mjs";
const defaultStyles = {
separatorBorder: Color.transparent
};
export function orthogonal(orientation) {
return orientation === 0 /* Orientation.VERTICAL */ ? 1 /* Orientation.HORIZONTAL */ : 0 /* Orientation.VERTICAL */;
}
export function isGridBranchNode(node) {
return !!node.children;
}
class LayoutController {
isLayoutEnabled;
constructor(isLayoutEnabled) {
this.isLayoutEnabled = isLayoutEnabled;
}
}
function toAbsoluteBoundarySashes(sashes, orientation) {
if (orientation === 1 /* 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, orientation) {
if (orientation === 1 /* 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 };
}
}
function validateIndex(index, numChildren) {
if (Math.abs(index) > numChildren) {
throw new Error('Invalid index');
}
return rot(index, numChildren + 1);
}
class BranchNode {
orientation;
layoutController;
splitviewProportionalLayout;
element;
children = [];
splitview;
_size;
get size() { return this._size; }
_orthogonalSize;
get orthogonalSize() { return this._orthogonalSize; }
_absoluteOffset = 0;
get absoluteOffset() { return this._absoluteOffset; }
_absoluteOrthogonalOffset = 0;
get absoluteOrthogonalOffset() { return this._absoluteOrthogonalOffset; }
absoluteOrthogonalSize = 0;
_styles;
get styles() { return this._styles; }
get width() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.size : this.orthogonalSize;
}
get height() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.orthogonalSize : this.size;
}
get top() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this._absoluteOffset : this._absoluteOrthogonalOffset;
}
get left() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this._absoluteOrthogonalOffset : this._absoluteOffset;
}
get minimumSize() {
return this.children.length === 0 ? 0 : Math.max(...this.children.map(c => c.minimumOrthogonalSize));
}
get maximumSize() {
return Math.min(...this.children.map(c => c.maximumOrthogonalSize));
}
get priority() {
if (this.children.length === 0) {
return 0 /* LayoutPriority.Normal */;
}
const priorities = this.children.map(c => typeof c.priority === 'undefined' ? 0 /* LayoutPriority.Normal */ : c.priority);
if (priorities.some(p => p === 2 /* LayoutPriority.High */)) {
return 2 /* LayoutPriority.High */;
}
else if (priorities.some(p => p === 1 /* LayoutPriority.Low */)) {
return 1 /* LayoutPriority.Low */;
}
return 0 /* LayoutPriority.Normal */;
}
get proportionalLayout() {
if (this.children.length === 0) {
return true;
}
return this.children.every(c => c.proportionalLayout);
}
get minimumOrthogonalSize() {
return this.splitview.minimumSize;
}
get maximumOrthogonalSize() {
return this.splitview.maximumSize;
}
get minimumWidth() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.minimumOrthogonalSize : this.minimumSize;
}
get minimumHeight() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.minimumSize : this.minimumOrthogonalSize;
}
get maximumWidth() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.maximumOrthogonalSize : this.maximumSize;
}
get maximumHeight() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.maximumSize : this.maximumOrthogonalSize;
}
_onDidChange = new Emitter();
onDidChange = this._onDidChange.event;
_onDidScroll = new Emitter();
onDidScrollDisposable = Disposable.None;
onDidScroll = this._onDidScroll.event;
childrenChangeDisposable = Disposable.None;
_onDidSashReset = new Emitter();
onDidSashReset = this._onDidSashReset.event;
splitviewSashResetDisposable = Disposable.None;
childrenSashResetDisposable = Disposable.None;
_boundarySashes = {};
get boundarySashes() { return this._boundarySashes; }
set boundarySashes(boundarySashes) {
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,
};
}
}
_edgeSnapping = false;
get edgeSnapping() { return this._edgeSnapping; }
set edgeSnapping(edgeSnapping) {
if (this._edgeSnapping === edgeSnapping) {
return;
}
this._edgeSnapping = edgeSnapping;
for (const child of this.children) {
if (child instanceof BranchNode) {
child.edgeSnapping = edgeSnapping;
}
}
this.updateSplitviewEdgeSnappingEnablement();
}
constructor(orientation, layoutController, styles, splitviewProportionalLayout, size = 0, orthogonalSize = 0, edgeSnapping = false, childDescriptors) {
this.orientation = orientation;
this.layoutController = layoutController;
this.splitviewProportionalLayout = splitviewProportionalLayout;
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: splitviewProportionalLayout });
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: splitviewProportionalLayout, 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) {
this._styles = styles;
this.splitview.style(styles);
for (const child of this.children) {
if (child instanceof BranchNode) {
child.style(styles);
}
}
}
layout(size, offset, ctx) {
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) {
for (const child of this.children) {
child.setVisible(visible);
}
}
addChild(node, size, index, skipLayout) {
index = validateIndex(index, this.children.length);
this.splitview.addView(node, size, index, skipLayout);
this._addChild(node, index);
this.onDidChildrenChange();
}
_addChild(node, index) {
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, sizing) {
index = validateIndex(index, this.children.length);
this.splitview.removeView(index, sizing);
this._removeChild(index);
this.onDidChildrenChange();
}
_removeChild(index) {
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, to) {
from = validateIndex(from, this.children.length);
to = validateIndex(to, this.children.length);
if (from === to) {
return;
}
if (from < to) {
to--;
}
this.splitview.moveView(from, to);
const child = this._removeChild(from);
this._addChild(child, to);
this.onDidChildrenChange();
}
swapChildren(from, to) {
from = validateIndex(from, this.children.length);
to = validateIndex(to, this.children.length);
if (from === to) {
return;
}
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, size) {
index = validateIndex(index, this.children.length);
this.splitview.resizeView(index, size);
}
isChildSizeMaximized(index) {
return this.splitview.isViewSizeMaximized(index);
}
distributeViewSizes(recursive = false) {
this.splitview.distributeViewSizes();
if (recursive) {
for (const child of this.children) {
if (child instanceof BranchNode) {
child.distributeViewSizes(true);
}
}
}
}
getChildSize(index) {
index = validateIndex(index, this.children.length);
return this.splitview.getViewSize(index);
}
isChildVisible(index) {
index = validateIndex(index, this.children.length);
return this.splitview.isViewVisible(index);
}
setChildVisible(index, visible) {
index = validateIndex(index, this.children.length);
if (this.splitview.isViewVisible(index) === visible) {
return;
}
this.splitview.setViewVisible(index, visible);
}
getChildCachedVisibleSize(index) {
index = validateIndex(index, this.children.length);
return this.splitview.getViewCachedVisibleSize(index);
}
onDidChildrenChange() {
this.updateChildrenEvents();
this._onDidChange.fire(undefined);
}
updateChildrenEvents() {
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) {
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 === 0 /* 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;
});
}
updateSplitviewEdgeSnappingEnablement() {
this.splitview.startSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset > 0;
this.splitview.endSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;
}
dispose() {
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) {
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 {
view;
orientation;
layoutController;
_size = 0;
get size() { return this._size; }
_orthogonalSize;
get orthogonalSize() { return this._orthogonalSize; }
absoluteOffset = 0;
absoluteOrthogonalOffset = 0;
onDidScroll = Event.None;
onDidSashReset = Event.None;
_onDidLinkedWidthNodeChange = new Relay();
_linkedWidthNode = undefined;
get linkedWidthNode() { return this._linkedWidthNode; }
set linkedWidthNode(node) {
this._onDidLinkedWidthNodeChange.input = node ? node._onDidViewChange : Event.None;
this._linkedWidthNode = node;
this._onDidSetLinkedNode.fire(undefined);
}
_onDidLinkedHeightNodeChange = new Relay();
_linkedHeightNode = undefined;
get linkedHeightNode() { return this._linkedHeightNode; }
set linkedHeightNode(node) {
this._onDidLinkedHeightNodeChange.input = node ? node._onDidViewChange : Event.None;
this._linkedHeightNode = node;
this._onDidSetLinkedNode.fire(undefined);
}
_onDidSetLinkedNode = new Emitter();
_onDidViewChange;
onDidChange;
disposables = new DisposableStore();
constructor(view, orientation, layoutController, orthogonalSize, size = 0) {
this.view = view;
this.orientation = orientation;
this.layoutController = layoutController;
this._orthogonalSize = orthogonalSize;
this._size = size;
const onDidChange = createLatchedOnDidChangeViewEvent(view);
this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === 0 /* Orientation.VERTICAL */ ? e.width : e.height), this.disposables);
this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);
}
get width() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.orthogonalSize : this.size;
}
get height() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.size : this.orthogonalSize;
}
get top() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.absoluteOffset : this.absoluteOrthogonalOffset;
}
get left() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.absoluteOrthogonalOffset : this.absoluteOffset;
}
get element() {
return this.view.element;
}
get minimumWidth() {
return this.linkedWidthNode ? Math.max(this.linkedWidthNode.view.minimumWidth, this.view.minimumWidth) : this.view.minimumWidth;
}
get maximumWidth() {
return this.linkedWidthNode ? Math.min(this.linkedWidthNode.view.maximumWidth, this.view.maximumWidth) : this.view.maximumWidth;
}
get minimumHeight() {
return this.linkedHeightNode ? Math.max(this.linkedHeightNode.view.minimumHeight, this.view.minimumHeight) : this.view.minimumHeight;
}
get maximumHeight() {
return this.linkedHeightNode ? Math.min(this.linkedHeightNode.view.maximumHeight, this.view.maximumHeight) : this.view.maximumHeight;
}
get minimumSize() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.minimumHeight : this.minimumWidth;
}
get maximumSize() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.maximumHeight : this.maximumWidth;
}
get priority() {
return this.view.priority;
}
get proportionalLayout() {
return this.view.proportionalLayout ?? true;
}
get snap() {
return this.view.snap;
}
get minimumOrthogonalSize() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.minimumWidth : this.minimumHeight;
}
get maximumOrthogonalSize() {
return this.orientation === 1 /* Orientation.HORIZONTAL */ ? this.maximumWidth : this.maximumHeight;
}
_boundarySashes = {};
get boundarySashes() { return this._boundarySashes; }
set boundarySashes(boundarySashes) {
this._boundarySashes = boundarySashes;
this.view.setBoundarySashes?.(toAbsoluteBoundarySashes(boundarySashes, this.orientation));
}
layout(size, offset, ctx) {
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);
}
cachedWidth = 0;
cachedHeight = 0;
cachedTop = 0;
cachedLeft = 0;
_layout(width, height, top, left) {
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) {
this.view.setVisible?.(visible);
}
dispose() {
this.disposables.dispose();
}
}
function flipNode(node, size, orthogonalSize) {
if (node instanceof BranchNode) {
const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.splitviewProportionalLayout, 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;
}
else {
return new LeafNode(node.view, orthogonal(node.orientation), node.layoutController, orthogonalSize);
}
}
/**
* The {@link GridView} is the UI component which implements a two dimensional
* flex-like layout algorithm for a collection of {@link IView} instances, which
* are mostly HTMLElement instances with size constraints. A {@link GridView} is a
* tree composition of multiple {@link SplitView} instances, orthogonal between
* one another. It will respect view's size contraints, just like the SplitView.
*
* It has a low-level index based API, allowing for fine grain performant operations.
* Look into the {@link Grid} widget for a higher-level API.
*
* Features:
* - flex-like layout algorithm
* - snap support
* - corner sash support
* - Alt key modifier behavior, macOS style
* - layout (de)serialization
*/
export class GridView {
/**
* The DOM element for this view.
*/
element;
styles;
proportionalLayout;
_root;
onDidSashResetRelay = new Relay();
_onDidScroll = new Relay();
_onDidChange = new Relay();
_boundarySashes = {};
/**
* The layout controller makes sure layout only propagates
* to the views after the very first call to {@link GridView.layout}.
*/
layoutController;
disposable2x2 = Disposable.None;
get root() { return this._root; }
set root(root) {
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;
}
/**
* Fires whenever the user double clicks a {@link Sash sash}.
*/
onDidSashReset = this.onDidSashResetRelay.event;
/**
* Fires whenever the user scrolls a {@link SplitView} within
* the grid.
*/
onDidScroll = this._onDidScroll.event;
/**
* Fires whenever a view within the grid changes its size constraints.
*/
onDidChange = this._onDidChange.event;
/**
* The width of the grid.
*/
get width() { return this.root.width; }
/**
* The height of the grid.
*/
get height() { return this.root.height; }
/**
* The minimum width of the grid.
*/
get minimumWidth() { return this.root.minimumWidth; }
/**
* The minimum height of the grid.
*/
get minimumHeight() { return this.root.minimumHeight; }
/**
* The maximum width of the grid.
*/
get maximumWidth() { return this.root.maximumHeight; }
/**
* The maximum height of the grid.
*/
get maximumHeight() { return this.root.maximumHeight; }
get orientation() { return this._root.orientation; }
get boundarySashes() { return this._boundarySashes; }
/**
* The orientation of the grid. Matches the orientation of the root
* {@link SplitView} in the grid's tree model.
*/
set orientation(orientation) {
if (this._root.orientation === orientation) {
return;
}
const { size, orthogonalSize, absoluteOffset, absoluteOrthogonalOffset } = this._root;
this.root = flipNode(this._root, orthogonalSize, size);
this.root.layout(size, 0, { orthogonalSize, absoluteOffset: absoluteOrthogonalOffset, absoluteOrthogonalOffset: absoluteOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
this.boundarySashes = this.boundarySashes;
}
/**
* A collection of sashes perpendicular to each edge of the grid.
* Corner sashes will be created for each intersection.
*/
set boundarySashes(boundarySashes) {
this._boundarySashes = boundarySashes;
this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation);
}
/**
* Enable/disable edge snapping across all grid views.
*/
set edgeSnapping(edgeSnapping) {
this.root.edgeSnapping = edgeSnapping;
}
/**
* Create a new {@link GridView} instance.
*
* @remarks It's the caller's responsibility to append the
* {@link GridView.element} to the page's DOM.
*/
constructor(options = {}) {
this.element = $('.monaco-grid-view');
this.styles = options.styles || defaultStyles;
this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true;
this.layoutController = new LayoutController(false);
this.root = new BranchNode(0 /* Orientation.VERTICAL */, this.layoutController, this.styles, this.proportionalLayout);
}
style(styles) {
this.styles = styles;
this.root.style(styles);
}
/**
* Layout the {@link GridView}.
*
* Optionally provide a `top` and `left` positions, those will propagate
* as an origin for positions passed to {@link IView.layout}.
*
* @param width The width of the {@link GridView}.
* @param height The height of the {@link GridView}.
* @param top Optional, the top location of the {@link GridView}.
* @param left Optional, the left location of the {@link GridView}.
*/
layout(width, height, top = 0, left = 0) {
this.layoutController.isLayoutEnabled = true;
const [size, orthogonalSize, offset, orthogonalOffset] = this.root.orientation === 1 /* Orientation.HORIZONTAL */ ? [height, width, top, left] : [width, height, left, top];
this.root.layout(size, 0, { orthogonalSize, absoluteOffset: offset, absoluteOrthogonalOffset: orthogonalOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
}
/**
* Add a {@link IView view} to this {@link GridView}.
*
* @param view The view to add.
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
* @param location The {@link GridLocation location} to insert the view on.
*/
addView(view, size, location) {
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 = 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);
}
this.trySet2x2();
}
/**
* Remove a {@link IView view} from this {@link GridView}.
*
* @param location The {@link GridLocation location} of the {@link IView view}.
* @param sizing Whether to distribute other {@link IView view}'s sizes.
*/
removeView(location, sizing) {
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) {
this.trySet2x2();
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;
this.trySet2x2();
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]);
}
this.trySet2x2();
return node.view;
}
/**
* Move a {@link IView view} within its parent.
*
* @param parentLocation The {@link GridLocation location} of the {@link IView view}'s parent.
* @param from The index of the {@link IView view} to move.
* @param to The index where the {@link IView view} should move to.
*/
moveView(parentLocation, from, to) {
const [, parent] = this.getNode(parentLocation);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
parent.moveChild(from, to);
this.trySet2x2();
}
/**
* Swap two {@link IView views} within the {@link GridView}.
*
* @param from The {@link GridLocation location} of one view.
* @param to The {@link GridLocation location} of another view.
*/
swapViews(from, to) {
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);
}
this.trySet2x2();
}
/**
* Resize a {@link IView view}.
*
* @param location The {@link GridLocation location} of the view.
* @param size The size the view should be. Optionally provide a single dimension.
*/
resizeView(location, size) {
const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
if (!size.width && !size.height) {
return;
}
const [parentSize, grandParentSize] = parent.orientation === 1 /* Orientation.HORIZONTAL */ ? [size.width, size.height] : [size.height, size.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);
}
this.trySet2x2();
}
/**
* Get the size of a {@link IView view}.
*
* @param location The {@link GridLocation location} of the view. Provide `undefined` to get
* the size of the grid itself.
*/
getViewSize(location) {
if (!location) {
return { width: this.root.width, height: this.root.height };
}
const [, node] = this.getNode(location);
return { width: node.width, height: node.height };
}
/**
* Get the cached visible size of a {@link IView view}. This was the size
* of the view at the moment it last became hidden.
*
* @param location The {@link GridLocation location} of the view.
*/
getViewCachedVisibleSize(location) {
const [rest, index] = tail(location);
const [, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
return parent.getChildCachedVisibleSize(index);
}
/**
* Maximize the size of a {@link IView view} by collapsing all other views
* to their minimum sizes.
*
* @param location The {@link GridLocation location} of the view.
*/
maximizeViewSize(location) {
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);
}
}
/**
* Returns whether all other {@link IView views} are at their minimum size.
*
* @param location The {@link GridLocation location} of the view.
*/
isViewSizeMaximized(location) {
const [ancestors, node] = this.getNode(location);
if (!(node instanceof LeafNode)) {
throw new Error('Invalid location');
}
for (let i = 0; i < ancestors.length; i++) {
if (!ancestors[i].isChildSizeMaximized(location[i])) {
return false;
}
}
return true;
}
/**
* Distribute the size among all {@link IView views} within the entire
* grid or within a single {@link SplitView}.
*
* @param location The {@link GridLocation location} of a view containing
* children views, which will have their sizes distributed within the parent
* view's size. Provide `undefined` to recursively distribute all views' sizes
* in the entire grid.
*/
distributeViewSizes(location) {
if (!location) {
this.root.distributeViewSizes(true);
return;
}
const [, node] = this.getNode(location);
if (!(node instanceof BranchNode)) {
throw new Error('Invalid location');
}
node.distributeViewSizes();
this.trySet2x2();
}
/**
* Returns whether a {@link IView view} is visible.
*
* @param location The {@link GridLocation location} of the view.
*/
isViewVisible(location) {
const [rest, index] = tail(location);
const [, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid from location');
}
return parent.isChildVisible(index);
}
/**
* Set the visibility state of a {@link IView view}.
*
* @param location The {@link GridLocation location} of the view.
*/
setViewVisible(location, visible) {
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(location) {
const node = location ? this.getNode(location)[1] : this._root;
return this._getViews(node, this.orientation);
}
/**
* Construct a new {@link GridView} from a JSON object.
*
* @param json The JSON object.
* @param deserializer A deserializer which can revive each view.
* @returns A new {@link GridView} instance.
*/
static deserialize(json, deserializer, options = {}) {
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, orientation, deserializer, height);
return result;
}
_deserialize(root, orientation, deserializer, orthogonalSize) {
this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize);
}
_deserializeNode(node, orientation, deserializer, orthogonalSize) {
let result;
if (node.type === 'branch') {
const serializedChildren = node.data;
const children = serializedChildren.map(serializedChild => {
return {
node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size),
visible: serializedChild.visible
};
});
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;
}
_getViews(node, orientation, cachedVisibleSize) {
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 = [];
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 };
}
getNode(location, node = this.root, path = []) {
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);
}
/**
* Attempt to lock the {@link Sash sashes} in this {@link GridView} so
* the grid behaves as a 2x2 matrix, with a corner sash in the middle.
*
* In case the grid isn't a 2x2 grid _and_ all sashes are not aligned,
* this method is a no-op.
*/
trySet2x2() {
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);
}
/**
* Populate a map with views to DOM nodes.
* @remarks To be used internally only.
*/
getViewMap(map, node) {
if (!node) {
node = this.root;
}
if (node instanceof BranchNode) {
node.children.forEach(child => this.getViewMap(map, child));
}
else {
map.set(node.view, node.element);
}
}
dispose() {
this.onDidSashResetRelay.dispose();
this.root.dispose();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}