Skip to content

Commit 61c52d0

Browse files
authored
GridLayout perf improvements (microsoft#79614)
* add deserialization to splitview * add new grid deserialization test to index.html * working mostly * branchnode should invert orthogonality in splitview * fix issue with sidebar snapping * cleanup and make splitview api more explicit * hook up children sash reset * adopt single deserialize (PR feedback)
1 parent 1126817 commit 61c52d0

6 files changed

Lines changed: 490 additions & 288 deletions

File tree

src/vs/base/browser/ui/grid/grid.ts

Lines changed: 18 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'vs/css!./gridview';
77
import { Orientation } from 'vs/base/browser/ui/sash/sash';
88
import { Disposable } from 'vs/base/common/lifecycle';
99
import { tail2 as tail, equals } from 'vs/base/common/arrays';
10-
import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, LayoutController, IGridViewOptions } from './gridview';
10+
import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions } from './gridview';
1111
import { Event } from 'vs/base/common/event';
1212
import { InvisibleSizing } from 'vs/base/browser/ui/splitview/splitview';
1313

@@ -217,9 +217,17 @@ export class Grid<T extends IView = IView> extends Disposable {
217217

218218
private didLayout = false;
219219

220-
constructor(view: T, options: IGridOptions = {}) {
220+
constructor(gridview: GridView, options?: IGridOptions);
221+
constructor(view: T, options?: IGridOptions);
222+
constructor(view: T | GridView, options: IGridOptions = {}) {
221223
super();
222-
this.gridview = new GridView(options);
224+
225+
if (view instanceof GridView) {
226+
this.gridview = view;
227+
this.gridview.getViewMap(this.views);
228+
} else {
229+
this.gridview = new GridView(options);
230+
}
223231
this._register(this.gridview);
224232

225233
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
@@ -228,7 +236,9 @@ export class Grid<T extends IView = IView> extends Disposable {
228236
? GridViewSizing.Invisible(options.firstViewVisibleCachedSize)
229237
: 0;
230238

231-
this._addView(view, size, [0]);
239+
if (!(view instanceof GridView)) {
240+
this._addView(view, size, [0]);
241+
}
232242
}
233243

234244
style(styles: IGridStyles): void {
@@ -469,12 +479,6 @@ export interface IViewDeserializer<T extends ISerializableView> {
469479
fromJSON(json: any): T;
470480
}
471481

472-
interface InitialLayoutContext<T extends ISerializableView> {
473-
width: number;
474-
height: number;
475-
root: GridBranchNode<T>;
476-
}
477-
478482
export interface ISerializedLeafNode {
479483
type: 'leaf';
480484
data: any;
@@ -567,39 +571,17 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
567571
throw new Error('Invalid JSON: \'height\' property must be a number.');
568572
}
569573

570-
const orientation = json.orientation;
571-
const width = json.width;
572-
const height = json.height;
573-
const box: Box = { top: 0, left: 0, width, height };
574-
575-
const root = SerializableGrid.deserializeNode(json.root, orientation, box, deserializer) as GridBranchNode<T>;
576-
const firstLeaf = SerializableGrid.getFirstLeaf(root);
577-
578-
if (!firstLeaf) {
579-
throw new Error('Invalid serialized state, first leaf not found');
580-
}
581-
582-
const layoutController = new LayoutController(false);
583-
options = { ...options, layoutController };
584-
585-
if (typeof firstLeaf.cachedVisibleSize === 'number') {
586-
options = { ...options, firstViewVisibleCachedSize: firstLeaf.cachedVisibleSize };
587-
}
588-
589-
const result = new SerializableGrid<T>(firstLeaf.view, options);
590-
result.orientation = orientation;
591-
result.restoreViews(firstLeaf.view, orientation, root);
592-
result.initialLayoutContext = { width, height, root };
574+
const gridview = GridView.deserialize(json, deserializer, options);
575+
const result = new SerializableGrid<T>(gridview, options);
593576

594-
layoutController.isLayoutEnabled = true;
595577
return result;
596578
}
597579

598580
/**
599581
* Useful information in order to proportionally restore view sizes
600582
* upon the very first layout call.
601583
*/
602-
private initialLayoutContext: InitialLayoutContext<T> | undefined;
584+
private initialLayoutContext: boolean = true;
603585

604586
serialize(): ISerializedGrid {
605587
return {
@@ -614,65 +596,8 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
614596
super.layout(width, height);
615597

616598
if (this.initialLayoutContext) {
617-
const widthScale = width / this.initialLayoutContext.width;
618-
const heightScale = height / this.initialLayoutContext.height;
619-
620-
this.restoreViewsSize([], this.initialLayoutContext.root, this.orientation, widthScale, heightScale);
621-
this.initialLayoutContext = undefined;
622-
623599
this.gridview.trySet2x2();
624-
}
625-
}
626-
627-
/**
628-
* Recursively restores views which were just deserialized.
629-
*/
630-
private restoreViews(referenceView: T, orientation: Orientation, node: GridNode<T>): void {
631-
if (!isGridBranchNode(node)) {
632-
return;
633-
}
634-
635-
const direction = orientation === Orientation.VERTICAL ? Direction.Down : Direction.Right;
636-
const firstLeaves = node.children.map(c => SerializableGrid.getFirstLeaf(c));
637-
638-
for (let i = 1; i < firstLeaves.length; i++) {
639-
const node = firstLeaves[i];
640-
const size: number | InvisibleSizing = typeof node.cachedVisibleSize === 'number'
641-
? GridViewSizing.Invisible(node.cachedVisibleSize)
642-
: (orientation === Orientation.VERTICAL ? node.box.height : node.box.width);
643-
this.addView(node.view, size, referenceView, direction);
644-
referenceView = node.view;
645-
}
646-
647-
for (let i = 0; i < node.children.length; i++) {
648-
this.restoreViews(firstLeaves[i].view, orthogonal(orientation), node.children[i]);
649-
}
650-
}
651-
652-
/**
653-
* Recursively restores view sizes.
654-
* This should be called only after the very first layout call.
655-
*/
656-
private restoreViewsSize(location: number[], node: GridNode<T>, orientation: Orientation, widthScale: number, heightScale: number): void {
657-
if (!isGridBranchNode(node)) {
658-
return;
659-
}
660-
661-
const scale = orientation === Orientation.VERTICAL ? heightScale : widthScale;
662-
663-
for (let i = 0; i < node.children.length; i++) {
664-
const child = node.children[i];
665-
const childLocation = [...location, i];
666-
667-
if (i < node.children.length - 1) {
668-
const size = orientation === Orientation.VERTICAL
669-
? { height: Math.floor(child.box.height * scale) }
670-
: { width: Math.floor(child.box.width * scale) };
671-
672-
this.gridview.resizeView(childLocation, size);
673-
}
674-
675-
this.restoreViewsSize(childLocation, child, orthogonal(orientation), widthScale, heightScale);
600+
this.initialLayoutContext = false;
676601
}
677602
}
678603
}

src/vs/base/browser/ui/grid/gridview.ts

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ export interface IView {
3434
setVisible?(visible: boolean): void;
3535
}
3636

37+
export interface ISerializableView extends IView {
38+
toJSON(): object;
39+
}
40+
41+
export interface IViewDeserializer<T extends ISerializableView> {
42+
fromJSON(json: any): T;
43+
}
44+
45+
export interface ISerializedLeafNode {
46+
type: 'leaf';
47+
data: any;
48+
size: number;
49+
visible?: boolean;
50+
}
51+
52+
export interface ISerializedBranchNode {
53+
type: 'branch';
54+
data: ISerializedNode[];
55+
size: number;
56+
}
57+
58+
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
59+
60+
export interface ISerializedGridView {
61+
root: ISerializedNode;
62+
orientation: Orientation;
63+
width: number;
64+
height: number;
65+
}
66+
3767
export function orthogonal(orientation: Orientation): Orientation {
3868
return orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
3969
}
@@ -179,18 +209,49 @@ class BranchNode implements ISplitView, IDisposable {
179209
styles: IGridViewStyles,
180210
readonly proportionalLayout: boolean,
181211
size: number = 0,
182-
orthogonalSize: number = 0
212+
orthogonalSize: number = 0,
213+
childDescriptors?: INodeDescriptor[]
183214
) {
184215
this._styles = styles;
185216
this._size = size;
186217
this._orthogonalSize = orthogonalSize;
187218

188219
this.element = $('.monaco-grid-branch-node');
189-
this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout });
190-
this.splitview.layout(size, orthogonalSize);
220+
221+
if (!childDescriptors) {
222+
// Normal behavior, we have no children yet, just set up the splitview
223+
this.splitview = new SplitView(this.element, { orientation, styles, proportionalLayout });
224+
this.splitview.layout(size, orthogonalSize);
225+
} else {
226+
// Reconstruction behavior, we want to reconstruct a splitview
227+
const descriptor = {
228+
views: childDescriptors.map(childDescriptor => {
229+
return {
230+
view: childDescriptor.node,
231+
size: childDescriptor.node.size,
232+
visible: childDescriptor.node instanceof LeafNode && childDescriptor.visible !== undefined ? childDescriptor.visible : true
233+
};
234+
}),
235+
size: this.orthogonalSize
236+
};
237+
238+
const options = { proportionalLayout, orientation, styles };
239+
240+
this.children = childDescriptors.map(c => c.node);
241+
this.splitview = new SplitView(this.element, { ...options, descriptor });
242+
243+
this.children.forEach((node, index) => {
244+
// Set up orthogonal sashes for children
245+
node.orthogonalStartSash = this.splitview.sashes[index - 1];
246+
node.orthogonalEndSash = this.splitview.sashes[index];
247+
});
248+
}
191249

192250
const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);
193251
this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);
252+
253+
const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));
254+
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
194255
}
195256

196257
style(styles: IGridViewStyles): void {
@@ -226,7 +287,7 @@ class BranchNode implements ISplitView, IDisposable {
226287
}
227288
}
228289

229-
addChild(node: Node, size: number | Sizing, index: number): void {
290+
addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void {
230291
if (index < 0 || index > this.children.length) {
231292
throw new Error('Invalid index');
232293
}
@@ -484,9 +545,11 @@ class LeafNode implements ISplitView, IDisposable {
484545
readonly view: IView,
485546
readonly orientation: Orientation,
486547
readonly layoutController: ILayoutController,
487-
orthogonalSize: number
548+
orthogonalSize: number,
549+
size: number = 0
488550
) {
489551
this._orthogonalSize = orthogonalSize;
552+
this._size = size;
490553

491554
this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height));
492555
this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);
@@ -577,6 +640,11 @@ class LeafNode implements ISplitView, IDisposable {
577640

578641
type Node = BranchNode | LeafNode;
579642

643+
export interface INodeDescriptor {
644+
node: Node;
645+
visible?: boolean;
646+
}
647+
580648
function flipNode<T extends Node>(node: T, size: number, orthogonalSize: number): T {
581649
if (node instanceof BranchNode) {
582650
const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.proportionalLayout, size, orthogonalSize);
@@ -680,6 +748,18 @@ export class GridView implements IDisposable {
680748
this.root = new BranchNode(Orientation.VERTICAL, this.layoutController, this.styles, this.proportionalLayout);
681749
}
682750

751+
getViewMap(map: Map<IView, HTMLElement>, node?: Node): void {
752+
if (!node) {
753+
node = this.root;
754+
}
755+
756+
if (node instanceof BranchNode) {
757+
node.children.forEach(child => this.getViewMap(map, child));
758+
} else {
759+
map.set(node.view, node.element);
760+
}
761+
}
762+
683763
style(styles: IGridViewStyles): void {
684764
this.styles = styles;
685765
this.root.style(styles);
@@ -953,6 +1033,47 @@ export class GridView implements IDisposable {
9531033
return this._getViews(node, this.orientation, { top: 0, left: 0, width: this.width, height: this.height });
9541034
}
9551035

1036+
static deserialize<T extends ISerializableView>(json: ISerializedGridView, deserializer: IViewDeserializer<T>, options: IGridViewOptions = {}): GridView {
1037+
if (typeof json.orientation !== 'number') {
1038+
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
1039+
} else if (typeof json.width !== 'number') {
1040+
throw new Error('Invalid JSON: \'width\' property must be a number.');
1041+
} else if (typeof json.height !== 'number') {
1042+
throw new Error('Invalid JSON: \'height\' property must be a number.');
1043+
}
1044+
1045+
const orientation = json.orientation;
1046+
const height = json.height;
1047+
1048+
const result = new GridView(options);
1049+
result._deserialize(json.root as ISerializedBranchNode, orientation, deserializer, height);
1050+
1051+
return result;
1052+
}
1053+
1054+
private _deserialize(root: ISerializedBranchNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): void {
1055+
this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize) as BranchNode;
1056+
}
1057+
1058+
private _deserializeNode(node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer<ISerializableView>, orthogonalSize: number): Node {
1059+
let result: Node;
1060+
if (node.type === 'branch') {
1061+
const serializedChildren = node.data as ISerializedNode[];
1062+
const children = serializedChildren.map(serializedChild => {
1063+
return {
1064+
node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size),
1065+
visible: (serializedChild as { visible?: boolean }).visible
1066+
} as INodeDescriptor;
1067+
});
1068+
1069+
result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, children);
1070+
} else {
1071+
result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size);
1072+
}
1073+
1074+
return result;
1075+
}
1076+
9561077
private _getViews(node: Node, orientation: Orientation, box: Box, cachedVisibleSize?: number): GridNode {
9571078
if (node instanceof LeafNode) {
9581079
return { view: node.view, box, cachedVisibleSize };

0 commit comments

Comments
 (0)