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