Path: blob/main/src/vs/base/browser/ui/grid/gridview.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $ } from '../../dom.js';6import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js';7import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js';8import { equals as arrayEquals, tail } from '../../../common/arrays.js';9import { Color } from '../../../common/color.js';10import { Emitter, Event, Relay } from '../../../common/event.js';11import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';12import { rot } from '../../../common/numbers.js';13import { isUndefined } from '../../../common/types.js';14import './gridview.css';1516export { Orientation } from '../sash/sash.js';17export { LayoutPriority, Sizing } from '../splitview/splitview.js';1819export interface IGridViewStyles extends ISplitViewStyles { }2021const defaultStyles: IGridViewStyles = {22separatorBorder: Color.transparent23};2425export interface IViewSize {26readonly width: number;27readonly height: number;28}2930interface IRelativeBoundarySashes {31readonly start?: Sash;32readonly end?: Sash;33readonly orthogonalStart?: Sash;34readonly orthogonalEnd?: Sash;35}3637/**38* The interface to implement for views within a {@link GridView}.39*/40export interface IView {4142/**43* The DOM element for this view.44*/45readonly element: HTMLElement;4647/**48* A minimum width for this view.49*50* @remarks If none, set it to `0`.51*/52readonly minimumWidth: number;5354/**55* A minimum width for this view.56*57* @remarks If none, set it to `Number.POSITIVE_INFINITY`.58*/59readonly maximumWidth: number;6061/**62* A minimum height for this view.63*64* @remarks If none, set it to `0`.65*/66readonly minimumHeight: number;6768/**69* A minimum height for this view.70*71* @remarks If none, set it to `Number.POSITIVE_INFINITY`.72*/73readonly maximumHeight: number;7475/**76* The priority of the view when the {@link GridView} layout algorithm77* runs. Views with higher priority will be resized first.78*79* @remarks Only used when `proportionalLayout` is false.80*/81readonly priority?: LayoutPriority;8283/**84* If the {@link GridView} supports proportional layout,85* this property allows for finer control over the proportional layout algorithm, per view.86*87* @defaultValue `true`88*/89readonly proportionalLayout?: boolean;9091/**92* Whether the view will snap whenever the user reaches its minimum size or93* attempts to grow it beyond the minimum size.94*95* @defaultValue `false`96*/97readonly snap?: boolean;9899/**100* View instances are supposed to fire this event whenever any of the constraint101* properties have changed:102*103* - {@link IView.minimumWidth}104* - {@link IView.maximumWidth}105* - {@link IView.minimumHeight}106* - {@link IView.maximumHeight}107* - {@link IView.priority}108* - {@link IView.snap}109*110* The {@link GridView} will relayout whenever that happens. The event can111* optionally emit the view's preferred size for that relayout.112*/113readonly onDidChange: Event<IViewSize | undefined>;114115/**116* This will be called by the {@link GridView} during layout. A view meant to117* pass along the layout information down to its descendants.118*/119layout(width: number, height: number, top: number, left: number): void;120121/**122* This will be called by the {@link GridView} whenever this view is made123* visible or hidden.124*125* @param visible Whether the view becomes visible.126*/127setVisible?(visible: boolean): void;128129/**130* This will be called by the {@link GridView} whenever this view is on131* an edge of the grid and the grid's132* {@link GridView.boundarySashes boundary sashes} change.133*/134setBoundarySashes?(sashes: IBoundarySashes): void;135}136137export interface ISerializableView extends IView {138toJSON(): object;139}140141export interface IViewDeserializer<T extends ISerializableView> {142fromJSON(json: any): T;143}144145export interface ISerializedLeafNode {146type: 'leaf';147data: unknown;148size: number;149visible?: boolean;150maximized?: boolean;151}152153export interface ISerializedBranchNode {154type: 'branch';155data: ISerializedNode[];156size: number;157visible?: boolean;158}159160export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;161162export interface ISerializedGridView {163root: ISerializedNode;164orientation: Orientation;165width: number;166height: number;167}168169export function orthogonal(orientation: Orientation): Orientation {170return orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;171}172173export interface Box {174readonly top: number;175readonly left: number;176readonly width: number;177readonly height: number;178}179180export interface GridLeafNode {181readonly view: IView;182readonly box: Box;183readonly cachedVisibleSize: number | undefined;184readonly maximized: boolean;185}186187export interface GridBranchNode {188readonly children: GridNode[];189readonly box: Box;190}191192export type GridNode = GridLeafNode | GridBranchNode;193194export function isGridBranchNode(node: GridNode): node is GridBranchNode {195return !!(node as any).children;196}197198class LayoutController {199constructor(public isLayoutEnabled: boolean) { }200}201202export interface IGridViewOptions {203204/**205* Styles overriding the {@link defaultStyles default ones}.206*/207readonly styles?: IGridViewStyles;208209/**210* Resize each view proportionally when resizing the {@link GridView}.211*212* @defaultValue `true`213*/214readonly proportionalLayout?: boolean; // default true215}216217interface ILayoutContext {218readonly orthogonalSize: number;219readonly absoluteOffset: number;220readonly absoluteOrthogonalOffset: number;221readonly absoluteSize: number;222readonly absoluteOrthogonalSize: number;223}224225function toAbsoluteBoundarySashes(sashes: IRelativeBoundarySashes, orientation: Orientation): IBoundarySashes {226if (orientation === Orientation.HORIZONTAL) {227return { left: sashes.start, right: sashes.end, top: sashes.orthogonalStart, bottom: sashes.orthogonalEnd };228} else {229return { top: sashes.start, bottom: sashes.end, left: sashes.orthogonalStart, right: sashes.orthogonalEnd };230}231}232233function fromAbsoluteBoundarySashes(sashes: IBoundarySashes, orientation: Orientation): IRelativeBoundarySashes {234if (orientation === Orientation.HORIZONTAL) {235return { start: sashes.left, end: sashes.right, orthogonalStart: sashes.top, orthogonalEnd: sashes.bottom };236} else {237return { start: sashes.top, end: sashes.bottom, orthogonalStart: sashes.left, orthogonalEnd: sashes.right };238}239}240241function validateIndex(index: number, numChildren: number): number {242if (Math.abs(index) > numChildren) {243throw new Error('Invalid index');244}245246return rot(index, numChildren + 1);247}248249class BranchNode implements ISplitView<ILayoutContext>, IDisposable {250251readonly element: HTMLElement;252readonly children: Node[] = [];253private splitview: SplitView<ILayoutContext, Node>;254255private _size: number;256get size(): number { return this._size; }257258private _orthogonalSize: number;259get orthogonalSize(): number { return this._orthogonalSize; }260261private _absoluteOffset: number = 0;262get absoluteOffset(): number { return this._absoluteOffset; }263264private _absoluteOrthogonalOffset: number = 0;265get absoluteOrthogonalOffset(): number { return this._absoluteOrthogonalOffset; }266267private absoluteOrthogonalSize: number = 0;268269private _styles: IGridViewStyles;270get styles(): IGridViewStyles { return this._styles; }271272get width(): number {273return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;274}275276get height(): number {277return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;278}279280get top(): number {281return this.orientation === Orientation.HORIZONTAL ? this._absoluteOffset : this._absoluteOrthogonalOffset;282}283284get left(): number {285return this.orientation === Orientation.HORIZONTAL ? this._absoluteOrthogonalOffset : this._absoluteOffset;286}287288get minimumSize(): number {289return this.children.length === 0 ? 0 : Math.max(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.minimumOrthogonalSize : 0));290}291292get maximumSize(): number {293return Math.min(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.maximumOrthogonalSize : Number.POSITIVE_INFINITY));294}295296get priority(): LayoutPriority {297if (this.children.length === 0) {298return LayoutPriority.Normal;299}300301const priorities = this.children.map(c => typeof c.priority === 'undefined' ? LayoutPriority.Normal : c.priority);302303if (priorities.some(p => p === LayoutPriority.High)) {304return LayoutPriority.High;305} else if (priorities.some(p => p === LayoutPriority.Low)) {306return LayoutPriority.Low;307}308309return LayoutPriority.Normal;310}311312get proportionalLayout(): boolean {313if (this.children.length === 0) {314return true;315}316317return this.children.every(c => c.proportionalLayout);318}319320get minimumOrthogonalSize(): number {321return this.splitview.minimumSize;322}323324get maximumOrthogonalSize(): number {325return this.splitview.maximumSize;326}327328get minimumWidth(): number {329return this.orientation === Orientation.HORIZONTAL ? this.minimumOrthogonalSize : this.minimumSize;330}331332get minimumHeight(): number {333return this.orientation === Orientation.HORIZONTAL ? this.minimumSize : this.minimumOrthogonalSize;334}335336get maximumWidth(): number {337return this.orientation === Orientation.HORIZONTAL ? this.maximumOrthogonalSize : this.maximumSize;338}339340get maximumHeight(): number {341return this.orientation === Orientation.HORIZONTAL ? this.maximumSize : this.maximumOrthogonalSize;342}343344private readonly _onDidChange = new Emitter<number | undefined>();345readonly onDidChange: Event<number | undefined> = this._onDidChange.event;346347private readonly _onDidVisibilityChange = new Emitter<boolean>();348readonly onDidVisibilityChange: Event<boolean> = this._onDidVisibilityChange.event;349private readonly childrenVisibilityChangeDisposable: DisposableStore = new DisposableStore();350351private _onDidScroll = new Emitter<void>();352private onDidScrollDisposable: IDisposable = Disposable.None;353readonly onDidScroll: Event<void> = this._onDidScroll.event;354355private childrenChangeDisposable: IDisposable = Disposable.None;356357private readonly _onDidSashReset = new Emitter<GridLocation>();358readonly onDidSashReset: Event<GridLocation> = this._onDidSashReset.event;359private splitviewSashResetDisposable: IDisposable = Disposable.None;360private childrenSashResetDisposable: IDisposable = Disposable.None;361362private _boundarySashes: IRelativeBoundarySashes = {};363get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }364set boundarySashes(boundarySashes: IRelativeBoundarySashes) {365if (this._boundarySashes.start === boundarySashes.start366&& this._boundarySashes.end === boundarySashes.end367&& this._boundarySashes.orthogonalStart === boundarySashes.orthogonalStart368&& this._boundarySashes.orthogonalEnd === boundarySashes.orthogonalEnd) {369return;370}371372this._boundarySashes = boundarySashes;373374this.splitview.orthogonalStartSash = boundarySashes.orthogonalStart;375this.splitview.orthogonalEndSash = boundarySashes.orthogonalEnd;376377for (let index = 0; index < this.children.length; index++) {378const child = this.children[index];379const first = index === 0;380const last = index === this.children.length - 1;381382child.boundarySashes = {383start: boundarySashes.orthogonalStart,384end: boundarySashes.orthogonalEnd,385orthogonalStart: first ? boundarySashes.start : child.boundarySashes.orthogonalStart,386orthogonalEnd: last ? boundarySashes.end : child.boundarySashes.orthogonalEnd,387};388}389}390391private _edgeSnapping = false;392get edgeSnapping(): boolean { return this._edgeSnapping; }393set edgeSnapping(edgeSnapping: boolean) {394if (this._edgeSnapping === edgeSnapping) {395return;396}397398this._edgeSnapping = edgeSnapping;399400for (const child of this.children) {401if (child instanceof BranchNode) {402child.edgeSnapping = edgeSnapping;403}404}405406this.updateSplitviewEdgeSnappingEnablement();407}408409constructor(410readonly orientation: Orientation,411readonly layoutController: LayoutController,412styles: IGridViewStyles,413readonly splitviewProportionalLayout: boolean,414size: number = 0,415orthogonalSize: number = 0,416edgeSnapping: boolean = false,417childDescriptors?: INodeDescriptor[]418) {419this._styles = styles;420this._size = size;421this._orthogonalSize = orthogonalSize;422423this.element = $('.monaco-grid-branch-node');424425if (!childDescriptors) {426// Normal behavior, we have no children yet, just set up the splitview427this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout: splitviewProportionalLayout });428this.splitview.layout(size, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });429} else {430// Reconstruction behavior, we want to reconstruct a splitview431const descriptor = {432views: childDescriptors.map(childDescriptor => {433return {434view: childDescriptor.node,435size: childDescriptor.node.size,436visible: childDescriptor.visible !== false437};438}),439size: this.orthogonalSize440};441442const options = { proportionalLayout: splitviewProportionalLayout, orientation, styles };443444this.children = childDescriptors.map(c => c.node);445this.splitview = new SplitView(this.element, { ...options, descriptor });446447this.children.forEach((node, index) => {448const first = index === 0;449const last = index === this.children.length;450451node.boundarySashes = {452start: this.boundarySashes.orthogonalStart,453end: this.boundarySashes.orthogonalEnd,454orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1],455orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index],456};457});458}459460const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);461this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);462463this.updateChildrenEvents();464}465466style(styles: IGridViewStyles): void {467this._styles = styles;468this.splitview.style(styles);469470for (const child of this.children) {471if (child instanceof BranchNode) {472child.style(styles);473}474}475}476477layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {478if (!this.layoutController.isLayoutEnabled) {479return;480}481482if (typeof ctx === 'undefined') {483throw new Error('Invalid state');484}485486// branch nodes should flip the normal/orthogonal directions487this._size = ctx.orthogonalSize;488this._orthogonalSize = size;489this._absoluteOffset = ctx.absoluteOffset + offset;490this._absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;491this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize;492493this.splitview.layout(ctx.orthogonalSize, {494orthogonalSize: size,495absoluteOffset: this._absoluteOrthogonalOffset,496absoluteOrthogonalOffset: this._absoluteOffset,497absoluteSize: ctx.absoluteOrthogonalSize,498absoluteOrthogonalSize: ctx.absoluteSize499});500501this.updateSplitviewEdgeSnappingEnablement();502}503504setVisible(visible: boolean): void {505for (const child of this.children) {506child.setVisible(visible);507}508}509510addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void {511index = validateIndex(index, this.children.length);512513this.splitview.addView(node, size, index, skipLayout);514this.children.splice(index, 0, node);515516this.updateBoundarySashes();517this.onDidChildrenChange();518}519520removeChild(index: number, sizing?: Sizing): Node {521index = validateIndex(index, this.children.length);522523const result = this.splitview.removeView(index, sizing);524this.children.splice(index, 1);525526this.updateBoundarySashes();527this.onDidChildrenChange();528529return result;530}531532removeAllChildren(): Node[] {533const result = this.splitview.removeAllViews();534535this.children.splice(0, this.children.length);536537this.updateBoundarySashes();538this.onDidChildrenChange();539540return result;541}542543moveChild(from: number, to: number): void {544from = validateIndex(from, this.children.length);545to = validateIndex(to, this.children.length);546547if (from === to) {548return;549}550551if (from < to) {552to -= 1;553}554555this.splitview.moveView(from, to);556this.children.splice(to, 0, this.children.splice(from, 1)[0]);557558this.updateBoundarySashes();559this.onDidChildrenChange();560}561562swapChildren(from: number, to: number): void {563from = validateIndex(from, this.children.length);564to = validateIndex(to, this.children.length);565566if (from === to) {567return;568}569570this.splitview.swapViews(from, to);571572// swap boundary sashes573[this.children[from].boundarySashes, this.children[to].boundarySashes]574= [this.children[from].boundarySashes, this.children[to].boundarySashes];575576// swap children577[this.children[from], this.children[to]] = [this.children[to], this.children[from]];578579this.onDidChildrenChange();580}581582resizeChild(index: number, size: number): void {583index = validateIndex(index, this.children.length);584585this.splitview.resizeView(index, size);586}587588isChildExpanded(index: number): boolean {589return this.splitview.isViewExpanded(index);590}591592distributeViewSizes(recursive = false): void {593this.splitview.distributeViewSizes();594595if (recursive) {596for (const child of this.children) {597if (child instanceof BranchNode) {598child.distributeViewSizes(true);599}600}601}602}603604getChildSize(index: number): number {605index = validateIndex(index, this.children.length);606607return this.splitview.getViewSize(index);608}609610isChildVisible(index: number): boolean {611index = validateIndex(index, this.children.length);612613return this.splitview.isViewVisible(index);614}615616setChildVisible(index: number, visible: boolean): void {617index = validateIndex(index, this.children.length);618619if (this.splitview.isViewVisible(index) === visible) {620return;621}622623const wereAllChildrenHidden = this.splitview.contentSize === 0;624this.splitview.setViewVisible(index, visible);625const areAllChildrenHidden = this.splitview.contentSize === 0;626627// If all children are hidden then the parent should hide the entire splitview628// If the entire splitview is hidden then the parent should show the splitview when a child is shown629if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) {630this._onDidVisibilityChange.fire(visible);631}632}633634getChildCachedVisibleSize(index: number): number | undefined {635index = validateIndex(index, this.children.length);636637return this.splitview.getViewCachedVisibleSize(index);638}639640private updateBoundarySashes(): void {641for (let i = 0; i < this.children.length; i++) {642this.children[i].boundarySashes = {643start: this.boundarySashes.orthogonalStart,644end: this.boundarySashes.orthogonalEnd,645orthogonalStart: i === 0 ? this.boundarySashes.start : this.splitview.sashes[i - 1],646orthogonalEnd: i === this.children.length - 1 ? this.boundarySashes.end : this.splitview.sashes[i],647};648}649}650651private onDidChildrenChange(): void {652this.updateChildrenEvents();653this._onDidChange.fire(undefined);654}655656private updateChildrenEvents(): void {657const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);658this.childrenChangeDisposable.dispose();659this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);660661const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));662this.childrenSashResetDisposable.dispose();663this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);664665const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll));666this.onDidScrollDisposable.dispose();667this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll);668669this.childrenVisibilityChangeDisposable.clear();670this.children.forEach((child, index) => {671if (child instanceof BranchNode) {672this.childrenVisibilityChangeDisposable.add(child.onDidVisibilityChange((visible) => {673this.setChildVisible(index, visible);674}));675}676});677}678679trySet2x2(other: BranchNode): IDisposable {680if (this.children.length !== 2 || other.children.length !== 2) {681return Disposable.None;682}683684if (this.getChildSize(0) !== other.getChildSize(0)) {685return Disposable.None;686}687688const [firstChild, secondChild] = this.children;689const [otherFirstChild, otherSecondChild] = other.children;690691if (!(firstChild instanceof LeafNode) || !(secondChild instanceof LeafNode)) {692return Disposable.None;693}694695if (!(otherFirstChild instanceof LeafNode) || !(otherSecondChild instanceof LeafNode)) {696return Disposable.None;697}698699if (this.orientation === Orientation.VERTICAL) {700secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = firstChild;701firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = secondChild;702otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = otherFirstChild;703otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = otherSecondChild;704} else {705otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = firstChild;706otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = secondChild;707firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = otherFirstChild;708secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = otherSecondChild;709}710711const mySash = this.splitview.sashes[0];712const otherSash = other.splitview.sashes[0];713mySash.linkedSash = otherSash;714otherSash.linkedSash = mySash;715716this._onDidChange.fire(undefined);717other._onDidChange.fire(undefined);718719return toDisposable(() => {720mySash.linkedSash = otherSash.linkedSash = undefined;721firstChild.linkedHeightNode = firstChild.linkedWidthNode = undefined;722secondChild.linkedHeightNode = secondChild.linkedWidthNode = undefined;723otherFirstChild.linkedHeightNode = otherFirstChild.linkedWidthNode = undefined;724otherSecondChild.linkedHeightNode = otherSecondChild.linkedWidthNode = undefined;725});726}727728private updateSplitviewEdgeSnappingEnablement(): void {729this.splitview.startSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset > 0;730this.splitview.endSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;731}732733dispose(): void {734for (const child of this.children) {735child.dispose();736}737738this._onDidChange.dispose();739this._onDidSashReset.dispose();740this._onDidVisibilityChange.dispose();741742this.childrenVisibilityChangeDisposable.dispose();743this.splitviewSashResetDisposable.dispose();744this.childrenSashResetDisposable.dispose();745this.childrenChangeDisposable.dispose();746this.onDidScrollDisposable.dispose();747this.splitview.dispose();748}749}750751/**752* Creates a latched event that avoids being fired when the view753* constraints do not change at all.754*/755function createLatchedOnDidChangeViewEvent(view: IView): Event<IViewSize | undefined> {756const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split<undefined, IViewSize>(view.onDidChange, isUndefined);757758return Event.any(759onDidSetViewSize,760Event.map(761Event.latch(762Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])),763arrayEquals764),765_ => undefined766)767);768}769770class LeafNode implements ISplitView<ILayoutContext>, IDisposable {771772private _size: number = 0;773get size(): number { return this._size; }774775private _orthogonalSize: number;776get orthogonalSize(): number { return this._orthogonalSize; }777778private absoluteOffset: number = 0;779private absoluteOrthogonalOffset: number = 0;780781readonly onDidScroll: Event<void> = Event.None;782readonly onDidSashReset: Event<GridLocation> = Event.None;783784private _onDidLinkedWidthNodeChange = new Relay<number | undefined>();785private _linkedWidthNode: LeafNode | undefined = undefined;786get linkedWidthNode(): LeafNode | undefined { return this._linkedWidthNode; }787set linkedWidthNode(node: LeafNode | undefined) {788this._onDidLinkedWidthNodeChange.input = node ? node._onDidViewChange : Event.None;789this._linkedWidthNode = node;790this._onDidSetLinkedNode.fire(undefined);791}792793private _onDidLinkedHeightNodeChange = new Relay<number | undefined>();794private _linkedHeightNode: LeafNode | undefined = undefined;795get linkedHeightNode(): LeafNode | undefined { return this._linkedHeightNode; }796set linkedHeightNode(node: LeafNode | undefined) {797this._onDidLinkedHeightNodeChange.input = node ? node._onDidViewChange : Event.None;798this._linkedHeightNode = node;799this._onDidSetLinkedNode.fire(undefined);800}801802private readonly _onDidSetLinkedNode = new Emitter<number | undefined>();803private _onDidViewChange: Event<number | undefined>;804readonly onDidChange: Event<number | undefined>;805806private readonly disposables = new DisposableStore();807808constructor(809readonly view: IView,810readonly orientation: Orientation,811readonly layoutController: LayoutController,812orthogonalSize: number,813size: number = 0814) {815this._orthogonalSize = orthogonalSize;816this._size = size;817818const onDidChange = createLatchedOnDidChangeViewEvent(view);819this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height), this.disposables);820this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);821}822823get width(): number {824return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;825}826827get height(): number {828return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;829}830831get top(): number {832return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset;833}834835get left(): number {836return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset;837}838839get element(): HTMLElement {840return this.view.element;841}842843private get minimumWidth(): number {844return this.linkedWidthNode ? Math.max(this.linkedWidthNode.view.minimumWidth, this.view.minimumWidth) : this.view.minimumWidth;845}846847private get maximumWidth(): number {848return this.linkedWidthNode ? Math.min(this.linkedWidthNode.view.maximumWidth, this.view.maximumWidth) : this.view.maximumWidth;849}850851private get minimumHeight(): number {852return this.linkedHeightNode ? Math.max(this.linkedHeightNode.view.minimumHeight, this.view.minimumHeight) : this.view.minimumHeight;853}854855private get maximumHeight(): number {856return this.linkedHeightNode ? Math.min(this.linkedHeightNode.view.maximumHeight, this.view.maximumHeight) : this.view.maximumHeight;857}858859get minimumSize(): number {860return this.orientation === Orientation.HORIZONTAL ? this.minimumHeight : this.minimumWidth;861}862863get maximumSize(): number {864return this.orientation === Orientation.HORIZONTAL ? this.maximumHeight : this.maximumWidth;865}866867get priority(): LayoutPriority | undefined {868return this.view.priority;869}870871get proportionalLayout(): boolean {872return this.view.proportionalLayout ?? true;873}874875get snap(): boolean | undefined {876return this.view.snap;877}878879get minimumOrthogonalSize(): number {880return this.orientation === Orientation.HORIZONTAL ? this.minimumWidth : this.minimumHeight;881}882883get maximumOrthogonalSize(): number {884return this.orientation === Orientation.HORIZONTAL ? this.maximumWidth : this.maximumHeight;885}886887private _boundarySashes: IRelativeBoundarySashes = {};888get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }889set boundarySashes(boundarySashes: IRelativeBoundarySashes) {890this._boundarySashes = boundarySashes;891892this.view.setBoundarySashes?.(toAbsoluteBoundarySashes(boundarySashes, this.orientation));893}894895layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {896if (!this.layoutController.isLayoutEnabled) {897return;898}899900if (typeof ctx === 'undefined') {901throw new Error('Invalid state');902}903904this._size = size;905this._orthogonalSize = ctx.orthogonalSize;906this.absoluteOffset = ctx.absoluteOffset + offset;907this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;908909this._layout(this.width, this.height, this.top, this.left);910}911912private cachedWidth: number = 0;913private cachedHeight: number = 0;914private cachedTop: number = 0;915private cachedLeft: number = 0;916917private _layout(width: number, height: number, top: number, left: number): void {918if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) {919return;920}921922this.cachedWidth = width;923this.cachedHeight = height;924this.cachedTop = top;925this.cachedLeft = left;926this.view.layout(width, height, top, left);927}928929setVisible(visible: boolean): void {930this.view.setVisible?.(visible);931}932933dispose(): void {934this.disposables.dispose();935}936}937938type Node = BranchNode | LeafNode;939940export interface INodeDescriptor {941node: Node;942visible?: boolean;943}944945function flipNode(node: BranchNode, size: number, orthogonalSize: number): BranchNode;946function flipNode(node: LeafNode, size: number, orthogonalSize: number): LeafNode;947function flipNode(node: Node, size: number, orthogonalSize: number): Node;948function flipNode(node: Node, size: number, orthogonalSize: number): Node {949if (node instanceof BranchNode) {950const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.splitviewProportionalLayout, size, orthogonalSize, node.edgeSnapping);951952let totalSize = 0;953954for (let i = node.children.length - 1; i >= 0; i--) {955const child = node.children[i];956const childSize = child instanceof BranchNode ? child.orthogonalSize : child.size;957958let newSize = node.size === 0 ? 0 : Math.round((size * childSize) / node.size);959totalSize += newSize;960961// The last view to add should adjust to rounding errors962if (i === 0) {963newSize += size - totalSize;964}965966result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true);967}968969node.dispose();970return result;971} else {972const result = new LeafNode(node.view, orthogonal(node.orientation), node.layoutController, orthogonalSize);973node.dispose();974return result;975}976}977978/**979* The location of a {@link IView view} within a {@link GridView}.980*981* A GridView is a tree composition of multiple {@link SplitView} instances, orthogonal982* between one another. Here's an example:983*984* ```985* +-----+---------------+986* | A | B |987* +-----+---------+-----+988* | C | |989* +---------------+ D |990* | E | |991* +---------------+-----+992* ```993*994* The above grid's tree structure is:995*996* ```997* Vertical SplitView998* +-Horizontal SplitView999* | +-A1000* | +-B1001* +- Horizontal SplitView1002* +-Vertical SplitView1003* | +-C1004* | +-E1005* +-D1006* ```1007*1008* So, {@link IView views} within a {@link GridView} can be referenced by1009* a sequence of indexes, each index referencing each SplitView. Here are1010* each view's locations, from the example above:1011*1012* - `A`: `[0,0]`1013* - `B`: `[0,1]`1014* - `C`: `[1,0,0]`1015* - `D`: `[1,1]`1016* - `E`: `[1,0,1]`1017*/1018export type GridLocation = number[];10191020/**1021* The {@link GridView} is the UI component which implements a two dimensional1022* flex-like layout algorithm for a collection of {@link IView} instances, which1023* are mostly HTMLElement instances with size constraints. A {@link GridView} is a1024* tree composition of multiple {@link SplitView} instances, orthogonal between1025* one another. It will respect view's size contraints, just like the SplitView.1026*1027* It has a low-level index based API, allowing for fine grain performant operations.1028* Look into the {@link Grid} widget for a higher-level API.1029*1030* Features:1031* - flex-like layout algorithm1032* - snap support1033* - corner sash support1034* - Alt key modifier behavior, macOS style1035* - layout (de)serialization1036*/1037export class GridView implements IDisposable {10381039/**1040* The DOM element for this view.1041*/1042readonly element: HTMLElement;10431044private styles: IGridViewStyles;1045private proportionalLayout: boolean;1046private _root!: BranchNode;1047private onDidSashResetRelay = new Relay<GridLocation>();1048private _onDidScroll = new Relay<void>();1049private _onDidChange = new Relay<IViewSize | undefined>();1050private _boundarySashes: IBoundarySashes = {};10511052/**1053* The layout controller makes sure layout only propagates1054* to the views after the very first call to {@link GridView.layout}.1055*/1056private layoutController: LayoutController;1057private disposable2x2: IDisposable = Disposable.None;10581059private get root(): BranchNode { return this._root; }10601061private set root(root: BranchNode) {1062const oldRoot = this._root;10631064if (oldRoot) {1065oldRoot.element.remove();1066oldRoot.dispose();1067}10681069this._root = root;1070this.element.appendChild(root.element);1071this.onDidSashResetRelay.input = root.onDidSashReset;1072this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO1073this._onDidScroll.input = root.onDidScroll;1074}10751076/**1077* Fires whenever the user double clicks a {@link Sash sash}.1078*/1079readonly onDidSashReset = this.onDidSashResetRelay.event;10801081/**1082* Fires whenever the user scrolls a {@link SplitView} within1083* the grid.1084*/1085readonly onDidScroll = this._onDidScroll.event;10861087/**1088* Fires whenever a view within the grid changes its size constraints.1089*/1090readonly onDidChange = this._onDidChange.event;10911092/**1093* The width of the grid.1094*/1095get width(): number { return this.root.width; }10961097/**1098* The height of the grid.1099*/1100get height(): number { return this.root.height; }11011102/**1103* The minimum width of the grid.1104*/1105get minimumWidth(): number { return this.root.minimumWidth; }11061107/**1108* The minimum height of the grid.1109*/1110get minimumHeight(): number { return this.root.minimumHeight; }11111112/**1113* The maximum width of the grid.1114*/1115get maximumWidth(): number { return this.root.maximumHeight; }11161117/**1118* The maximum height of the grid.1119*/1120get maximumHeight(): number { return this.root.maximumHeight; }11211122get orientation(): Orientation { return this._root.orientation; }1123get boundarySashes(): IBoundarySashes { return this._boundarySashes; }11241125/**1126* The orientation of the grid. Matches the orientation of the root1127* {@link SplitView} in the grid's tree model.1128*/1129set orientation(orientation: Orientation) {1130if (this._root.orientation === orientation) {1131return;1132}11331134const { size, orthogonalSize, absoluteOffset, absoluteOrthogonalOffset } = this._root;1135this.root = flipNode(this._root, orthogonalSize, size);1136this.root.layout(size, 0, { orthogonalSize, absoluteOffset: absoluteOrthogonalOffset, absoluteOrthogonalOffset: absoluteOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });1137this.boundarySashes = this.boundarySashes;1138}11391140/**1141* A collection of sashes perpendicular to each edge of the grid.1142* Corner sashes will be created for each intersection.1143*/1144set boundarySashes(boundarySashes: IBoundarySashes) {1145this._boundarySashes = boundarySashes;1146this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation);1147}11481149/**1150* Enable/disable edge snapping across all grid views.1151*/1152set edgeSnapping(edgeSnapping: boolean) {1153this.root.edgeSnapping = edgeSnapping;1154}11551156private maximizedNode: LeafNode | undefined = undefined;11571158private readonly _onDidChangeViewMaximized = new Emitter<boolean>();1159readonly onDidChangeViewMaximized = this._onDidChangeViewMaximized.event;11601161/**1162* Create a new {@link GridView} instance.1163*1164* @remarks It's the caller's responsibility to append the1165* {@link GridView.element} to the page's DOM.1166*/1167constructor(options: IGridViewOptions = {}) {1168this.element = $('.monaco-grid-view');1169this.styles = options.styles || defaultStyles;1170this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true;1171this.layoutController = new LayoutController(false);1172this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout);1173}11741175style(styles: IGridViewStyles): void {1176this.styles = styles;1177this.root.style(styles);1178}11791180/**1181* Layout the {@link GridView}.1182*1183* Optionally provide a `top` and `left` positions, those will propagate1184* as an origin for positions passed to {@link IView.layout}.1185*1186* @param width The width of the {@link GridView}.1187* @param height The height of the {@link GridView}.1188* @param top Optional, the top location of the {@link GridView}.1189* @param left Optional, the left location of the {@link GridView}.1190*/1191layout(width: number, height: number, top: number = 0, left: number = 0): void {1192this.layoutController.isLayoutEnabled = true;11931194const [size, orthogonalSize, offset, orthogonalOffset] = this.root.orientation === Orientation.HORIZONTAL ? [height, width, top, left] : [width, height, left, top];1195this.root.layout(size, 0, { orthogonalSize, absoluteOffset: offset, absoluteOrthogonalOffset: orthogonalOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });1196}11971198/**1199* Add a {@link IView view} to this {@link GridView}.1200*1201* @param view The view to add.1202* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.1203* @param location The {@link GridLocation location} to insert the view on.1204*/1205addView(view: IView, size: number | Sizing, location: GridLocation): void {1206if (this.hasMaximizedView()) {1207this.exitMaximizedView();1208}12091210this.disposable2x2.dispose();1211this.disposable2x2 = Disposable.None;12121213const [rest, index] = tail(location);1214const [pathToParent, parent] = this.getNode(rest);12151216if (parent instanceof BranchNode) {1217const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize);12181219try {1220parent.addChild(node, size, index);1221} catch (err) {1222node.dispose();1223throw err;1224}1225} else {1226const [, grandParent] = tail(pathToParent);1227const [, parentIndex] = tail(rest);12281229let newSiblingSize: number | Sizing = 0;12301231const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(parentIndex);1232if (typeof newSiblingCachedVisibleSize === 'number') {1233newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize);1234}12351236const oldChild = grandParent.removeChild(parentIndex);1237oldChild.dispose();12381239const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping);1240grandParent.addChild(newParent, parent.size, parentIndex);12411242const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size);1243newParent.addChild(newSibling, newSiblingSize, 0);12441245if (typeof size !== 'number' && size.type === 'split') {1246size = Sizing.Split(0);1247}12481249const node = new LeafNode(view, grandParent.orientation, this.layoutController, parent.size);1250newParent.addChild(node, size, index);1251}12521253this.trySet2x2();1254}12551256/**1257* Remove a {@link IView view} from this {@link GridView}.1258*1259* @param location The {@link GridLocation location} of the {@link IView view}.1260* @param sizing Whether to distribute other {@link IView view}'s sizes.1261*/1262removeView(location: GridLocation, sizing?: DistributeSizing | AutoSizing): IView {1263if (this.hasMaximizedView()) {1264this.exitMaximizedView();1265}12661267this.disposable2x2.dispose();1268this.disposable2x2 = Disposable.None;12691270const [rest, index] = tail(location);1271const [pathToParent, parent] = this.getNode(rest);12721273if (!(parent instanceof BranchNode)) {1274throw new Error('Invalid location');1275}12761277const node = parent.children[index];12781279if (!(node instanceof LeafNode)) {1280throw new Error('Invalid location');1281}12821283parent.removeChild(index, sizing);1284node.dispose();12851286if (parent.children.length === 0) {1287throw new Error('Invalid grid state');1288}12891290if (parent.children.length > 1) {1291this.trySet2x2();1292return node.view;1293}12941295if (pathToParent.length === 0) { // parent is root1296const sibling = parent.children[0];12971298if (sibling instanceof LeafNode) {1299return node.view;1300}13011302// we must promote sibling to be the new root1303parent.removeChild(0);1304parent.dispose();1305this.root = sibling;1306this.boundarySashes = this.boundarySashes;1307this.trySet2x2();1308return node.view;1309}13101311const [, grandParent] = tail(pathToParent);1312const [, parentIndex] = tail(rest);13131314const isSiblingVisible = parent.isChildVisible(0);1315const sibling = parent.removeChild(0);13161317const sizes = grandParent.children.map((_, i) => grandParent.getChildSize(i));1318grandParent.removeChild(parentIndex, sizing);1319parent.dispose();13201321if (sibling instanceof BranchNode) {1322sizes.splice(parentIndex, 1, ...sibling.children.map(c => c.size));13231324const siblingChildren = sibling.removeAllChildren();13251326for (let i = 0; i < siblingChildren.length; i++) {1327grandParent.addChild(siblingChildren[i], siblingChildren[i].size, parentIndex + i);1328}1329} else {1330const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size);1331const sizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize);1332grandParent.addChild(newSibling, sizing, parentIndex);1333}13341335sibling.dispose();13361337for (let i = 0; i < sizes.length; i++) {1338grandParent.resizeChild(i, sizes[i]);1339}13401341this.trySet2x2();1342return node.view;1343}13441345/**1346* Move a {@link IView view} within its parent.1347*1348* @param parentLocation The {@link GridLocation location} of the {@link IView view}'s parent.1349* @param from The index of the {@link IView view} to move.1350* @param to The index where the {@link IView view} should move to.1351*/1352moveView(parentLocation: GridLocation, from: number, to: number): void {1353if (this.hasMaximizedView()) {1354this.exitMaximizedView();1355}13561357const [, parent] = this.getNode(parentLocation);13581359if (!(parent instanceof BranchNode)) {1360throw new Error('Invalid location');1361}13621363parent.moveChild(from, to);13641365this.trySet2x2();1366}13671368/**1369* Swap two {@link IView views} within the {@link GridView}.1370*1371* @param from The {@link GridLocation location} of one view.1372* @param to The {@link GridLocation location} of another view.1373*/1374swapViews(from: GridLocation, to: GridLocation): void {1375if (this.hasMaximizedView()) {1376this.exitMaximizedView();1377}13781379const [fromRest, fromIndex] = tail(from);1380const [, fromParent] = this.getNode(fromRest);13811382if (!(fromParent instanceof BranchNode)) {1383throw new Error('Invalid from location');1384}13851386const fromSize = fromParent.getChildSize(fromIndex);1387const fromNode = fromParent.children[fromIndex];13881389if (!(fromNode instanceof LeafNode)) {1390throw new Error('Invalid from location');1391}13921393const [toRest, toIndex] = tail(to);1394const [, toParent] = this.getNode(toRest);13951396if (!(toParent instanceof BranchNode)) {1397throw new Error('Invalid to location');1398}13991400const toSize = toParent.getChildSize(toIndex);1401const toNode = toParent.children[toIndex];14021403if (!(toNode instanceof LeafNode)) {1404throw new Error('Invalid to location');1405}14061407if (fromParent === toParent) {1408fromParent.swapChildren(fromIndex, toIndex);1409} else {1410fromParent.removeChild(fromIndex);1411toParent.removeChild(toIndex);14121413fromParent.addChild(toNode, fromSize, fromIndex);1414toParent.addChild(fromNode, toSize, toIndex);1415}14161417this.trySet2x2();1418}14191420/**1421* Resize a {@link IView view}.1422*1423* @param location The {@link GridLocation location} of the view.1424* @param size The size the view should be. Optionally provide a single dimension.1425*/1426resizeView(location: GridLocation, size: Partial<IViewSize>): void {1427if (this.hasMaximizedView()) {1428this.exitMaximizedView();1429}14301431const [rest, index] = tail(location);1432const [pathToParent, parent] = this.getNode(rest);14331434if (!(parent instanceof BranchNode)) {1435throw new Error('Invalid location');1436}14371438if (!size.width && !size.height) {1439return;1440}14411442const [parentSize, grandParentSize] = parent.orientation === Orientation.HORIZONTAL ? [size.width, size.height] : [size.height, size.width];14431444if (typeof grandParentSize === 'number' && pathToParent.length > 0) {1445const [, grandParent] = tail(pathToParent);1446const [, parentIndex] = tail(rest);14471448grandParent.resizeChild(parentIndex, grandParentSize);1449}14501451if (typeof parentSize === 'number') {1452parent.resizeChild(index, parentSize);1453}14541455this.trySet2x2();1456}14571458/**1459* Get the size of a {@link IView view}.1460*1461* @param location The {@link GridLocation location} of the view. Provide `undefined` to get1462* the size of the grid itself.1463*/1464getViewSize(location?: GridLocation): IViewSize {1465if (!location) {1466return { width: this.root.width, height: this.root.height };1467}14681469const [, node] = this.getNode(location);1470return { width: node.width, height: node.height };1471}14721473/**1474* Get the cached visible size of a {@link IView view}. This was the size1475* of the view at the moment it last became hidden.1476*1477* @param location The {@link GridLocation location} of the view.1478*/1479getViewCachedVisibleSize(location: GridLocation): number | undefined {1480const [rest, index] = tail(location);1481const [, parent] = this.getNode(rest);14821483if (!(parent instanceof BranchNode)) {1484throw new Error('Invalid location');1485}14861487return parent.getChildCachedVisibleSize(index);1488}14891490/**1491* Maximize the size of a {@link IView view} by collapsing all other views1492* to their minimum sizes.1493*1494* @param location The {@link GridLocation location} of the view.1495*/1496expandView(location: GridLocation): void {1497if (this.hasMaximizedView()) {1498this.exitMaximizedView();1499}15001501const [ancestors, node] = this.getNode(location);15021503if (!(node instanceof LeafNode)) {1504throw new Error('Invalid location');1505}15061507for (let i = 0; i < ancestors.length; i++) {1508ancestors[i].resizeChild(location[i], Number.POSITIVE_INFINITY);1509}1510}15111512/**1513* Returns whether all other {@link IView views} are at their minimum size.1514*1515* @param location The {@link GridLocation location} of the view.1516*/1517isViewExpanded(location: GridLocation): boolean {1518if (this.hasMaximizedView()) {1519// No view can be expanded when a view is maximized1520return false;1521}15221523const [ancestors, node] = this.getNode(location);15241525if (!(node instanceof LeafNode)) {1526throw new Error('Invalid location');1527}15281529for (let i = 0; i < ancestors.length; i++) {1530if (!ancestors[i].isChildExpanded(location[i])) {1531return false;1532}1533}15341535return true;1536}15371538maximizeView(location: GridLocation) {1539const [, nodeToMaximize] = this.getNode(location);1540if (!(nodeToMaximize instanceof LeafNode)) {1541throw new Error('Location is not a LeafNode');1542}15431544if (this.maximizedNode === nodeToMaximize) {1545return;1546}15471548if (this.hasMaximizedView()) {1549this.exitMaximizedView();1550}15511552function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {1553for (let i = 0; i < parent.children.length; i++) {1554const child = parent.children[i];1555if (child instanceof LeafNode) {1556if (child !== exclude) {1557parent.setChildVisible(i, false);1558}1559} else {1560hideAllViewsBut(child, exclude);1561}1562}1563}15641565hideAllViewsBut(this.root, nodeToMaximize);15661567this.maximizedNode = nodeToMaximize;1568this._onDidChangeViewMaximized.fire(true);1569}15701571exitMaximizedView(): void {1572if (!this.maximizedNode) {1573return;1574}1575this.maximizedNode = undefined;15761577// When hiding a view, it's previous size is cached.1578// To restore the sizes of all views, they need to be made visible in reverse order.1579function showViewsInReverseOrder(parent: BranchNode): void {1580for (let index = parent.children.length - 1; index >= 0; index--) {1581const child = parent.children[index];1582if (child instanceof LeafNode) {1583parent.setChildVisible(index, true);1584} else {1585showViewsInReverseOrder(child);1586}1587}1588}15891590showViewsInReverseOrder(this.root);15911592this._onDidChangeViewMaximized.fire(false);1593}15941595hasMaximizedView(): boolean {1596return this.maximizedNode !== undefined;1597}15981599/**1600* Returns whether the {@link IView view} is maximized.1601*1602* @param location The {@link GridLocation location} of the view.1603*/1604isViewMaximized(location: GridLocation): boolean {1605const [, node] = this.getNode(location);1606if (!(node instanceof LeafNode)) {1607throw new Error('Location is not a LeafNode');1608}1609return node === this.maximizedNode;1610}16111612/**1613* Distribute the size among all {@link IView views} within the entire1614* grid or within a single {@link SplitView}.1615*1616* @param location The {@link GridLocation location} of a view containing1617* children views, which will have their sizes distributed within the parent1618* view's size. Provide `undefined` to recursively distribute all views' sizes1619* in the entire grid.1620*/1621distributeViewSizes(location?: GridLocation): void {1622if (this.hasMaximizedView()) {1623this.exitMaximizedView();1624}16251626if (!location) {1627this.root.distributeViewSizes(true);1628return;1629}16301631const [, node] = this.getNode(location);16321633if (!(node instanceof BranchNode)) {1634throw new Error('Invalid location');1635}16361637node.distributeViewSizes();1638this.trySet2x2();1639}16401641/**1642* Returns whether a {@link IView view} is visible.1643*1644* @param location The {@link GridLocation location} of the view.1645*/1646isViewVisible(location: GridLocation): boolean {1647const [rest, index] = tail(location);1648const [, parent] = this.getNode(rest);16491650if (!(parent instanceof BranchNode)) {1651throw new Error('Invalid from location');1652}16531654return parent.isChildVisible(index);1655}16561657/**1658* Set the visibility state of a {@link IView view}.1659*1660* @param location The {@link GridLocation location} of the view.1661*/1662setViewVisible(location: GridLocation, visible: boolean): void {1663if (this.hasMaximizedView()) {1664this.exitMaximizedView();1665return;1666}16671668const [rest, index] = tail(location);1669const [, parent] = this.getNode(rest);16701671if (!(parent instanceof BranchNode)) {1672throw new Error('Invalid from location');1673}16741675parent.setChildVisible(index, visible);1676}16771678/**1679* Returns a descriptor for the entire grid.1680*/1681getView(): GridBranchNode;16821683/**1684* Returns a descriptor for a {@link GridLocation subtree} within the1685* {@link GridView}.1686*1687* @param location The {@link GridLocation location} of the root of1688* the {@link GridLocation subtree}.1689*/1690getView(location: GridLocation): GridNode;1691getView(location?: GridLocation): GridNode {1692const node = location ? this.getNode(location)[1] : this._root;1693return this._getViews(node, this.orientation);1694}16951696/**1697* Construct a new {@link GridView} from a JSON object.1698*1699* @param json The JSON object.1700* @param deserializer A deserializer which can revive each view.1701* @returns A new {@link GridView} instance.1702*/1703static deserialize<T extends ISerializableView>(json: ISerializedGridView, deserializer: IViewDeserializer<T>, options: IGridViewOptions = {}): GridView {1704if (typeof json.orientation !== 'number') {1705throw new Error('Invalid JSON: \'orientation\' property must be a number.');1706} else if (typeof json.width !== 'number') {1707throw new Error('Invalid JSON: \'width\' property must be a number.');1708} else if (typeof json.height !== 'number') {1709throw new Error('Invalid JSON: \'height\' property must be a number.');1710} else if (json.root?.type !== 'branch') {1711throw new Error('Invalid JSON: \'root\' property must have \'type\' value of branch.');1712}17131714const orientation = json.orientation;1715const height = json.height;17161717const result = new GridView(options);1718result._deserialize(json.root as ISerializedBranchNode, orientation, deserializer, height);17191720return result;1721}17221723private _deserialize(root: ISerializedBranchNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): void {1724this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize) as BranchNode;1725}17261727private _deserializeNode(node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): Node {1728let result: Node;1729if (node.type === 'branch') {1730const serializedChildren = node.data as ISerializedNode[];1731const children = serializedChildren.map(serializedChild => {1732return {1733node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size),1734visible: (serializedChild as { visible?: boolean }).visible1735} satisfies INodeDescriptor;1736});17371738result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children);1739} else {1740result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size);1741if (node.maximized && !this.maximizedNode) {1742this.maximizedNode = result;1743this._onDidChangeViewMaximized.fire(true);1744}1745}17461747return result;1748}17491750private _getViews(node: Node, orientation: Orientation, cachedVisibleSize?: number): GridNode {1751const box = { top: node.top, left: node.left, width: node.width, height: node.height };17521753if (node instanceof LeafNode) {1754return { view: node.view, box, cachedVisibleSize, maximized: this.maximizedNode === node };1755}17561757const children: GridNode[] = [];17581759for (let i = 0; i < node.children.length; i++) {1760const child = node.children[i];1761const cachedVisibleSize = node.getChildCachedVisibleSize(i);17621763children.push(this._getViews(child, orthogonal(orientation), cachedVisibleSize));1764}17651766return { children, box };1767}17681769private getNode(location: GridLocation, node: Node = this.root, path: BranchNode[] = []): [BranchNode[], Node] {1770if (location.length === 0) {1771return [path, node];1772}17731774if (!(node instanceof BranchNode)) {1775throw new Error('Invalid location');1776}17771778const [index, ...rest] = location;17791780if (index < 0 || index >= node.children.length) {1781throw new Error('Invalid location');1782}17831784const child = node.children[index];1785path.push(node);17861787return this.getNode(rest, child, path);1788}17891790/**1791* Attempt to lock the {@link Sash sashes} in this {@link GridView} so1792* the grid behaves as a 2x2 matrix, with a corner sash in the middle.1793*1794* In case the grid isn't a 2x2 grid _and_ all sashes are not aligned,1795* this method is a no-op.1796*/1797trySet2x2(): void {1798this.disposable2x2.dispose();1799this.disposable2x2 = Disposable.None;18001801if (this.root.children.length !== 2) {1802return;1803}18041805const [first, second] = this.root.children;18061807if (!(first instanceof BranchNode) || !(second instanceof BranchNode)) {1808return;1809}18101811this.disposable2x2 = first.trySet2x2(second);1812}18131814/**1815* Populate a map with views to DOM nodes.1816* @remarks To be used internally only.1817*/1818getViewMap(map: Map<IView, HTMLElement>, node?: Node): void {1819if (!node) {1820node = this.root;1821}18221823if (node instanceof BranchNode) {1824node.children.forEach(child => this.getViewMap(map, child));1825} else {1826map.set(node.view, node.element);1827}1828}18291830dispose(): void {1831this.onDidSashResetRelay.dispose();1832this.root.dispose();1833this.element.remove();1834}1835}183618371838