28 changed files with 2081 additions and 283 deletions
@ -0,0 +1,6 @@ |
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal interface ICompositionTargetDebugEvents |
|||
{ |
|||
void RectInvalidated(Rect rc); |
|||
} |
|||
@ -0,0 +1,358 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
/// <remarks>
|
|||
/// This struct implements the most commonly-used part of the dictionary API, but does
|
|||
/// not implement <see cref="IDictionary{TKey, TValue}"/>. In particular, this struct
|
|||
/// is not enumerable. Enumeration is intended to be done by index for better performance.
|
|||
/// </remarks>
|
|||
internal struct AvaloniaPropertyDictionary<TValue> |
|||
{ |
|||
private const int DefaultInitialCapacity = 4; |
|||
private Entry[]? _entries; |
|||
private int _entryCount; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
|
|||
/// class that is empty and has the default initial capacity.
|
|||
/// </summary>
|
|||
public AvaloniaPropertyDictionary() |
|||
{ |
|||
_entries = null; |
|||
_entryCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaPropertyDictionary{TValue}"/>
|
|||
/// class that is empty and has the specified initial capacity.
|
|||
/// </summary>
|
|||
/// <param name="capactity">
|
|||
/// The initial number of elements that the collection can contain.
|
|||
/// </param>
|
|||
public AvaloniaPropertyDictionary(int capactity) |
|||
{ |
|||
_entries = new Entry[capactity]; |
|||
_entryCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of key/value pairs contained in the collection.
|
|||
/// </summary>
|
|||
public int Count => _entryCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value associated with the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The key to get or set.</param>
|
|||
/// <returns>
|
|||
/// The value associated with the specified key. If the key is not found, a get operation
|
|||
/// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a
|
|||
/// new element for the specified key.
|
|||
/// </returns>
|
|||
/// <exception cref="KeyNotFoundException">
|
|||
/// The key does not exist in the collection.
|
|||
/// </exception>
|
|||
public TValue this[AvaloniaProperty property] |
|||
{ |
|||
get |
|||
{ |
|||
if (!TryGetEntry(property.Id, out var index)) |
|||
ThrowNotFound(); |
|||
return _entries[index].Value; |
|||
} |
|||
set |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
_entries[index] = new Entry(property, value); |
|||
else |
|||
InsertEntry(new Entry(property, value), index); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">
|
|||
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
|
|||
/// </param>
|
|||
public TValue this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
if (index >= _entryCount) |
|||
ThrowOutOfRange(); |
|||
return _entries![index].Value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds the specified key and value to the dictionary.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The value of the element to add.</param>
|
|||
public void Add(AvaloniaProperty property, TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
ThrowDuplicate(); |
|||
InsertEntry(new Entry(property, value), index); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes all keys and values from the collection.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The Count property is set to 0, and references to other objects from elements of the
|
|||
/// collection are also released. The capacity remains unchanged.
|
|||
/// </remarks>
|
|||
public void Clear() |
|||
{ |
|||
if (_entries is not null) |
|||
{ |
|||
Array.Clear(_entries, 0, _entries.Length); |
|||
_entryCount = 0; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether the collection contains the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _); |
|||
|
|||
/// <summary>
|
|||
/// Gets the key and value at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">
|
|||
/// The index of the entry, between 0 and <see cref="Count"/> - 1.
|
|||
/// </param>
|
|||
/// <param name="key">
|
|||
/// When this method returns, contains the key at the specified index.
|
|||
/// </param>
|
|||
/// <param name="value">
|
|||
/// When this method returns, contains the value at the specified index.
|
|||
/// </param>
|
|||
public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value) |
|||
{ |
|||
if (index >= _entryCount) |
|||
ThrowOutOfRange(); |
|||
ref var entry = ref _entries![index]; |
|||
key = entry.Property; |
|||
value = entry.Value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the value of the specified key from the collection.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <returns>
|
|||
/// true if the element is successfully found and removed; otherwise, false. This method
|
|||
/// returns false if key is not found in the collection.
|
|||
/// </returns>
|
|||
public bool Remove(AvaloniaProperty property) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); |
|||
_entryCount--; |
|||
_entries[_entryCount] = default; |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the value of the specified key from the collection, and copies the element to
|
|||
/// the value parameter.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The removed element.</param>
|
|||
/// <returns>
|
|||
/// true if the element is successfully found and removed; otherwise, false. This method
|
|||
/// returns false if key is not found in the collection.
|
|||
/// </returns>
|
|||
public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
value = _entries[index].Value; |
|||
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); |
|||
_entryCount--; |
|||
_entries[_entryCount] = default; |
|||
return true; |
|||
} |
|||
|
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to add the specified key and value to the collection.
|
|||
/// </summary>
|
|||
/// <param name="property">The key.</param>
|
|||
/// <param name="value">The value of the element to add.</param>
|
|||
/// <returns></returns>
|
|||
public bool TryAdd(AvaloniaProperty property, TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
return false; |
|||
InsertEntry(new Entry(property, value), index); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value associated with the specified key.
|
|||
/// </summary>
|
|||
/// <param name="property">The property key.</param>
|
|||
/// <param name="value">
|
|||
/// When this method returns, contains the value associated with the specified key,
|
|||
/// if the property is found; otherwise, null. This parameter is passed uninitialized.
|
|||
/// </param>
|
|||
/// <returns></returns>
|
|||
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
if (TryGetEntry(property.Id, out var index)) |
|||
{ |
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
[MemberNotNullWhen(true, nameof(_entries))] |
|||
private bool TryGetEntry(int propertyId, out int index) |
|||
{ |
|||
int checkIndex; |
|||
int iLo = 0; |
|||
int iHi = _entryCount; |
|||
|
|||
if (iHi <= 0) |
|||
{ |
|||
index = 0; |
|||
return false; |
|||
} |
|||
|
|||
// Do a binary search to find the value
|
|||
while (iHi - iLo > 3) |
|||
{ |
|||
int iPv = (iHi + iLo) / 2; |
|||
checkIndex = _entries![iPv].Property.Id; |
|||
|
|||
if (propertyId == checkIndex) |
|||
{ |
|||
index = iPv; |
|||
return true; |
|||
} |
|||
|
|||
if (propertyId <= checkIndex) |
|||
{ |
|||
iHi = iPv; |
|||
} |
|||
else |
|||
{ |
|||
iLo = iPv + 1; |
|||
} |
|||
} |
|||
|
|||
// Now we only have three values to search; switch to a linear search
|
|||
do |
|||
{ |
|||
checkIndex = _entries![iLo].Property.Id; |
|||
|
|||
if (checkIndex == propertyId) |
|||
{ |
|||
index = iLo; |
|||
return true; |
|||
} |
|||
|
|||
if (checkIndex > propertyId) |
|||
{ |
|||
// we've gone past the targetIndex - return not found
|
|||
break; |
|||
} |
|||
|
|||
iLo++; |
|||
} while (iLo < iHi); |
|||
|
|||
index = iLo; |
|||
return false; |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_entries))] |
|||
private void InsertEntry(Entry entry, int entryIndex) |
|||
{ |
|||
if (_entryCount > 0) |
|||
{ |
|||
if (_entryCount == _entries!.Length) |
|||
{ |
|||
const double growthFactor = 1.2; |
|||
var newSize = (int)(_entryCount * growthFactor); |
|||
|
|||
if (newSize == _entryCount) |
|||
{ |
|||
newSize++; |
|||
} |
|||
|
|||
var destEntries = new Entry[newSize]; |
|||
|
|||
Array.Copy(_entries, 0, destEntries, 0, entryIndex); |
|||
|
|||
destEntries[entryIndex] = entry; |
|||
|
|||
Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex); |
|||
|
|||
_entries = destEntries; |
|||
} |
|||
else |
|||
{ |
|||
Array.Copy( |
|||
_entries, |
|||
entryIndex, |
|||
_entries, |
|||
entryIndex + 1, |
|||
_entryCount - entryIndex); |
|||
|
|||
_entries[entryIndex] = entry; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_entries ??= new Entry[DefaultInitialCapacity]; |
|||
_entries[0] = entry; |
|||
} |
|||
|
|||
_entryCount++; |
|||
} |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowOutOfRange() => throw new IndexOutOfRangeException(); |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowDuplicate() => |
|||
throw new ArgumentException("An item with the same key has already been added."); |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowNotFound() => throw new KeyNotFoundException(); |
|||
|
|||
private readonly struct Entry |
|||
{ |
|||
public readonly AvaloniaProperty Property; |
|||
public readonly TValue Value; |
|||
|
|||
public Entry(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Property = property; |
|||
Value = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,180 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
internal sealed class AvaloniaPropertyValueStore<TValue> |
|||
{ |
|||
// The last item in the list is always int.MaxValue.
|
|||
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; |
|||
|
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStore() |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
} |
|||
|
|||
public int Count => _entries.Length - 1; |
|||
public TValue this[int index] => _entries[index].Value; |
|||
|
|||
private (int, bool) TryFindEntry(int propertyId) |
|||
{ |
|||
if (_entries.Length <= 12) |
|||
{ |
|||
// For small lists, we use an optimized linear search. Since the last item in the list
|
|||
// is always int.MaxValue, we can skip a conditional branch in each iteration.
|
|||
// By unrolling the loop, we can skip another unconditional branch in each iteration.
|
|||
|
|||
if (_entries[0].PropertyId >= propertyId) |
|||
return (0, _entries[0].PropertyId == propertyId); |
|||
if (_entries[1].PropertyId >= propertyId) |
|||
return (1, _entries[1].PropertyId == propertyId); |
|||
if (_entries[2].PropertyId >= propertyId) |
|||
return (2, _entries[2].PropertyId == propertyId); |
|||
if (_entries[3].PropertyId >= propertyId) |
|||
return (3, _entries[3].PropertyId == propertyId); |
|||
if (_entries[4].PropertyId >= propertyId) |
|||
return (4, _entries[4].PropertyId == propertyId); |
|||
if (_entries[5].PropertyId >= propertyId) |
|||
return (5, _entries[5].PropertyId == propertyId); |
|||
if (_entries[6].PropertyId >= propertyId) |
|||
return (6, _entries[6].PropertyId == propertyId); |
|||
if (_entries[7].PropertyId >= propertyId) |
|||
return (7, _entries[7].PropertyId == propertyId); |
|||
if (_entries[8].PropertyId >= propertyId) |
|||
return (8, _entries[8].PropertyId == propertyId); |
|||
if (_entries[9].PropertyId >= propertyId) |
|||
return (9, _entries[9].PropertyId == propertyId); |
|||
if (_entries[10].PropertyId >= propertyId) |
|||
return (10, _entries[10].PropertyId == propertyId); |
|||
} |
|||
else |
|||
{ |
|||
int low = 0; |
|||
int high = _entries.Length; |
|||
int id; |
|||
|
|||
while (high - low > 3) |
|||
{ |
|||
int pivot = (high + low) / 2; |
|||
id = _entries[pivot].PropertyId; |
|||
|
|||
if (propertyId == id) |
|||
return (pivot, true); |
|||
|
|||
if (propertyId <= id) |
|||
high = pivot; |
|||
else |
|||
low = pivot + 1; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
id = _entries[low].PropertyId; |
|||
|
|||
if (id == propertyId) |
|||
return (low, true); |
|||
|
|||
if (id > propertyId) |
|||
break; |
|||
|
|||
++low; |
|||
} |
|||
while (low < high); |
|||
} |
|||
|
|||
return (0, false); |
|||
} |
|||
|
|||
public bool Contains(AvaloniaProperty property) => TryFindEntry(property.Id).Item2; |
|||
|
|||
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
(int index, bool found) = TryFindEntry(property.Id); |
|||
if (!found) |
|||
{ |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Entry[] entries = new Entry[_entries.Length + 1]; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (_entries[i].PropertyId > property.Id) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
Array.Copy(_entries, 0, entries, 0, i); |
|||
} |
|||
|
|||
entries[i] = new Entry { PropertyId = property.Id, Value = value }; |
|||
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
|
|||
public void SetValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
var (index, found) = TryFindEntry(property.Id); |
|||
|
|||
if (found) |
|||
_entries[index].Value = value; |
|||
else |
|||
AddValue(property, value); |
|||
} |
|||
|
|||
public void Remove(AvaloniaProperty property) |
|||
{ |
|||
var (index, found) = TryFindEntry(property.Id); |
|||
|
|||
if (found) |
|||
{ |
|||
var newLength = _entries.Length - 1; |
|||
|
|||
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
|
|||
if (newLength == 1) |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
|
|||
return; |
|||
} |
|||
|
|||
var entries = new Entry[newLength]; |
|||
|
|||
int ix = 0; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (i != index) |
|||
{ |
|||
entries[ix++] = _entries[i]; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
internal int PropertyId; |
|||
internal TValue Value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,417 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Rendering; |
|||
|
|||
public class CompositorHitTestingTests : CompositorTestsBase |
|||
{ |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Find_Controls_At_Point() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
var border = new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}; |
|||
|
|||
s.TopLevel.Content = border; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null, border); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Empty_Controls_At_Point() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
var border = new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}; |
|||
|
|||
s.TopLevel.Content = border; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Invisible_Controls_At_Point() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Border visible, border; |
|||
s.TopLevel.Content = border = new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center, |
|||
IsVisible = false, |
|||
Child = visible = new Border |
|||
{ |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Stretch, |
|||
VerticalAlignment = VerticalAlignment.Stretch, |
|||
} |
|||
}; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Control_Outside_Point() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
var border = new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
|
|||
}; |
|||
s.TopLevel.Content = border; |
|||
|
|||
s.AssertHitTest(new Point(10, 10), null); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Return_Top_Controls_First() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Panel container = new Panel |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Children = |
|||
{ |
|||
new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}, |
|||
new Border |
|||
{ |
|||
Width = 50, |
|||
Height = 50, |
|||
Background = Brushes.Blue, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
} |
|||
} |
|||
}; |
|||
s.TopLevel.Content = container; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null, container.Children[1], container.Children[0]); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Return_Top_Controls_First_With_ZIndex() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Panel container = new Panel |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Children = |
|||
{ |
|||
new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
ZIndex = 1, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}, |
|||
new Border |
|||
{ |
|||
Width = 50, |
|||
Height = 50, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}, |
|||
new Border |
|||
{ |
|||
Width = 75, |
|||
Height = 75, |
|||
ZIndex = 2, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
} |
|||
} |
|||
|
|||
}; |
|||
s.TopLevel.Content = container; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null, new[] { container.Children[2], container.Children[0], container.Children[1] }); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Find_Control_Translated_Outside_Parent_Bounds() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Border target; |
|||
Panel container = new Panel |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.Red, |
|||
ClipToBounds = false, |
|||
Children = |
|||
{ |
|||
new Border |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
ZIndex = 1, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Left, |
|||
VerticalAlignment = VerticalAlignment.Top, |
|||
Child = target = new Border |
|||
{ |
|||
Width = 50, |
|||
Height = 50, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Left, |
|||
VerticalAlignment = VerticalAlignment.Top, |
|||
RenderTransform = new TranslateTransform(110, 110), |
|||
} |
|||
}, |
|||
} |
|||
}; |
|||
s.TopLevel.Content = container; |
|||
|
|||
s.AssertHitTest(new Point(120, 120), null, target, container); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Border target; |
|||
Panel container = new Panel |
|||
{ |
|||
Width = 100, |
|||
Height = 200, |
|||
Background = Brushes.Red, |
|||
Children = |
|||
{ |
|||
new Panel() |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
Margin = new Thickness(0, 100, 0, 0), |
|||
ClipToBounds = true, |
|||
Children = |
|||
{ |
|||
(target = new Border() |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
Margin = new Thickness(0, -100, 0, 0) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
}; |
|||
s.TopLevel.Content = container; |
|||
|
|||
s.AssertHitTest(new Point(50, 50), null, container); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Control_Outside_Scroll_Viewport() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(100, 200))) |
|||
{ |
|||
Border target; |
|||
Border item1; |
|||
Border item2; |
|||
ScrollContentPresenter scroll; |
|||
Panel container = new Panel |
|||
{ |
|||
Width = 100, |
|||
Height = 200, |
|||
Background = Brushes.Red, |
|||
Children = |
|||
{ |
|||
(target = new Border() |
|||
{ |
|||
Name = "b1", |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
}), |
|||
new Border() |
|||
{ |
|||
Name = "b2", |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
Margin = new Thickness(0, 100, 0, 0), |
|||
Child = scroll = new ScrollContentPresenter() |
|||
{ |
|||
CanHorizontallyScroll = true, |
|||
CanVerticallyScroll = true, |
|||
Content = new StackPanel() |
|||
{ |
|||
Children = |
|||
{ |
|||
(item1 = new Border() |
|||
{ |
|||
Name = "b3", |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
}), |
|||
(item2 = new Border() |
|||
{ |
|||
Name = "b4", |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
}), |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
s.TopLevel.Content = container; |
|||
|
|||
scroll.UpdateChild(); |
|||
|
|||
s.AssertHitTestFirst(new Point(50, 150), null, item1); |
|||
|
|||
s.AssertHitTestFirst(new Point(50,50), null, target); |
|||
|
|||
scroll.Offset = new Vector(0, 100); |
|||
|
|||
s.AssertHitTestFirst(new Point(50, 150), null, item2); |
|||
|
|||
s.AssertHitTestFirst(new Point(50,50), null, target); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Not_Find_Path_When_Outside_Fill() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(200, 200))) |
|||
{ |
|||
Path path = new Path |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Fill = Brushes.Red, |
|||
Data = StreamGeometry.Parse("M100,0 L0,100 100,100") |
|||
}; |
|||
s.TopLevel.Content = path; |
|||
|
|||
s.AssertHitTest(new Point(100, 100), null, path); |
|||
s.AssertHitTest(new Point(10, 10), null); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Respect_Geometry_Clip() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(400, 400))) |
|||
{ |
|||
Canvas canvas; |
|||
Border border = new Border |
|||
{ |
|||
Background = Brushes.Red, |
|||
Clip = StreamGeometry.Parse("M100,0 L0,100 100,100"), |
|||
Width = 200, |
|||
Height = 200, |
|||
Child = canvas = new Canvas |
|||
{ |
|||
Background = Brushes.Yellow, |
|||
Margin = new Thickness(10), |
|||
} |
|||
|
|||
}; |
|||
s.TopLevel.Content = border; |
|||
|
|||
s.RunJobs(); |
|||
Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds); |
|||
|
|||
s.AssertHitTest(new Point(200,200), null, canvas, border); |
|||
|
|||
s.AssertHitTest(new Point(110, 110), null); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void HitTest_Should_Accommodate_ICustomHitTest() |
|||
{ |
|||
using (var s = new CompositorServices(new Size(300, 200))) |
|||
{ |
|||
Border border = new CustomHitTestBorder |
|||
{ |
|||
ClipToBounds = false, |
|||
Width = 100, |
|||
Height = 100, |
|||
Background = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}; |
|||
|
|||
s.TopLevel.Content = border; |
|||
|
|||
s.AssertHitTest(75, 100, null, border); |
|||
s.AssertHitTest(125, 100, null, border); |
|||
s.AssertHitTest(175, 100, null); |
|||
} |
|||
} |
|||
|
|||
private IDisposable TestApplication() |
|||
{ |
|||
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Rendering; |
|||
|
|||
public class CompositorInvalidationTests : CompositorTestsBase |
|||
{ |
|||
[Fact] |
|||
public void Control_Should_Invalidate_Own_Rect_When_Added() |
|||
{ |
|||
using (var s = new CompositorCanvas()) |
|||
{ |
|||
var control = new Border() |
|||
{ |
|||
Background = Brushes.Red, Width = 20, Height = 10, |
|||
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50 |
|||
}; |
|||
s.Canvas.Children.Add(control); |
|||
s.AssertRects(new Rect(30, 50, 20, 10)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Should_Invalidate_Own_Rect_When_Removed() |
|||
{ |
|||
using (var s = new CompositorCanvas()) |
|||
{ |
|||
var control = new Border() |
|||
{ |
|||
Background = Brushes.Red, Width = 20, Height = 10, |
|||
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50 |
|||
}; |
|||
s.Canvas.Children.Add(control); |
|||
s.RunJobs(); |
|||
s.Events.Rects.Clear(); |
|||
s.Canvas.Children.Remove(control); |
|||
s.AssertRects(new Rect(30, 50, 20, 10)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Should_Invalidate_Both_Own_Rects_When_Moved() |
|||
{ |
|||
using (var s = new CompositorCanvas()) |
|||
{ |
|||
var control = new Border() |
|||
{ |
|||
Background = Brushes.Red, Width = 20, Height = 10, |
|||
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50 |
|||
}; |
|||
s.Canvas.Children.Add(control); |
|||
s.RunJobs(); |
|||
s.Events.Rects.Clear(); |
|||
control[Canvas.LeftProperty] = 55; |
|||
s.AssertRects(new Rect(30, 50, 20, 10), |
|||
new Rect(55, 50, 20, 10) |
|||
); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Should_Invalidate_Child_Rects_When_Moved() |
|||
{ |
|||
using (var s = new CompositorCanvas()) |
|||
{ |
|||
var control = new Decorator() |
|||
{ |
|||
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, |
|||
Padding = new Thickness(10), |
|||
Child = new Border() |
|||
{ |
|||
Width = 20, Height = 10, |
|||
Background = Brushes.Red |
|||
} |
|||
}; |
|||
s.Canvas.Children.Add(control); |
|||
s.RunJobs(); |
|||
s.Events.Rects.Clear(); |
|||
control[Canvas.LeftProperty] = 55; |
|||
s.AssertRects(new Rect(40, 60, 20, 10), |
|||
new Rect(65, 60, 20, 10) |
|||
); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Should_Invalidate_Child_Rects_When_Becomes_Invisible() |
|||
{ |
|||
using (var s = new CompositorCanvas()) |
|||
{ |
|||
var control = new Decorator() |
|||
{ |
|||
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, |
|||
Padding = new Thickness(10), |
|||
Child = new Border() |
|||
{ |
|||
Width = 20, Height = 10, |
|||
Background = Brushes.Red |
|||
} |
|||
}; |
|||
s.Canvas.Children.Add(control); |
|||
s.RunJobs(); |
|||
s.Events.Rects.Clear(); |
|||
control.IsVisible = false; |
|||
s.AssertRects(new Rect(40, 60, 20, 10)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Linq; |
|||
using System.Reactive.Disposables; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Embedding; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using JetBrains.Annotations; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Rendering; |
|||
|
|||
public class CompositorTestsBase |
|||
{ |
|||
public class DebugEvents : ICompositionTargetDebugEvents |
|||
{ |
|||
public List<Rect> Rects = new(); |
|||
|
|||
public void RectInvalidated(Rect rc) |
|||
{ |
|||
Rects.Add(rc); |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
Rects.Clear(); |
|||
} |
|||
} |
|||
|
|||
public class ManualRenderTimer : IRenderTimer |
|||
{ |
|||
public event Action<TimeSpan> Tick; |
|||
public bool RunsInBackground => false; |
|||
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); |
|||
public Task TriggerBackgroundTick() => Task.Run(TriggerTick); |
|||
} |
|||
|
|||
class TopLevelImpl : ITopLevelImpl |
|||
{ |
|||
private readonly Compositor _compositor; |
|||
public CompositingRenderer Renderer { get; private set; } |
|||
|
|||
public TopLevelImpl(Compositor compositor, Size clientSize) |
|||
{ |
|||
ClientSize = clientSize; |
|||
_compositor = compositor; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Size ClientSize { get; } |
|||
public Size? FrameSize { get; } |
|||
public double RenderScaling => 1; |
|||
public IEnumerable<object> Surfaces { get; } = Array.Empty<object>(); |
|||
public Action<RawInputEventArgs> Input { get; set; } |
|||
public Action<Rect> Paint { get; set; } |
|||
public Action<Size, PlatformResizeReason> Resized { get; set; } |
|||
public Action<double> ScalingChanged { get; set; } |
|||
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) |
|||
{ |
|||
return Renderer = new CompositingRenderer(root, _compositor); |
|||
} |
|||
|
|||
public void Invalidate(Rect rect) |
|||
{ |
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) |
|||
{ |
|||
} |
|||
|
|||
public Point PointToClient(PixelPoint point) => default; |
|||
|
|||
public PixelPoint PointToScreen(Point point) => new(); |
|||
|
|||
public void SetCursor(ICursorImpl cursor) |
|||
{ |
|||
} |
|||
|
|||
public Action Closed { get; set; } |
|||
public Action LostFocus { get; set; } |
|||
public IMouseDevice MouseDevice { get; } = new MouseDevice(); |
|||
public IPopupImpl CreatePopup() => throw new NotImplementedException(); |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
|||
{ |
|||
} |
|||
|
|||
public WindowTransparencyLevel TransparencyLevel { get; } |
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
|||
} |
|||
|
|||
protected class CompositorServices : IDisposable |
|||
{ |
|||
private readonly IDisposable _app; |
|||
public Compositor Compositor { get; } |
|||
public ManualRenderTimer Timer { get; } = new(); |
|||
public EmbeddableControlRoot TopLevel { get; } |
|||
public CompositingRenderer Renderer { get; } = null!; |
|||
public DebugEvents Events { get; } = new(); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
TopLevel.Renderer.Stop(); |
|||
TopLevel.Dispose(); |
|||
_app.Dispose(); |
|||
} |
|||
|
|||
public CompositorServices(Size? size = null) |
|||
{ |
|||
_app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); |
|||
try |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer); |
|||
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>() |
|||
.ToConstant(new RenderLoop(Timer, Dispatcher.UIThread)); |
|||
|
|||
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null); |
|||
var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000)); |
|||
TopLevel = new EmbeddableControlRoot(impl) |
|||
{ |
|||
Template = new FuncControlTemplate((parent, scope) => |
|||
{ |
|||
var presenter = new ContentPresenter |
|||
{ |
|||
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty) |
|||
}; |
|||
scope.Register("PART_ContentPresenter", presenter); |
|||
return presenter; |
|||
}) |
|||
}; |
|||
Renderer = impl.Renderer; |
|||
TopLevel.Prepare(); |
|||
TopLevel.Renderer.Start(); |
|||
RunJobs(); |
|||
Renderer.CompositionTarget.Server.DebugEvents = Events; |
|||
} |
|||
catch |
|||
{ |
|||
_app.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public void RunJobs() |
|||
{ |
|||
Dispatcher.UIThread.RunJobs(); |
|||
Timer.TriggerTick(); |
|||
Dispatcher.UIThread.RunJobs(); |
|||
} |
|||
|
|||
public void AssertRects(params Rect[] rects) |
|||
{ |
|||
RunJobs(); |
|||
var toAssert = rects.Select(x => x.ToString()).Distinct().OrderBy(x => x); |
|||
var invalidated = Events.Rects.Select(x => x.ToString()).Distinct().OrderBy(x => x); |
|||
Assert.Equal(toAssert, invalidated); |
|||
Events.Rects.Clear(); |
|||
} |
|||
|
|||
public void AssertHitTest(double x, double y, Func<IVisual, bool> filter, params object[] expected) |
|||
=> AssertHitTest(new Point(x, y), filter, expected); |
|||
public void AssertHitTest(Point pt, Func<IVisual, bool> filter, params object[] expected) |
|||
{ |
|||
RunJobs(); |
|||
var tested = Renderer.HitTest(pt, TopLevel, filter); |
|||
Assert.Equal(expected, tested); |
|||
} |
|||
|
|||
public void AssertHitTestFirst(Point pt, Func<IVisual, bool> filter, object expected) |
|||
{ |
|||
RunJobs(); |
|||
var tested = Renderer.HitTest(pt, TopLevel, filter).First(); |
|||
Assert.Equal(expected, tested); |
|||
} |
|||
} |
|||
|
|||
|
|||
protected class CompositorCanvas : CompositorServices |
|||
{ |
|||
public Canvas Canvas { get; } = new(); |
|||
|
|||
public CompositorCanvas() |
|||
{ |
|||
TopLevel.Content = Canvas; |
|||
RunJobs(); |
|||
Events.Reset(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,318 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Utilities |
|||
{ |
|||
public class AvaloniaPropertyDictionaryTests |
|||
{ |
|||
private static AvaloniaProperty[] TestProperties; |
|||
|
|||
static AvaloniaPropertyDictionaryTests() |
|||
{ |
|||
TestProperties = new AvaloniaProperty[100]; |
|||
|
|||
for (var i = 0; i < 100; ++i) |
|||
{ |
|||
TestProperties[i] = new StyledProperty<string>( |
|||
$"Test{i}", |
|||
typeof(AvaloniaPropertyDictionaryTests), |
|||
new StyledPropertyMetadata<string>()); |
|||
} |
|||
|
|||
Shuffle(TestProperties, 42); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Property_Indexer_Finds_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
var result = target[property]; |
|||
|
|||
Assert.Equal($"Value{index}", result); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Property_Indexer_Throws_If_Value_Not_Found(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.Throws<KeyNotFoundException>(() => target[property]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Property_Indexer_Adds_New_Value(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
target[property] = "new"; |
|||
|
|||
Assert.Equal("new", target[property]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Property_Indexer_Sets_Existing_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.Equal($"Value{index}", target[property]); |
|||
|
|||
target[property] = "new"; |
|||
|
|||
Assert.Equal("new", target[property]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Int_Indexer_Finds_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var result = target[index]; |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Int_Indexer_Throws_If_Index_Out_Of_Range(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
|
|||
Assert.Throws<IndexOutOfRangeException>(() => target[index]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Add_Adds_New_Value(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
target.Add(property, "new"); |
|||
|
|||
Assert.Equal("new", target[property]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Add_Throws_If_Key_Exists(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.Throws<ArgumentException>(() => target.Add(property, "new")); |
|||
} |
|||
|
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void ContainsKey_Returns_True_If_Value_Exists(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.True(target.ContainsKey(property)); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void ContainsKey_Returns_False_If_Value_Does_Not_Exist(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.False(target.ContainsKey(property)); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void GetKeyValue_Finds_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
|
|||
target.GetKeyValue(index, out var property, out var value); |
|||
|
|||
Assert.NotNull(property); |
|||
Assert.NotNull(value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void GetKeyValue_Throws_If_Index_Out_Of_Range(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
|
|||
Assert.Throws<IndexOutOfRangeException>(() => target.GetKeyValue(index, out var _, out var _)); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Remove_Removes_Value(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.True(target.Remove(property)); |
|||
Assert.False(target.ContainsKey(property)); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Remove_Returns_False_If_Value_Not_Present(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.False(target.Remove(property)); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void Remove_Returns_Existing_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.True(target.Remove(property, out var value)); |
|||
Assert.Equal($"Value{index}", value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void TryAdd_Adds_New_Value(int count) |
|||
{ |
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.True(target.TryAdd(property, "new")); |
|||
|
|||
Assert.Equal("new", target[property]); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void TryAdd_Returns_False_If_Key_Exists(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.False(target.TryAdd(property, "new")); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void TryGetValue_Finds_Value(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count / 2; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.True(target.TryGetValue(property, out var value)); |
|||
Assert.Equal($"Value{index}", value); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(Counts))] |
|||
public void TryGetValue_Returns_False_If_Key_Does_Not_Exist(int count) |
|||
{ |
|||
if (count == 0) |
|||
return; |
|||
|
|||
var target = CreateTarget(count); |
|||
var index = count; |
|||
var property = TestProperties[index]; |
|||
|
|||
Assert.False(target.TryGetValue(property, out var value)); |
|||
Assert.Null(value); |
|||
} |
|||
|
|||
public static TheoryData<int> Counts() |
|||
{ |
|||
var result = new TheoryData<int>(); |
|||
result.Add(0); |
|||
result.Add(1); |
|||
result.Add(10); |
|||
result.Add(13); |
|||
result.Add(50); |
|||
result.Add(72); |
|||
return result; |
|||
} |
|||
|
|||
private static AvaloniaPropertyDictionary<string> CreateTarget(int items) |
|||
{ |
|||
var result = new AvaloniaPropertyDictionary<string>(); |
|||
|
|||
for (var i = 0; i < items; ++i) |
|||
result.Add(TestProperties[i], $"Value{i}"); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static void Shuffle<T>(T[] array, int seed) |
|||
{ |
|||
var rng = new Random(seed); |
|||
|
|||
int n = array.Length; |
|||
while (n > 1) |
|||
{ |
|||
int k = rng.Next(n--); |
|||
T temp = array[n]; |
|||
array[n] = array[k]; |
|||
array[k] = temp; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,489 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Utilities; |
|||
using BenchmarkDotNet.Attributes; |
|||
|
|||
namespace Avalonia.Benchmarks.Utilities; |
|||
|
|||
// TODO: Remove after review together with related benchmark code.
|
|||
internal sealed class AvaloniaPropertyValueStoreOld<TValue> |
|||
{ |
|||
// The last item in the list is always int.MaxValue.
|
|||
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } }; |
|||
|
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStoreOld() |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
} |
|||
|
|||
public int Count => _entries.Length - 1; |
|||
public TValue this[int index] => _entries[index].Value; |
|||
|
|||
private (int, bool) TryFindEntry(int propertyId) |
|||
{ |
|||
if (_entries.Length <= 12) |
|||
{ |
|||
// For small lists, we use an optimized linear search. Since the last item in the list
|
|||
// is always int.MaxValue, we can skip a conditional branch in each iteration.
|
|||
// By unrolling the loop, we can skip another unconditional branch in each iteration.
|
|||
|
|||
if (_entries[0].PropertyId >= propertyId) |
|||
return (0, _entries[0].PropertyId == propertyId); |
|||
if (_entries[1].PropertyId >= propertyId) |
|||
return (1, _entries[1].PropertyId == propertyId); |
|||
if (_entries[2].PropertyId >= propertyId) |
|||
return (2, _entries[2].PropertyId == propertyId); |
|||
if (_entries[3].PropertyId >= propertyId) |
|||
return (3, _entries[3].PropertyId == propertyId); |
|||
if (_entries[4].PropertyId >= propertyId) |
|||
return (4, _entries[4].PropertyId == propertyId); |
|||
if (_entries[5].PropertyId >= propertyId) |
|||
return (5, _entries[5].PropertyId == propertyId); |
|||
if (_entries[6].PropertyId >= propertyId) |
|||
return (6, _entries[6].PropertyId == propertyId); |
|||
if (_entries[7].PropertyId >= propertyId) |
|||
return (7, _entries[7].PropertyId == propertyId); |
|||
if (_entries[8].PropertyId >= propertyId) |
|||
return (8, _entries[8].PropertyId == propertyId); |
|||
if (_entries[9].PropertyId >= propertyId) |
|||
return (9, _entries[9].PropertyId == propertyId); |
|||
if (_entries[10].PropertyId >= propertyId) |
|||
return (10, _entries[10].PropertyId == propertyId); |
|||
} |
|||
else |
|||
{ |
|||
var low = 0; |
|||
var high = _entries.Length; |
|||
int id; |
|||
|
|||
while (high - low > 3) |
|||
{ |
|||
var pivot = (high + low) / 2; |
|||
id = _entries[pivot].PropertyId; |
|||
|
|||
if (propertyId == id) |
|||
return (pivot, true); |
|||
|
|||
if (propertyId <= id) |
|||
high = pivot; |
|||
else |
|||
low = pivot + 1; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
id = _entries[low].PropertyId; |
|||
|
|||
if (id == propertyId) |
|||
return (low, true); |
|||
|
|||
if (id > propertyId) |
|||
break; |
|||
|
|||
++low; |
|||
} |
|||
while (low < high); |
|||
} |
|||
|
|||
return (0, false); |
|||
} |
|||
|
|||
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) |
|||
{ |
|||
(var index, var found) = TryFindEntry(property.Id); |
|||
if (!found) |
|||
{ |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
var entries = new Entry[_entries.Length + 1]; |
|||
|
|||
for (var i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (_entries[i].PropertyId > property.Id) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
Array.Copy(_entries, 0, entries, 0, i); |
|||
} |
|||
|
|||
entries[i] = new Entry { PropertyId = property.Id, Value = value }; |
|||
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
|
|||
public void SetValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
_entries[TryFindEntry(property.Id).Item1].Value = value; |
|||
} |
|||
|
|||
public void Remove(AvaloniaProperty property) |
|||
{ |
|||
var (index, found) = TryFindEntry(property.Id); |
|||
|
|||
if (found) |
|||
{ |
|||
var newLength = _entries.Length - 1; |
|||
|
|||
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
|
|||
if (newLength == 1) |
|||
{ |
|||
_entries = s_emptyEntries; |
|||
|
|||
return; |
|||
} |
|||
|
|||
var entries = new Entry[newLength]; |
|||
|
|||
var ix = 0; |
|||
|
|||
for (var i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (i != index) |
|||
{ |
|||
entries[ix++] = _entries[i]; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
internal int PropertyId; |
|||
internal TValue Value; |
|||
} |
|||
} |
|||
|
|||
internal class MockProperty : StyledProperty<int> |
|||
{ |
|||
public MockProperty([JetBrains.Annotations.NotNull] string name) : base(name, typeof(object), new StyledPropertyMetadata<int>()) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
internal static class MockProperties |
|||
{ |
|||
public static readonly AvaloniaProperty[] ShuffledProperties; |
|||
|
|||
static MockProperties() |
|||
{ |
|||
ShuffledProperties = new AvaloniaProperty[32]; |
|||
|
|||
for (var i = 0; i < ShuffledProperties.Length; i++) |
|||
{ |
|||
ShuffledProperties[i] = new MockProperty($"Property#{i}"); |
|||
} |
|||
|
|||
Shuffle(ShuffledProperties, 42); |
|||
} |
|||
|
|||
private static void Shuffle<T>(T[] array, int seed) |
|||
{ |
|||
var rng = new Random(seed); |
|||
|
|||
var n = array.Length; |
|||
while (n > 1) |
|||
{ |
|||
var k = rng.Next(n--); |
|||
var temp = array[n]; |
|||
array[n] = array[k]; |
|||
array[k] = temp; |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MemoryDiagnoser] |
|||
public class ValueStore_Lookup |
|||
{ |
|||
[Params(2, 6, 10, 20, 30)] |
|||
public int PropertyCount; |
|||
|
|||
public AvaloniaProperty[] Properties => MockProperties.ShuffledProperties; |
|||
|
|||
private AvaloniaPropertyDictionary<object> _store; |
|||
private AvaloniaPropertyValueStoreOld<object> _oldStore; |
|||
private Dictionary<AvaloniaProperty, object> _dictionary; |
|||
|
|||
[GlobalSetup] |
|||
public void GlobalSetup() |
|||
{ |
|||
_store = new AvaloniaPropertyDictionary<object>(); |
|||
_oldStore = new AvaloniaPropertyValueStoreOld<object>(); |
|||
_dictionary = new Dictionary<AvaloniaProperty, object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
_store.Add(Properties[i], null); |
|||
_oldStore.AddValue(Properties[i], null); |
|||
_dictionary.Add(Properties[i], null); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void LookupProperties() |
|||
{ |
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
_store.TryGetValue(Properties[i], out _); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void LookupProperties_Old() |
|||
{ |
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
_oldStore.TryGetValue(Properties[i], out _); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void LookupProperties_Dict() |
|||
{ |
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
_dictionary.TryGetValue(Properties[i], out _); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MemoryDiagnoser] |
|||
public class ValueStore_AddBenchmarks |
|||
{ |
|||
[Params(2, 6, 10, 20, 30)] |
|||
public int PropertyCount; |
|||
|
|||
public AvaloniaProperty[] Properties => MockProperties.ShuffledProperties; |
|||
|
|||
[Benchmark] |
|||
public void Add() |
|||
{ |
|||
var store = new AvaloniaPropertyDictionary<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void Add_Old() |
|||
{ |
|||
var store = new AvaloniaPropertyValueStoreOld<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.AddValue(Properties[i], null); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void Add_Dict() |
|||
{ |
|||
var store = new Dictionary<AvaloniaProperty, object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MemoryDiagnoser] |
|||
public class ValueStore_AddRemoveBenchmarks |
|||
{ |
|||
[Params(2, 6, 10, 20, 30)] |
|||
public int PropertyCount; |
|||
|
|||
public AvaloniaProperty[] Properties => MockProperties.ShuffledProperties; |
|||
|
|||
[Benchmark] |
|||
public void AddAndRemoveValue() |
|||
{ |
|||
var store = new AvaloniaPropertyDictionary<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
} |
|||
|
|||
for (var i = PropertyCount - 1; i >= 0; i--) |
|||
{ |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void AddAndRemoveValue_Old() |
|||
{ |
|||
var store = new AvaloniaPropertyValueStoreOld<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.AddValue(Properties[i], null); |
|||
} |
|||
|
|||
for (var i = PropertyCount - 1; i >= 0; i--) |
|||
{ |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void AddAndRemoveValue_Dict() |
|||
{ |
|||
var store = new Dictionary<AvaloniaProperty, object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
} |
|||
|
|||
for (var i = PropertyCount - 1; i >= 0; i--) |
|||
{ |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MemoryDiagnoser] |
|||
public class ValueStore_AddRemoveInterleavedBenchmarks |
|||
{ |
|||
[Params(2, 6, 10, 20, 30)] |
|||
public int PropertyCount; |
|||
|
|||
public AvaloniaProperty[] Properties => MockProperties.ShuffledProperties; |
|||
|
|||
[Benchmark] |
|||
public void AddAndRemoveValueInterleaved() |
|||
{ |
|||
var store = new AvaloniaPropertyDictionary<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void AddAndRemoveValueInterleaved_Old() |
|||
{ |
|||
var store = new AvaloniaPropertyValueStoreOld<object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.AddValue(Properties[i], null); |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void AddAndRemoveValueInterleaved_Dict() |
|||
{ |
|||
var store = new Dictionary<AvaloniaProperty, object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
store.Add(Properties[i], null); |
|||
store.Remove(Properties[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
[MemoryDiagnoser] |
|||
public class ValueStore_Enumeration |
|||
{ |
|||
[Params(2, 6, 10, 20, 30)] |
|||
public int PropertyCount; |
|||
|
|||
public AvaloniaProperty[] Properties => MockProperties.ShuffledProperties; |
|||
|
|||
private AvaloniaPropertyDictionary<object> _store; |
|||
private AvaloniaPropertyValueStoreOld<object> _oldStore; |
|||
private Dictionary<AvaloniaProperty, object> _dictionary; |
|||
|
|||
[GlobalSetup] |
|||
public void GlobalSetup() |
|||
{ |
|||
_store = new AvaloniaPropertyDictionary<object>(); |
|||
_oldStore = new AvaloniaPropertyValueStoreOld<object>(); |
|||
_dictionary = new Dictionary<AvaloniaProperty, object>(); |
|||
|
|||
for (var i = 0; i < PropertyCount; i++) |
|||
{ |
|||
_store.Add(Properties[i], null); |
|||
_oldStore.AddValue(Properties[i], null); |
|||
_dictionary.Add(Properties[i], null); |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int Enumerate() |
|||
{ |
|||
var result = 0; |
|||
|
|||
for (var i = 0; i < _store.Count; i++) |
|||
{ |
|||
result += _store[i] is null ? 1 : 0; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public int Enumerate_Old() |
|||
{ |
|||
var result = 0; |
|||
|
|||
for (var i = 0; i < _store.Count; i++) |
|||
{ |
|||
result += _oldStore[i] is null ? 1 : 0; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int Enumerate_Dict() |
|||
{ |
|||
var result = 0; |
|||
|
|||
foreach (var i in _dictionary) |
|||
{ |
|||
result += i.Value is null ? 1 : 0; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Benchmark] |
|||
public int Enumerate_DictValues() |
|||
{ |
|||
var result = 0; |
|||
|
|||
foreach (var i in _dictionary.Values) |
|||
{ |
|||
result += i is null ? 1 : 0; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue