diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/TakenSpotsReferenceHolder.cs b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/TakenSpotsReferenceHolder.cs index 25806300a95..9474082e25e 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/TakenSpotsReferenceHolder.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/TakenSpotsReferenceHolder.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections; +using System.Drawing; +using Microsoft.Toolkit.Diagnostics; namespace Microsoft.Toolkit.Uwp.UI.Controls { @@ -16,23 +14,81 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls /// iterator. /// This is used so we can better isolate our logic and make it easier to test. /// - internal class TakenSpotsReferenceHolder + internal sealed class TakenSpotsReferenceHolder { /// - /// Gets or sets the array to hold taken spots. - /// True value indicates an item in the layout is fixed to that position. - /// False values indicate free openings where an item can be placed. + /// The instance used to efficiently track empty spots. /// - public bool[,] SpotsTaken { get; set; } + private readonly BitArray spotsTaken; + /// + /// Initializes a new instance of the class. + /// + /// The number of rows to track. + /// The number of columns to track. public TakenSpotsReferenceHolder(int rows, int columns) { - SpotsTaken = new bool[rows, columns]; + Guard.IsGreaterThanOrEqualTo(rows, 0, nameof(rows)); + Guard.IsGreaterThanOrEqualTo(columns, 0, nameof(columns)); + + Height = rows; + Width = columns; + + this.spotsTaken = new BitArray(rows * columns); + } + + /// + /// Gets the height of the grid to monitor. + /// + public int Height { get; } + + /// + /// Gets the width of the grid to monitor. + /// + public int Width { get; } + + /// + /// Gets or sets the value of a specified grid cell. + /// + /// The vertical offset. + /// The horizontal offset. + public bool this[int i, int j] + { + get => this.spotsTaken[(i * Width) + j]; + set => this.spotsTaken[(i * Width) + j] = value; + } + + /// + /// Fills the specified area in the current grid with a given value. + /// If invalid coordinates are given, they will simply be ignored and no exception will be thrown. + /// + /// The value to fill the target area with. + /// The row to start on (inclusive, 0-based index). + /// The column to start on (inclusive, 0-based index). + /// The positive width of area to fill. + /// The positive height of area to fill. + public void Fill(bool value, int row, int column, int width, int height) + { + Rectangle bounds = new Rectangle(0, 0, Width, Height); + + // Precompute bounds to skip branching in main loop + bounds.Intersect(new Rectangle(column, row, width, height)); + + for (int i = bounds.Top; i < bounds.Bottom; i++) + { + for (int j = bounds.Left; j < bounds.Right; j++) + { + this[i, j] = value; + } + } } - public TakenSpotsReferenceHolder(bool[,] array) + /// + /// Resets the current reference holder. + /// + public void Reset() { - SpotsTaken = array; + this.spotsTaken.SetAll(false); } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.Helpers.cs b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.Helpers.cs index 7263e513c74..9d16b5cd986 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.Helpers.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.Helpers.cs @@ -22,16 +22,16 @@ public partial class UniformGrid : Grid { if (topdown) { - var rows = arrayref.SpotsTaken.GetLength(0); + var rows = arrayref.Height; // Layout spots from Top-Bottom, Left-Right (right-left handled automatically by Grid with Flow-Direction). // Effectively transpose the Grid Layout. - for (int c = 0; c < arrayref.SpotsTaken.GetLength(1); c++) + for (int c = 0; c < arrayref.Width; c++) { int start = (c == 0 && firstcolumn > 0 && firstcolumn < rows) ? firstcolumn : 0; for (int r = start; r < rows; r++) { - if (!arrayref.SpotsTaken[r, c]) + if (!arrayref[r, c]) { yield return (r, c); } @@ -40,17 +40,17 @@ public partial class UniformGrid : Grid } else { - var columns = arrayref.SpotsTaken.GetLength(1); + var columns = arrayref.Width; // Layout spots as normal from Left-Right. // (right-left handled automatically by Grid with Flow-Direction // during its layout, internal model is always left-right). - for (int r = 0; r < arrayref.SpotsTaken.GetLength(0); r++) + for (int r = 0; r < arrayref.Height; r++) { int start = (r == 0 && firstcolumn > 0 && firstcolumn < columns) ? firstcolumn : 0; for (int c = start; c < columns; c++) { - if (!arrayref.SpotsTaken[r, c]) + if (!arrayref[r, c]) { yield return (r, c); } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.cs b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.cs index 2d19bc59bd0..a638a1db26c 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls/UniformGrid/UniformGrid.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Toolkit.Extensions; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -20,6 +19,11 @@ public partial class UniformGrid : Grid // Internal list we use to keep track of items that we don't have space to layout. private List _overflow = new List(); + /// + /// The instance in use, if any. + /// + private TakenSpotsReferenceHolder _spotref; + /// protected override Size MeasureOverride(Size availableSize) { @@ -36,7 +40,20 @@ protected override Size MeasureOverride(Size availableSize) SetupRowDefinitions(rows); SetupColumnDefinitions(columns); - var spotref = new TakenSpotsReferenceHolder(rows, columns); + TakenSpotsReferenceHolder spotref; + + // If the last spot holder matches the size currently in use, just reset + // that instance and reuse it to avoid allocating a new bit array. + if (_spotref != null && _spotref.Height == rows && _spotref.Width == columns) + { + spotref = _spotref; + + spotref.Reset(); + } + else + { + spotref = _spotref = new TakenSpotsReferenceHolder(rows, columns); + } // Figure out which children we should automatically layout and where available openings are. foreach (var child in visible) @@ -56,7 +73,8 @@ protected override Size MeasureOverride(Size availableSize) else { SetAutoLayout(child, false); - spotref.SpotsTaken.Fill(true, row, col, colspan, rowspan); // row, col, width, height + + spotref.Fill(true, row, col, colspan, rowspan); } } @@ -100,7 +118,7 @@ protected override Size MeasureOverride(Size availableSize) if (rowspan > 1 || colspan > 1) { // TODO: Need to tie this into iterator - spotref.SpotsTaken.Fill(true, row, column, GetColumnSpan(child), GetRowSpan(child)); // row, col, width, height + spotref.Fill(true, row, column, colspan, rowspan); } } else @@ -135,7 +153,7 @@ protected override Size MeasureOverride(Size availableSize) } // Return our desired size based on the largest child we found, our dimensions, and spacing. - var desiredSize = new Size((maxWidth * (double)columns) + columnSpacingSize, (maxHeight * (double)rows) + rowSpacingSize); + var desiredSize = new Size((maxWidth * columns) + columnSpacingSize, (maxHeight * rows) + rowSpacingSize); // Required to perform regular grid measurement, but ignore result. base.MeasureOverride(desiredSize); diff --git a/Microsoft.Toolkit/Extensions/ArrayExtensions.cs b/Microsoft.Toolkit/Extensions/ArrayExtensions.cs index a7e870492e8..f0cf789dd02 100644 --- a/Microsoft.Toolkit/Extensions/ArrayExtensions.cs +++ b/Microsoft.Toolkit/Extensions/ArrayExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Microsoft.Toolkit.Extensions { @@ -13,71 +14,6 @@ namespace Microsoft.Toolkit.Extensions /// public static class ArrayExtensions { - /// - /// Fills elements of a rectangular array at the given position and size to a specific value. - /// Ranges given will fill in as many elements as possible, ignoring positions outside the bounds of the array. - /// - /// The element type of the array. - /// The source array. - /// Value to fill with. - /// Row to start on (inclusive, zero-index). - /// Column to start on (inclusive, zero-index). - /// Positive width of area to fill. - /// Positive height of area to fill. - public static void Fill(this T[,] array, T value, int row, int col, int width, int height) - { - for (int r = row; r < row + height; r++) - { - for (int c = col; c < col + width; c++) - { - if (r >= 0 && c >= 0 && r < array.GetLength(0) && c < array.GetLength(1)) - { - array[r, c] = value; - } - } - } - } - - /// - /// Yields a row from a rectangular array. - /// - /// The element type of the array. - /// The source array. - /// Row record to retrieve, 0-based index. - /// Yielded row. - public static IEnumerable GetRow(this T[,] rectarray, int row) - { - if (row < 0 || row >= rectarray.GetLength(0)) - { - throw new ArgumentOutOfRangeException(nameof(row)); - } - - for (int c = 0; c < rectarray.GetLength(1); c++) - { - yield return rectarray[row, c]; - } - } - - /// - /// Yields a column from a rectangular array. - /// - /// The element type of the array. - /// The source array. - /// Column record to retrieve, 0-based index. - /// Yielded column. - public static IEnumerable GetColumn(this T[,] rectarray, int column) - { - if (column < 0 || column >= rectarray.GetLength(1)) - { - throw new ArgumentOutOfRangeException(nameof(column)); - } - - for (int r = 0; r < rectarray.GetLength(0); r++) - { - yield return rectarray[r, column]; - } - } - /// /// Yields a column from a jagged array. /// An exception will be thrown if the column is out of bounds, and return default in places where there are no elements from inner arrays. @@ -98,7 +34,7 @@ public static IEnumerable GetColumn(this T[][] rectarray, int column) { if (column >= rectarray[r].Length) { - yield return default(T); + yield return default; continue; } @@ -112,10 +48,28 @@ public static IEnumerable GetColumn(this T[][] rectarray, int column) /// /// The element type of the array. /// The source array. - /// String representation of the array. + /// The representation of the array. public static string ToArrayString(this T[] array) { - return "[" + string.Join(",\t", array) + "]"; + // The returned string will be in the following format: + // [1, 2, 3] + StringBuilder builder = new StringBuilder(); + + builder.Append('['); + + for (int i = 0; i < array.Length; i++) + { + if (i != 0) + { + builder.Append(",\t"); + } + + builder.Append(array[i].ToString()); + } + + builder.Append(']'); + + return builder.ToString(); } /// @@ -126,32 +80,89 @@ public static string ToArrayString(this T[] array) /// String representation of the array. public static string ToArrayString(this T[][] mdarray) { - string[] inner = new string[mdarray.GetLength(0)]; + // The returned string uses the same format as the overload for 2D arrays + StringBuilder builder = new StringBuilder(); - for (int r = 0; r < mdarray.GetLength(0); r++) + builder.Append('['); + + for (int i = 0; i < mdarray.Length; i++) { - inner[r] = string.Join(",\t", mdarray[r]); + if (i != 0) + { + builder.Append(','); + builder.Append(Environment.NewLine); + builder.Append(' '); + } + + builder.Append('['); + + T[] row = mdarray[i]; + + for (int j = 0; j < row.Length; j++) + { + if (j != 0) + { + builder.Append(",\t"); + } + + builder.Append(row[j].ToString()); + } + + builder.Append(']'); } - return "[[" + string.Join("]," + Environment.NewLine + " [", inner) + "]]"; + builder.Append(']'); + + return builder.ToString(); } /// - /// Returns a simple string representation of a rectangular array. + /// Returns a simple string representation of a 2D array. /// /// The element type of the array. - /// The source array. - /// String representation of the array. - public static string ToArrayString(this T[,] rectarray) + /// The source array. + /// The representation of the array. + public static string ToArrayString(this T[,] array) { - string[] inner = new string[rectarray.GetLength(0)]; + // The returned string will be in the following format: + // [[1, 2, 3], + // [4, 5, 6], + // [7, 8, 9]] + StringBuilder builder = new StringBuilder(); - for (int r = 0; r < rectarray.GetLength(0); r++) + builder.Append('['); + + int + height = array.GetLength(0), + width = array.GetLength(1); + + for (int i = 0; i < height; i++) { - inner[r] = string.Join(",\t", rectarray.GetRow(r)); + if (i != 0) + { + builder.Append(','); + builder.Append(Environment.NewLine); + builder.Append(' '); + } + + builder.Append('['); + + for (int j = 0; j < width; j++) + { + if (j != 0) + { + builder.Append(",\t"); + } + + builder.Append(array[i, j].ToString()); + } + + builder.Append(']'); } - return "[[" + string.Join("]," + Environment.NewLine + " [", inner) + "]]"; + builder.Append(']'); + + return builder.ToString(); } } } diff --git a/UnitTests/UnitTests.Shared/Extensions/Test_ArrayExtensions.cs b/UnitTests/UnitTests.Shared/Extensions/Test_ArrayExtensions.cs index 9c7877dfd70..110ffdda7d9 100644 --- a/UnitTests/UnitTests.Shared/Extensions/Test_ArrayExtensions.cs +++ b/UnitTests/UnitTests.Shared/Extensions/Test_ArrayExtensions.cs @@ -13,161 +13,6 @@ namespace UnitTests.Extensions [TestClass] public class Test_ArrayExtensions { - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayMid() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, 1, 1, 3, 2); - - var expected = new bool[,] - { - { false, false, false, false, false }, - { false, true, true, true, false }, - { false, true, true, true, false }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayTwice() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, 0, 0, 1, 2); - test.Fill(true, 1, 3, 2, 2); - - var expected = new bool[,] - { - { true, false, false, false, false }, - { true, false, false, true, true }, - { false, false, false, true, true }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayNegativeSize() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, 3, 4, -3, -2); - - // TODO: We may want to think about this pattern in the future: - /*var expected = new bool[,] - { - { false, false, false, false, false }, - { false, false, false, false, false }, - { false, false, true, true, true }, - { false, false, true, true, true }, - };*/ - - var expected = new bool[,] - { - { false, false, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayBottomEdgeBoundary() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, 1, 2, 2, 4); - - var expected = new bool[,] - { - { false, false, false, false, false }, - { false, false, true, true, false }, - { false, false, true, true, false }, - { false, false, true, true, false }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayTopLeftCornerNegativeBoundary() - { - bool[,] test = new bool[4, 5]; - - test.Fill(true, -1, -1, 3, 3); - - var expected = new bool[,] - { - { true, true, false, false, false }, - { true, true, false, false, false }, - { false, false, false, false, false }, - { false, false, false, false, false }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_FillArrayBottomRightCornerBoundary() - { - bool[,] test = new bool[5, 4]; - - test.Fill(true, 3, 2, 3, 3); - - var expected = new bool[,] - { - { false, false, false, false }, - { false, false, false, false }, - { false, false, false, false }, - { false, false, true, true }, - { false, false, true, true }, - }; - - CollectionAssert.AreEqual( - expected, - test, - "Fill failed. Expected:\n{0}.\nActual:\n{1}", - expected.ToArrayString(), - test.ToArrayString()); - } - [TestCategory("ArrayExtensions")] [TestMethod] public void Test_ArrayExtensions_Jagged_GetColumn() @@ -206,82 +51,6 @@ public void Test_ArrayExtensions_Jagged_GetColumn_Exception() }); } - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_Rectangular_GetColumn() - { - int[,] array = - { - { 5, 2, 4 }, - { 6, 3, 9 }, - { 7, -1, 0 } - }; - - var col = array.GetColumn(1).ToArray(); - - CollectionAssert.AreEquivalent(new int[] { 2, 3, -1 }, col); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_Rectangular_GetColumn_Exception() - { - int[,] array = - { - { 5, 2, 4 }, - { 6, 3, 0 }, - { 7, 0, 0 } - }; - - Assert.ThrowsException(() => - { - array.GetColumn(-1).ToArray(); - }); - - Assert.ThrowsException(() => - { - array.GetColumn(3).ToArray(); - }); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_Rectangular_GetRow() - { - int[,] array = - { - { 5, 2, 4 }, - { 6, 3, 9 }, - { 7, -1, 0 } - }; - - var col = array.GetRow(1).ToArray(); - - CollectionAssert.AreEquivalent(new int[] { 6, 3, 9 }, col); - } - - [TestCategory("ArrayExtensions")] - [TestMethod] - public void Test_ArrayExtensions_Rectangular_GetRow_Exception() - { - int[,] array = - { - { 5, 2, 4 }, - { 6, 3, 0 }, - { 7, 0, 0 } - }; - - Assert.ThrowsException(() => - { - array.GetRow(-1).ToArray(); - }); - - Assert.ThrowsException(() => - { - array.GetRow(3).ToArray(); - }); - } - [TestCategory("ArrayExtensions")] [TestMethod] public void Test_ArrayExtensions_Rectangular_ToString() diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_UniformGrid_FreeSpots.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_UniformGrid_FreeSpots.cs index 1a22e5c0780..9b6bfb31cd5 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_UniformGrid_FreeSpots.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_UniformGrid_FreeSpots.cs @@ -13,11 +13,31 @@ namespace UnitTests.UI.Controls [TestClass] public class Test_UniformGrid_FreeSpots { + /// + /// Creates a instance with the specified values. + /// + /// The source array to populate the instance to return. + /// A with the given values. + private static TakenSpotsReferenceHolder CreateTakenSpotsReferenceHolder(bool[,] array) + { + var refHolder = new TakenSpotsReferenceHolder(array.GetLength(0), array.GetLength(1)); + + for (int i = 0; i < array.GetLength(0); i++) + { + for (int j = 0; j < array.GetLength(1); j++) + { + refHolder[i, j] = array[i, j]; + } + } + + return refHolder; + } + [TestCategory("UniformGrid")] [UITestMethod] public void Test_UniformGrid_GetFreeSpots_Basic() { - var testRef = new TakenSpotsReferenceHolder(new bool[4, 5] + var testRef = CreateTakenSpotsReferenceHolder(new bool[4, 5] { { false, true, false, true, false }, { false, true, true, true, false }, @@ -47,7 +67,7 @@ public void Test_UniformGrid_GetFreeSpots_Basic() [UITestMethod] public void Test_UniformGrid_GetFreeSpots_FirstColumn() { - var testRef = new TakenSpotsReferenceHolder(new bool[4, 5] + var testRef = CreateTakenSpotsReferenceHolder(new bool[4, 5] { { true, false, false, true, false }, { false, true, true, true, false }, @@ -77,7 +97,7 @@ public void Test_UniformGrid_GetFreeSpots_FirstColumn() [UITestMethod] public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBoundMinusOne() { - var testRef = new TakenSpotsReferenceHolder(new bool[3, 3] + var testRef = CreateTakenSpotsReferenceHolder(new bool[3, 3] { { false, false, false }, { false, false, false }, @@ -105,7 +125,7 @@ public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBoundMinusOne() [UITestMethod] public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBound() { - var testRef = new TakenSpotsReferenceHolder(new bool[3, 3] + var testRef = CreateTakenSpotsReferenceHolder(new bool[3, 3] { { false, false, false }, { false, false, false }, @@ -133,7 +153,7 @@ public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBound() [UITestMethod] public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBound_TopDown() { - var testRef = new TakenSpotsReferenceHolder(new bool[3, 3] + var testRef = CreateTakenSpotsReferenceHolder(new bool[3, 3] { { false, false, false }, { false, false, false }, @@ -161,7 +181,7 @@ public void Test_UniformGrid_GetFreeSpots_FirstColumnEndBound_TopDown() [UITestMethod] public void Test_UniformGrid_GetFreeSpots_VerticalOrientation() { - var testRef = new TakenSpotsReferenceHolder(new bool[4, 5] + var testRef = CreateTakenSpotsReferenceHolder(new bool[4, 5] { { false, false, false, true, false }, { false, true, true, false, false },