Repository: microsoft/vscode (TypeScript)
Date: 2022-12-01 15:19:06+00:00
Task type: Bug Fix

Issue:
Grid: Wrong position provided in layout
Was finally able to repo this but not reliably. It seems to rely on a new `SplitView` being created, but this doesn't always happen. The most reliable repo for me:

1. Start with a notebook and text editor in a single editor group
2. Drag the notebook editor down to create a new group below the text editor
3. Then grab the notebook editor and drag it right to move the bottom editor to the right

---

@joaomoreno @sbatten  I think the root cause is that the notebook widget is passed the wrong position. I'm going to work around this by making the notebook editor ignore the position it is given, however this will also undo the performance fixes we made this iteration for notebook layout

I don't fully understand the grid layout code but it seems that `SplitView.layoutContext` is being set incorrectly in cases where a new BranchNode/SplitView is created. Here's what I see happening after step 3 of the repo above:

1. The `GridView.orientation` setter is called
2. This calls `flipNode` which creates a new `BranchNode`
1. In the `BranchNode` constructor, a new Split View is created
4. `layout` is called on it. At this point, `this.layoutContext` of the split view is:

    ```
    {orthogonalSize: 1129, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: 759, absoluteOrthogonalSize: 1129}
    ```

5. By the time layout is called on the notebook editor widget, `position.left` is computed as zero. This results in the wrong layout. 

Looking at the code, we can see that `0` is passed in as the absolute offset in step 4: 

<img width="1104" alt="Screenshot 2022-11-30 at 12 48 54 PM" src="https://user-images.githubusercontent.com/12821956/204905281-5f6b3189-eefc-4349-83f5-2dbe20798047.png">

If I then slightly resize the window, here's what happens:

1. We call `layout` on the split View for the new group 
4. However now `this.layoutContext` is:

    ```
    {orthogonalSize: 1129, absoluteOffset: 265, absoluteOrthogonalOffset: 28, absoluteSize: 760, absoluteOrthogonalSize: 1129}
    ```

5. When we layout the notebook editor, `position.left` is now correctly computed as `658`

_Originally posted by @mjbvz in https://github.com/microsoft/vscode/issues/167087#issuecomment-1332589925_
      


Issue discussion:

Solution patch:
diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts
--- a/src/vs/base/browser/ui/grid/gridview.ts
+++ b/src/vs/base/browser/ui/grid/gridview.ts
@@ -255,8 +255,12 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
 	private _orthogonalSize: number;
 	get orthogonalSize(): number { return this._orthogonalSize; }
 
-	private absoluteOffset: number = 0;
-	private absoluteOrthogonalOffset: number = 0;
+	private _absoluteOffset: number = 0;
+	get absoluteOffset(): number { return this._absoluteOffset; }
+
+	private _absoluteOrthogonalOffset: number = 0;
+	get absoluteOrthogonalOffset(): number { return this._absoluteOrthogonalOffset; }
+
 	private absoluteOrthogonalSize: number = 0;
 
 	private _styles: IGridViewStyles;
@@ -271,11 +275,11 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
 	}
 
 	get top(): number {
-		return this.orientation === Orientation.HORIZONTAL ? this.absoluteOffset : this.absoluteOrthogonalOffset;
+		return this.orientation === Orientation.HORIZONTAL ? this._absoluteOffset : this._absoluteOrthogonalOffset;
 	}
 
 	get left(): number {
-		return this.orientation === Orientation.HORIZONTAL ? this.absoluteOrthogonalOffset : this.absoluteOffset;
+		return this.orientation === Orientation.HORIZONTAL ? this._absoluteOrthogonalOffset : this._absoluteOffset;
 	}
 
 	get minimumSize(): number {
@@ -460,14 +464,14 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
 		// branch nodes should flip the normal/orthogonal directions
 		this._size = ctx.orthogonalSize;
 		this._orthogonalSize = size;
-		this.absoluteOffset = ctx.absoluteOffset + offset;
-		this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
+		this._absoluteOffset = ctx.absoluteOffset + offset;
+		this._absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
 		this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize;
 
 		this.splitview.layout(ctx.orthogonalSize, {
 			orthogonalSize: size,
-			absoluteOffset: this.absoluteOrthogonalOffset,
-			absoluteOrthogonalOffset: this.absoluteOffset,
+			absoluteOffset: this._absoluteOrthogonalOffset,
+			absoluteOrthogonalOffset: this._absoluteOffset,
 			absoluteSize: ctx.absoluteOrthogonalSize,
 			absoluteOrthogonalSize: ctx.absoluteSize
 		});
@@ -705,8 +709,8 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
 	}
 
 	private updateSplitviewEdgeSnappingEnablement(): void {
-		this.splitview.startSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset > 0;
-		this.splitview.endSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;
+		this.splitview.startSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset > 0;
+		this.splitview.endSnappingEnabled = this._edgeSnapping || this._absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;
 	}
 
 	dispose(): void {
@@ -1097,9 +1101,9 @@ export class GridView implements IDisposable {
 			return;
 		}
 
-		const { size, orthogonalSize } = this._root;
+		const { size, orthogonalSize, absoluteOffset, absoluteOrthogonalOffset } = this._root;
 		this.root = flipNode(this._root, orthogonalSize, size);
-		this.root.layout(size, 0, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
+		this.root.layout(size, 0, { orthogonalSize, absoluteOffset: absoluteOrthogonalOffset, absoluteOrthogonalOffset: absoluteOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
 		this.boundarySashes = this.boundarySashes;
 	}
 


Test patch:
diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts
--- a/src/vs/base/test/browser/ui/grid/gridview.test.ts
+++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts
@@ -5,7 +5,7 @@
 
 import * as assert from 'assert';
 import { $ } from 'vs/base/browser/dom';
-import { GridView, IView, Sizing } from 'vs/base/browser/ui/grid/gridview';
+import { GridView, IView, Orientation, Sizing } from 'vs/base/browser/ui/grid/gridview';
 import { nodesToArrays, TestView } from './util';
 
 suite('Gridview', function () {
@@ -222,4 +222,22 @@ suite('Gridview', function () {
 		assert.deepStrictEqual(view2.size, [400, 300]);
 		assert.deepStrictEqual(view3.size, [400, 300]);
 	});
+
+	test('flipping orientation should preserve absolute offsets', function () {
+		const view1 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY);
+		gridview.addView(view1, 200, [0]);
+
+		const view2 = new TestView(50, Number.POSITIVE_INFINITY, 50, Number.POSITIVE_INFINITY);
+		gridview.addView(view2, 200, [1]);
+
+		gridview.layout(800, 600, 100, 200);
+
+		assert.deepStrictEqual([view1.top, view1.left], [100, 200]);
+		assert.deepStrictEqual([view2.top, view2.left], [100 + 300, 200]);
+
+		gridview.orientation = Orientation.HORIZONTAL;
+
+		assert.deepStrictEqual([view1.top, view1.left], [100, 200]);
+		assert.deepStrictEqual([view2.top, view2.left], [100, 200 + 400]);
+	});
 });
diff --git a/src/vs/base/test/browser/ui/grid/util.ts b/src/vs/base/test/browser/ui/grid/util.ts
--- a/src/vs/base/test/browser/ui/grid/util.ts
+++ b/src/vs/base/test/browser/ui/grid/util.ts
@@ -37,10 +37,16 @@ export class TestView implements IView {
 	private _height = 0;
 	get height(): number { return this._height; }
 
+	private _top = 0;
+	get top(): number { return this._top; }
+
+	private _left = 0;
+	get left(): number { return this._left; }
+
 	get size(): [number, number] { return [this.width, this.height]; }
 
-	private readonly _onDidLayout = new Emitter<{ width: number; height: number }>();
-	readonly onDidLayout: Event<{ width: number; height: number }> = this._onDidLayout.event;
+	private readonly _onDidLayout = new Emitter<{ width: number; height: number; top: number; left: number }>();
+	readonly onDidLayout: Event<{ width: number; height: number; top: number; left: number }> = this._onDidLayout.event;
 
 	private readonly _onDidFocus = new Emitter<void>();
 	readonly onDidFocus: Event<void> = this._onDidFocus.event;
@@ -55,10 +61,12 @@ export class TestView implements IView {
 		assert(_minimumHeight <= _maximumHeight, 'gridview view minimum height must be <= maximum height');
 	}
 
-	layout(width: number, height: number): void {
+	layout(width: number, height: number, top: number, left: number): void {
 		this._width = width;
 		this._height = height;
-		this._onDidLayout.fire({ width, height });
+		this._top = top;
+		this._left = left;
+		this._onDidLayout.fire({ width, height, top, left });
 	}
 
 	focus(): void {