Path: blob/main/src/vs/base/browser/ui/grid/gridview.ts
5255 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 {195// eslint-disable-next-line local/code-no-any-casts196return !!(node as any).children;197}198199class LayoutController {200constructor(public isLayoutEnabled: boolean) { }201}202203export interface IGridViewOptions {204205/**206* Styles overriding the {@link defaultStyles default ones}.207*/208readonly styles?: IGridViewStyles;209210/**211* Resize each view proportionally when resizing the {@link GridView}.212*213* @defaultValue `true`214*/215readonly proportionalLayout?: boolean; // default true216}217218interface ILayoutContext {219readonly orthogonalSize: number;220readonly absoluteOffset: number;221readonly absoluteOrthogonalOffset: number;222readonly absoluteSize: number;223readonly absoluteOrthogonalSize: number;224}225226function toAbsoluteBoundarySashes(sashes: IRelativeBoundarySashes, orientation: Orientation): IBoundarySashes {227if (orientation === Orientation.HORIZONTAL) {228return { left: sashes.start, right: sashes.end, top: sashes.orthogonalStart, bottom: sashes.orthogonalEnd };229} else {230return { top: sashes.start, bottom: sashes.end, left: sashes.orthogonalStart, right: sashes.orthogonalEnd };231}232}233234function fromAbsoluteBoundarySashes(sashes: IBoundarySashes, orientation: Orientation): IRelativeBoundarySashes {235if (orientation === Orientation.HORIZONTAL) {236return { start: sashes.left, end: sashes.right, orthogonalStart: sashes.top, orthogonalEnd: sashes.bottom };237} else {238return { start: sashes.top, end: sashes.bottom, orthogonalStart: sashes.left, orthogonalEnd: sashes.right };239}240}241242function validateIndex(index: number, numChildren: number): number {243if (Math.abs(index) > numChildren) {244throw new Error('Invalid index');245}246247return rot(index, numChildren + 1);248}249250class BranchNode implements ISplitView<ILayoutContext>, IDisposable {251252readonly element: HTMLElement;253readonly children: Node[] = [];254private splitview: SplitView<ILayoutContext, Node>;255256private _size: number;257get size(): number { return this._size; }258259private _orthogonalSize: number;260get orthogonalSize(): number { return this._orthogonalSize; }261262private _absoluteOffset: number = 0;263get absoluteOffset(): number { return this._absoluteOffset; }264265private _absoluteOrthogonalOffset: number = 0;266get absoluteOrthogonalOffset(): number { return this._absoluteOrthogonalOffset; }267268private absoluteOrthogonalSize: number = 0;269270private _styles: IGridViewStyles;271get styles(): IGridViewStyles { return this._styles; }272273get width(): number {274return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;275}276277get height(): number {278return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;279}280281get top(): number {282return this.orientation === Orientation.HORIZONTAL ? this._absoluteOffset : this._absoluteOrthogonalOffset;283}284285get left(): number {286return this.orientation === Orientation.HORIZONTAL ? this._absoluteOrthogonalOffset : this._absoluteOffset;287}288289get minimumSize(): number {290return this.children.length === 0 ? 0 : Math.max(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.minimumOrthogonalSize : 0));291}292293get maximumSize(): number {294return Math.min(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.maximumOrthogonalSize : Number.POSITIVE_INFINITY));295}296297get priority(): LayoutPriority {298if (this.children.length === 0) {299return LayoutPriority.Normal;300}301302const priorities = this.children.map(c => typeof c.priority === 'undefined' ? LayoutPriority.Normal : c.priority);303304if (priorities.some(p => p === LayoutPriority.High)) {305return LayoutPriority.High;306} else if (priorities.some(p => p === LayoutPriority.Low)) {307return LayoutPriority.Low;308}309310return LayoutPriority.Normal;311}312313get proportionalLayout(): boolean {314if (this.children.length === 0) {315return true;316}317318return this.children.every(c => c.proportionalLayout);319}320321get minimumOrthogonalSize(): number {322return this.splitview.minimumSize;323}324325get maximumOrthogonalSize(): number {326return this.splitview.maximumSize;327}328329get minimumWidth(): number {330return this.orientation === Orientation.HORIZONTAL ? this.minimumOrthogonalSize : this.minimumSize;331}332333get minimumHeight(): number {334return this.orientation === Orientation.HORIZONTAL ? this.minimumSize : this.minimumOrthogonalSize;335}336337get maximumWidth(): number {338return this.orientation === Orientation.HORIZONTAL ? this.maximumOrthogonalSize : this.maximumSize;339}340341get maximumHeight(): number {342return this.orientation === Orientation.HORIZONTAL ? this.maximumSize : this.maximumOrthogonalSize;343}344345private readonly _onDidChange = new Emitter<number | undefined>();346readonly onDidChange: Event<number | undefined> = this._onDidChange.event;347348private readonly _onDidVisibilityChange = new Emitter<boolean>();349readonly onDidVisibilityChange: Event<boolean> = this._onDidVisibilityChange.event;350private readonly childrenVisibilityChangeDisposable: DisposableStore = new DisposableStore();351352private _onDidScroll = new Emitter<void>();353private onDidScrollDisposable: IDisposable = Disposable.None;354readonly onDidScroll: Event<void> = this._onDidScroll.event;355356private childrenChangeDisposable: IDisposable = Disposable.None;357358private readonly _onDidSashReset = new Emitter<GridLocation>();359readonly onDidSashReset: Event<GridLocation> = this._onDidSashReset.event;360private splitviewSashResetDisposable: IDisposable = Disposable.None;361private childrenSashResetDisposable: IDisposable = Disposable.None;362363private _boundarySashes: IRelativeBoundarySashes = {};364get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }365set boundarySashes(boundarySashes: IRelativeBoundarySashes) {366if (this._boundarySashes.start === boundarySashes.start367&& this._boundarySashes.end === boundarySashes.end368&& this._boundarySashes.orthogonalStart === boundarySashes.orthogonalStart369&& this._boundarySashes.orthogonalEnd === boundarySashes.orthogonalEnd) {370return;371}372373this._boundarySashes = boundarySashes;374375this.splitview.orthogonalStartSash = boundarySashes.orthogonalStart;376this.splitview.orthogonalEndSash = boundarySashes.orthogonalEnd;377378for (let index = 0; index < this.children.length; index++) {379const child = this.children[index];380const first = index === 0;381const last = index === this.children.length - 1;382383child.boundarySashes = {384start: boundarySashes.orthogonalStart,385end: boundarySashes.orthogonalEnd,386orthogonalStart: first ? boundarySashes.start : child.boundarySashes.orthogonalStart,387orthogonalEnd: last ? boundarySashes.end : child.boundarySashes.orthogonalEnd,388};389}390}391392private _edgeSnapping = false;393get edgeSnapping(): boolean { return this._edgeSnapping; }394set edgeSnapping(edgeSnapping: boolean) {395if (this._edgeSnapping === edgeSnapping) {396return;397}398399this._edgeSnapping = edgeSnapping;400401for (const child of this.children) {402if (child instanceof BranchNode) {403child.edgeSnapping = edgeSnapping;404}405}406407this.updateSplitviewEdgeSnappingEnablement();408}409410constructor(411readonly orientation: Orientation,412readonly layoutController: LayoutController,413styles: IGridViewStyles,414readonly splitviewProportionalLayout: boolean,415size: number = 0,416orthogonalSize: number = 0,417edgeSnapping: boolean = false,418childDescriptors?: INodeDescriptor[]419) {420this._styles = styles;421this._size = size;422this._orthogonalSize = orthogonalSize;423424this.element = $('.monaco-grid-branch-node');425426if (!childDescriptors) {427// Normal behavior, we have no children yet, just set up the splitview428this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout: splitviewProportionalLayout });429this.splitview.layout(size, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });430} else {431// Reconstruction behavior, we want to reconstruct a splitview432const descriptor = {433views: childDescriptors.map(childDescriptor => {434return {435view: childDescriptor.node,436size: childDescriptor.node.size,437visible: childDescriptor.visible !== false438};439}),440size: this.orthogonalSize441};442443const options = { proportionalLayout: splitviewProportionalLayout, orientation, styles };444445this.children = childDescriptors.map(c => c.node);446this.splitview = new SplitView(this.element, { ...options, descriptor });447448this.children.forEach((node, index) => {449const first = index === 0;450const last = index === this.children.length;451452node.boundarySashes = {453start: this.boundarySashes.orthogonalStart,454end: this.boundarySashes.orthogonalEnd,455orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1],456orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index],457};458});459}460461const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);462this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);463464this.updateChildrenEvents();465}466467style(styles: IGridViewStyles): void {468this._styles = styles;469this.splitview.style(styles);470471for (const child of this.children) {472if (child instanceof BranchNode) {473child.style(styles);474}475}476}477478layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {479if (!this.layoutController.isLayoutEnabled) {480return;481}482483if (typeof ctx === 'undefined') {484throw new Error('Invalid state');485}486487// branch nodes should flip the normal/orthogonal directions488this._size = ctx.orthogonalSize;489this._orthogonalSize = size;490this._absoluteOffset = ctx.absoluteOffset + offset;491this._absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;492this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize;493494this.splitview.layout(ctx.orthogonalSize, {495orthogonalSize: size,496absoluteOffset: this._absoluteOrthogonalOffset,497absoluteOrthogonalOffset: this._absoluteOffset,498absoluteSize: ctx.absoluteOrthogonalSize,499absoluteOrthogonalSize: ctx.absoluteSize500});501502this.updateSplitviewEdgeSnappingEnablement();503}504505setVisible(visible: boolean): void {506for (const child of this.children) {507child.setVisible(visible);508}509}510511addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void {512index = validateIndex(index, this.children.length);513514this.splitview.addView(node, size, index, skipLayout);515this.children.splice(index, 0, node);516517this.updateBoundarySashes();518this.onDidChildrenChange();519}520521removeChild(index: number, sizing?: Sizing): Node {522index = validateIndex(index, this.children.length);523524const result = this.splitview.removeView(index, sizing);525this.children.splice(index, 1);526527this.updateBoundarySashes();528this.onDidChildrenChange();529530return result;531}532533removeAllChildren(): Node[] {534const result = this.splitview.removeAllViews();535536this.children.splice(0, this.children.length);537538this.updateBoundarySashes();539this.onDidChildrenChange();540541return result;542}543544moveChild(from: number, to: number): void {545from = validateIndex(from, this.children.length);546to = validateIndex(to, this.children.length);547548if (from === to) {549return;550}551552if (from < to) {553to -= 1;554}555556this.splitview.moveView(from, to);557this.children.splice(to, 0, this.children.splice(from, 1)[0]);558559this.updateBoundarySashes();560this.onDidChildrenChange();561}562563swapChildren(from: number, to: number): void {564from = validateIndex(from, this.children.length);565to = validateIndex(to, this.children.length);566567if (from === to) {568return;569}570571this.splitview.swapViews(from, to);572573// swap boundary sashes574[this.children[from].boundarySashes, this.children[to].boundarySashes]575= [this.children[from].boundarySashes, this.children[to].boundarySashes];576577// swap children578[this.children[from], this.children[to]] = [this.children[to], this.children[from]];579580this.onDidChildrenChange();581}582583resizeChild(index: number, size: number): void {584index = validateIndex(index, this.children.length);585586this.splitview.resizeView(index, size);587}588589isChildExpanded(index: number): boolean {590return this.splitview.isViewExpanded(index);591}592593distributeViewSizes(recursive = false): void {594this.splitview.distributeViewSizes();595596if (recursive) {597for (const child of this.children) {598if (child instanceof BranchNode) {599child.distributeViewSizes(true);600}601}602}603}604605getChildSize(index: number): number {606index = validateIndex(index, this.children.length);607608return this.splitview.getViewSize(index);609}610611isChildVisible(index: number): boolean {612index = validateIndex(index, this.children.length);613614return this.splitview.isViewVisible(index);615}616617setChildVisible(index: number, visible: boolean): void {618index = validateIndex(index, this.children.length);619620if (this.splitview.isViewVisible(index) === visible) {621return;622}623624const wereAllChildrenHidden = this.splitview.contentSize === 0;625this.splitview.setViewVisible(index, visible);626const areAllChildrenHidden = this.splitview.contentSize === 0;627628// If all children are hidden then the parent should hide the entire splitview629// If the entire splitview is hidden then the parent should show the splitview when a child is shown630if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) {631this._onDidVisibilityChange.fire(visible);632}633}634635getChildCachedVisibleSize(index: number): number | undefined {636index = validateIndex(index, this.children.length);637638return this.splitview.getViewCachedVisibleSize(index);639}640641private updateBoundarySashes(): void {642for (let i = 0; i < this.children.length; i++) {643this.children[i].boundarySashes = {644start: this.boundarySashes.orthogonalStart,645end: this.boundarySashes.orthogonalEnd,646orthogonalStart: i === 0 ? this.boundarySashes.start : this.splitview.sashes[i - 1],647orthogonalEnd: i === this.children.length - 1 ? this.boundarySashes.end : this.splitview.sashes[i],648};649}650}651652private onDidChildrenChange(): void {653this.updateChildrenEvents();654this._onDidChange.fire(undefined);655}656657private updateChildrenEvents(): void {658const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);659this.childrenChangeDisposable.dispose();660this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);661662const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));663this.childrenSashResetDisposable.dispose();664this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);665666const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll));667this.onDidScrollDisposable.dispose();668this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll);669670this.childrenVisibilityChangeDisposable.clear();671this.children.forEach((child, index) => {672if (child instanceof BranchNode) {673this.childrenVisibilityChangeDisposable.add(child.onDidVisibilityChange((visible) => {674this.setChildVisible(index, visible);675}));676}677});678}679680trySet2x2(other: BranchNode): IDisposable {681if (this.children.length !== 2 || other.children.length !== 2) {682return Disposable.None;683}684685if (this.getChildSize(0) !== other.getChildSize(0)) {686return Disposable.None;687}688689const [firstChild, secondChild] = this.children;690const [otherFirstChild, otherSecondChild] = other.children;691692if (!(firstChild instanceof LeafNode) || !(secondChild instanceof LeafNode)) {693return Disposable.None;694}695696if (!(otherFirstChild instanceof LeafNode) || !(otherSecondChild instanceof LeafNode)) {697return Disposable.None;698}699700if (this.orientation === Orientation.VERTICAL) {701secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = firstChild;702firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = secondChild;703otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = otherFirstChild;704otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = otherSecondChild;705} else {706otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = firstChild;707otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = secondChild;708firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = otherFirstChild;709secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = otherSecondChild;710}711712const mySash = this.splitview.sashes[0];713const otherSash = other.splitview.sashes[0];714mySash.linkedSash = otherSash;715otherSash.linkedSash = mySash;716717this._onDidChange.fire(undefined);718other._onDidChange.fire(undefined);719720return toDisposable(() => {721mySash.linkedSash = otherSash.linkedSash = undefined;722firstChild.linkedHeightNode = firstChild.linkedWidthNode = undefined;723secondChild.linkedHeightNode = secondChild.linkedWidthNode = undefined;724otherFirstChild.linkedHeightNode = otherFirstChild.linkedWidthNode = undefined;725otherSecondChild.linkedHeightNode = otherSecondChild.linkedWidthNode = undefined;726});727}728729private updateSplitviewEdgeSnappingEnablement(): void {730this.splitview.startSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset > 0;731this.splitview.endSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;732}733734dispose(): void {735for (const child of this.children) {736child.dispose();737}738739this._onDidChange.dispose();740this._onDidSashReset.dispose();741this._onDidVisibilityChange.dispose();742743this.childrenVisibilityChangeDisposable.dispose();744this.splitviewSashResetDisposable.dispose();745this.childrenSashResetDisposable.dispose();746this.childrenChangeDisposable.dispose();747this.onDidScrollDisposable.dispose();748this.splitview.dispose();749}750}751752/**753* Creates a latched event that avoids being fired when the view754* constraints do not change at all.755*/756function createLatchedOnDidChangeViewEvent(view: IView): Event<IViewSize | undefined> {757const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split<undefined, IViewSize>(view.onDidChange, isUndefined);758759return Event.any(760onDidSetViewSize,761Event.map(762Event.latch(763Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])),764arrayEquals765),766_ => undefined767)768);769}770771class LeafNode implements ISplitView<ILayoutContext>, IDisposable {772773private _size: number = 0;774get size(): number { return this._size; }775776private _orthogonalSize: number;777get orthogonalSize(): number { return this._orthogonalSize; }778779private absoluteOffset: number = 0;780private absoluteOrthogonalOffset: number = 0;781782readonly onDidScroll: Event<void> = Event.None;783readonly onDidSashReset: Event<GridLocation> = Event.None;784785private _onDidLinkedWidthNodeChange = new Relay<number | undefined>();786private _linkedWidthNode: LeafNode | undefined = undefined;787get linkedWidthNode(): LeafNode | undefined { return this._linkedWidthNode; }788set linkedWidthNode(node: LeafNode | undefined) {789this._onDidLinkedWidthNodeChange.input = node ? node._onDidViewChange : Event.None;790this._linkedWidthNode = node;791this._onDidSetLinkedNode.fire(undefined);792}793794private _onDidLinkedHeightNodeChange = new Relay<number | undefined>();795private _linkedHeightNode: LeafNode | undefined = undefined;796get linkedHeightNode(): LeafNode | undefined { return this._linkedHeightNode; }797set linkedHeightNode(node: LeafNode | undefined) {798this._onDidLinkedHeightNodeChange.input = node ? node._onDidViewChange : Event.None;799this._linkedHeightNode = node;800this._onDidSetLinkedNode.fire(undefined);801}802803private readonly _onDidSetLinkedNode = new Emitter<number | undefined>();804private _onDidViewChange: Event<number | undefined>;805readonly onDidChange: Event<number | undefined>;806807private readonly disposables = new DisposableStore();808809constructor(810readonly view: IView,811readonly orientation: Orientation,812readonly layoutController: LayoutController,813orthogonalSize: number,814size: number = 0815) {816this._orthogonalSize = orthogonalSize;817this._size = size;818819const onDidChange = createLatchedOnDidChangeViewEvent(view);820this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height), this.disposables);821this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);822}823824get width(): number {825return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;826}827828get height(): number {829return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;830}831832get top(): number {833return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset;834}835836get left(): number {837return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset;838}839840get element(): HTMLElement {841return this.view.element;842}843844private get minimumWidth(): number {845return this.linkedWidthNode ? Math.max(this.linkedWidthNode.view.minimumWidth, this.view.minimumWidth) : this.view.minimumWidth;846}847848private get maximumWidth(): number {849return this.linkedWidthNode ? Math.min(this.linkedWidthNode.view.maximumWidth, this.view.maximumWidth) : this.view.maximumWidth;850}851852private get minimumHeight(): number {853return this.linkedHeightNode ? Math.max(this.linkedHeightNode.view.minimumHeight, this.view.minimumHeight) : this.view.minimumHeight;854}855856private get maximumHeight(): number {857return this.linkedHeightNode ? Math.min(this.linkedHeightNode.view.maximumHeight, this.view.maximumHeight) : this.view.maximumHeight;858}859860get minimumSize(): number {861return this.orientation === Orientation.HORIZONTAL ? this.minimumHeight : this.minimumWidth;862}863864get maximumSize(): number {865return this.orientation === Orientation.HORIZONTAL ? this.maximumHeight : this.maximumWidth;866}867868get priority(): LayoutPriority | undefined {869return this.view.priority;870}871872get proportionalLayout(): boolean {873return this.view.proportionalLayout ?? true;874}875876get snap(): boolean | undefined {877return this.view.snap;878}879880get minimumOrthogonalSize(): number {881return this.orientation === Orientation.HORIZONTAL ? this.minimumWidth : this.minimumHeight;882}883884get maximumOrthogonalSize(): number {885return this.orientation === Orientation.HORIZONTAL ? this.maximumWidth : this.maximumHeight;886}887888private _boundarySashes: IRelativeBoundarySashes = {};889get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }890set boundarySashes(boundarySashes: IRelativeBoundarySashes) {891this._boundarySashes = boundarySashes;892893this.view.setBoundarySashes?.(toAbsoluteBoundarySashes(boundarySashes, this.orientation));894}895896layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {897if (!this.layoutController.isLayoutEnabled) {898return;899}900901if (typeof ctx === 'undefined') {902throw new Error('Invalid state');903}904905this._size = size;906this._orthogonalSize = ctx.orthogonalSize;907this.absoluteOffset = ctx.absoluteOffset + offset;908this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;909910this._layout(this.width, this.height, this.top, this.left);911}912913private cachedWidth: number = 0;914private cachedHeight: number = 0;915private cachedTop: number = 0;916private cachedLeft: number = 0;917918private _layout(width: number, height: number, top: number, left: number): void {919if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) {920return;921}922923this.cachedWidth = width;924this.cachedHeight = height;925this.cachedTop = top;926this.cachedLeft = left;927this.view.layout(width, height, top, left);928}929930setVisible(visible: boolean): void {931this.view.setVisible?.(visible);932}933934dispose(): void {935this.disposables.dispose();936}937}938939type Node = BranchNode | LeafNode;940941export interface INodeDescriptor {942node: Node;943visible?: boolean;944}945946function flipNode(node: BranchNode, size: number, orthogonalSize: number): BranchNode;947function flipNode(node: LeafNode, size: number, orthogonalSize: number): LeafNode;948function flipNode(node: Node, size: number, orthogonalSize: number): Node;949function flipNode(node: Node, size: number, orthogonalSize: number): Node {950if (node instanceof BranchNode) {951const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.splitviewProportionalLayout, size, orthogonalSize, node.edgeSnapping);952953let totalSize = 0;954955for (let i = node.children.length - 1; i >= 0; i--) {956const child = node.children[i];957const childSize = child instanceof BranchNode ? child.orthogonalSize : child.size;958959let newSize = node.size === 0 ? 0 : Math.round((size * childSize) / node.size);960totalSize += newSize;961962// The last view to add should adjust to rounding errors963if (i === 0) {964newSize += size - totalSize;965}966967result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true);968}969970node.dispose();971return result;972} else {973const result = new LeafNode(node.view, orthogonal(node.orientation), node.layoutController, orthogonalSize);974node.dispose();975return result;976}977}978979/**980* The location of a {@link IView view} within a {@link GridView}.981*982* A GridView is a tree composition of multiple {@link SplitView} instances, orthogonal983* between one another. Here's an example:984*985* ```986* +-----+---------------+987* | A | B |988* +-----+---------+-----+989* | C | |990* +---------------+ D |991* | E | |992* +---------------+-----+993* ```994*995* The above grid's tree structure is:996*997* ```998* Vertical SplitView999* +-Horizontal SplitView1000* | +-A1001* | +-B1002* +- Horizontal SplitView1003* +-Vertical SplitView1004* | +-C1005* | +-E1006* +-D1007* ```1008*1009* So, {@link IView views} within a {@link GridView} can be referenced by1010* a sequence of indexes, each index referencing each SplitView. Here are1011* each view's locations, from the example above:1012*1013* - `A`: `[0,0]`1014* - `B`: `[0,1]`1015* - `C`: `[1,0,0]`1016* - `D`: `[1,1]`1017* - `E`: `[1,0,1]`1018*/1019export type GridLocation = number[];10201021/**1022* The {@link GridView} is the UI component which implements a two dimensional1023* flex-like layout algorithm for a collection of {@link IView} instances, which1024* are mostly HTMLElement instances with size constraints. A {@link GridView} is a1025* tree composition of multiple {@link SplitView} instances, orthogonal between1026* one another. It will respect view's size contraints, just like the SplitView.1027*1028* It has a low-level index based API, allowing for fine grain performant operations.1029* Look into the {@link Grid} widget for a higher-level API.1030*1031* Features:1032* - flex-like layout algorithm1033* - snap support1034* - corner sash support1035* - Alt key modifier behavior, macOS style1036* - layout (de)serialization1037*/1038export class GridView implements IDisposable {10391040/**1041* The DOM element for this view.1042*/1043readonly element: HTMLElement;10441045private styles: IGridViewStyles;1046private proportionalLayout: boolean;1047private _root!: BranchNode;1048private onDidSashResetRelay = new Relay<GridLocation>();1049private _onDidScroll = new Relay<void>();1050private _onDidChange = new Relay<IViewSize | undefined>();1051private _boundarySashes: IBoundarySashes = {};10521053/**1054* The layout controller makes sure layout only propagates1055* to the views after the very first call to {@link GridView.layout}.1056*/1057private layoutController: LayoutController;1058private disposable2x2: IDisposable = Disposable.None;10591060private get root(): BranchNode { return this._root; }10611062private set root(root: BranchNode) {1063const oldRoot = this._root;10641065if (oldRoot) {1066oldRoot.element.remove();1067oldRoot.dispose();1068}10691070this._root = root;1071this.element.appendChild(root.element);1072this.onDidSashResetRelay.input = root.onDidSashReset;1073this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO1074this._onDidScroll.input = root.onDidScroll;1075}10761077/**1078* Fires whenever the user double clicks a {@link Sash sash}.1079*/1080readonly onDidSashReset = this.onDidSashResetRelay.event;10811082/**1083* Fires whenever the user scrolls a {@link SplitView} within1084* the grid.1085*/1086readonly onDidScroll = this._onDidScroll.event;10871088/**1089* Fires whenever a view within the grid changes its size constraints.1090*/1091readonly onDidChange = this._onDidChange.event;10921093/**1094* The width of the grid.1095*/1096get width(): number { return this.root.width; }10971098/**1099* The height of the grid.1100*/1101get height(): number { return this.root.height; }11021103/**1104* The minimum width of the grid.1105*/1106get minimumWidth(): number { return this.root.minimumWidth; }11071108/**1109* The minimum height of the grid.1110*/1111get minimumHeight(): number { return this.root.minimumHeight; }11121113/**1114* The maximum width of the grid.1115*/1116get maximumWidth(): number { return this.root.maximumHeight; }11171118/**1119* The maximum height of the grid.1120*/1121get maximumHeight(): number { return this.root.maximumHeight; }11221123get orientation(): Orientation { return this._root.orientation; }1124get boundarySashes(): IBoundarySashes { return this._boundarySashes; }11251126/**1127* The orientation of the grid. Matches the orientation of the root1128* {@link SplitView} in the grid's tree model.1129*/1130set orientation(orientation: Orientation) {1131if (this._root.orientation === orientation) {1132return;1133}11341135const { size, orthogonalSize, absoluteOffset, absoluteOrthogonalOffset } = this._root;1136this.root = flipNode(this._root, orthogonalSize, size);1137this.root.layout(size, 0, { orthogonalSize, absoluteOffset: absoluteOrthogonalOffset, absoluteOrthogonalOffset: absoluteOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });1138this.boundarySashes = this.boundarySashes;1139}11401141/**1142* A collection of sashes perpendicular to each edge of the grid.1143* Corner sashes will be created for each intersection.1144*/1145set boundarySashes(boundarySashes: IBoundarySashes) {1146this._boundarySashes = boundarySashes;1147this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation);1148}11491150/**1151* Enable/disable edge snapping across all grid views.1152*/1153set edgeSnapping(edgeSnapping: boolean) {1154this.root.edgeSnapping = edgeSnapping;1155}11561157private maximizedNode: LeafNode | undefined = undefined;11581159private readonly _onDidChangeViewMaximized = new Emitter<boolean>();1160readonly onDidChangeViewMaximized = this._onDidChangeViewMaximized.event;11611162/**1163* Create a new {@link GridView} instance.1164*1165* @remarks It's the caller's responsibility to append the1166* {@link GridView.element} to the page's DOM.1167*/1168constructor(options: IGridViewOptions = {}) {1169this.element = $('.monaco-grid-view');1170this.styles = options.styles || defaultStyles;1171this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true;1172this.layoutController = new LayoutController(false);1173this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout);1174}11751176style(styles: IGridViewStyles): void {1177this.styles = styles;1178this.root.style(styles);1179}11801181/**1182* Layout the {@link GridView}.1183*1184* Optionally provide a `top` and `left` positions, those will propagate1185* as an origin for positions passed to {@link IView.layout}.1186*1187* @param width The width of the {@link GridView}.1188* @param height The height of the {@link GridView}.1189* @param top Optional, the top location of the {@link GridView}.1190* @param left Optional, the left location of the {@link GridView}.1191*/1192layout(width: number, height: number, top: number = 0, left: number = 0): void {1193this.layoutController.isLayoutEnabled = true;11941195const [size, orthogonalSize, offset, orthogonalOffset] = this.root.orientation === Orientation.HORIZONTAL ? [height, width, top, left] : [width, height, left, top];1196this.root.layout(size, 0, { orthogonalSize, absoluteOffset: offset, absoluteOrthogonalOffset: orthogonalOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });1197}11981199/**1200* Add a {@link IView view} to this {@link GridView}.1201*1202* @param view The view to add.1203* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.1204* @param location The {@link GridLocation location} to insert the view on.1205*/1206addView(view: IView, size: number | Sizing, location: GridLocation): void {1207if (this.hasMaximizedView()) {1208this.exitMaximizedView();1209}12101211this.disposable2x2.dispose();1212this.disposable2x2 = Disposable.None;12131214const [rest, index] = tail(location);1215const [pathToParent, parent] = this.getNode(rest);12161217if (parent instanceof BranchNode) {1218const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize);12191220try {1221parent.addChild(node, size, index);1222} catch (err) {1223node.dispose();1224throw err;1225}1226} else {1227const [, grandParent] = tail(pathToParent);1228const [, parentIndex] = tail(rest);12291230let newSiblingSize: number | Sizing = 0;12311232const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(parentIndex);1233if (typeof newSiblingCachedVisibleSize === 'number') {1234newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize);1235}12361237const oldChild = grandParent.removeChild(parentIndex);1238oldChild.dispose();12391240const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping);1241grandParent.addChild(newParent, parent.size, parentIndex);12421243const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size);1244newParent.addChild(newSibling, newSiblingSize, 0);12451246if (typeof size !== 'number' && size.type === 'split') {1247size = Sizing.Split(0);1248}12491250const node = new LeafNode(view, grandParent.orientation, this.layoutController, parent.size);1251newParent.addChild(node, size, index);1252}12531254this.trySet2x2();1255}12561257/**1258* Remove a {@link IView view} from this {@link GridView}.1259*1260* @param location The {@link GridLocation location} of the {@link IView view}.1261* @param sizing Whether to distribute other {@link IView view}'s sizes.1262*/1263removeView(location: GridLocation, sizing?: DistributeSizing | AutoSizing): IView {1264if (this.hasMaximizedView()) {1265this.exitMaximizedView();1266}12671268this.disposable2x2.dispose();1269this.disposable2x2 = Disposable.None;12701271const [rest, index] = tail(location);1272const [pathToParent, parent] = this.getNode(rest);12731274if (!(parent instanceof BranchNode)) {1275throw new Error('Invalid location');1276}12771278const node = parent.children[index];12791280if (!(node instanceof LeafNode)) {1281throw new Error('Invalid location');1282}12831284parent.removeChild(index, sizing);1285node.dispose();12861287if (parent.children.length === 0) {1288throw new Error('Invalid grid state');1289}12901291if (parent.children.length > 1) {1292this.trySet2x2();1293return node.view;1294}12951296if (pathToParent.length === 0) { // parent is root1297const sibling = parent.children[0];12981299if (sibling instanceof LeafNode) {1300return node.view;1301}13021303// we must promote sibling to be the new root1304parent.removeChild(0);1305parent.dispose();1306this.root = sibling;1307this.boundarySashes = this.boundarySashes;1308this.trySet2x2();1309return node.view;1310}13111312const [, grandParent] = tail(pathToParent);1313const [, parentIndex] = tail(rest);13141315const isSiblingVisible = parent.isChildVisible(0);1316const sibling = parent.removeChild(0);13171318const sizes = grandParent.children.map((_, i) => grandParent.getChildSize(i));1319grandParent.removeChild(parentIndex, sizing);1320parent.dispose();13211322if (sibling instanceof BranchNode) {1323sizes.splice(parentIndex, 1, ...sibling.children.map(c => c.size));13241325const siblingChildren = sibling.removeAllChildren();13261327for (let i = 0; i < siblingChildren.length; i++) {1328grandParent.addChild(siblingChildren[i], siblingChildren[i].size, parentIndex + i);1329}1330} else {1331const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size);1332const sizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize);1333grandParent.addChild(newSibling, sizing, parentIndex);1334}13351336sibling.dispose();13371338for (let i = 0; i < sizes.length; i++) {1339grandParent.resizeChild(i, sizes[i]);1340}13411342this.trySet2x2();1343return node.view;1344}13451346/**1347* Move a {@link IView view} within its parent.1348*1349* @param parentLocation The {@link GridLocation location} of the {@link IView view}'s parent.1350* @param from The index of the {@link IView view} to move.1351* @param to The index where the {@link IView view} should move to.1352*/1353moveView(parentLocation: GridLocation, from: number, to: number): void {1354if (this.hasMaximizedView()) {1355this.exitMaximizedView();1356}13571358const [, parent] = this.getNode(parentLocation);13591360if (!(parent instanceof BranchNode)) {1361throw new Error('Invalid location');1362}13631364parent.moveChild(from, to);13651366this.trySet2x2();1367}13681369/**1370* Swap two {@link IView views} within the {@link GridView}.1371*1372* @param from The {@link GridLocation location} of one view.1373* @param to The {@link GridLocation location} of another view.1374*/1375swapViews(from: GridLocation, to: GridLocation): void {1376if (this.hasMaximizedView()) {1377this.exitMaximizedView();1378}13791380const [fromRest, fromIndex] = tail(from);1381const [, fromParent] = this.getNode(fromRest);13821383if (!(fromParent instanceof BranchNode)) {1384throw new Error('Invalid from location');1385}13861387const fromSize = fromParent.getChildSize(fromIndex);1388const fromNode = fromParent.children[fromIndex];13891390if (!(fromNode instanceof LeafNode)) {1391throw new Error('Invalid from location');1392}13931394const [toRest, toIndex] = tail(to);1395const [, toParent] = this.getNode(toRest);13961397if (!(toParent instanceof BranchNode)) {1398throw new Error('Invalid to location');1399}14001401const toSize = toParent.getChildSize(toIndex);1402const toNode = toParent.children[toIndex];14031404if (!(toNode instanceof LeafNode)) {1405throw new Error('Invalid to location');1406}14071408if (fromParent === toParent) {1409fromParent.swapChildren(fromIndex, toIndex);1410} else {1411fromParent.removeChild(fromIndex);1412toParent.removeChild(toIndex);14131414fromParent.addChild(toNode, fromSize, fromIndex);1415toParent.addChild(fromNode, toSize, toIndex);1416}14171418this.trySet2x2();1419}14201421/**1422* Resize a {@link IView view}.1423*1424* @param location The {@link GridLocation location} of the view.1425* @param size The size the view should be. Optionally provide a single dimension.1426*/1427resizeView(location: GridLocation, size: Partial<IViewSize>): void {1428if (this.hasMaximizedView()) {1429this.exitMaximizedView();1430}14311432const [rest, index] = tail(location);1433const [pathToParent, parent] = this.getNode(rest);14341435if (!(parent instanceof BranchNode)) {1436throw new Error('Invalid location');1437}14381439if (!size.width && !size.height) {1440return;1441}14421443const [parentSize, grandParentSize] = parent.orientation === Orientation.HORIZONTAL ? [size.width, size.height] : [size.height, size.width];14441445if (typeof grandParentSize === 'number' && pathToParent.length > 0) {1446const [, grandParent] = tail(pathToParent);1447const [, parentIndex] = tail(rest);14481449grandParent.resizeChild(parentIndex, grandParentSize);1450}14511452if (typeof parentSize === 'number') {1453parent.resizeChild(index, parentSize);1454}14551456this.trySet2x2();1457}14581459/**1460* Get the size of a {@link IView view}.1461*1462* @param location The {@link GridLocation location} of the view. Provide `undefined` to get1463* the size of the grid itself.1464*/1465getViewSize(location?: GridLocation): IViewSize {1466if (!location) {1467return { width: this.root.width, height: this.root.height };1468}14691470const [, node] = this.getNode(location);1471return { width: node.width, height: node.height };1472}14731474/**1475* Get the cached visible size of a {@link IView view}. This was the size1476* of the view at the moment it last became hidden.1477*1478* @param location The {@link GridLocation location} of the view.1479*/1480getViewCachedVisibleSize(location: GridLocation): number | undefined {1481const [rest, index] = tail(location);1482const [, parent] = this.getNode(rest);14831484if (!(parent instanceof BranchNode)) {1485throw new Error('Invalid location');1486}14871488return parent.getChildCachedVisibleSize(index);1489}14901491/**1492* Maximize the size of a {@link IView view} by collapsing all other views1493* to their minimum sizes.1494*1495* @param location The {@link GridLocation location} of the view.1496*/1497expandView(location: GridLocation): void {1498if (this.hasMaximizedView()) {1499this.exitMaximizedView();1500}15011502const [ancestors, node] = this.getNode(location);15031504if (!(node instanceof LeafNode)) {1505throw new Error('Invalid location');1506}15071508for (let i = 0; i < ancestors.length; i++) {1509ancestors[i].resizeChild(location[i], Number.POSITIVE_INFINITY);1510}1511}15121513/**1514* Returns whether all other {@link IView views} are at their minimum size.1515*1516* @param location The {@link GridLocation location} of the view.1517*/1518isViewExpanded(location: GridLocation): boolean {1519if (this.hasMaximizedView()) {1520// No view can be expanded when a view is maximized1521return false;1522}15231524const [ancestors, node] = this.getNode(location);15251526if (!(node instanceof LeafNode)) {1527throw new Error('Invalid location');1528}15291530for (let i = 0; i < ancestors.length; i++) {1531if (!ancestors[i].isChildExpanded(location[i])) {1532return false;1533}1534}15351536return true;1537}15381539maximizeView(location: GridLocation) {1540const [, nodeToMaximize] = this.getNode(location);1541if (!(nodeToMaximize instanceof LeafNode)) {1542throw new Error('Location is not a LeafNode');1543}15441545if (this.maximizedNode === nodeToMaximize) {1546return;1547}15481549if (this.hasMaximizedView()) {1550this.exitMaximizedView();1551}15521553function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {1554for (let i = 0; i < parent.children.length; i++) {1555const child = parent.children[i];1556if (child instanceof LeafNode) {1557if (child !== exclude) {1558parent.setChildVisible(i, false);1559}1560} else {1561hideAllViewsBut(child, exclude);1562}1563}1564}15651566hideAllViewsBut(this.root, nodeToMaximize);15671568this.maximizedNode = nodeToMaximize;1569this._onDidChangeViewMaximized.fire(true);1570}15711572exitMaximizedView(): void {1573if (!this.maximizedNode) {1574return;1575}1576this.maximizedNode = undefined;15771578// When hiding a view, it's previous size is cached.1579// To restore the sizes of all views, they need to be made visible in reverse order.1580function showViewsInReverseOrder(parent: BranchNode): void {1581for (let index = parent.children.length - 1; index >= 0; index--) {1582const child = parent.children[index];1583if (child instanceof LeafNode) {1584parent.setChildVisible(index, true);1585} else {1586showViewsInReverseOrder(child);1587}1588}1589}15901591showViewsInReverseOrder(this.root);15921593this._onDidChangeViewMaximized.fire(false);1594}15951596hasMaximizedView(): boolean {1597return this.maximizedNode !== undefined;1598}15991600/**1601* Returns whether the {@link IView view} is maximized.1602*1603* @param location The {@link GridLocation location} of the view.1604*/1605isViewMaximized(location: GridLocation): boolean {1606const [, node] = this.getNode(location);1607if (!(node instanceof LeafNode)) {1608throw new Error('Location is not a LeafNode');1609}1610return node === this.maximizedNode;1611}16121613/**1614* Distribute the size among all {@link IView views} within the entire1615* grid or within a single {@link SplitView}.1616*1617* @param location The {@link GridLocation location} of a view containing1618* children views, which will have their sizes distributed within the parent1619* view's size. Provide `undefined` to recursively distribute all views' sizes1620* in the entire grid.1621*/1622distributeViewSizes(location?: GridLocation): void {1623if (this.hasMaximizedView()) {1624this.exitMaximizedView();1625}16261627if (!location) {1628this.root.distributeViewSizes(true);1629return;1630}16311632const [, node] = this.getNode(location);16331634if (!(node instanceof BranchNode)) {1635throw new Error('Invalid location');1636}16371638node.distributeViewSizes();1639this.trySet2x2();1640}16411642/**1643* Returns whether a {@link IView view} is visible.1644*1645* @param location The {@link GridLocation location} of the view.1646*/1647isViewVisible(location: GridLocation): boolean {1648const [rest, index] = tail(location);1649const [, parent] = this.getNode(rest);16501651if (!(parent instanceof BranchNode)) {1652throw new Error('Invalid from location');1653}16541655return parent.isChildVisible(index);1656}16571658/**1659* Set the visibility state of a {@link IView view}.1660*1661* @param location The {@link GridLocation location} of the view.1662*/1663setViewVisible(location: GridLocation, visible: boolean): void {1664if (this.hasMaximizedView()) {1665this.exitMaximizedView();1666return;1667}16681669const [rest, index] = tail(location);1670const [, parent] = this.getNode(rest);16711672if (!(parent instanceof BranchNode)) {1673throw new Error('Invalid from location');1674}16751676parent.setChildVisible(index, visible);1677}16781679/**1680* Returns a descriptor for the entire grid.1681*/1682getView(): GridBranchNode;16831684/**1685* Returns a descriptor for a {@link GridLocation subtree} within the1686* {@link GridView}.1687*1688* @param location The {@link GridLocation location} of the root of1689* the {@link GridLocation subtree}.1690*/1691getView(location: GridLocation): GridNode;1692getView(location?: GridLocation): GridNode {1693const node = location ? this.getNode(location)[1] : this._root;1694return this._getViews(node, this.orientation);1695}16961697/**1698* Construct a new {@link GridView} from a JSON object.1699*1700* @param json The JSON object.1701* @param deserializer A deserializer which can revive each view.1702* @returns A new {@link GridView} instance.1703*/1704static deserialize<T extends ISerializableView>(json: ISerializedGridView, deserializer: IViewDeserializer<T>, options: IGridViewOptions = {}): GridView {1705if (typeof json.orientation !== 'number') {1706throw new Error('Invalid JSON: \'orientation\' property must be a number.');1707} else if (typeof json.width !== 'number') {1708throw new Error('Invalid JSON: \'width\' property must be a number.');1709} else if (typeof json.height !== 'number') {1710throw new Error('Invalid JSON: \'height\' property must be a number.');1711} else if (json.root?.type !== 'branch') {1712throw new Error('Invalid JSON: \'root\' property must have \'type\' value of branch.');1713}17141715const orientation = json.orientation;1716const height = json.height;17171718const result = new GridView(options);1719result._deserialize(json.root, orientation, deserializer, height);17201721return result;1722}17231724private _deserialize(root: ISerializedBranchNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): void {1725this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize) as BranchNode;1726}17271728private _deserializeNode(node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): Node {1729let result: Node;1730if (node.type === 'branch') {1731const serializedChildren = node.data;1732const children = serializedChildren.map(serializedChild => {1733return {1734node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size),1735visible: (serializedChild as { visible?: boolean }).visible1736} satisfies INodeDescriptor;1737});17381739result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children);1740} else {1741result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size);1742if (node.maximized && !this.maximizedNode) {1743this.maximizedNode = result;1744this._onDidChangeViewMaximized.fire(true);1745}1746}17471748return result;1749}17501751private _getViews(node: Node, orientation: Orientation, cachedVisibleSize?: number): GridNode {1752const box = { top: node.top, left: node.left, width: node.width, height: node.height };17531754if (node instanceof LeafNode) {1755return { view: node.view, box, cachedVisibleSize, maximized: this.maximizedNode === node };1756}17571758const children: GridNode[] = [];17591760for (let i = 0; i < node.children.length; i++) {1761const child = node.children[i];1762const cachedVisibleSize = node.getChildCachedVisibleSize(i);17631764children.push(this._getViews(child, orthogonal(orientation), cachedVisibleSize));1765}17661767return { children, box };1768}17691770private getNode(location: GridLocation, node: Node = this.root, path: BranchNode[] = []): [BranchNode[], Node] {1771if (location.length === 0) {1772return [path, node];1773}17741775if (!(node instanceof BranchNode)) {1776throw new Error('Invalid location');1777}17781779const [index, ...rest] = location;17801781if (index < 0 || index >= node.children.length) {1782throw new Error('Invalid location');1783}17841785const child = node.children[index];1786path.push(node);17871788return this.getNode(rest, child, path);1789}17901791/**1792* Attempt to lock the {@link Sash sashes} in this {@link GridView} so1793* the grid behaves as a 2x2 matrix, with a corner sash in the middle.1794*1795* In case the grid isn't a 2x2 grid _and_ all sashes are not aligned,1796* this method is a no-op.1797*/1798trySet2x2(): void {1799this.disposable2x2.dispose();1800this.disposable2x2 = Disposable.None;18011802if (this.root.children.length !== 2) {1803return;1804}18051806const [first, second] = this.root.children;18071808if (!(first instanceof BranchNode) || !(second instanceof BranchNode)) {1809return;1810}18111812this.disposable2x2 = first.trySet2x2(second);1813}18141815/**1816* Populate a map with views to DOM nodes.1817* @remarks To be used internally only.1818*/1819getViewMap(map: Map<IView, HTMLElement>, node?: Node): void {1820if (!node) {1821node = this.root;1822}18231824if (node instanceof BranchNode) {1825node.children.forEach(child => this.getViewMap(map, child));1826} else {1827map.set(node.view, node.element);1828}1829}18301831dispose(): void {1832this.onDidSashResetRelay.dispose();1833this.root.dispose();1834this.element.remove();1835}1836}183718381839