From d7db8c7b3fa83994d286eed3051998f96f292079 Mon Sep 17 00:00:00 2001 From: erri120 Date: Wed, 13 Dec 2023 11:45:00 +0100 Subject: [PATCH] Add entire column/row (#814) * Split all rows of a column into a new column * Update tests * Split to add row --- .../WorkspaceSystem/GridUtils.cs | 152 +++++++++++++----- .../WorkspaceSystem/WorkspaceGridState.cs | 21 ++- .../GridUtilsTests/GetPossibleStatesTests.cs | 52 ++++-- .../ColumnEnumeratorTests.cs | 2 +- .../RowEnumeratorTests.cs | 2 +- 5 files changed, 163 insertions(+), 66 deletions(-) diff --git a/src/NexusMods.App.UI/WorkspaceSystem/GridUtils.cs b/src/NexusMods.App.UI/WorkspaceSystem/GridUtils.cs index 497df6d364..c9110de8c6 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/GridUtils.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/GridUtils.cs @@ -68,35 +68,42 @@ private static List GetPossibleStatesForHorizontal( { var res = new List(); - Span seenColumns = stackalloc WorkspaceGridState.ColumnInfo[maxColumns]; - using var columnEnumerator = new WorkspaceGridState.ColumnEnumerator(currentState, seenColumns); + var (columnCount, maxRowCount) = currentState.CountColumns(); - var columnCount = 0; - var rowCount = 0; + Span seenColumns = stackalloc WorkspaceGridState.ColumnInfo[columnCount]; + using var columnEnumerator = new WorkspaceGridState.ColumnEnumerator(currentState, seenColumns); // Step 1: Iterate over all columns. // NOTE(erri120): this will fill up seenColumns Span rowBuffer = stackalloc PanelGridState[maxRows]; while (columnEnumerator.MoveNext(rowBuffer)) { - columnCount += 1; - var column = columnEnumerator.Current; var rows = column.Rows; - rowCount = Math.Max(rowCount, rows.Length); // NOTE(erri120): In a horizontal layout, the rows can move independent of rows in other columns. - if (rows.Length == maxRows) continue; - foreach (var panelToSplit in rows) + if (rows.Length != maxRows) + { + foreach (var panelToSplit in rows) + { + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: false, inverse: false)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: false, inverse: true)); + } + } + + // NOTE(erri120): If these conditions are met, we can split all rows of the current column in half + // to create a new column. Note that this requires knowing the current amount of columns, so two + // iterations are required (that's what CountColumns does). + if (rows.Length > 1 && columnCount != maxColumns) { - res.Add(CreateResult(currentState, panelToSplit, splitVertically: false, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, splitVertically: false, inverse: true)); + res.Add(AddColumn(currentState, column.Info, rows, inverse: false)); + res.Add(AddColumn(currentState, column.Info, rows, inverse: true)); } } var seenColumnSlice = seenColumns[..columnCount]; - Span seenRows = stackalloc WorkspaceGridState.RowInfo[rowCount]; + Span seenRows = stackalloc WorkspaceGridState.RowInfo[maxRowCount]; using var rowEnumerator = new WorkspaceGridState.RowEnumerator(currentState, seenRows); // Step 2: Iterate over all rows. @@ -114,8 +121,8 @@ private static List GetPossibleStatesForHorizontal( if (columnCount == 1) { - res.Add(CreateResult(currentState, panelToSplit, splitVertically: true, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, splitVertically: true, inverse: true)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: true, inverse: false)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: true, inverse: true)); continue; } @@ -128,8 +135,8 @@ private static List GetPossibleStatesForHorizontal( var updatedLogicalBounds = new Rect(rect.X, rect.Y, seenColumn.X, rect.Height); var newPanelLogicalBounds = new Rect(seenColumn.X, rect.Y, seenColumn.Width, rect.Height); - res.Add(CreateResult(currentState, panelToSplit,updatedLogicalBounds,newPanelLogicalBounds, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, updatedLogicalBounds, newPanelLogicalBounds, inverse: true)); + res.Add(SplitPanelWithBounds(currentState, panelToSplit,updatedLogicalBounds,newPanelLogicalBounds, inverse: false)); + res.Add(SplitPanelWithBounds(currentState, panelToSplit, updatedLogicalBounds, newPanelLogicalBounds, inverse: true)); } } } @@ -145,34 +152,41 @@ private static List GetPossibleStatesForVertical( { var res = new List(); - Span seenRows = stackalloc WorkspaceGridState.RowInfo[maxRows]; - using var rowEnumerator = new WorkspaceGridState.RowEnumerator(currentState, seenRows); + var (rowCount, maxColumnCount) = currentState.CountRows(); - var rowCount = 0; - var columnCount = 0; + Span seenRows = stackalloc WorkspaceGridState.RowInfo[rowCount]; + using var rowEnumerator = new WorkspaceGridState.RowEnumerator(currentState, seenRows); // Step 1: Iterate over all rows. Span columnBuffer = stackalloc PanelGridState[maxColumns]; while (rowEnumerator.MoveNext(columnBuffer)) { - rowCount += 1; - var row = rowEnumerator.Current; var columns = row.Columns; - columnCount = Math.Max(columnCount, columns.Length); // NOTE(erri120): In a vertical layout, the columns can move independent of columns in other columns. - if (columns.Length == maxColumns) continue; - foreach (var panelToSplit in columns) + if (columns.Length != maxColumns) { - res.Add(CreateResult(currentState, panelToSplit, splitVertically: true, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, splitVertically: true, inverse: true)); + foreach (var panelToSplit in columns) + { + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: true, inverse: false)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: true, inverse: true)); + } + } + + // NOTE(erri120): If these conditions are met, we can split all columns of the current ro win half + // to create a new row. Note that this requires knowing the current amount of rows, so two + // iterations are required (that's what CountRows does). + if (columns.Length > 1 && rowCount != maxRows) + { + res.Add(AddRow(currentState, row.Info, columns, inverse: false)); + res.Add(AddRow(currentState, row.Info, columns, inverse: true)); } } var seenRowSlice = seenRows[..rowCount]; - Span seenColumns = stackalloc WorkspaceGridState.ColumnInfo[columnCount]; + Span seenColumns = stackalloc WorkspaceGridState.ColumnInfo[maxColumnCount]; using var columnEnumerator = new WorkspaceGridState.ColumnEnumerator(currentState, seenColumns); // Step 2: Iterate over all columns. @@ -190,8 +204,8 @@ private static List GetPossibleStatesForVertical( if (rowCount == 1) { - res.Add(CreateResult(currentState, panelToSplit, splitVertically: false, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, splitVertically: false, inverse: true)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: false, inverse: false)); + res.Add(SplitPanelInHalf(currentState, panelToSplit, splitVertically: false, inverse: true)); } foreach (var seenRow in seenRowSlice) @@ -203,8 +217,8 @@ private static List GetPossibleStatesForVertical( var updatedLogicalBounds = new Rect(rect.X, rect.Y, rect.Width, seenRow.Y); var newPanelLogicalBounds = new Rect(rect.X, seenRow.Y, rect.Width, seenRow.Height); - res.Add(CreateResult(currentState, panelToSplit,updatedLogicalBounds,newPanelLogicalBounds, inverse: false)); - res.Add(CreateResult(currentState, panelToSplit, updatedLogicalBounds, newPanelLogicalBounds, inverse: true)); + res.Add(SplitPanelWithBounds(currentState, panelToSplit,updatedLogicalBounds,newPanelLogicalBounds, inverse: false)); + res.Add(SplitPanelWithBounds(currentState, panelToSplit, updatedLogicalBounds, newPanelLogicalBounds, inverse: true)); } } } @@ -213,30 +227,88 @@ private static List GetPossibleStatesForVertical( return res; } - private static WorkspaceGridState CreateResult( + private static WorkspaceGridState AddRow( + WorkspaceGridState currentState, + WorkspaceGridState.RowInfo rowInfo, + ReadOnlySpan panelsToSplit, + bool inverse) + { + var newHeight = rowInfo.Height / 2; + var currentY = inverse ? newHeight : rowInfo.Y; + var newY = inverse ? rowInfo.Y : newHeight; + + Span updatedValues = stackalloc PanelGridState[panelsToSplit.Length + 1]; + for (var i = 0; i < panelsToSplit.Length; i++) + { + var panelToSplit = panelsToSplit[i]; + var rect = panelToSplit.Rect; + panelToSplit.Rect = new Rect(rect.X, currentY, rect.Width, newHeight); + + updatedValues[i] = panelToSplit; + } + + updatedValues[panelsToSplit.Length] = new PanelGridState( + PanelId.DefaultValue, + new Rect(0, newY, 1.0, newHeight) + ); + + var res = currentState.UnionById(updatedValues); + return res; + } + + private static WorkspaceGridState AddColumn( + WorkspaceGridState currentState, + WorkspaceGridState.ColumnInfo columnInfo, + ReadOnlySpan panelsToSplit, + bool inverse) + { + var newWidth = columnInfo.Width / 2; + var currentX = inverse ? newWidth : columnInfo.X; + var newX = inverse ? columnInfo.X : newWidth; + + Span updatedValues = stackalloc PanelGridState[panelsToSplit.Length + 1]; + for (var i = 0; i < panelsToSplit.Length; i++) + { + var panelToSplit = panelsToSplit[i]; + var rect = panelToSplit.Rect; + panelToSplit.Rect = new Rect(currentX, rect.Y, newWidth, rect.Height); + + updatedValues[i] = panelToSplit; + } + + updatedValues[panelsToSplit.Length] = new PanelGridState( + PanelId.DefaultValue, + new Rect(newX, 0, newWidth, 1.0) + ); + + var res = currentState.UnionById(updatedValues); + return res; + } + + private static WorkspaceGridState SplitPanelWithBounds( WorkspaceGridState currentState, PanelGridState panelToSplit, - Rect updatedLogicalBounds, - Rect newPanelLogicalBounds, + Rect updatedBounds, + Rect newPanelBounds, bool inverse) { Span updatedValues = stackalloc PanelGridState[2]; if (inverse) { - updatedValues[0] = new PanelGridState(PanelId.DefaultValue, updatedLogicalBounds); - updatedValues[1] = panelToSplit with { Rect = newPanelLogicalBounds }; + updatedValues[0] = new PanelGridState(PanelId.DefaultValue, updatedBounds); + updatedValues[1] = panelToSplit with { Rect = newPanelBounds }; } else { - updatedValues[0] = panelToSplit with { Rect = updatedLogicalBounds }; - updatedValues[1] = new PanelGridState(PanelId.DefaultValue, newPanelLogicalBounds); + updatedValues[0] = panelToSplit with { Rect = updatedBounds }; + updatedValues[1] = new PanelGridState(PanelId.DefaultValue, newPanelBounds); } var res = currentState.UnionById(updatedValues); return res; } - private static WorkspaceGridState CreateResult( + private static WorkspaceGridState SplitPanelInHalf( WorkspaceGridState workspaceState, PanelGridState panelToSplit, bool splitVertically, diff --git a/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceGridState.cs b/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceGridState.cs index d1ada1f6ff..f345890127 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceGridState.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceGridState.cs @@ -99,9 +99,11 @@ public WorkspaceGridState UnionById(ReadOnlySpan other) public AdjacentPanelEnumerator EnumerateAdjacentPanels(PanelGridState anchor, bool includeAnchor) => new(this, anchor, includeAnchor); - public int CountColumns() + public (int columnCount, int maxRowCount) CountColumns() { - var res = 0; + // TODO: return maxRowCount as well + var columnCount = 0; + var maxRowCount = 0; Span seenColumns = stackalloc ColumnInfo[MaxColumns]; using var enumerator = new ColumnEnumerator(this, seenColumns); @@ -109,15 +111,17 @@ public int CountColumns() Span rowBuffer = stackalloc PanelGridState[MaxRows]; while (enumerator.MoveNext(rowBuffer)) { - res += 1; + columnCount += 1; + maxRowCount = Math.Max(maxRowCount, enumerator.Current.Rows.Length); } - return res; + return (columnCount, maxRowCount); } - public int CountRows() + public (int rowCount, int maxColumnCount) CountRows() { - var res = 0; + var rowCount = 0; + var maxColumnCount = 0; Span seenRows = stackalloc RowInfo[MaxRows]; using var enumerator = new RowEnumerator(this, seenRows); @@ -125,10 +129,11 @@ public int CountRows() Span columnBuffer = stackalloc PanelGridState[MaxColumns]; while (enumerator.MoveNext(columnBuffer)) { - res += 1; + rowCount += 1; + maxColumnCount = Math.Max(maxColumnCount, enumerator.Current.Columns.Length); } - return res; + return (rowCount, maxColumnCount); } [Conditional("DEBUG")] diff --git a/tests/NexusMods.UI.Tests/WorkspaceSystem/GridUtilsTests/GetPossibleStatesTests.cs b/tests/NexusMods.UI.Tests/WorkspaceSystem/GridUtilsTests/GetPossibleStatesTests.cs index 1f59951853..57b44bd89a 100644 --- a/tests/NexusMods.UI.Tests/WorkspaceSystem/GridUtilsTests/GetPossibleStatesTests.cs +++ b/tests/NexusMods.UI.Tests/WorkspaceSystem/GridUtilsTests/GetPossibleStatesTests.cs @@ -104,12 +104,12 @@ public static IEnumerable TestData_GetPossibleStates_Vertical_Generate // | 1 | 2 | // | 1 | 2 | // Possible States: - // 1) split the first panel horizontally, the new panel is in the second row - // 2) split the first panel horizontally, the new panel is in the first row - // 3) split the second panel horizontally, the new panel is in the second row - // 4) split the second panel horizontally, the new panel is in the first row - // TODO: 5) split both the first and second panel horizontally, the new panel will take up the entirety of the second row - // TODO: 6) split both the first and second panel horizontally, the new panel will take up the entirety of the first row + // 1) split both the first and second panel horizontally, the new panel will take up the entirety of the second row + // 2) split both the first and second panel horizontally, the new panel will take up the entirety of the first row + // 3) split the first panel horizontally, the new panel is in the second row + // 4) split the first panel horizontally, the new panel is in the first row + // 5) split the second panel horizontally, the new panel is in the second row + // 6) split the second panel horizontally, the new panel is in the first row yield return new object[] { "vertical | two column", @@ -120,6 +120,18 @@ public static IEnumerable TestData_GetPossibleStates_Vertical_Generate ), new[] { + CreateState( + isHorizontal: false, + new PanelGridState(firstPanelId, new Rect(0, 0, 0.5, 0.5)), + new PanelGridState(secondPanelId, new Rect(0.5, 0, 0.5, 0.5)), + new PanelGridState(newPanelId, new Rect(0, 0.5, 1.0, 0.5)) + ), + CreateState( + isHorizontal: false, + new PanelGridState(newPanelId, new Rect(0, 0, 1.0, 0.5)), + new PanelGridState(firstPanelId, new Rect(0, 0.5, 0.5, 0.5)), + new PanelGridState(secondPanelId, new Rect(0.5, 0.5, 0.5, 0.5)) + ), CreateState( isHorizontal: false, new PanelGridState(firstPanelId, new Rect(0, 0, 0.5, 0.5)), @@ -155,8 +167,6 @@ public static IEnumerable TestData_GetPossibleStates_Vertical_Generate // 2) split the first panel vertically, the new panel is in the first column // 3) split the second panel vertically, the new panel is in the second column // 4) split the second panel vertically, the new panel is in the first column - // TODO: 5) split both the first and second panel vertically, the new panel will take up the entirety of the second column - // TODO: 6) split both the first and second panel vertically, the new panel will take up the entirety of the first column for (var height = min; height < max; height += step) { yield return new object[] @@ -342,8 +352,6 @@ public static IEnumerable TestData_GetPossibleStates_Horizontal_Genera // 2) split the first panel horizontally, the new panel is in the first row // 3) split the second panel horizontally, the new panel is in the second row // 4) split the second panel horizontally, the new panel is in the first row - // TODO: 5) split both the first and second panel horizontally, the new panel will take up the entirety of the second row - // TODO: 6) split both the first and second panel horizontally, the new panel will take up the entirety of the first row for (var width = min; width < max; width += step) { yield return new object[] @@ -388,12 +396,12 @@ public static IEnumerable TestData_GetPossibleStates_Horizontal_Genera // | 1 | 1 | // | 2 | 2 | // Possible States: - // 1) split the first panel vertically, the new panel is in the second column - // 2) split the first panel vertically, the new panel is in the first column - // 3) split the second panel vertically, the new panel is in the second column - // 4) split the second panel vertically, the new panel is in the first column - // TODO: 5) split both the first and second panel vertically, the new panel will take up the entirety of the second column - // TODO: 6) split both the first and second panel vertically, the new panel will take up the entirety of the first column + // 1) split both the first and second panel vertically, the new panel will take up the entirety of the second column + // 2) split both the first and second panel vertically, the new panel will take up the entirety of the first column + // 3) split the first panel vertically, the new panel is in the second column + // 4) split the first panel vertically, the new panel is in the first column + // 5) split the second panel vertically, the new panel is in the second column + // 6) split the second panel vertically, the new panel is in the first column yield return new object[] { "horizontal | two rows", @@ -404,6 +412,18 @@ public static IEnumerable TestData_GetPossibleStates_Horizontal_Genera ), new[] { + CreateState( + isHorizontal: true, + new PanelGridState(firstPanelId, new Rect(0, 0, 0.5, 0.5)), + new PanelGridState(secondPanelId, new Rect(0, 0.5, 0.5, 0.5)), + new PanelGridState(newPanelId, new Rect(0.5, 0, 0.5, 1)) + ), + CreateState( + isHorizontal: true, + new PanelGridState(newPanelId, new Rect(0, 0, 0.5, 1)), + new PanelGridState(firstPanelId, new Rect(0.5, 0, 0.5, 0.5)), + new PanelGridState(secondPanelId, new Rect(0.5, 0.5, 0.5, 0.5)) + ), CreateState( isHorizontal: true, new PanelGridState(firstPanelId, new Rect(0, 0, 0.5, 0.5)), diff --git a/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/ColumnEnumeratorTests.cs b/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/ColumnEnumeratorTests.cs index 17b318a4a0..3f9243b237 100644 --- a/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/ColumnEnumeratorTests.cs +++ b/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/ColumnEnumeratorTests.cs @@ -37,7 +37,7 @@ public void Test_ColumnEnumerator( actualOutput.Rows.Should().Equal(expectedOutput.Rows); } - currentState.CountColumns().Should().Be(expectedOutputs.Length); + currentState.CountColumns().columnCount.Should().Be(expectedOutputs.Length); } [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")] diff --git a/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/RowEnumeratorTests.cs b/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/RowEnumeratorTests.cs index b937ee4c41..5bf2f6cc26 100644 --- a/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/RowEnumeratorTests.cs +++ b/tests/NexusMods.UI.Tests/WorkspaceSystem/WorkspaceGridStateTests/RowEnumeratorTests.cs @@ -37,7 +37,7 @@ public void Test_RowEnumerator( actualOutput.Columns.Should().Equal(expectedOutput.Columns); } - currentState.CountRows().Should().Be(expectedOutputs.Length); + currentState.CountRows().rowCount.Should().Be(expectedOutputs.Length); } [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]