diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs index 1251a022da..32d8c2b051 100644 --- a/src/Avalonia.Controls/IndexPath.cs +++ b/src/Avalonia.Controls/IndexPath.cs @@ -5,42 +5,68 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; + +#nullable enable namespace Avalonia.Controls { - public sealed class IndexPath : IComparable + public readonly struct IndexPath : IComparable, IEquatable { - private readonly List _path = new List(); + public static readonly IndexPath Unselected = default; + + private readonly int _index; + private readonly int[]? _path; - internal IndexPath(int index) + public IndexPath(int index) { - _path.Add(index); + _index = index + 1; + _path = null; } - internal IndexPath(int groupIndex, int itemIndex) + public IndexPath(int groupIndex, int itemIndex) { - _path.Add(groupIndex); - _path.Add(itemIndex); + _index = 0; + _path = new[] { groupIndex, itemIndex }; } - internal IndexPath(IEnumerable indices) + public IndexPath(IEnumerable? indices) { if (indices != null) { - _path.AddRange(indices); + _index = 0; + _path = indices.ToArray(); + } + else + { + _index = 0; + _path = null; } } - public int GetSize() => _path.Count; - public int GetAt(int index) => _path[index]; + private IndexPath(int[] basePath, int index) + { + basePath = basePath ?? throw new ArgumentNullException(nameof(basePath)); + + _index = 0; + _path = new int[basePath.Length + 1]; + Array.Copy(basePath, _path, basePath.Length); + _path[basePath.Length] = index; + } + + public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1); + + public int GetAt(int index) + { + return _path?[index] ?? (_index - 1); + } public int CompareTo(IndexPath other) { var rhsPath = other; int compareResult = 0; - int lhsCount = _path.Count; - int rhsCount = rhsPath._path.Count; + int lhsCount = GetSize(); + int rhsCount = rhsPath.GetSize(); if (lhsCount == 0 || rhsCount == 0) { @@ -52,12 +78,12 @@ namespace Avalonia.Controls // both paths are non-empty, but can be of different size for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++) { - if (_path[i] < rhsPath._path[i]) + if (GetAt(i) < rhsPath.GetAt(i)) { compareResult = -1; break; } - else if (_path[i] > rhsPath._path[i]) + else if (GetAt(i) > rhsPath.GetAt(i)) { compareResult = 1; break; @@ -78,14 +104,34 @@ namespace Avalonia.Controls public IndexPath CloneWithChildIndex(int childIndex) { - var newPath = new List(_path); - newPath.Add(childIndex); - return new IndexPath(newPath); + if (_path != null) + { + return new IndexPath(_path, childIndex); + } + else if (_index != 0) + { + return new IndexPath(_index - 1, childIndex); + } + else + { + return new IndexPath(childIndex); + } } public override string ToString() { - return "R." + string.Join(".", _path); + if (_path != null) + { + return "R" + string.Join(".", _path); + } + else if (_index != 0) + { + return "R" + (_index - 1); + } + else + { + return "R"; + } } public static IndexPath CreateFrom(int index) => new IndexPath(index); @@ -94,5 +140,34 @@ namespace Avalonia.Controls public static IndexPath CreateFromIndices(IList indices) => new IndexPath(indices); + public override bool Equals(object obj) => obj is IndexPath other && Equals(other); + + public bool Equals(IndexPath other) => CompareTo(other) == 0; + + public override int GetHashCode() + { + var hashCode = -504981047; + + if (_path != null) + { + foreach (var i in _path) + { + hashCode = hashCode * -1521134295 + i.GetHashCode(); + } + } + else + { + hashCode = hashCode * -1521134295 + _index.GetHashCode(); + } + + return hashCode; + } + + public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; } + public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; } + public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; } + public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; } + public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; } + public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; } } } diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 6b48e02a21..ea3a09d4e7 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls { get { - IndexPath anchor = null; + IndexPath anchor = default; if (_rootNode.AnchorIndex >= 0) { @@ -113,7 +113,7 @@ namespace Avalonia.Controls { get { - IndexPath selectedIndex = null; + IndexPath selectedIndex = default; var selectedIndices = SelectedIndices; if (selectedIndices?.Count > 0) @@ -248,7 +248,7 @@ namespace Avalonia.Controls (infos, index) => // callback for GetAt(index) { var currentIndex = 0; - IndexPath path = null; + IndexPath path = default; foreach (var info in infos) { @@ -505,7 +505,7 @@ namespace Avalonia.Controls if (resetAnchor) { - AnchorIndex = null; + AnchorIndex = default; } if (raiseSelectionChanged) diff --git a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs index 52e29da0e5..38b1dde5d7 100644 --- a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs +++ b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs @@ -157,7 +157,6 @@ namespace Avalonia.Controls.Utils public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode parent) { node = node ?? throw new ArgumentNullException(nameof(node)); - indexPath = indexPath ?? throw new ArgumentNullException(nameof(indexPath)); Node = node; Path = indexPath; @@ -167,7 +166,6 @@ namespace Avalonia.Controls.Utils public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath) { node = node ?? throw new ArgumentNullException(nameof(node)); - indexPath = indexPath ?? throw new ArgumentNullException(nameof(indexPath)); Node = node; Path = indexPath; diff --git a/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs b/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs new file mode 100644 index 0000000000..190e92ed5e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs @@ -0,0 +1,81 @@ +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class IndexPathTests + { + [Fact] + public void Simple_Index() + { + var a = new IndexPath(1); + + Assert.Equal(1, a.GetSize()); + Assert.Equal(1, a.GetAt(0)); + } + + [Fact] + public void Equal_Paths() + { + var a = new IndexPath(1); + var b = new IndexPath(1); + + Assert.True(a == b); + Assert.False(a != b); + Assert.True(a.Equals(b)); + Assert.Equal(0, a.CompareTo(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Unequal_Paths() + { + var a = new IndexPath(1); + var b = new IndexPath(2); + + Assert.False(a == b); + Assert.True(a != b); + Assert.False(a.Equals(b)); + Assert.Equal(-1, a.CompareTo(b)); + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Equal_Null_Path() + { + var a = new IndexPath(null); + var b = new IndexPath(null); + + Assert.True(a == b); + Assert.False(a != b); + Assert.True(a.Equals(b)); + Assert.Equal(0, a.CompareTo(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Unequal_Null_Path() + { + var a = new IndexPath(null); + var b = new IndexPath(2); + + Assert.False(a == b); + Assert.True(a != b); + Assert.False(a.Equals(b)); + Assert.Equal(-1, a.CompareTo(b)); + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Default_Is_Null_Path() + { + var a = new IndexPath(null); + var b = default(IndexPath); + + Assert.True(a == b); + Assert.False(a != b); + Assert.True(a.Equals(b)); + Assert.Equal(0, a.CompareTo(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + } +}