Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/grid/grid.ts
5220 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 { IBoundarySashes, Orientation } from '../sash/sash.js';
7
import { equals, tail } from '../../../common/arrays.js';
8
import { Event } from '../../../common/event.js';
9
import { Disposable } from '../../../common/lifecycle.js';
10
import './gridview.css';
11
import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview.js';
12
import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js';
13
14
export type { IViewSize };
15
export { LayoutPriority, Orientation, orthogonal } from './gridview.js';
16
17
export const enum Direction {
18
Up,
19
Down,
20
Left,
21
Right
22
}
23
24
function oppositeDirection(direction: Direction): Direction {
25
switch (direction) {
26
case Direction.Up: return Direction.Down;
27
case Direction.Down: return Direction.Up;
28
case Direction.Left: return Direction.Right;
29
case Direction.Right: return Direction.Left;
30
}
31
}
32
33
/**
34
* The interface to implement for views within a {@link Grid}.
35
*/
36
export interface IView extends IGridViewView {
37
38
/**
39
* The preferred width for when the user double clicks a sash
40
* adjacent to this view.
41
*/
42
readonly preferredWidth?: number;
43
44
/**
45
* The preferred height for when the user double clicks a sash
46
* adjacent to this view.
47
*/
48
readonly preferredHeight?: number;
49
}
50
51
export interface GridLeafNode<T extends IView> {
52
readonly view: T;
53
readonly box: Box;
54
readonly cachedVisibleSize: number | undefined;
55
readonly maximized: boolean;
56
}
57
58
export interface GridBranchNode<T extends IView> {
59
readonly children: GridNode<T>[];
60
readonly box: Box;
61
}
62
63
export type GridNode<T extends IView> = GridLeafNode<T> | GridBranchNode<T>;
64
65
export function isGridBranchNode<T extends IView>(node: GridNode<T>): node is GridBranchNode<T> {
66
// eslint-disable-next-line local/code-no-any-casts
67
return !!(node as any).children;
68
}
69
70
function getGridNode<T extends IView>(node: GridNode<T>, location: GridLocation): GridNode<T> {
71
if (location.length === 0) {
72
return node;
73
}
74
75
if (!isGridBranchNode(node)) {
76
throw new Error('Invalid location');
77
}
78
79
const [index, ...rest] = location;
80
return getGridNode(node.children[index], rest);
81
}
82
83
interface Range {
84
readonly start: number;
85
readonly end: number;
86
}
87
88
function intersects(one: Range, other: Range): boolean {
89
return !(one.start >= other.end || other.start >= one.end);
90
}
91
92
interface Boundary {
93
readonly offset: number;
94
readonly range: Range;
95
}
96
97
function getBoxBoundary(box: Box, direction: Direction): Boundary {
98
const orientation = getDirectionOrientation(direction);
99
const offset = direction === Direction.Up ? box.top :
100
direction === Direction.Right ? box.left + box.width :
101
direction === Direction.Down ? box.top + box.height :
102
box.left;
103
104
const range = {
105
start: orientation === Orientation.HORIZONTAL ? box.top : box.left,
106
end: orientation === Orientation.HORIZONTAL ? box.top + box.height : box.left + box.width
107
};
108
109
return { offset, range };
110
}
111
112
function findAdjacentBoxLeafNodes<T extends IView>(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): GridLeafNode<T>[] {
113
const result: GridLeafNode<T>[] = [];
114
115
function _(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): void {
116
if (isGridBranchNode(boxNode)) {
117
for (const child of boxNode.children) {
118
_(child, direction, boundary);
119
}
120
} else {
121
const { offset, range } = getBoxBoundary(boxNode.box, direction);
122
123
if (offset === boundary.offset && intersects(range, boundary.range)) {
124
result.push(boxNode);
125
}
126
}
127
}
128
129
_(boxNode, direction, boundary);
130
return result;
131
}
132
133
function getLocationOrientation(rootOrientation: Orientation, location: GridLocation): Orientation {
134
return location.length % 2 === 0 ? orthogonal(rootOrientation) : rootOrientation;
135
}
136
137
function getDirectionOrientation(direction: Direction): Orientation {
138
return direction === Direction.Up || direction === Direction.Down ? Orientation.VERTICAL : Orientation.HORIZONTAL;
139
}
140
141
export function getRelativeLocation(rootOrientation: Orientation, location: GridLocation, direction: Direction): GridLocation {
142
const orientation = getLocationOrientation(rootOrientation, location);
143
const directionOrientation = getDirectionOrientation(direction);
144
145
if (orientation === directionOrientation) {
146
let [rest, index] = tail(location);
147
148
if (direction === Direction.Right || direction === Direction.Down) {
149
index += 1;
150
}
151
152
return [...rest, index];
153
} else {
154
const index = (direction === Direction.Right || direction === Direction.Down) ? 1 : 0;
155
return [...location, index];
156
}
157
}
158
159
function indexInParent(element: HTMLElement): number {
160
const parentElement = element.parentElement;
161
162
if (!parentElement) {
163
throw new Error('Invalid grid element');
164
}
165
166
let el = parentElement.firstElementChild;
167
let index = 0;
168
169
while (el !== element && el !== parentElement.lastElementChild && el) {
170
el = el.nextElementSibling;
171
index++;
172
}
173
174
return index;
175
}
176
177
/**
178
* Find the grid location of a specific DOM element by traversing the parent
179
* chain and finding each child index on the way.
180
*
181
* This will break as soon as DOM structures of the Splitview or Gridview change.
182
*/
183
function getGridLocation(element: HTMLElement): GridLocation {
184
const parentElement = element.parentElement;
185
186
if (!parentElement) {
187
throw new Error('Invalid grid element');
188
}
189
190
if (/\bmonaco-grid-view\b/.test(parentElement.className)) {
191
return [];
192
}
193
194
const index = indexInParent(parentElement);
195
const ancestor = parentElement.parentElement!.parentElement!.parentElement!.parentElement!;
196
return [...getGridLocation(ancestor), index];
197
}
198
199
export type DistributeSizing = { type: 'distribute' };
200
export type SplitSizing = { type: 'split' };
201
export type AutoSizing = { type: 'auto' };
202
export type InvisibleSizing = { type: 'invisible'; cachedVisibleSize: number };
203
export type Sizing = DistributeSizing | SplitSizing | AutoSizing | InvisibleSizing;
204
205
export namespace Sizing {
206
export const Distribute: DistributeSizing = { type: 'distribute' };
207
export const Split: SplitSizing = { type: 'split' };
208
export const Auto: AutoSizing = { type: 'auto' };
209
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
210
}
211
212
export interface IGridStyles extends IGridViewStyles { }
213
export interface IGridOptions extends IGridViewOptions { }
214
215
/**
216
* The {@link Grid} exposes a Grid widget in a friendlier API than the underlying
217
* {@link GridView} widget. Namely, all mutation operations are addressed by the
218
* model elements, rather than indexes.
219
*
220
* It support the same features as the {@link GridView}.
221
*/
222
export class Grid<T extends IView = IView> extends Disposable {
223
224
protected gridview: GridView;
225
private views = new Map<T, HTMLElement>();
226
227
/**
228
* The orientation of the grid. Matches the orientation of the root
229
* {@link SplitView} in the grid's {@link GridLocation} model.
230
*/
231
get orientation(): Orientation { return this.gridview.orientation; }
232
set orientation(orientation: Orientation) { this.gridview.orientation = orientation; }
233
234
/**
235
* The width of the grid.
236
*/
237
get width(): number { return this.gridview.width; }
238
239
/**
240
* The height of the grid.
241
*/
242
get height(): number { return this.gridview.height; }
243
244
/**
245
* The minimum width of the grid.
246
*/
247
get minimumWidth(): number { return this.gridview.minimumWidth; }
248
249
/**
250
* The minimum height of the grid.
251
*/
252
get minimumHeight(): number { return this.gridview.minimumHeight; }
253
254
/**
255
* The maximum width of the grid.
256
*/
257
get maximumWidth(): number { return this.gridview.maximumWidth; }
258
259
/**
260
* The maximum height of the grid.
261
*/
262
get maximumHeight(): number { return this.gridview.maximumHeight; }
263
264
/**
265
* Fires whenever a view within the grid changes its size constraints.
266
*/
267
readonly onDidChange: Event<{ width: number; height: number } | undefined>;
268
269
/**
270
* Fires whenever the user scrolls a {@link SplitView} within
271
* the grid.
272
*/
273
readonly onDidScroll: Event<void>;
274
275
/**
276
* A collection of sashes perpendicular to each edge of the grid.
277
* Corner sashes will be created for each intersection.
278
*/
279
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
280
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
281
282
/**
283
* Enable/disable edge snapping across all grid views.
284
*/
285
set edgeSnapping(edgeSnapping: boolean) { this.gridview.edgeSnapping = edgeSnapping; }
286
287
/**
288
* The DOM element for this view.
289
*/
290
get element(): HTMLElement { return this.gridview.element; }
291
292
private didLayout = false;
293
294
readonly onDidChangeViewMaximized: Event<boolean>;
295
/**
296
* Create a new {@link Grid}. A grid must *always* have a view
297
* inside.
298
*
299
* @param view An initial view for this Grid.
300
*/
301
constructor(view: T | GridView, options: IGridOptions = {}) {
302
super();
303
304
if (view instanceof GridView) {
305
this.gridview = view;
306
this.gridview.getViewMap(this.views);
307
} else {
308
this.gridview = new GridView(options);
309
}
310
311
this._register(this.gridview);
312
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
313
314
if (!(view instanceof GridView)) {
315
this._addView(view, 0, [0]);
316
}
317
318
this.onDidChange = this.gridview.onDidChange;
319
this.onDidScroll = this.gridview.onDidScroll;
320
this.onDidChangeViewMaximized = this.gridview.onDidChangeViewMaximized;
321
}
322
323
style(styles: IGridStyles): void {
324
this.gridview.style(styles);
325
}
326
327
/**
328
* Layout the {@link Grid}.
329
*
330
* Optionally provide a `top` and `left` positions, those will propagate
331
* as an origin for positions passed to {@link IView.layout}.
332
*
333
* @param width The width of the {@link Grid}.
334
* @param height The height of the {@link Grid}.
335
* @param top Optional, the top location of the {@link Grid}.
336
* @param left Optional, the left location of the {@link Grid}.
337
*/
338
layout(width: number, height: number, top: number = 0, left: number = 0): void {
339
this.gridview.layout(width, height, top, left);
340
this.didLayout = true;
341
}
342
343
/**
344
* Add a {@link IView view} to this {@link Grid}, based on another reference view.
345
*
346
* Take this grid as an example:
347
*
348
* ```
349
* +-----+---------------+
350
* | A | B |
351
* +-----+---------+-----+
352
* | C | |
353
* +---------------+ D |
354
* | E | |
355
* +---------------+-----+
356
* ```
357
*
358
* Calling `addView(X, Sizing.Distribute, C, Direction.Right)` will make the following
359
* changes:
360
*
361
* ```
362
* +-----+---------------+
363
* | A | B |
364
* +-----+-+-------+-----+
365
* | C | X | |
366
* +-------+-------+ D |
367
* | E | |
368
* +---------------+-----+
369
* ```
370
*
371
* Or `addView(X, Sizing.Distribute, D, Direction.Down)`:
372
*
373
* ```
374
* +-----+---------------+
375
* | A | B |
376
* +-----+---------+-----+
377
* | C | D |
378
* +---------------+-----+
379
* | E | X |
380
* +---------------+-----+
381
* ```
382
*
383
* @param newView The view to add.
384
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
385
* @param referenceView Another view to place this new view next to.
386
* @param direction The direction the new view should be placed next to the reference view.
387
*/
388
addView(newView: T, size: number | Sizing, referenceView: T, direction: Direction): void {
389
if (this.views.has(newView)) {
390
throw new Error('Can\'t add same view twice');
391
}
392
393
const orientation = getDirectionOrientation(direction);
394
395
if (this.views.size === 1 && this.orientation !== orientation) {
396
this.orientation = orientation;
397
}
398
399
const referenceLocation = this.getViewLocation(referenceView);
400
const location = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
401
402
let viewSize: number | GridViewSizing;
403
404
if (typeof size === 'number') {
405
viewSize = size;
406
} else if (size.type === 'split') {
407
const [, index] = tail(referenceLocation);
408
viewSize = GridViewSizing.Split(index);
409
} else if (size.type === 'distribute') {
410
viewSize = GridViewSizing.Distribute;
411
} else if (size.type === 'auto') {
412
const [, index] = tail(referenceLocation);
413
viewSize = GridViewSizing.Auto(index);
414
} else {
415
viewSize = size;
416
}
417
418
this._addView(newView, viewSize, location);
419
}
420
421
private addViewAt(newView: T, size: number | DistributeSizing | InvisibleSizing, location: GridLocation): void {
422
if (this.views.has(newView)) {
423
throw new Error('Can\'t add same view twice');
424
}
425
426
let viewSize: number | GridViewSizing;
427
428
if (typeof size === 'number') {
429
viewSize = size;
430
} else if (size.type === 'distribute') {
431
viewSize = GridViewSizing.Distribute;
432
} else {
433
viewSize = size;
434
}
435
436
this._addView(newView, viewSize, location);
437
}
438
439
protected _addView(newView: T, size: number | GridViewSizing, location: GridLocation): void {
440
this.views.set(newView, newView.element);
441
this.gridview.addView(newView, size, location);
442
}
443
444
/**
445
* Remove a {@link IView view} from this {@link Grid}.
446
*
447
* @param view The {@link IView view} to remove.
448
* @param sizing Whether to distribute other {@link IView view}'s sizes.
449
*/
450
removeView(view: T, sizing?: Sizing): void {
451
if (this.views.size === 1) {
452
throw new Error('Can\'t remove last view');
453
}
454
455
const location = this.getViewLocation(view);
456
457
let gridViewSizing: DistributeSizing | SplitViewAutoSizing | undefined;
458
459
if (sizing?.type === 'distribute') {
460
gridViewSizing = GridViewSizing.Distribute;
461
} else if (sizing?.type === 'auto') {
462
const index = location[location.length - 1];
463
gridViewSizing = GridViewSizing.Auto(index === 0 ? 1 : index - 1);
464
}
465
466
this.gridview.removeView(location, gridViewSizing);
467
this.views.delete(view);
468
}
469
470
/**
471
* Move a {@link IView view} to another location in the grid.
472
*
473
* @remarks See {@link Grid.addView}.
474
*
475
* @param view The {@link IView view} to move.
476
* @param sizing Either a fixed size, or a dynamic {@link Sizing} strategy.
477
* @param referenceView Another view to place the view next to.
478
* @param direction The direction the view should be placed next to the reference view.
479
*/
480
moveView(view: T, sizing: number | Sizing, referenceView: T, direction: Direction): void {
481
const sourceLocation = this.getViewLocation(view);
482
const [sourceParentLocation, from] = tail(sourceLocation);
483
484
const referenceLocation = this.getViewLocation(referenceView);
485
const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
486
const [targetParentLocation, to] = tail(targetLocation);
487
488
if (equals(sourceParentLocation, targetParentLocation)) {
489
this.gridview.moveView(sourceParentLocation, from, to);
490
} else {
491
this.removeView(view, typeof sizing === 'number' ? undefined : sizing);
492
this.addView(view, sizing, referenceView, direction);
493
}
494
}
495
496
/**
497
* Move a {@link IView view} to another location in the grid.
498
*
499
* @remarks Internal method, do not use without knowing what you're doing.
500
* @remarks See {@link GridView.moveView}.
501
*
502
* @param view The {@link IView view} to move.
503
* @param location The {@link GridLocation location} to insert the view on.
504
*/
505
moveViewTo(view: T, location: GridLocation): void {
506
const sourceLocation = this.getViewLocation(view);
507
const [sourceParentLocation, from] = tail(sourceLocation);
508
const [targetParentLocation, to] = tail(location);
509
510
if (equals(sourceParentLocation, targetParentLocation)) {
511
this.gridview.moveView(sourceParentLocation, from, to);
512
} else {
513
const size = this.getViewSize(view);
514
const orientation = getLocationOrientation(this.gridview.orientation, sourceLocation);
515
const cachedViewSize = this.getViewCachedVisibleSize(view);
516
const sizing = typeof cachedViewSize === 'undefined'
517
? (orientation === Orientation.HORIZONTAL ? size.width : size.height)
518
: Sizing.Invisible(cachedViewSize);
519
520
this.removeView(view);
521
this.addViewAt(view, sizing, location);
522
}
523
}
524
525
/**
526
* Swap two {@link IView views} within the {@link Grid}.
527
*
528
* @param from One {@link IView view}.
529
* @param to Another {@link IView view}.
530
*/
531
swapViews(from: T, to: T): void {
532
const fromLocation = this.getViewLocation(from);
533
const toLocation = this.getViewLocation(to);
534
return this.gridview.swapViews(fromLocation, toLocation);
535
}
536
537
/**
538
* Resize a {@link IView view}.
539
*
540
* @param view The {@link IView view} to resize.
541
* @param size The size the view should be.
542
*/
543
resizeView(view: T, size: IViewSize): void {
544
const location = this.getViewLocation(view);
545
return this.gridview.resizeView(location, size);
546
}
547
548
/**
549
* Returns whether all other {@link IView views} are at their minimum size.
550
*
551
* @param view The reference {@link IView view}.
552
*/
553
isViewExpanded(view: T): boolean {
554
const location = this.getViewLocation(view);
555
return this.gridview.isViewExpanded(location);
556
}
557
558
/**
559
* Returns whether the {@link IView view} is maximized.
560
*
561
* @param view The reference {@link IView view}.
562
*/
563
isViewMaximized(view: T): boolean {
564
const location = this.getViewLocation(view);
565
return this.gridview.isViewMaximized(location);
566
}
567
568
/**
569
* Returns whether the {@link IView view} is maximized.
570
*
571
* @param view The reference {@link IView view}.
572
*/
573
hasMaximizedView(): boolean {
574
return this.gridview.hasMaximizedView();
575
}
576
577
/**
578
* Get the size of a {@link IView view}.
579
*
580
* @param view The {@link IView view}. Provide `undefined` to get the size
581
* of the grid itself.
582
*/
583
getViewSize(view?: T): IViewSize {
584
if (!view) {
585
return this.gridview.getViewSize();
586
}
587
588
const location = this.getViewLocation(view);
589
return this.gridview.getViewSize(location);
590
}
591
592
/**
593
* Get the cached visible size of a {@link IView view}. This was the size
594
* of the view at the moment it last became hidden.
595
*
596
* @param view The {@link IView view}.
597
*/
598
getViewCachedVisibleSize(view: T): number | undefined {
599
const location = this.getViewLocation(view);
600
return this.gridview.getViewCachedVisibleSize(location);
601
}
602
603
/**
604
* Maximizes the specified view and hides all other views.
605
* @param view The view to maximize.
606
*/
607
maximizeView(view: T) {
608
if (this.views.size < 2) {
609
throw new Error('At least two views are required to maximize a view');
610
}
611
const location = this.getViewLocation(view);
612
this.gridview.maximizeView(location);
613
}
614
615
exitMaximizedView(): void {
616
this.gridview.exitMaximizedView();
617
}
618
619
/**
620
* Expand the size of a {@link IView view} by collapsing all other views
621
* to their minimum sizes.
622
*
623
* @param view The {@link IView view}.
624
*/
625
expandView(view: T): void {
626
const location = this.getViewLocation(view);
627
this.gridview.expandView(location);
628
}
629
630
/**
631
* Distribute the size among all {@link IView views} within the entire
632
* grid or within a single {@link SplitView}.
633
*/
634
distributeViewSizes(): void {
635
this.gridview.distributeViewSizes();
636
}
637
638
/**
639
* Returns whether a {@link IView view} is visible.
640
*
641
* @param view The {@link IView view}.
642
*/
643
isViewVisible(view: T): boolean {
644
const location = this.getViewLocation(view);
645
return this.gridview.isViewVisible(location);
646
}
647
648
/**
649
* Set the visibility state of a {@link IView view}.
650
*
651
* @param view The {@link IView view}.
652
*/
653
setViewVisible(view: T, visible: boolean): void {
654
const location = this.getViewLocation(view);
655
this.gridview.setViewVisible(location, visible);
656
}
657
658
/**
659
* Returns a descriptor for the entire grid.
660
*/
661
getViews(): GridBranchNode<T> {
662
return this.gridview.getView() as GridBranchNode<T>;
663
}
664
665
/**
666
* Utility method to return the collection all views which intersect
667
* a view's edge.
668
*
669
* @param view The {@link IView view}.
670
* @param direction Which direction edge to be considered.
671
* @param wrap Whether the grid wraps around (from right to left, from bottom to top).
672
*/
673
getNeighborViews(view: T, direction: Direction, wrap: boolean = false): T[] {
674
if (!this.didLayout) {
675
throw new Error('Can\'t call getNeighborViews before first layout');
676
}
677
678
const location = this.getViewLocation(view);
679
const root = this.getViews();
680
const node = getGridNode(root, location);
681
let boundary = getBoxBoundary(node.box, direction);
682
683
if (wrap) {
684
if (direction === Direction.Up && node.box.top === 0) {
685
boundary = { offset: root.box.top + root.box.height, range: boundary.range };
686
} else if (direction === Direction.Right && node.box.left + node.box.width === root.box.width) {
687
boundary = { offset: 0, range: boundary.range };
688
} else if (direction === Direction.Down && node.box.top + node.box.height === root.box.height) {
689
boundary = { offset: 0, range: boundary.range };
690
} else if (direction === Direction.Left && node.box.left === 0) {
691
boundary = { offset: root.box.left + root.box.width, range: boundary.range };
692
}
693
}
694
695
return findAdjacentBoxLeafNodes(root, oppositeDirection(direction), boundary)
696
.map(node => node.view);
697
}
698
699
private getViewLocation(view: T): GridLocation {
700
const element = this.views.get(view);
701
702
if (!element) {
703
throw new Error('View not found');
704
}
705
706
return getGridLocation(element);
707
}
708
709
private onDidSashReset(location: GridLocation): void {
710
const resizeToPreferredSize = (location: GridLocation): boolean => {
711
const node = this.gridview.getView(location) as GridNode<T>;
712
713
if (isGridBranchNode(node)) {
714
return false;
715
}
716
717
const direction = getLocationOrientation(this.orientation, location);
718
const size = direction === Orientation.HORIZONTAL ? node.view.preferredWidth : node.view.preferredHeight;
719
720
if (typeof size !== 'number') {
721
return false;
722
}
723
724
const viewSize = direction === Orientation.HORIZONTAL ? { width: Math.round(size) } : { height: Math.round(size) };
725
this.gridview.resizeView(location, viewSize);
726
return true;
727
};
728
729
if (resizeToPreferredSize(location)) {
730
return;
731
}
732
733
const [parentLocation, index] = tail(location);
734
735
if (resizeToPreferredSize([...parentLocation, index + 1])) {
736
return;
737
}
738
739
this.gridview.distributeViewSizes(parentLocation);
740
}
741
}
742
743
export interface ISerializableView extends IView {
744
toJSON(): object;
745
}
746
747
export interface IViewDeserializer<T extends ISerializableView> {
748
fromJSON(json: any): T;
749
}
750
751
export interface ISerializedLeafNode {
752
type: 'leaf';
753
data: unknown;
754
size: number;
755
visible?: boolean;
756
maximized?: boolean;
757
}
758
759
export interface ISerializedBranchNode {
760
type: 'branch';
761
data: ISerializedNode[];
762
size: number;
763
visible?: boolean;
764
}
765
766
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
767
768
export interface ISerializedGrid {
769
root: ISerializedNode;
770
orientation: Orientation;
771
width: number;
772
height: number;
773
}
774
775
/**
776
* A {@link Grid} which can serialize itself.
777
*/
778
export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
779
780
private static serializeNode<T extends ISerializableView>(node: GridNode<T>, orientation: Orientation): ISerializedNode {
781
const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height;
782
783
if (!isGridBranchNode(node)) {
784
const serializedLeafNode: ISerializedLeafNode = { type: 'leaf', data: node.view.toJSON(), size };
785
786
if (typeof node.cachedVisibleSize === 'number') {
787
serializedLeafNode.size = node.cachedVisibleSize;
788
serializedLeafNode.visible = false;
789
} else if (node.maximized) {
790
serializedLeafNode.maximized = true;
791
}
792
793
return serializedLeafNode;
794
}
795
796
const data = node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation)));
797
if (data.some(c => c.visible !== false)) {
798
return { type: 'branch', data: data, size };
799
}
800
return { type: 'branch', data: data, size, visible: false };
801
}
802
803
/**
804
* Construct a new {@link SerializableGrid} from a JSON object.
805
*
806
* @param json The JSON object.
807
* @param deserializer A deserializer which can revive each view.
808
* @returns A new {@link SerializableGrid} instance.
809
*/
810
static deserialize<T extends ISerializableView>(json: ISerializedGrid, deserializer: IViewDeserializer<T>, options: IGridOptions = {}): SerializableGrid<T> {
811
if (typeof json.orientation !== 'number') {
812
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
813
} else if (typeof json.width !== 'number') {
814
throw new Error('Invalid JSON: \'width\' property must be a number.');
815
} else if (typeof json.height !== 'number') {
816
throw new Error('Invalid JSON: \'height\' property must be a number.');
817
}
818
819
const gridview = GridView.deserialize(json, deserializer, options);
820
const result = new SerializableGrid<T>(gridview, options);
821
822
return result;
823
}
824
825
/**
826
* Construct a new {@link SerializableGrid} from a grid descriptor.
827
*
828
* @param gridDescriptor A grid descriptor in which leaf nodes point to actual views.
829
* @returns A new {@link SerializableGrid} instance.
830
*/
831
static from<T extends ISerializableView>(gridDescriptor: GridDescriptor<T>, options: IGridOptions = {}): SerializableGrid<T> {
832
return SerializableGrid.deserialize(createSerializedGrid(gridDescriptor), { fromJSON: view => view }, options);
833
}
834
835
/**
836
* Useful information in order to proportionally restore view sizes
837
* upon the very first layout call.
838
*/
839
private initialLayoutContext: boolean = true;
840
841
/**
842
* Serialize this grid into a JSON object.
843
*/
844
serialize(): ISerializedGrid {
845
return {
846
root: SerializableGrid.serializeNode(this.getViews(), this.orientation),
847
orientation: this.orientation,
848
width: this.width,
849
height: this.height
850
};
851
}
852
853
override layout(width: number, height: number, top: number = 0, left: number = 0): void {
854
super.layout(width, height, top, left);
855
856
if (this.initialLayoutContext) {
857
this.initialLayoutContext = false;
858
this.gridview.trySet2x2();
859
}
860
}
861
}
862
863
export type GridLeafNodeDescriptor<T> = { size?: number; data?: any };
864
export type GridBranchNodeDescriptor<T> = { size?: number; groups: GridNodeDescriptor<T>[] };
865
export type GridNodeDescriptor<T> = GridBranchNodeDescriptor<T> | GridLeafNodeDescriptor<T>;
866
export type GridDescriptor<T> = { orientation: Orientation } & GridBranchNodeDescriptor<T>;
867
868
function isGridBranchNodeDescriptor<T>(nodeDescriptor: GridNodeDescriptor<T>): nodeDescriptor is GridBranchNodeDescriptor<T> {
869
return !!(nodeDescriptor as GridBranchNodeDescriptor<T>).groups;
870
}
871
872
export function sanitizeGridNodeDescriptor<T>(nodeDescriptor: GridNodeDescriptor<T>, rootNode: boolean): void {
873
// eslint-disable-next-line local/code-no-any-casts
874
if (!rootNode && (nodeDescriptor as any).groups && (nodeDescriptor as any).groups.length <= 1) {
875
// eslint-disable-next-line local/code-no-any-casts
876
(nodeDescriptor as any).groups = undefined;
877
}
878
879
if (!isGridBranchNodeDescriptor(nodeDescriptor)) {
880
return;
881
}
882
883
let totalDefinedSize = 0;
884
let totalDefinedSizeCount = 0;
885
886
for (const child of nodeDescriptor.groups) {
887
sanitizeGridNodeDescriptor(child, false);
888
889
if (child.size) {
890
totalDefinedSize += child.size;
891
totalDefinedSizeCount++;
892
}
893
}
894
895
const totalUndefinedSize = totalDefinedSizeCount > 0 ? totalDefinedSize : 1;
896
const totalUndefinedSizeCount = nodeDescriptor.groups.length - totalDefinedSizeCount;
897
const eachUndefinedSize = totalUndefinedSize / totalUndefinedSizeCount;
898
899
for (const child of nodeDescriptor.groups) {
900
if (!child.size) {
901
child.size = eachUndefinedSize;
902
}
903
}
904
}
905
906
function createSerializedNode<T>(nodeDescriptor: GridNodeDescriptor<T>): ISerializedNode {
907
if (isGridBranchNodeDescriptor(nodeDescriptor)) {
908
return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! };
909
} else {
910
return { type: 'leaf', data: nodeDescriptor.data, size: nodeDescriptor.size! };
911
}
912
}
913
914
function getDimensions(node: ISerializedNode, orientation: Orientation): { width?: number; height?: number } {
915
if (node.type === 'branch') {
916
const childrenDimensions = node.data.map(c => getDimensions(c, orthogonal(orientation)));
917
918
if (orientation === Orientation.VERTICAL) {
919
const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0)));
920
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.height || 0), 0);
921
return { width, height };
922
} else {
923
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.width || 0), 0);
924
const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0)));
925
return { width, height };
926
}
927
} else {
928
const width = orientation === Orientation.VERTICAL ? node.size : undefined;
929
const height = orientation === Orientation.VERTICAL ? undefined : node.size;
930
return { width, height };
931
}
932
}
933
934
/**
935
* Creates a new JSON object from a {@link GridDescriptor}, which can
936
* be deserialized by {@link SerializableGrid.deserialize}.
937
*/
938
export function createSerializedGrid<T>(gridDescriptor: GridDescriptor<T>): ISerializedGrid {
939
sanitizeGridNodeDescriptor(gridDescriptor, true);
940
941
const root = createSerializedNode(gridDescriptor);
942
const { width, height } = getDimensions(root, gridDescriptor.orientation);
943
944
return {
945
root,
946
orientation: gridDescriptor.orientation,
947
width: width || 1,
948
height: height || 1
949
};
950
}
951
952