Browse Source

Merge branch 'refactor/avaloniapropertystore' into refactor/new-value-store

refactor/style-priorities
Steven Kirk 4 years ago
parent
commit
d45f96daa3
  1. 1
      samples/ControlCatalog/App.xaml
  2. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  3. 34
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  4. 13
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  5. 44
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  6. 6
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  7. 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  8. 35
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  9. 10
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  11. 2
      src/Avalonia.Base/StyledElement.cs
  12. 358
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  13. 180
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  14. 8
      src/Avalonia.Controls/TrayIcon.cs
  15. 1
      src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
  16. 1
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  17. 20
      src/Windows/Avalonia.Win32/IconImpl.cs
  18. 4
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  19. 29
      src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs
  20. 8
      src/Windows/Avalonia.Win32/Win32Platform.cs
  21. 417
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  22. 109
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs
  23. 208
      tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs
  24. 318
      tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs
  25. 3
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  26. 33
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  27. 489
      tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs
  28. 27
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

1
samples/ControlCatalog/App.xaml

@ -43,6 +43,7 @@
<NativeMenuItemSeparator /> <NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" /> <NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" /> <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
<NativeMenuItem Header="Disabled option" IsEnabled="False" />
</NativeMenu> </NativeMenu>
</NativeMenuItem> </NativeMenuItem>
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" /> <NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -23,6 +23,7 @@
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />

34
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -24,11 +24,12 @@ public class CompositingRenderer : IRendererWithCompositor
DrawingContext _recordingContext; DrawingContext _recordingContext;
private HashSet<Visual> _dirty = new(); private HashSet<Visual> _dirty = new();
private HashSet<Visual> _recalculateChildren = new(); private HashSet<Visual> _recalculateChildren = new();
private readonly CompositionTarget _target;
private bool _queuedUpdate; private bool _queuedUpdate;
private Action _update; private Action _update;
private Action _invalidateScene; private Action _invalidateScene;
internal CompositionTarget CompositionTarget;
/// <summary> /// <summary>
/// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
/// </summary> /// </summary>
@ -40,8 +41,8 @@ public class CompositingRenderer : IRendererWithCompositor
_root = root; _root = root;
_compositor = compositor; _compositor = compositor;
_recordingContext = new DrawingContext(_recorder); _recordingContext = new DrawingContext(_recorder);
_target = compositor.CreateCompositionTarget(root.CreateRenderTarget); CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
_target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
_update = Update; _update = Update;
_invalidateScene = InvalidateScene; _invalidateScene = InvalidateScene;
} }
@ -49,15 +50,15 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/> /// <inheritdoc/>
public bool DrawFps public bool DrawFps
{ {
get => _target.DrawFps; get => CompositionTarget.DrawFps;
set => _target.DrawFps = value; set => CompositionTarget.DrawFps = value;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool DrawDirtyRects public bool DrawDirtyRects
{ {
get => _target.DrawDirtyRects; get => CompositionTarget.DrawDirtyRects;
set => _target.DrawDirtyRects = value; set => CompositionTarget.DrawDirtyRects = value;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -81,12 +82,11 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter) public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
{ {
var res = _target.TryHitTest(p, filter); var res = CompositionTarget.TryHitTest(p, filter);
if(res == null) if(res == null)
yield break; yield break;
for (var index = res.Count - 1; index >= 0; index--) foreach(var v in res)
{ {
var v = res[index];
if (v is CompositionDrawListVisual dv) if (v is CompositionDrawListVisual dv)
{ {
if (filter == null || filter(dv.Visual)) if (filter == null || filter(dv.Visual))
@ -234,8 +234,8 @@ public class CompositingRenderer : IRendererWithCompositor
SyncChildren(v); SyncChildren(v);
_dirty.Clear(); _dirty.Clear();
_recalculateChildren.Clear(); _recalculateChildren.Clear();
_target.Size = _root.ClientSize; CompositionTarget.Size = _root.ClientSize;
_target.Scaling = _root.RenderScaling; CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene); Compositor.InvokeOnNextCommit(_invalidateScene);
} }
@ -246,24 +246,24 @@ public class CompositingRenderer : IRendererWithCompositor
public void Paint(Rect rect) public void Paint(Rect rect)
{ {
Update(); Update();
_target.RequestRedraw(); CompositionTarget.RequestRedraw();
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
Compositor.RequestCommitAsync().Wait(); Compositor.RequestCommitAsync().Wait();
else else
_target.ImmediateUIThreadRender(); CompositionTarget.ImmediateUIThreadRender();
} }
public void Start() => _target.IsEnabled = true; public void Start() => CompositionTarget.IsEnabled = true;
public void Stop() public void Stop()
{ {
_target.IsEnabled = false; CompositionTarget.IsEnabled = false;
} }
public void Dispose() public void Dispose()
{ {
Stop(); Stop();
_target.Dispose(); CompositionTarget.Dispose();
// Wait for the composition batch to be applied and rendered to guarantee that // Wait for the composition batch to be applied and rendered to guarantee that
// render target is not used anymore and can be safely disposed // render target is not used anymore and can be safely disposed

13
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@ -54,13 +54,20 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
internal override bool HitTest(Point pt, Func<IVisual, bool>? filter) internal override bool HitTest(Point pt, Func<IVisual, bool>? filter)
{ {
if (DrawList == null) var custom = Visual as ICustomHitTest;
if (DrawList == null && custom == null)
return false; return false;
if (filter != null && !filter(Visual)) if (filter != null && !filter(Visual))
return false; return false;
if (Visual is ICustomHitTest custom) if (custom != null)
{
// Simulate the old behavior
// TODO: Change behavior once legacy renderers are removed
pt += new Point(Offset.X, Offset.Y);
return custom.HitTest(pt); return custom.HitTest(pt);
foreach (var op in DrawList) }
foreach (var op in DrawList!)
if (op.Item.HitTest(pt)) if (op.Item.HitTest(pt))
return true; return true;
return false; return false;

44
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -62,7 +62,7 @@ namespace Avalonia.Rendering.Composition
bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
{ {
var m = visual.TryGetServerTransform(); var m = visual.TryGetServerGlobalTransform();
if (m == null) if (m == null)
{ {
matrix = default; matrix = default;
@ -73,52 +73,44 @@ namespace Avalonia.Rendering.Composition
return m33.TryInvert(out matrix); return m33.TryInvert(out matrix);
} }
bool TryTransformTo(CompositionVisual visual, ref Point v) bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v)
{ {
v = default;
if (TryGetInvertedTransform(visual, out var m)) if (TryGetInvertedTransform(visual, out var m))
{ {
v = v * m; v = globalPoint * m;
return true; return true;
} }
return false; return false;
} }
bool HitTestCore(CompositionVisual visual, Point point, PooledList<CompositionVisual> result, void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
Func<IVisual, bool>? filter) Func<IVisual, bool>? filter)
{ {
//TODO: Check readback too
if (visual.Visible == false) if (visual.Visible == false)
return false; return;
if (!TryTransformTo(visual, ref point)) if (!TryTransformTo(visual, globalPoint, out var point))
return false; return;
if (visual.ClipToBounds if (visual.ClipToBounds
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
return false; return;
if (visual.Clip?.FillContains(point) == false)
return false;
bool success = false; if (visual.Clip?.FillContains(point) == false)
// Hit-test the current node return;
if (visual.HitTest(point, filter))
{ // Inspect children
result.Add(visual);
success = true;
}
// Inspect children too
if (visual is CompositionContainerVisual cv) if (visual is CompositionContainerVisual cv)
for (var c = cv.Children.Count - 1; c >= 0; c--) for (var c = cv.Children.Count - 1; c >= 0; c--)
{ {
var ch = cv.Children[c]; var ch = cv.Children[c];
var hit = HitTestCore(ch, point, result, filter); HitTestCore(ch, globalPoint, result, filter);
if (hit)
return true;
} }
return success; // Hit-test the current node
if (visual.HitTest(point, filter))
result.Add(visual);
} }
/// <summary> /// <summary>

6
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@ -0,0 +1,6 @@
namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents
{
void RectInvalidated(Rect rc);
}

3
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -33,7 +33,7 @@ namespace Avalonia.Rendering.Composition.Server
private HashSet<ServerCompositionVisual> _attachedVisuals = new(); private HashSet<ServerCompositionVisual> _attachedVisuals = new();
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new(); private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public ReadbackIndices Readback { get; } = new(); public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; } public int RenderedVisuals { get; set; }
@ -173,6 +173,7 @@ namespace Avalonia.Rendering.Composition.Server
if(rect.IsEmpty) if(rect.IsEmpty)
return; return;
var snapped = SnapToDevicePixels(rect, Scaling); var snapped = SnapToDevicePixels(rect, Scaling);
DebugEvents?.RectInvalidated(rect);
_dirtyRect = _dirtyRect.Union(snapped); _dirtyRect = _dirtyRect.Union(snapped);
_redrawRequested = true; _redrawRequested = true;
} }

35
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -119,14 +119,15 @@ namespace Avalonia.Rendering.Composition.Server
var oldTransformedContentBounds = TransformedOwnContentBounds; var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
var dirtyOldBounds = false;
if (_parent?.IsDirtyComposition == true) if (_parent?.IsDirtyComposition == true)
{ {
IsDirtyComposition = true; IsDirtyComposition = true;
_isDirtyForUpdate = true; _isDirtyForUpdate = true;
dirtyOldBounds = true;
} }
var invalidateOldBounds = _isDirtyForUpdate;
var invalidateNewBounds = _isDirtyForUpdate;
GlobalTransformMatrix = newTransform; GlobalTransformMatrix = newTransform;
@ -157,30 +158,20 @@ namespace Avalonia.Rendering.Composition.Server
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface && IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
!_combinedTransformedClipBounds.IsEmpty; !_combinedTransformedClipBounds.IsEmpty;
if (wasVisible != IsVisibleInFrame)
_isDirtyForUpdate = true;
// Invalidate previous rect and queue new rect based on visibility
if (positionChanged)
{
if (wasVisible)
dirtyOldBounds = true;
if (IsVisibleInFrame) if (wasVisible != IsVisibleInFrame || positionChanged)
_isDirtyForUpdate = true; {
invalidateOldBounds |= wasVisible;
invalidateNewBounds |= IsVisibleInFrame;
} }
// Invalidate new bounds // Invalidate new bounds
if (IsVisibleInFrame && _isDirtyForUpdate) if (invalidateNewBounds)
{
dirtyOldBounds = true;
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds));
}
if (dirtyOldBounds && wasVisible) if (invalidateOldBounds)
AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds));
@ -190,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server
var i = Root!.Readback; var i = Root!.Readback;
ref var readback = ref GetReadback(i.WriteIndex); ref var readback = ref GetReadback(i.WriteIndex);
readback.Revision = root.Revision; readback.Revision = root.Revision;
readback.Matrix = CombinedTransformMatrix; readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id; readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame; readback.Visible = IsVisibleInFrame;
} }

10
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Transport;
@ -17,6 +17,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
int _usage; int _usage;
readonly int[] _usageStatistics = new int[10]; readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot; int _usageStatisticsSlot;
bool _reclaimImmediately;
public int CurrentUsage => _usage; public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count; public int CurrentPool => _pool.Count;
@ -27,7 +28,10 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
GC.SuppressFinalize(needsFinalize); GC.SuppressFinalize(needsFinalize);
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this); var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
StartUpdateTimer(startTimer, updateRef); if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null)
_reclaimImmediately = true;
else
StartUpdateTimer(startTimer, updateRef);
} }
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef) static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
@ -90,7 +94,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
lock (_pool) lock (_pool)
{ {
_usage--; _usage--;
if (!_disposed) if (!_disposed && !_reclaimImmediately)
{ {
_pool.Push(item); _pool.Push(item);
return; return;

2
src/Avalonia.Base/Rendering/Composition/Visual.cs

@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition
} }
} }
internal Matrix4x4? TryGetServerTransform() internal Matrix4x4? TryGetServerGlobalTransform()
{ {
if (Root == null) if (Root == null)
return null; return null;

2
src/Avalonia.Base/StyledElement.cs

@ -333,7 +333,7 @@ namespace Avalonia
throw new InvalidOperationException("BeginInit was not called."); throw new InvalidOperationException("BeginInit was not called.");
} }
if (--_initCount == 0 && _logicalRoot != null) if (--_initCount == 0 && _logicalRoot is not null)
{ {
ApplyStyling(); ApplyStyling();
InitializeIfNeeded(); InitializeIfNeeded();

358
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -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;
}
}
}
}

180
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -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;
}
}
}

8
src/Avalonia.Controls/TrayIcon.cs

@ -61,6 +61,10 @@ namespace Avalonia.Controls
args.NewValue.Value.CollectionChanged += Icons_CollectionChanged; args.NewValue.Value.CollectionChanged += Icons_CollectionChanged;
} }
} }
else
{
throw new InvalidOperationException("TrayIcon.Icons must be set on the Application.");
}
}); });
var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized."); var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");
@ -123,9 +127,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsVisibleProperty = public static readonly StyledProperty<bool> IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner<TrayIcon>(); Visual.IsVisibleProperty.AddOwner<TrayIcon>();
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); public static void SetIcons(Application o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); public static TrayIcons GetIcons(Application o) => o.GetValue(IconsProperty);
/// <summary> /// <summary>
/// Gets or sets the <see cref="Command"/> property of a TrayIcon. /// Gets or sets the <see cref="Command"/> property of a TrayIcon.

1
src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml

@ -14,6 +14,7 @@
<Menu.Styles> <Menu.Styles>
<Style Selector="MenuItem"> <Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/> <Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/> <Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>

1
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -15,6 +15,7 @@
<Menu.Styles> <Menu.Styles>
<Style x:CompileBindings="False" Selector="MenuItem"> <Style x:CompileBindings="False" Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/> <Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/> <Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>

20
src/Windows/Avalonia.Win32/IconImpl.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using Avalonia.Platform; using Avalonia.Platform;
@ -8,31 +7,18 @@ namespace Avalonia.Win32
{ {
class IconImpl : IWindowIconImpl class IconImpl : IWindowIconImpl
{ {
private Bitmap bitmap; private readonly Icon icon;
private Icon icon;
public IconImpl(Bitmap bitmap)
{
this.bitmap = bitmap;
}
public IconImpl(Icon icon) public IconImpl(Icon icon)
{ {
this.icon = icon; this.icon = icon;
} }
public IntPtr HIcon => icon?.Handle ?? bitmap.GetHicon(); public IntPtr HIcon => icon.Handle;
public void Save(Stream outputStream) public void Save(Stream outputStream)
{ {
if (icon != null) icon.Save(outputStream);
{
icon.Save(outputStream);
}
else
{
bitmap.Save(outputStream, ImageFormat.Png);
}
} }
} }
} }

4
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -18,6 +18,8 @@ namespace Avalonia.Win32
[Unstable] [Unstable]
public class TrayIconImpl : ITrayIconImpl public class TrayIconImpl : ITrayIconImpl
{ {
private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon();
private readonly int _uniqueId; private readonly int _uniqueId;
private static int s_nextUniqueId; private static int s_nextUniqueId;
private bool _iconAdded; private bool _iconAdded;
@ -86,7 +88,7 @@ namespace Avalonia.Win32
uID = _uniqueId, uID = _uniqueId,
uFlags = NIF.TIP | NIF.MESSAGE, uFlags = NIF.TIP | NIF.MESSAGE,
uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE,
hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon, hIcon = _icon?.HIcon ?? s_emptyIcon,
szTip = _tooltipText ?? "" szTip = _tooltipText ?? ""
}; };

29
src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs

@ -1,6 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Utilities;
#nullable enable #nullable enable
@ -15,17 +19,28 @@ namespace Avalonia.Win32
_nativeMenu = nativeMenu; _nativeMenu = nativeMenu;
} }
private IEnumerable<MenuItem> Populate(NativeMenu nativeMenu) private AvaloniaList<MenuItem> Populate(NativeMenu nativeMenu)
{ {
var result = new AvaloniaList<MenuItem>();
foreach (var menuItem in nativeMenu.Items) foreach (var menuItem in nativeMenu.Items)
{ {
if (menuItem is NativeMenuItemSeparator) if (menuItem is NativeMenuItemSeparator)
{ {
yield return new MenuItem { Header = "-" }; result.Add(new MenuItem { Header = "-" });
} }
else if (menuItem is NativeMenuItem item) else if (menuItem is NativeMenuItem item)
{ {
var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter }; var newItem = new MenuItem
{
[!MenuItem.HeaderProperty] = item.GetObservable(NativeMenuItem.HeaderProperty).ToBinding(),
[!MenuItem.IconProperty] = item.GetObservable(NativeMenuItem.IconProperty)
.Select(i => i is {} bitmap ? new Image { Source = bitmap } : null).ToBinding(),
[!MenuItem.IsEnabledProperty] = item.GetObservable(NativeMenuItem.IsEnabledProperty).ToBinding(),
[!MenuItem.CommandProperty] = item.GetObservable(NativeMenuItem.CommandProperty).ToBinding(),
[!MenuItem.CommandParameterProperty] = item.GetObservable(NativeMenuItem.CommandParameterProperty).ToBinding(),
[!MenuItem.InputGestureProperty] = item.GetObservable(NativeMenuItem.GestureProperty).ToBinding()
};
if (item.Menu != null) if (item.Menu != null)
{ {
@ -33,15 +48,17 @@ namespace Avalonia.Win32
} }
else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge)
{ {
newItem.Click += (_, __) => bridge.RaiseClicked(); newItem.Click += (_, _) => bridge.RaiseClicked();
} }
yield return newItem; result.Add(newItem);
} }
} }
return result;
} }
public IEnumerable<MenuItem>? GetMenu() public AvaloniaList<MenuItem>? GetMenu()
{ {
if (_nativeMenu != null) if (_nativeMenu != null)
{ {

8
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -359,7 +359,7 @@ namespace Avalonia.Win32
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
bitmap.Save(memoryStream); bitmap.Save(memoryStream);
return new IconImpl(new System.Drawing.Bitmap(memoryStream)); return CreateIconImpl(memoryStream);
} }
} }
@ -367,11 +367,15 @@ namespace Avalonia.Win32
{ {
try try
{ {
// new Icon() will work only if stream is an "ico" file.
return new IconImpl(new System.Drawing.Icon(stream)); return new IconImpl(new System.Drawing.Icon(stream));
} }
catch (ArgumentException) catch (ArgumentException)
{ {
return new IconImpl(new System.Drawing.Bitmap(stream)); // Fallback to Bitmap creation and converting into a windows icon.
using var icon = new System.Drawing.Bitmap(stream);
var hIcon = icon.GetHicon();
return new IconImpl(System.Drawing.Icon.FromHandle(hIcon));
} }
} }

417
tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs

@ -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);
}
}

109
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs

@ -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));
}
}
}

208
tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs

@ -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();
}
}
}

318
tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs

@ -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;
}
}
}
}

3
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -6,6 +6,9 @@
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />

33
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@ -55,6 +55,39 @@ namespace Avalonia.Benchmarks.Layout
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
} }
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateControl()
{
var control = new Control();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateDecorator()
{
var control = new Decorator();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateScrollViewer()
{
var control = new ScrollViewer();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark] [Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
public void CreateButton() public void CreateButton()

489
tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs

@ -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;
}
}

27
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -4,6 +4,7 @@ using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Rendering;
using Moq; using Moq;
namespace Avalonia.UnitTests namespace Avalonia.UnitTests
@ -25,9 +26,33 @@ namespace Avalonia.UnitTests
return Mock.Of<IGeometryImpl>(x => x.Bounds == rect); return Mock.Of<IGeometryImpl>(x => x.Bounds == rect);
} }
class MockRenderTarget : IRenderTarget
{
public void Dispose()
{
}
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() =>
{
var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()))
.Returns(CreateDrawingContext(null));
return r.Object;
}
);
return m.Object;
}
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{ {
return Mock.Of<IRenderTarget>(); return new MockRenderTarget();
} }
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)

Loading…
Cancel
Save