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