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