diff --git a/Directory.Build.props b/Directory.Build.props index b52a8875..eb4ed89d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,14 +3,17 @@ enable - true - nullable - net6.0 12 false false + + + true + nullable + + 11.0.0 diff --git a/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj b/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj index e1ca2c40..07408080 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj +++ b/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj @@ -1,6 +1,6 @@  - net5.0 + netstandard2.0;net5.0;net6.0 True Avalonia.Controls @@ -12,4 +12,13 @@ + + + + + + + + + diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/AnonymousRow.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/AnonymousRow.cs index 6829da91..acd7aa11 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/AnonymousRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/AnonymousRow.cs @@ -17,6 +17,10 @@ internal class AnonymousRow : IRow, IModelIndexableRow private int _modelIndex; [AllowNull] private TModel _model; +#if NETSTANDARD2_0 + object? IRow.Model => _model; +#endif + public object? Header => _modelIndex; public TModel Model => _model; public int ModelIndex => _modelIndex; diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/HierarchicalRow.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/HierarchicalRow.cs index 9aa76da2..4c45dad3 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/HierarchicalRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/HierarchicalRow.cs @@ -22,6 +22,10 @@ public class HierarchicalRow : NotifyingBase, private bool _isExpanded; private bool? _showExpander; +#if NETSTANDARD2_0 + object? IRow.Model => Model; +#endif + public HierarchicalRow( IExpanderRowController controller, IExpanderColumn expanderColumn, diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IRow`1.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IRow`1.cs index d48b2c0e..78244e0a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IRow`1.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IRow`1.cs @@ -11,10 +11,12 @@ public interface IRow : IRow /// new TModel Model { get; } +#if !NETSTANDARD2_0 /// /// Gets the untyped row model. /// object? IRow.Model => Model; +#endif /// /// Updates the model index due to a change in the data source. diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 11e88c52..448a682a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -649,8 +649,8 @@ private Rect EstimateViewport(Size availableSize) return new Rect( 0, 0, - double.IsFinite(availableSize.Width) ? availableSize.Width : 0, - double.IsFinite(availableSize.Height) ? availableSize.Height : 0); + Double.IsFinite(availableSize.Width) ? availableSize.Width : 0, + Double.IsFinite(availableSize.Height) ? availableSize.Height : 0); } private void RecycleElement(Control element, int index) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs index e0bdd0df..d02618ea 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs @@ -12,15 +12,15 @@ public interface ITreeDataGridSelectionInteraction { public event EventHandler? SelectionChanged; - bool IsCellSelected(int columnIndex, int rowIndex) => false; - bool IsRowSelected(IRow rowModel) => false; - bool IsRowSelected(int rowIndex) => false; - public void OnKeyDown(TreeDataGrid sender, KeyEventArgs e) { } - public void OnPreviewKeyDown(TreeDataGrid sender, KeyEventArgs e) { } - public void OnKeyUp(TreeDataGrid sender, KeyEventArgs e) { } - public void OnTextInput(TreeDataGrid sender, TextInputEventArgs e) { } - public void OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) { } - public void OnPointerMoved(TreeDataGrid sender, PointerEventArgs e) { } - public void OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e) { } + bool IsCellSelected(int columnIndex, int rowIndex); + bool IsRowSelected(IRow rowModel); + bool IsRowSelected(int rowIndex); + void OnKeyDown(TreeDataGrid sender, KeyEventArgs e); + void OnPreviewKeyDown(TreeDataGrid sender, KeyEventArgs e); + void OnKeyUp(TreeDataGrid sender, KeyEventArgs e); + void OnTextInput(TreeDataGrid sender, TextInputEventArgs e); + void OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e); + void OnPointerMoved(TreeDataGrid sender, PointerEventArgs e); + void OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e); } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 19d709ef..5bbda6c4 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -121,8 +121,14 @@ sender.Rows is null || }; var anchor = shift ? _rangeAnchor : GetAnchor(); + +#if NETSTANDARD + var columnIndex = Math.Min(Math.Max(anchor.x + x, 0), sender.Columns.Count - 1); + var rowIndex = Math.Min(Math.Max(anchor.y + y, 0), sender.Rows.Count - 1); +#else var columnIndex = Math.Clamp(anchor.x + x, 0, sender.Columns.Count - 1); var rowIndex = Math.Clamp(anchor.y + y, 0, sender.Rows.Count - 1); +#endif if (!shift) Select(columnIndex, rowIndex); @@ -171,6 +177,18 @@ e.Source is Control source && } } + bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel) => false; + + bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex) => false; + + void ITreeDataGridSelectionInteraction.OnPreviewKeyDown(TreeDataGrid sender, KeyEventArgs e) { } + + void ITreeDataGridSelectionInteraction.OnKeyUp(TreeDataGrid sender, KeyEventArgs e) { } + + void ITreeDataGridSelectionInteraction.OnTextInput(TreeDataGrid sender, TextInputEventArgs e) { } + + void ITreeDataGridSelectionInteraction.OnPointerMoved(TreeDataGrid sender, PointerEventArgs e) { } + private void BeginBatchUpdate() { _selectedColumns.BeginBatchUpdate(); diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs index ec112459..93dae8bc 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs @@ -356,6 +356,12 @@ void ITreeDataGridSelectionInteraction.OnTextInput(TreeDataGrid sender, TextInpu HandleTextInput(e.Text, sender, _source.Rows.ModelIndexToRowIndex(AnchorIndex)); } + bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) => false; + + void ITreeDataGridSelectionInteraction.OnKeyUp(TreeDataGrid sender, KeyEventArgs e) { } + + void ITreeDataGridSelectionInteraction.OnPointerMoved(TreeDataGrid sender, PointerEventArgs e) { } + protected internal override IEnumerable? GetChildren(TModel node) { if (_source is HierarchicalTreeDataGridSource treeSource) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeSelectionModelBase.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeSelectionModelBase.cs index e3525f64..32e571cd 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeSelectionModelBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeSelectionModelBase.cs @@ -519,8 +519,10 @@ private int CommitSelect(IndexRanges selectedRanges) { var result = 0; - foreach (var (parent, ranges) in selectedRanges.Ranges) + foreach (var row in selectedRanges.Ranges) { + var parent = row.Key; + var ranges = row.Value; var node = GetOrCreateNode(parent); if (node is not null) @@ -537,8 +539,10 @@ private int CommitDeselect(IndexRanges selectedRanges) { var result = 0; - foreach (var (parent, ranges) in selectedRanges.Ranges) + foreach (var row in selectedRanges.Ranges) { + var parent = row.Key; + var ranges = row.Value; var node = GetOrCreateNode(parent); if (node is not null) diff --git a/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/BitOperations.cs b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/BitOperations.cs new file mode 100644 index 00000000..4a44232c --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/BitOperations.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Some routines inspired by the Stanford Bit Twiddling Hacks by Sean Eron Anderson: +// http://graphics.stanford.edu/~seander/bithacks.html + +namespace System.Numerics +{ + /// + /// Utility methods for intrinsic bit-twiddling operations. + /// The methods use hardware intrinsics when available on the underlying platform, + /// otherwise they use optimized software fallbacks. + /// + internal static class BitOperations + { + // C# no-alloc optimization that directly wraps the data section of the dll (similar to string constants) + // https://github.com/dotnet/roslyn/pull/24621 + + private static ReadOnlySpan Log2DeBruijn => // 32 + [ + 00, 09, 01, 10, 13, 21, 02, 29, + 11, 14, 16, 18, 22, 25, 03, 30, + 08, 12, 20, 28, 15, 17, 24, 07, + 19, 27, 23, 06, 26, 05, 04, 31 + ]; + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Log2(uint value) + { + // The 0->0 contract is fulfilled by setting the LSB to 1. + // Log(1) is 0, and setting the LSB for values > 1 does not change the log2 result. + value |= 1; + + // Fallback contract is 0->0 + return Log2SoftwareFallback(value); + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Log2(ulong value) + { + value |= 1; + + uint hi = (uint)(value >> 32); + + if (hi == 0) + { + return Log2((uint)value); + } + + return 32 + Log2(hi); + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since log(0) is undefined. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Log2(nuint value) + { +#if TARGET_64BIT + return Log2((ulong)value); +#else + return Log2((uint)value); +#endif + } + + /// + /// Returns the integer (floor) log of the specified value, base 2. + /// Note that by convention, input value 0 returns 0 since Log(0) is undefined. + /// Does not directly use any hardware intrinsics, nor does it incur branching. + /// + /// The value. + private static int Log2SoftwareFallback(uint value) + { + // No AggressiveInlining due to large method size + // Has conventional contract 0->0 (Log(0) is undefined) + + // Fill trailing zeros with ones, eg 00010010 becomes 00011111 + value |= value >> 01; + value |= value >> 02; + value |= value >> 04; + value |= value >> 08; + value |= value >> 16; + + // uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check + return Unsafe.AddByteOffset( + // Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_1100_0100_1010_1100_1101_1101u + ref MemoryMarshal.GetReference(Log2DeBruijn), + // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here + (IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Double.cs b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Double.cs new file mode 100644 index 00000000..40a4d399 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Double.cs @@ -0,0 +1,10 @@ +namespace Avalonia; + + +internal static class Double +{ + public static bool IsFinite(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/IsExternalInit.cs b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/IsExternalInit.cs new file mode 100644 index 00000000..2822f9d1 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices; + +/// +/// A class which allows to use C# 9's init and record features +/// in older target frameworks like .NET Standard 2.0 +/// +internal static class IsExternalInit { } diff --git a/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Range.cs b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Range.cs new file mode 100644 index 00000000..35a9bdcb --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/StandardExtensions/Range.cs @@ -0,0 +1,271 @@ +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return Start.GetHashCode() * 31 + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// + public static T[] GetSubArray(T[] array, Range range) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + + if (default(T) != null || typeof(T[]) == array.GetType()) + { + // We know the type of the array to be exactly T[]. + + if (length == 0) + { + return []; + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs index 1483405a..7ea7994e 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs @@ -605,7 +605,9 @@ private void AutoScroll(bool direction) _autoScrollTimer.Start(); } +#if !NETSTANDARD2_0 [MemberNotNullWhen(true, nameof(_source))] +#endif private bool CalculateAutoDragDrop( TreeDataGridRow targetRow, DragEventArgs e, diff --git a/src/Avalonia.Controls.TreeDataGrid/Utils/StableSort.cs b/src/Avalonia.Controls.TreeDataGrid/Utils/StableSort.cs index 5ff0c1f3..127c489c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Utils/StableSort.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Utils/StableSort.cs @@ -19,8 +19,13 @@ public static List SortedMap(IReadOnlyList elements, Comparison map.Add(i); } +#if NETSTANDARD2_0 + map.Sort(compare); +#else var span = CollectionsMarshal.AsSpan(map); SortHelper.Sort(span, compare); +#endif + return map; } }