Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/grid/gridview.ts
5255 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $ } from '../../dom.js';
7
import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js';
8
import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js';
9
import { equals as arrayEquals, tail } from '../../../common/arrays.js';
10
import { Color } from '../../../common/color.js';
11
import { Emitter, Event, Relay } from '../../../common/event.js';
12
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';
13
import { rot } from '../../../common/numbers.js';
14
import { isUndefined } from '../../../common/types.js';
15
import './gridview.css';
16
17
export { Orientation } from '../sash/sash.js';
18
export { LayoutPriority, Sizing } from '../splitview/splitview.js';
19
20
export interface IGridViewStyles extends ISplitViewStyles { }
21
22
const defaultStyles: IGridViewStyles = {
23
separatorBorder: Color.transparent
24
};
25
26
export interface IViewSize {
27
readonly width: number;
28
readonly height: number;
29
}
30
31
interface IRelativeBoundarySashes {
32
readonly start?: Sash;
33
readonly end?: Sash;
34
readonly orthogonalStart?: Sash;
35
readonly orthogonalEnd?: Sash;
36
}
37
38
/**
39
* The interface to implement for views within a {@link GridView}.
40
*/
41
export interface IView {
42
43
/**
44
* The DOM element for this view.
45
*/
46
readonly element: HTMLElement;
47
48
/**
49
* A minimum width for this view.
50
*
51
* @remarks If none, set it to `0`.
52
*/
53
readonly minimumWidth: number;
54
55
/**
56
* A minimum width for this view.
57
*
58
* @remarks If none, set it to `Number.POSITIVE_INFINITY`.
59
*/
60
readonly maximumWidth: number;
61
62
/**
63
* A minimum height for this view.
64
*
65
* @remarks If none, set it to `0`.
66
*/
67
readonly minimumHeight: number;
68
69
/**
70
* A minimum height for this view.
71
*
72
* @remarks If none, set it to `Number.POSITIVE_INFINITY`.
73
*/
74
readonly maximumHeight: number;
75
76
/**
77
* The priority of the view when the {@link GridView} layout algorithm
78
* runs. Views with higher priority will be resized first.
79
*
80
* @remarks Only used when `proportionalLayout` is false.
81
*/
82
readonly priority?: LayoutPriority;
83
84
/**
85
* If the {@link GridView} supports proportional layout,
86
* this property allows for finer control over the proportional layout algorithm, per view.
87
*
88
* @defaultValue `true`
89
*/
90
readonly proportionalLayout?: boolean;
91
92
/**
93
* Whether the view will snap whenever the user reaches its minimum size or
94
* attempts to grow it beyond the minimum size.
95
*
96
* @defaultValue `false`
97
*/
98
readonly snap?: boolean;
99
100
/**
101
* View instances are supposed to fire this event whenever any of the constraint
102
* properties have changed:
103
*
104
* - {@link IView.minimumWidth}
105
* - {@link IView.maximumWidth}
106
* - {@link IView.minimumHeight}
107
* - {@link IView.maximumHeight}
108
* - {@link IView.priority}
109
* - {@link IView.snap}
110
*
111
* The {@link GridView} will relayout whenever that happens. The event can
112
* optionally emit the view's preferred size for that relayout.
113
*/
114
readonly onDidChange: Event<IViewSize | undefined>;
115
116
/**
117
* This will be called by the {@link GridView} during layout. A view meant to
118
* pass along the layout information down to its descendants.
119
*/
120
layout(width: number, height: number, top: number, left: number): void;
121
122
/**
123
* This will be called by the {@link GridView} whenever this view is made
124
* visible or hidden.
125
*
126
* @param visible Whether the view becomes visible.
127
*/
128
setVisible?(visible: boolean): void;
129
130
/**
131
* This will be called by the {@link GridView} whenever this view is on
132
* an edge of the grid and the grid's
133
* {@link GridView.boundarySashes boundary sashes} change.
134
*/
135
setBoundarySashes?(sashes: IBoundarySashes): void;
136
}
137
138
export interface ISerializableView extends IView {
139
toJSON(): object;
140
}
141
142
export interface IViewDeserializer<T extends ISerializableView> {
143
fromJSON(json: any): T;
144
}
145
146
export interface ISerializedLeafNode {
147
type: 'leaf';
148
data: unknown;
149
size: number;
150
visible?: boolean;
151
maximized?: boolean;
152
}
153
154
export interface ISerializedBranchNode {
155
type: 'branch';
156
data: ISerializedNode[];
157
size: number;
158
visible?: boolean;
159
}
160
161
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
162
163
export interface ISerializedGridView {
164
root: ISerializedNode;
165
orientation: Orientation;
166
width: number;
167
height: number;
168
}
169
170
export function orthogonal(orientation: Orientation): Orientation {
171
return orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
172
}
173
174
export interface Box {
175
readonly top: number;
176
readonly left: number;
177
readonly width: number;
178
readonly height: number;
179
}
180
181
export interface GridLeafNode {
182
readonly view: IView;
183
readonly box: Box;
184
readonly cachedVisibleSize: number | undefined;
185
readonly maximized: boolean;
186
}
187
188
export interface GridBranchNode {
189
readonly children: GridNode[];
190
readonly box: Box;
191
}
192
193
export type GridNode = GridLeafNode | GridBranchNode;
194
195
export function isGridBranchNode(node: GridNode): node is GridBranchNode {
196
// eslint-disable-next-line local/code-no-any-casts
197
return !!(node as any).children;
198
}
199
200
class LayoutController {
201
constructor(public isLayoutEnabled: boolean) { }
202
}
203
204
export interface IGridViewOptions {
205
206
/**
207
* Styles overriding the {@link defaultStyles default ones}.
208
*/
209
readonly styles?: IGridViewStyles;
210
211
/**
212
* Resize each view proportionally when resizing the {@link GridView}.
213
*
214
* @defaultValue `true`
215
*/
216
readonly proportionalLayout?: boolean; // default true
217
}
218
219
interface ILayoutContext {
220
readonly orthogonalSize: number;
221
readonly absoluteOffset: number;
222
readonly absoluteOrthogonalOffset: number;
223
readonly absoluteSize: number;
224
readonly absoluteOrthogonalSize: number;
225
}
226
227
function toAbsoluteBoundarySashes(sashes: IRelativeBoundarySashes, orientation: Orientation): IBoundarySashes {
228
if (orientation === Orientation.HORIZONTAL) {
229
return { left: sashes.start, right: sashes.end, top: sashes.orthogonalStart, bottom: sashes.orthogonalEnd };
230
} else {
231
return { top: sashes.start, bottom: sashes.end, left: sashes.orthogonalStart, right: sashes.orthogonalEnd };
232
}
233
}
234
235
function fromAbsoluteBoundarySashes(sashes: IBoundarySashes, orientation: Orientation): IRelativeBoundarySashes {
236
if (orientation === Orientation.HORIZONTAL) {
237
return { start: sashes.left, end: sashes.right, orthogonalStart: sashes.top, orthogonalEnd: sashes.bottom };
238
} else {
239
return { start: sashes.top, end: sashes.bottom, orthogonalStart: sashes.left, orthogonalEnd: sashes.right };
240
}
241
}
242
243
function validateIndex(index: number, numChildren: number): number {
244
if (Math.abs(index) > numChildren) {
245
throw new Error('Invalid index');
246
}
247
248
return rot(index, numChildren + 1);
249
}
250
251
class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
252
253
readonly element: HTMLElement;
254
readonly children: Node[] = [];
255
private splitview: SplitView<ILayoutContext, Node>;
256
257
private _size: number;
258
get size(): number { return this._size; }
259
260
private _orthogonalSize: number;
261
get orthogonalSize(): number { return this._orthogonalSize; }
262
263
private _absoluteOffset: number = 0;
264
get absoluteOffset(): number { return this._absoluteOffset; }
265
266
private _absoluteOrthogonalOffset: number = 0;
267
get absoluteOrthogonalOffset(): number { return this._absoluteOrthogonalOffset; }
268
269
private absoluteOrthogonalSize: number = 0;
270
271
private _styles: IGridViewStyles;
272
get styles(): IGridViewStyles { return this._styles; }
273
274
get width(): number {
275
return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;
276
}
277
278
get height(): number {
279
return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;
280
}
281
282
get top(): number {
283
return this.orientation === Orientation.HORIZONTAL ? this._absoluteOffset : this._absoluteOrthogonalOffset;
284
}
285
286
get left(): number {
287
return this.orientation === Orientation.HORIZONTAL ? this._absoluteOrthogonalOffset : this._absoluteOffset;
288
}
289
290
get minimumSize(): number {
291
return this.children.length === 0 ? 0 : Math.max(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.minimumOrthogonalSize : 0));
292
}
293
294
get maximumSize(): number {
295
return Math.min(...this.children.map((c, index) => this.splitview.isViewVisible(index) ? c.maximumOrthogonalSize : Number.POSITIVE_INFINITY));
296
}
297
298
get priority(): LayoutPriority {
299
if (this.children.length === 0) {
300
return LayoutPriority.Normal;
301
}
302
303
const priorities = this.children.map(c => typeof c.priority === 'undefined' ? LayoutPriority.Normal : c.priority);
304
305
if (priorities.some(p => p === LayoutPriority.High)) {
306
return LayoutPriority.High;
307
} else if (priorities.some(p => p === LayoutPriority.Low)) {
308
return LayoutPriority.Low;
309
}
310
311
return LayoutPriority.Normal;
312
}
313
314
get proportionalLayout(): boolean {
315
if (this.children.length === 0) {
316
return true;
317
}
318
319
return this.children.every(c => c.proportionalLayout);
320
}
321
322
get minimumOrthogonalSize(): number {
323
return this.splitview.minimumSize;
324
}
325
326
get maximumOrthogonalSize(): number {
327
return this.splitview.maximumSize;
328
}
329
330
get minimumWidth(): number {
331
return this.orientation === Orientation.HORIZONTAL ? this.minimumOrthogonalSize : this.minimumSize;
332
}
333
334
get minimumHeight(): number {
335
return this.orientation === Orientation.HORIZONTAL ? this.minimumSize : this.minimumOrthogonalSize;
336
}
337
338
get maximumWidth(): number {
339
return this.orientation === Orientation.HORIZONTAL ? this.maximumOrthogonalSize : this.maximumSize;
340
}
341
342
get maximumHeight(): number {
343
return this.orientation === Orientation.HORIZONTAL ? this.maximumSize : this.maximumOrthogonalSize;
344
}
345
346
private readonly _onDidChange = new Emitter<number | undefined>();
347
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
348
349
private readonly _onDidVisibilityChange = new Emitter<boolean>();
350
readonly onDidVisibilityChange: Event<boolean> = this._onDidVisibilityChange.event;
351
private readonly childrenVisibilityChangeDisposable: DisposableStore = new DisposableStore();
352
353
private _onDidScroll = new Emitter<void>();
354
private onDidScrollDisposable: IDisposable = Disposable.None;
355
readonly onDidScroll: Event<void> = this._onDidScroll.event;
356
357
private childrenChangeDisposable: IDisposable = Disposable.None;
358
359
private readonly _onDidSashReset = new Emitter<GridLocation>();
360
readonly onDidSashReset: Event<GridLocation> = this._onDidSashReset.event;
361
private splitviewSashResetDisposable: IDisposable = Disposable.None;
362
private childrenSashResetDisposable: IDisposable = Disposable.None;
363
364
private _boundarySashes: IRelativeBoundarySashes = {};
365
get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }
366
set boundarySashes(boundarySashes: IRelativeBoundarySashes) {
367
if (this._boundarySashes.start === boundarySashes.start
368
&& this._boundarySashes.end === boundarySashes.end
369
&& this._boundarySashes.orthogonalStart === boundarySashes.orthogonalStart
370
&& this._boundarySashes.orthogonalEnd === boundarySashes.orthogonalEnd) {
371
return;
372
}
373
374
this._boundarySashes = boundarySashes;
375
376
this.splitview.orthogonalStartSash = boundarySashes.orthogonalStart;
377
this.splitview.orthogonalEndSash = boundarySashes.orthogonalEnd;
378
379
for (let index = 0; index < this.children.length; index++) {
380
const child = this.children[index];
381
const first = index === 0;
382
const last = index === this.children.length - 1;
383
384
child.boundarySashes = {
385
start: boundarySashes.orthogonalStart,
386
end: boundarySashes.orthogonalEnd,
387
orthogonalStart: first ? boundarySashes.start : child.boundarySashes.orthogonalStart,
388
orthogonalEnd: last ? boundarySashes.end : child.boundarySashes.orthogonalEnd,
389
};
390
}
391
}
392
393
private _edgeSnapping = false;
394
get edgeSnapping(): boolean { return this._edgeSnapping; }
395
set edgeSnapping(edgeSnapping: boolean) {
396
if (this._edgeSnapping === edgeSnapping) {
397
return;
398
}
399
400
this._edgeSnapping = edgeSnapping;
401
402
for (const child of this.children) {
403
if (child instanceof BranchNode) {
404
child.edgeSnapping = edgeSnapping;
405
}
406
}
407
408
this.updateSplitviewEdgeSnappingEnablement();
409
}
410
411
constructor(
412
readonly orientation: Orientation,
413
readonly layoutController: LayoutController,
414
styles: IGridViewStyles,
415
readonly splitviewProportionalLayout: boolean,
416
size: number = 0,
417
orthogonalSize: number = 0,
418
edgeSnapping: boolean = false,
419
childDescriptors?: INodeDescriptor[]
420
) {
421
this._styles = styles;
422
this._size = size;
423
this._orthogonalSize = orthogonalSize;
424
425
this.element = $('.monaco-grid-branch-node');
426
427
if (!childDescriptors) {
428
// Normal behavior, we have no children yet, just set up the splitview
429
this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout: splitviewProportionalLayout });
430
this.splitview.layout(size, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
431
} else {
432
// Reconstruction behavior, we want to reconstruct a splitview
433
const descriptor = {
434
views: childDescriptors.map(childDescriptor => {
435
return {
436
view: childDescriptor.node,
437
size: childDescriptor.node.size,
438
visible: childDescriptor.visible !== false
439
};
440
}),
441
size: this.orthogonalSize
442
};
443
444
const options = { proportionalLayout: splitviewProportionalLayout, orientation, styles };
445
446
this.children = childDescriptors.map(c => c.node);
447
this.splitview = new SplitView(this.element, { ...options, descriptor });
448
449
this.children.forEach((node, index) => {
450
const first = index === 0;
451
const last = index === this.children.length;
452
453
node.boundarySashes = {
454
start: this.boundarySashes.orthogonalStart,
455
end: this.boundarySashes.orthogonalEnd,
456
orthogonalStart: first ? this.boundarySashes.start : this.splitview.sashes[index - 1],
457
orthogonalEnd: last ? this.boundarySashes.end : this.splitview.sashes[index],
458
};
459
});
460
}
461
462
const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);
463
this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);
464
465
this.updateChildrenEvents();
466
}
467
468
style(styles: IGridViewStyles): void {
469
this._styles = styles;
470
this.splitview.style(styles);
471
472
for (const child of this.children) {
473
if (child instanceof BranchNode) {
474
child.style(styles);
475
}
476
}
477
}
478
479
layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {
480
if (!this.layoutController.isLayoutEnabled) {
481
return;
482
}
483
484
if (typeof ctx === 'undefined') {
485
throw new Error('Invalid state');
486
}
487
488
// branch nodes should flip the normal/orthogonal directions
489
this._size = ctx.orthogonalSize;
490
this._orthogonalSize = size;
491
this._absoluteOffset = ctx.absoluteOffset + offset;
492
this._absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
493
this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize;
494
495
this.splitview.layout(ctx.orthogonalSize, {
496
orthogonalSize: size,
497
absoluteOffset: this._absoluteOrthogonalOffset,
498
absoluteOrthogonalOffset: this._absoluteOffset,
499
absoluteSize: ctx.absoluteOrthogonalSize,
500
absoluteOrthogonalSize: ctx.absoluteSize
501
});
502
503
this.updateSplitviewEdgeSnappingEnablement();
504
}
505
506
setVisible(visible: boolean): void {
507
for (const child of this.children) {
508
child.setVisible(visible);
509
}
510
}
511
512
addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void {
513
index = validateIndex(index, this.children.length);
514
515
this.splitview.addView(node, size, index, skipLayout);
516
this.children.splice(index, 0, node);
517
518
this.updateBoundarySashes();
519
this.onDidChildrenChange();
520
}
521
522
removeChild(index: number, sizing?: Sizing): Node {
523
index = validateIndex(index, this.children.length);
524
525
const result = this.splitview.removeView(index, sizing);
526
this.children.splice(index, 1);
527
528
this.updateBoundarySashes();
529
this.onDidChildrenChange();
530
531
return result;
532
}
533
534
removeAllChildren(): Node[] {
535
const result = this.splitview.removeAllViews();
536
537
this.children.splice(0, this.children.length);
538
539
this.updateBoundarySashes();
540
this.onDidChildrenChange();
541
542
return result;
543
}
544
545
moveChild(from: number, to: number): void {
546
from = validateIndex(from, this.children.length);
547
to = validateIndex(to, this.children.length);
548
549
if (from === to) {
550
return;
551
}
552
553
if (from < to) {
554
to -= 1;
555
}
556
557
this.splitview.moveView(from, to);
558
this.children.splice(to, 0, this.children.splice(from, 1)[0]);
559
560
this.updateBoundarySashes();
561
this.onDidChildrenChange();
562
}
563
564
swapChildren(from: number, to: number): void {
565
from = validateIndex(from, this.children.length);
566
to = validateIndex(to, this.children.length);
567
568
if (from === to) {
569
return;
570
}
571
572
this.splitview.swapViews(from, to);
573
574
// swap boundary sashes
575
[this.children[from].boundarySashes, this.children[to].boundarySashes]
576
= [this.children[from].boundarySashes, this.children[to].boundarySashes];
577
578
// swap children
579
[this.children[from], this.children[to]] = [this.children[to], this.children[from]];
580
581
this.onDidChildrenChange();
582
}
583
584
resizeChild(index: number, size: number): void {
585
index = validateIndex(index, this.children.length);
586
587
this.splitview.resizeView(index, size);
588
}
589
590
isChildExpanded(index: number): boolean {
591
return this.splitview.isViewExpanded(index);
592
}
593
594
distributeViewSizes(recursive = false): void {
595
this.splitview.distributeViewSizes();
596
597
if (recursive) {
598
for (const child of this.children) {
599
if (child instanceof BranchNode) {
600
child.distributeViewSizes(true);
601
}
602
}
603
}
604
}
605
606
getChildSize(index: number): number {
607
index = validateIndex(index, this.children.length);
608
609
return this.splitview.getViewSize(index);
610
}
611
612
isChildVisible(index: number): boolean {
613
index = validateIndex(index, this.children.length);
614
615
return this.splitview.isViewVisible(index);
616
}
617
618
setChildVisible(index: number, visible: boolean): void {
619
index = validateIndex(index, this.children.length);
620
621
if (this.splitview.isViewVisible(index) === visible) {
622
return;
623
}
624
625
const wereAllChildrenHidden = this.splitview.contentSize === 0;
626
this.splitview.setViewVisible(index, visible);
627
const areAllChildrenHidden = this.splitview.contentSize === 0;
628
629
// If all children are hidden then the parent should hide the entire splitview
630
// If the entire splitview is hidden then the parent should show the splitview when a child is shown
631
if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) {
632
this._onDidVisibilityChange.fire(visible);
633
}
634
}
635
636
getChildCachedVisibleSize(index: number): number | undefined {
637
index = validateIndex(index, this.children.length);
638
639
return this.splitview.getViewCachedVisibleSize(index);
640
}
641
642
private updateBoundarySashes(): void {
643
for (let i = 0; i < this.children.length; i++) {
644
this.children[i].boundarySashes = {
645
start: this.boundarySashes.orthogonalStart,
646
end: this.boundarySashes.orthogonalEnd,
647
orthogonalStart: i === 0 ? this.boundarySashes.start : this.splitview.sashes[i - 1],
648
orthogonalEnd: i === this.children.length - 1 ? this.boundarySashes.end : this.splitview.sashes[i],
649
};
650
}
651
}
652
653
private onDidChildrenChange(): void {
654
this.updateChildrenEvents();
655
this._onDidChange.fire(undefined);
656
}
657
658
private updateChildrenEvents(): void {
659
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
660
this.childrenChangeDisposable.dispose();
661
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
662
663
const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));
664
this.childrenSashResetDisposable.dispose();
665
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
666
667
const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll));
668
this.onDidScrollDisposable.dispose();
669
this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll);
670
671
this.childrenVisibilityChangeDisposable.clear();
672
this.children.forEach((child, index) => {
673
if (child instanceof BranchNode) {
674
this.childrenVisibilityChangeDisposable.add(child.onDidVisibilityChange((visible) => {
675
this.setChildVisible(index, visible);
676
}));
677
}
678
});
679
}
680
681
trySet2x2(other: BranchNode): IDisposable {
682
if (this.children.length !== 2 || other.children.length !== 2) {
683
return Disposable.None;
684
}
685
686
if (this.getChildSize(0) !== other.getChildSize(0)) {
687
return Disposable.None;
688
}
689
690
const [firstChild, secondChild] = this.children;
691
const [otherFirstChild, otherSecondChild] = other.children;
692
693
if (!(firstChild instanceof LeafNode) || !(secondChild instanceof LeafNode)) {
694
return Disposable.None;
695
}
696
697
if (!(otherFirstChild instanceof LeafNode) || !(otherSecondChild instanceof LeafNode)) {
698
return Disposable.None;
699
}
700
701
if (this.orientation === Orientation.VERTICAL) {
702
secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = firstChild;
703
firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = secondChild;
704
otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = otherFirstChild;
705
otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = otherSecondChild;
706
} else {
707
otherFirstChild.linkedWidthNode = secondChild.linkedHeightNode = firstChild;
708
otherSecondChild.linkedWidthNode = firstChild.linkedHeightNode = secondChild;
709
firstChild.linkedWidthNode = otherSecondChild.linkedHeightNode = otherFirstChild;
710
secondChild.linkedWidthNode = otherFirstChild.linkedHeightNode = otherSecondChild;
711
}
712
713
const mySash = this.splitview.sashes[0];
714
const otherSash = other.splitview.sashes[0];
715
mySash.linkedSash = otherSash;
716
otherSash.linkedSash = mySash;
717
718
this._onDidChange.fire(undefined);
719
other._onDidChange.fire(undefined);
720
721
return toDisposable(() => {
722
mySash.linkedSash = otherSash.linkedSash = undefined;
723
firstChild.linkedHeightNode = firstChild.linkedWidthNode = undefined;
724
secondChild.linkedHeightNode = secondChild.linkedWidthNode = undefined;
725
otherFirstChild.linkedHeightNode = otherFirstChild.linkedWidthNode = undefined;
726
otherSecondChild.linkedHeightNode = otherSecondChild.linkedWidthNode = undefined;
727
});
728
}
729
730
private updateSplitviewEdgeSnappingEnablement(): void {
731
this.splitview.startSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset > 0;
732
this.splitview.endSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;
733
}
734
735
dispose(): void {
736
for (const child of this.children) {
737
child.dispose();
738
}
739
740
this._onDidChange.dispose();
741
this._onDidSashReset.dispose();
742
this._onDidVisibilityChange.dispose();
743
744
this.childrenVisibilityChangeDisposable.dispose();
745
this.splitviewSashResetDisposable.dispose();
746
this.childrenSashResetDisposable.dispose();
747
this.childrenChangeDisposable.dispose();
748
this.onDidScrollDisposable.dispose();
749
this.splitview.dispose();
750
}
751
}
752
753
/**
754
* Creates a latched event that avoids being fired when the view
755
* constraints do not change at all.
756
*/
757
function createLatchedOnDidChangeViewEvent(view: IView): Event<IViewSize | undefined> {
758
const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split<undefined, IViewSize>(view.onDidChange, isUndefined);
759
760
return Event.any(
761
onDidSetViewSize,
762
Event.map(
763
Event.latch(
764
Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])),
765
arrayEquals
766
),
767
_ => undefined
768
)
769
);
770
}
771
772
class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
773
774
private _size: number = 0;
775
get size(): number { return this._size; }
776
777
private _orthogonalSize: number;
778
get orthogonalSize(): number { return this._orthogonalSize; }
779
780
private absoluteOffset: number = 0;
781
private absoluteOrthogonalOffset: number = 0;
782
783
readonly onDidScroll: Event<void> = Event.None;
784
readonly onDidSashReset: Event<GridLocation> = Event.None;
785
786
private _onDidLinkedWidthNodeChange = new Relay<number | undefined>();
787
private _linkedWidthNode: LeafNode | undefined = undefined;
788
get linkedWidthNode(): LeafNode | undefined { return this._linkedWidthNode; }
789
set linkedWidthNode(node: LeafNode | undefined) {
790
this._onDidLinkedWidthNodeChange.input = node ? node._onDidViewChange : Event.None;
791
this._linkedWidthNode = node;
792
this._onDidSetLinkedNode.fire(undefined);
793
}
794
795
private _onDidLinkedHeightNodeChange = new Relay<number | undefined>();
796
private _linkedHeightNode: LeafNode | undefined = undefined;
797
get linkedHeightNode(): LeafNode | undefined { return this._linkedHeightNode; }
798
set linkedHeightNode(node: LeafNode | undefined) {
799
this._onDidLinkedHeightNodeChange.input = node ? node._onDidViewChange : Event.None;
800
this._linkedHeightNode = node;
801
this._onDidSetLinkedNode.fire(undefined);
802
}
803
804
private readonly _onDidSetLinkedNode = new Emitter<number | undefined>();
805
private _onDidViewChange: Event<number | undefined>;
806
readonly onDidChange: Event<number | undefined>;
807
808
private readonly disposables = new DisposableStore();
809
810
constructor(
811
readonly view: IView,
812
readonly orientation: Orientation,
813
readonly layoutController: LayoutController,
814
orthogonalSize: number,
815
size: number = 0
816
) {
817
this._orthogonalSize = orthogonalSize;
818
this._size = size;
819
820
const onDidChange = createLatchedOnDidChangeViewEvent(view);
821
this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height), this.disposables);
822
this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);
823
}
824
825
get width(): number {
826
return this.orientation === Orientation.HORIZONTAL ? this.orthogonalSize : this.size;
827
}
828
829
get height(): number {
830
return this.orientation === Orientation.HORIZONTAL ? this.size : this.orthogonalSize;
831
}
832
833
get top(): number {
834
return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset;
835
}
836
837
get left(): number {
838
return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset;
839
}
840
841
get element(): HTMLElement {
842
return this.view.element;
843
}
844
845
private get minimumWidth(): number {
846
return this.linkedWidthNode ? Math.max(this.linkedWidthNode.view.minimumWidth, this.view.minimumWidth) : this.view.minimumWidth;
847
}
848
849
private get maximumWidth(): number {
850
return this.linkedWidthNode ? Math.min(this.linkedWidthNode.view.maximumWidth, this.view.maximumWidth) : this.view.maximumWidth;
851
}
852
853
private get minimumHeight(): number {
854
return this.linkedHeightNode ? Math.max(this.linkedHeightNode.view.minimumHeight, this.view.minimumHeight) : this.view.minimumHeight;
855
}
856
857
private get maximumHeight(): number {
858
return this.linkedHeightNode ? Math.min(this.linkedHeightNode.view.maximumHeight, this.view.maximumHeight) : this.view.maximumHeight;
859
}
860
861
get minimumSize(): number {
862
return this.orientation === Orientation.HORIZONTAL ? this.minimumHeight : this.minimumWidth;
863
}
864
865
get maximumSize(): number {
866
return this.orientation === Orientation.HORIZONTAL ? this.maximumHeight : this.maximumWidth;
867
}
868
869
get priority(): LayoutPriority | undefined {
870
return this.view.priority;
871
}
872
873
get proportionalLayout(): boolean {
874
return this.view.proportionalLayout ?? true;
875
}
876
877
get snap(): boolean | undefined {
878
return this.view.snap;
879
}
880
881
get minimumOrthogonalSize(): number {
882
return this.orientation === Orientation.HORIZONTAL ? this.minimumWidth : this.minimumHeight;
883
}
884
885
get maximumOrthogonalSize(): number {
886
return this.orientation === Orientation.HORIZONTAL ? this.maximumWidth : this.maximumHeight;
887
}
888
889
private _boundarySashes: IRelativeBoundarySashes = {};
890
get boundarySashes(): IRelativeBoundarySashes { return this._boundarySashes; }
891
set boundarySashes(boundarySashes: IRelativeBoundarySashes) {
892
this._boundarySashes = boundarySashes;
893
894
this.view.setBoundarySashes?.(toAbsoluteBoundarySashes(boundarySashes, this.orientation));
895
}
896
897
layout(size: number, offset: number, ctx: ILayoutContext | undefined): void {
898
if (!this.layoutController.isLayoutEnabled) {
899
return;
900
}
901
902
if (typeof ctx === 'undefined') {
903
throw new Error('Invalid state');
904
}
905
906
this._size = size;
907
this._orthogonalSize = ctx.orthogonalSize;
908
this.absoluteOffset = ctx.absoluteOffset + offset;
909
this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
910
911
this._layout(this.width, this.height, this.top, this.left);
912
}
913
914
private cachedWidth: number = 0;
915
private cachedHeight: number = 0;
916
private cachedTop: number = 0;
917
private cachedLeft: number = 0;
918
919
private _layout(width: number, height: number, top: number, left: number): void {
920
if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) {
921
return;
922
}
923
924
this.cachedWidth = width;
925
this.cachedHeight = height;
926
this.cachedTop = top;
927
this.cachedLeft = left;
928
this.view.layout(width, height, top, left);
929
}
930
931
setVisible(visible: boolean): void {
932
this.view.setVisible?.(visible);
933
}
934
935
dispose(): void {
936
this.disposables.dispose();
937
}
938
}
939
940
type Node = BranchNode | LeafNode;
941
942
export interface INodeDescriptor {
943
node: Node;
944
visible?: boolean;
945
}
946
947
function flipNode(node: BranchNode, size: number, orthogonalSize: number): BranchNode;
948
function flipNode(node: LeafNode, size: number, orthogonalSize: number): LeafNode;
949
function flipNode(node: Node, size: number, orthogonalSize: number): Node;
950
function flipNode(node: Node, size: number, orthogonalSize: number): Node {
951
if (node instanceof BranchNode) {
952
const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.splitviewProportionalLayout, size, orthogonalSize, node.edgeSnapping);
953
954
let totalSize = 0;
955
956
for (let i = node.children.length - 1; i >= 0; i--) {
957
const child = node.children[i];
958
const childSize = child instanceof BranchNode ? child.orthogonalSize : child.size;
959
960
let newSize = node.size === 0 ? 0 : Math.round((size * childSize) / node.size);
961
totalSize += newSize;
962
963
// The last view to add should adjust to rounding errors
964
if (i === 0) {
965
newSize += size - totalSize;
966
}
967
968
result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true);
969
}
970
971
node.dispose();
972
return result;
973
} else {
974
const result = new LeafNode(node.view, orthogonal(node.orientation), node.layoutController, orthogonalSize);
975
node.dispose();
976
return result;
977
}
978
}
979
980
/**
981
* The location of a {@link IView view} within a {@link GridView}.
982
*
983
* A GridView is a tree composition of multiple {@link SplitView} instances, orthogonal
984
* between one another. Here's an example:
985
*
986
* ```
987
* +-----+---------------+
988
* | A | B |
989
* +-----+---------+-----+
990
* | C | |
991
* +---------------+ D |
992
* | E | |
993
* +---------------+-----+
994
* ```
995
*
996
* The above grid's tree structure is:
997
*
998
* ```
999
* Vertical SplitView
1000
* +-Horizontal SplitView
1001
* | +-A
1002
* | +-B
1003
* +- Horizontal SplitView
1004
* +-Vertical SplitView
1005
* | +-C
1006
* | +-E
1007
* +-D
1008
* ```
1009
*
1010
* So, {@link IView views} within a {@link GridView} can be referenced by
1011
* a sequence of indexes, each index referencing each SplitView. Here are
1012
* each view's locations, from the example above:
1013
*
1014
* - `A`: `[0,0]`
1015
* - `B`: `[0,1]`
1016
* - `C`: `[1,0,0]`
1017
* - `D`: `[1,1]`
1018
* - `E`: `[1,0,1]`
1019
*/
1020
export type GridLocation = number[];
1021
1022
/**
1023
* The {@link GridView} is the UI component which implements a two dimensional
1024
* flex-like layout algorithm for a collection of {@link IView} instances, which
1025
* are mostly HTMLElement instances with size constraints. A {@link GridView} is a
1026
* tree composition of multiple {@link SplitView} instances, orthogonal between
1027
* one another. It will respect view's size contraints, just like the SplitView.
1028
*
1029
* It has a low-level index based API, allowing for fine grain performant operations.
1030
* Look into the {@link Grid} widget for a higher-level API.
1031
*
1032
* Features:
1033
* - flex-like layout algorithm
1034
* - snap support
1035
* - corner sash support
1036
* - Alt key modifier behavior, macOS style
1037
* - layout (de)serialization
1038
*/
1039
export class GridView implements IDisposable {
1040
1041
/**
1042
* The DOM element for this view.
1043
*/
1044
readonly element: HTMLElement;
1045
1046
private styles: IGridViewStyles;
1047
private proportionalLayout: boolean;
1048
private _root!: BranchNode;
1049
private onDidSashResetRelay = new Relay<GridLocation>();
1050
private _onDidScroll = new Relay<void>();
1051
private _onDidChange = new Relay<IViewSize | undefined>();
1052
private _boundarySashes: IBoundarySashes = {};
1053
1054
/**
1055
* The layout controller makes sure layout only propagates
1056
* to the views after the very first call to {@link GridView.layout}.
1057
*/
1058
private layoutController: LayoutController;
1059
private disposable2x2: IDisposable = Disposable.None;
1060
1061
private get root(): BranchNode { return this._root; }
1062
1063
private set root(root: BranchNode) {
1064
const oldRoot = this._root;
1065
1066
if (oldRoot) {
1067
oldRoot.element.remove();
1068
oldRoot.dispose();
1069
}
1070
1071
this._root = root;
1072
this.element.appendChild(root.element);
1073
this.onDidSashResetRelay.input = root.onDidSashReset;
1074
this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO
1075
this._onDidScroll.input = root.onDidScroll;
1076
}
1077
1078
/**
1079
* Fires whenever the user double clicks a {@link Sash sash}.
1080
*/
1081
readonly onDidSashReset = this.onDidSashResetRelay.event;
1082
1083
/**
1084
* Fires whenever the user scrolls a {@link SplitView} within
1085
* the grid.
1086
*/
1087
readonly onDidScroll = this._onDidScroll.event;
1088
1089
/**
1090
* Fires whenever a view within the grid changes its size constraints.
1091
*/
1092
readonly onDidChange = this._onDidChange.event;
1093
1094
/**
1095
* The width of the grid.
1096
*/
1097
get width(): number { return this.root.width; }
1098
1099
/**
1100
* The height of the grid.
1101
*/
1102
get height(): number { return this.root.height; }
1103
1104
/**
1105
* The minimum width of the grid.
1106
*/
1107
get minimumWidth(): number { return this.root.minimumWidth; }
1108
1109
/**
1110
* The minimum height of the grid.
1111
*/
1112
get minimumHeight(): number { return this.root.minimumHeight; }
1113
1114
/**
1115
* The maximum width of the grid.
1116
*/
1117
get maximumWidth(): number { return this.root.maximumHeight; }
1118
1119
/**
1120
* The maximum height of the grid.
1121
*/
1122
get maximumHeight(): number { return this.root.maximumHeight; }
1123
1124
get orientation(): Orientation { return this._root.orientation; }
1125
get boundarySashes(): IBoundarySashes { return this._boundarySashes; }
1126
1127
/**
1128
* The orientation of the grid. Matches the orientation of the root
1129
* {@link SplitView} in the grid's tree model.
1130
*/
1131
set orientation(orientation: Orientation) {
1132
if (this._root.orientation === orientation) {
1133
return;
1134
}
1135
1136
const { size, orthogonalSize, absoluteOffset, absoluteOrthogonalOffset } = this._root;
1137
this.root = flipNode(this._root, orthogonalSize, size);
1138
this.root.layout(size, 0, { orthogonalSize, absoluteOffset: absoluteOrthogonalOffset, absoluteOrthogonalOffset: absoluteOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
1139
this.boundarySashes = this.boundarySashes;
1140
}
1141
1142
/**
1143
* A collection of sashes perpendicular to each edge of the grid.
1144
* Corner sashes will be created for each intersection.
1145
*/
1146
set boundarySashes(boundarySashes: IBoundarySashes) {
1147
this._boundarySashes = boundarySashes;
1148
this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation);
1149
}
1150
1151
/**
1152
* Enable/disable edge snapping across all grid views.
1153
*/
1154
set edgeSnapping(edgeSnapping: boolean) {
1155
this.root.edgeSnapping = edgeSnapping;
1156
}
1157
1158
private maximizedNode: LeafNode | undefined = undefined;
1159
1160
private readonly _onDidChangeViewMaximized = new Emitter<boolean>();
1161
readonly onDidChangeViewMaximized = this._onDidChangeViewMaximized.event;
1162
1163
/**
1164
* Create a new {@link GridView} instance.
1165
*
1166
* @remarks It's the caller's responsibility to append the
1167
* {@link GridView.element} to the page's DOM.
1168
*/
1169
constructor(options: IGridViewOptions = {}) {
1170
this.element = $('.monaco-grid-view');
1171
this.styles = options.styles || defaultStyles;
1172
this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true;
1173
this.layoutController = new LayoutController(false);
1174
this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout);
1175
}
1176
1177
style(styles: IGridViewStyles): void {
1178
this.styles = styles;
1179
this.root.style(styles);
1180
}
1181
1182
/**
1183
* Layout the {@link GridView}.
1184
*
1185
* Optionally provide a `top` and `left` positions, those will propagate
1186
* as an origin for positions passed to {@link IView.layout}.
1187
*
1188
* @param width The width of the {@link GridView}.
1189
* @param height The height of the {@link GridView}.
1190
* @param top Optional, the top location of the {@link GridView}.
1191
* @param left Optional, the left location of the {@link GridView}.
1192
*/
1193
layout(width: number, height: number, top: number = 0, left: number = 0): void {
1194
this.layoutController.isLayoutEnabled = true;
1195
1196
const [size, orthogonalSize, offset, orthogonalOffset] = this.root.orientation === Orientation.HORIZONTAL ? [height, width, top, left] : [width, height, left, top];
1197
this.root.layout(size, 0, { orthogonalSize, absoluteOffset: offset, absoluteOrthogonalOffset: orthogonalOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
1198
}
1199
1200
/**
1201
* Add a {@link IView view} to this {@link GridView}.
1202
*
1203
* @param view The view to add.
1204
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
1205
* @param location The {@link GridLocation location} to insert the view on.
1206
*/
1207
addView(view: IView, size: number | Sizing, location: GridLocation): void {
1208
if (this.hasMaximizedView()) {
1209
this.exitMaximizedView();
1210
}
1211
1212
this.disposable2x2.dispose();
1213
this.disposable2x2 = Disposable.None;
1214
1215
const [rest, index] = tail(location);
1216
const [pathToParent, parent] = this.getNode(rest);
1217
1218
if (parent instanceof BranchNode) {
1219
const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize);
1220
1221
try {
1222
parent.addChild(node, size, index);
1223
} catch (err) {
1224
node.dispose();
1225
throw err;
1226
}
1227
} else {
1228
const [, grandParent] = tail(pathToParent);
1229
const [, parentIndex] = tail(rest);
1230
1231
let newSiblingSize: number | Sizing = 0;
1232
1233
const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(parentIndex);
1234
if (typeof newSiblingCachedVisibleSize === 'number') {
1235
newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize);
1236
}
1237
1238
const oldChild = grandParent.removeChild(parentIndex);
1239
oldChild.dispose();
1240
1241
const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping);
1242
grandParent.addChild(newParent, parent.size, parentIndex);
1243
1244
const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size);
1245
newParent.addChild(newSibling, newSiblingSize, 0);
1246
1247
if (typeof size !== 'number' && size.type === 'split') {
1248
size = Sizing.Split(0);
1249
}
1250
1251
const node = new LeafNode(view, grandParent.orientation, this.layoutController, parent.size);
1252
newParent.addChild(node, size, index);
1253
}
1254
1255
this.trySet2x2();
1256
}
1257
1258
/**
1259
* Remove a {@link IView view} from this {@link GridView}.
1260
*
1261
* @param location The {@link GridLocation location} of the {@link IView view}.
1262
* @param sizing Whether to distribute other {@link IView view}'s sizes.
1263
*/
1264
removeView(location: GridLocation, sizing?: DistributeSizing | AutoSizing): IView {
1265
if (this.hasMaximizedView()) {
1266
this.exitMaximizedView();
1267
}
1268
1269
this.disposable2x2.dispose();
1270
this.disposable2x2 = Disposable.None;
1271
1272
const [rest, index] = tail(location);
1273
const [pathToParent, parent] = this.getNode(rest);
1274
1275
if (!(parent instanceof BranchNode)) {
1276
throw new Error('Invalid location');
1277
}
1278
1279
const node = parent.children[index];
1280
1281
if (!(node instanceof LeafNode)) {
1282
throw new Error('Invalid location');
1283
}
1284
1285
parent.removeChild(index, sizing);
1286
node.dispose();
1287
1288
if (parent.children.length === 0) {
1289
throw new Error('Invalid grid state');
1290
}
1291
1292
if (parent.children.length > 1) {
1293
this.trySet2x2();
1294
return node.view;
1295
}
1296
1297
if (pathToParent.length === 0) { // parent is root
1298
const sibling = parent.children[0];
1299
1300
if (sibling instanceof LeafNode) {
1301
return node.view;
1302
}
1303
1304
// we must promote sibling to be the new root
1305
parent.removeChild(0);
1306
parent.dispose();
1307
this.root = sibling;
1308
this.boundarySashes = this.boundarySashes;
1309
this.trySet2x2();
1310
return node.view;
1311
}
1312
1313
const [, grandParent] = tail(pathToParent);
1314
const [, parentIndex] = tail(rest);
1315
1316
const isSiblingVisible = parent.isChildVisible(0);
1317
const sibling = parent.removeChild(0);
1318
1319
const sizes = grandParent.children.map((_, i) => grandParent.getChildSize(i));
1320
grandParent.removeChild(parentIndex, sizing);
1321
parent.dispose();
1322
1323
if (sibling instanceof BranchNode) {
1324
sizes.splice(parentIndex, 1, ...sibling.children.map(c => c.size));
1325
1326
const siblingChildren = sibling.removeAllChildren();
1327
1328
for (let i = 0; i < siblingChildren.length; i++) {
1329
grandParent.addChild(siblingChildren[i], siblingChildren[i].size, parentIndex + i);
1330
}
1331
} else {
1332
const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size);
1333
const sizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize);
1334
grandParent.addChild(newSibling, sizing, parentIndex);
1335
}
1336
1337
sibling.dispose();
1338
1339
for (let i = 0; i < sizes.length; i++) {
1340
grandParent.resizeChild(i, sizes[i]);
1341
}
1342
1343
this.trySet2x2();
1344
return node.view;
1345
}
1346
1347
/**
1348
* Move a {@link IView view} within its parent.
1349
*
1350
* @param parentLocation The {@link GridLocation location} of the {@link IView view}'s parent.
1351
* @param from The index of the {@link IView view} to move.
1352
* @param to The index where the {@link IView view} should move to.
1353
*/
1354
moveView(parentLocation: GridLocation, from: number, to: number): void {
1355
if (this.hasMaximizedView()) {
1356
this.exitMaximizedView();
1357
}
1358
1359
const [, parent] = this.getNode(parentLocation);
1360
1361
if (!(parent instanceof BranchNode)) {
1362
throw new Error('Invalid location');
1363
}
1364
1365
parent.moveChild(from, to);
1366
1367
this.trySet2x2();
1368
}
1369
1370
/**
1371
* Swap two {@link IView views} within the {@link GridView}.
1372
*
1373
* @param from The {@link GridLocation location} of one view.
1374
* @param to The {@link GridLocation location} of another view.
1375
*/
1376
swapViews(from: GridLocation, to: GridLocation): void {
1377
if (this.hasMaximizedView()) {
1378
this.exitMaximizedView();
1379
}
1380
1381
const [fromRest, fromIndex] = tail(from);
1382
const [, fromParent] = this.getNode(fromRest);
1383
1384
if (!(fromParent instanceof BranchNode)) {
1385
throw new Error('Invalid from location');
1386
}
1387
1388
const fromSize = fromParent.getChildSize(fromIndex);
1389
const fromNode = fromParent.children[fromIndex];
1390
1391
if (!(fromNode instanceof LeafNode)) {
1392
throw new Error('Invalid from location');
1393
}
1394
1395
const [toRest, toIndex] = tail(to);
1396
const [, toParent] = this.getNode(toRest);
1397
1398
if (!(toParent instanceof BranchNode)) {
1399
throw new Error('Invalid to location');
1400
}
1401
1402
const toSize = toParent.getChildSize(toIndex);
1403
const toNode = toParent.children[toIndex];
1404
1405
if (!(toNode instanceof LeafNode)) {
1406
throw new Error('Invalid to location');
1407
}
1408
1409
if (fromParent === toParent) {
1410
fromParent.swapChildren(fromIndex, toIndex);
1411
} else {
1412
fromParent.removeChild(fromIndex);
1413
toParent.removeChild(toIndex);
1414
1415
fromParent.addChild(toNode, fromSize, fromIndex);
1416
toParent.addChild(fromNode, toSize, toIndex);
1417
}
1418
1419
this.trySet2x2();
1420
}
1421
1422
/**
1423
* Resize a {@link IView view}.
1424
*
1425
* @param location The {@link GridLocation location} of the view.
1426
* @param size The size the view should be. Optionally provide a single dimension.
1427
*/
1428
resizeView(location: GridLocation, size: Partial<IViewSize>): void {
1429
if (this.hasMaximizedView()) {
1430
this.exitMaximizedView();
1431
}
1432
1433
const [rest, index] = tail(location);
1434
const [pathToParent, parent] = this.getNode(rest);
1435
1436
if (!(parent instanceof BranchNode)) {
1437
throw new Error('Invalid location');
1438
}
1439
1440
if (!size.width && !size.height) {
1441
return;
1442
}
1443
1444
const [parentSize, grandParentSize] = parent.orientation === Orientation.HORIZONTAL ? [size.width, size.height] : [size.height, size.width];
1445
1446
if (typeof grandParentSize === 'number' && pathToParent.length > 0) {
1447
const [, grandParent] = tail(pathToParent);
1448
const [, parentIndex] = tail(rest);
1449
1450
grandParent.resizeChild(parentIndex, grandParentSize);
1451
}
1452
1453
if (typeof parentSize === 'number') {
1454
parent.resizeChild(index, parentSize);
1455
}
1456
1457
this.trySet2x2();
1458
}
1459
1460
/**
1461
* Get the size of a {@link IView view}.
1462
*
1463
* @param location The {@link GridLocation location} of the view. Provide `undefined` to get
1464
* the size of the grid itself.
1465
*/
1466
getViewSize(location?: GridLocation): IViewSize {
1467
if (!location) {
1468
return { width: this.root.width, height: this.root.height };
1469
}
1470
1471
const [, node] = this.getNode(location);
1472
return { width: node.width, height: node.height };
1473
}
1474
1475
/**
1476
* Get the cached visible size of a {@link IView view}. This was the size
1477
* of the view at the moment it last became hidden.
1478
*
1479
* @param location The {@link GridLocation location} of the view.
1480
*/
1481
getViewCachedVisibleSize(location: GridLocation): number | undefined {
1482
const [rest, index] = tail(location);
1483
const [, parent] = this.getNode(rest);
1484
1485
if (!(parent instanceof BranchNode)) {
1486
throw new Error('Invalid location');
1487
}
1488
1489
return parent.getChildCachedVisibleSize(index);
1490
}
1491
1492
/**
1493
* Maximize the size of a {@link IView view} by collapsing all other views
1494
* to their minimum sizes.
1495
*
1496
* @param location The {@link GridLocation location} of the view.
1497
*/
1498
expandView(location: GridLocation): void {
1499
if (this.hasMaximizedView()) {
1500
this.exitMaximizedView();
1501
}
1502
1503
const [ancestors, node] = this.getNode(location);
1504
1505
if (!(node instanceof LeafNode)) {
1506
throw new Error('Invalid location');
1507
}
1508
1509
for (let i = 0; i < ancestors.length; i++) {
1510
ancestors[i].resizeChild(location[i], Number.POSITIVE_INFINITY);
1511
}
1512
}
1513
1514
/**
1515
* Returns whether all other {@link IView views} are at their minimum size.
1516
*
1517
* @param location The {@link GridLocation location} of the view.
1518
*/
1519
isViewExpanded(location: GridLocation): boolean {
1520
if (this.hasMaximizedView()) {
1521
// No view can be expanded when a view is maximized
1522
return false;
1523
}
1524
1525
const [ancestors, node] = this.getNode(location);
1526
1527
if (!(node instanceof LeafNode)) {
1528
throw new Error('Invalid location');
1529
}
1530
1531
for (let i = 0; i < ancestors.length; i++) {
1532
if (!ancestors[i].isChildExpanded(location[i])) {
1533
return false;
1534
}
1535
}
1536
1537
return true;
1538
}
1539
1540
maximizeView(location: GridLocation) {
1541
const [, nodeToMaximize] = this.getNode(location);
1542
if (!(nodeToMaximize instanceof LeafNode)) {
1543
throw new Error('Location is not a LeafNode');
1544
}
1545
1546
if (this.maximizedNode === nodeToMaximize) {
1547
return;
1548
}
1549
1550
if (this.hasMaximizedView()) {
1551
this.exitMaximizedView();
1552
}
1553
1554
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
1555
for (let i = 0; i < parent.children.length; i++) {
1556
const child = parent.children[i];
1557
if (child instanceof LeafNode) {
1558
if (child !== exclude) {
1559
parent.setChildVisible(i, false);
1560
}
1561
} else {
1562
hideAllViewsBut(child, exclude);
1563
}
1564
}
1565
}
1566
1567
hideAllViewsBut(this.root, nodeToMaximize);
1568
1569
this.maximizedNode = nodeToMaximize;
1570
this._onDidChangeViewMaximized.fire(true);
1571
}
1572
1573
exitMaximizedView(): void {
1574
if (!this.maximizedNode) {
1575
return;
1576
}
1577
this.maximizedNode = undefined;
1578
1579
// When hiding a view, it's previous size is cached.
1580
// To restore the sizes of all views, they need to be made visible in reverse order.
1581
function showViewsInReverseOrder(parent: BranchNode): void {
1582
for (let index = parent.children.length - 1; index >= 0; index--) {
1583
const child = parent.children[index];
1584
if (child instanceof LeafNode) {
1585
parent.setChildVisible(index, true);
1586
} else {
1587
showViewsInReverseOrder(child);
1588
}
1589
}
1590
}
1591
1592
showViewsInReverseOrder(this.root);
1593
1594
this._onDidChangeViewMaximized.fire(false);
1595
}
1596
1597
hasMaximizedView(): boolean {
1598
return this.maximizedNode !== undefined;
1599
}
1600
1601
/**
1602
* Returns whether the {@link IView view} is maximized.
1603
*
1604
* @param location The {@link GridLocation location} of the view.
1605
*/
1606
isViewMaximized(location: GridLocation): boolean {
1607
const [, node] = this.getNode(location);
1608
if (!(node instanceof LeafNode)) {
1609
throw new Error('Location is not a LeafNode');
1610
}
1611
return node === this.maximizedNode;
1612
}
1613
1614
/**
1615
* Distribute the size among all {@link IView views} within the entire
1616
* grid or within a single {@link SplitView}.
1617
*
1618
* @param location The {@link GridLocation location} of a view containing
1619
* children views, which will have their sizes distributed within the parent
1620
* view's size. Provide `undefined` to recursively distribute all views' sizes
1621
* in the entire grid.
1622
*/
1623
distributeViewSizes(location?: GridLocation): void {
1624
if (this.hasMaximizedView()) {
1625
this.exitMaximizedView();
1626
}
1627
1628
if (!location) {
1629
this.root.distributeViewSizes(true);
1630
return;
1631
}
1632
1633
const [, node] = this.getNode(location);
1634
1635
if (!(node instanceof BranchNode)) {
1636
throw new Error('Invalid location');
1637
}
1638
1639
node.distributeViewSizes();
1640
this.trySet2x2();
1641
}
1642
1643
/**
1644
* Returns whether a {@link IView view} is visible.
1645
*
1646
* @param location The {@link GridLocation location} of the view.
1647
*/
1648
isViewVisible(location: GridLocation): boolean {
1649
const [rest, index] = tail(location);
1650
const [, parent] = this.getNode(rest);
1651
1652
if (!(parent instanceof BranchNode)) {
1653
throw new Error('Invalid from location');
1654
}
1655
1656
return parent.isChildVisible(index);
1657
}
1658
1659
/**
1660
* Set the visibility state of a {@link IView view}.
1661
*
1662
* @param location The {@link GridLocation location} of the view.
1663
*/
1664
setViewVisible(location: GridLocation, visible: boolean): void {
1665
if (this.hasMaximizedView()) {
1666
this.exitMaximizedView();
1667
return;
1668
}
1669
1670
const [rest, index] = tail(location);
1671
const [, parent] = this.getNode(rest);
1672
1673
if (!(parent instanceof BranchNode)) {
1674
throw new Error('Invalid from location');
1675
}
1676
1677
parent.setChildVisible(index, visible);
1678
}
1679
1680
/**
1681
* Returns a descriptor for the entire grid.
1682
*/
1683
getView(): GridBranchNode;
1684
1685
/**
1686
* Returns a descriptor for a {@link GridLocation subtree} within the
1687
* {@link GridView}.
1688
*
1689
* @param location The {@link GridLocation location} of the root of
1690
* the {@link GridLocation subtree}.
1691
*/
1692
getView(location: GridLocation): GridNode;
1693
getView(location?: GridLocation): GridNode {
1694
const node = location ? this.getNode(location)[1] : this._root;
1695
return this._getViews(node, this.orientation);
1696
}
1697
1698
/**
1699
* Construct a new {@link GridView} from a JSON object.
1700
*
1701
* @param json The JSON object.
1702
* @param deserializer A deserializer which can revive each view.
1703
* @returns A new {@link GridView} instance.
1704
*/
1705
static deserialize<T extends ISerializableView>(json: ISerializedGridView, deserializer: IViewDeserializer<T>, options: IGridViewOptions = {}): GridView {
1706
if (typeof json.orientation !== 'number') {
1707
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
1708
} else if (typeof json.width !== 'number') {
1709
throw new Error('Invalid JSON: \'width\' property must be a number.');
1710
} else if (typeof json.height !== 'number') {
1711
throw new Error('Invalid JSON: \'height\' property must be a number.');
1712
} else if (json.root?.type !== 'branch') {
1713
throw new Error('Invalid JSON: \'root\' property must have \'type\' value of branch.');
1714
}
1715
1716
const orientation = json.orientation;
1717
const height = json.height;
1718
1719
const result = new GridView(options);
1720
result._deserialize(json.root, orientation, deserializer, height);
1721
1722
return result;
1723
}
1724
1725
private _deserialize(root: ISerializedBranchNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): void {
1726
this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize) as BranchNode;
1727
}
1728
1729
private _deserializeNode(node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): Node {
1730
let result: Node;
1731
if (node.type === 'branch') {
1732
const serializedChildren = node.data;
1733
const children = serializedChildren.map(serializedChild => {
1734
return {
1735
node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size),
1736
visible: (serializedChild as { visible?: boolean }).visible
1737
} satisfies INodeDescriptor;
1738
});
1739
1740
result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children);
1741
} else {
1742
result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size);
1743
if (node.maximized && !this.maximizedNode) {
1744
this.maximizedNode = result;
1745
this._onDidChangeViewMaximized.fire(true);
1746
}
1747
}
1748
1749
return result;
1750
}
1751
1752
private _getViews(node: Node, orientation: Orientation, cachedVisibleSize?: number): GridNode {
1753
const box = { top: node.top, left: node.left, width: node.width, height: node.height };
1754
1755
if (node instanceof LeafNode) {
1756
return { view: node.view, box, cachedVisibleSize, maximized: this.maximizedNode === node };
1757
}
1758
1759
const children: GridNode[] = [];
1760
1761
for (let i = 0; i < node.children.length; i++) {
1762
const child = node.children[i];
1763
const cachedVisibleSize = node.getChildCachedVisibleSize(i);
1764
1765
children.push(this._getViews(child, orthogonal(orientation), cachedVisibleSize));
1766
}
1767
1768
return { children, box };
1769
}
1770
1771
private getNode(location: GridLocation, node: Node = this.root, path: BranchNode[] = []): [BranchNode[], Node] {
1772
if (location.length === 0) {
1773
return [path, node];
1774
}
1775
1776
if (!(node instanceof BranchNode)) {
1777
throw new Error('Invalid location');
1778
}
1779
1780
const [index, ...rest] = location;
1781
1782
if (index < 0 || index >= node.children.length) {
1783
throw new Error('Invalid location');
1784
}
1785
1786
const child = node.children[index];
1787
path.push(node);
1788
1789
return this.getNode(rest, child, path);
1790
}
1791
1792
/**
1793
* Attempt to lock the {@link Sash sashes} in this {@link GridView} so
1794
* the grid behaves as a 2x2 matrix, with a corner sash in the middle.
1795
*
1796
* In case the grid isn't a 2x2 grid _and_ all sashes are not aligned,
1797
* this method is a no-op.
1798
*/
1799
trySet2x2(): void {
1800
this.disposable2x2.dispose();
1801
this.disposable2x2 = Disposable.None;
1802
1803
if (this.root.children.length !== 2) {
1804
return;
1805
}
1806
1807
const [first, second] = this.root.children;
1808
1809
if (!(first instanceof BranchNode) || !(second instanceof BranchNode)) {
1810
return;
1811
}
1812
1813
this.disposable2x2 = first.trySet2x2(second);
1814
}
1815
1816
/**
1817
* Populate a map with views to DOM nodes.
1818
* @remarks To be used internally only.
1819
*/
1820
getViewMap(map: Map<IView, HTMLElement>, node?: Node): void {
1821
if (!node) {
1822
node = this.root;
1823
}
1824
1825
if (node instanceof BranchNode) {
1826
node.children.forEach(child => this.getViewMap(map, child));
1827
} else {
1828
map.set(node.view, node.element);
1829
}
1830
}
1831
1832
dispose(): void {
1833
this.onDidSashResetRelay.dispose();
1834
this.root.dispose();
1835
this.element.remove();
1836
}
1837
}
1838
1839