diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index d0e1bd885e..fab4622303 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -43,6 +43,7 @@
+
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index b11a6027f2..9de7cb2dfe 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -23,6 +23,7 @@
+
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index 282973c26a..a571a0518b 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -24,11 +24,12 @@ public class CompositingRenderer : IRendererWithCompositor
DrawingContext _recordingContext;
private HashSet _dirty = new();
private HashSet _recalculateChildren = new();
- private readonly CompositionTarget _target;
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
+ internal CompositionTarget CompositionTarget;
+
///
/// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
///
@@ -40,8 +41,8 @@ public class CompositingRenderer : IRendererWithCompositor
_root = root;
_compositor = compositor;
_recordingContext = new DrawingContext(_recorder);
- _target = compositor.CreateCompositionTarget(root.CreateRenderTarget);
- _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
+ CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+ CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
_update = Update;
_invalidateScene = InvalidateScene;
}
@@ -49,15 +50,15 @@ public class CompositingRenderer : IRendererWithCompositor
///
public bool DrawFps
{
- get => _target.DrawFps;
- set => _target.DrawFps = value;
+ get => CompositionTarget.DrawFps;
+ set => CompositionTarget.DrawFps = value;
}
///
public bool DrawDirtyRects
{
- get => _target.DrawDirtyRects;
- set => _target.DrawDirtyRects = value;
+ get => CompositionTarget.DrawDirtyRects;
+ set => CompositionTarget.DrawDirtyRects = value;
}
///
@@ -81,12 +82,11 @@ public class CompositingRenderer : IRendererWithCompositor
///
public IEnumerable HitTest(Point p, IVisual root, Func? filter)
{
- var res = _target.TryHitTest(p, filter);
+ var res = CompositionTarget.TryHitTest(p, filter);
if(res == null)
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 (filter == null || filter(dv.Visual))
@@ -234,8 +234,8 @@ public class CompositingRenderer : IRendererWithCompositor
SyncChildren(v);
_dirty.Clear();
_recalculateChildren.Clear();
- _target.Size = _root.ClientSize;
- _target.Scaling = _root.RenderScaling;
+ CompositionTarget.Size = _root.ClientSize;
+ CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene);
}
@@ -246,24 +246,24 @@ public class CompositingRenderer : IRendererWithCompositor
public void Paint(Rect rect)
{
Update();
- _target.RequestRedraw();
+ CompositionTarget.RequestRedraw();
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
Compositor.RequestCommitAsync().Wait();
else
- _target.ImmediateUIThreadRender();
+ CompositionTarget.ImmediateUIThreadRender();
}
- public void Start() => _target.IsEnabled = true;
+ public void Start() => CompositionTarget.IsEnabled = true;
public void Stop()
{
- _target.IsEnabled = false;
+ CompositionTarget.IsEnabled = false;
}
public void Dispose()
{
Stop();
- _target.Dispose();
+ CompositionTarget.Dispose();
// Wait for the composition batch to be applied and rendered to guarantee that
// render target is not used anymore and can be safely disposed
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
index 47cfcd325b..49aea1c3dc 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
@@ -54,13 +54,20 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
internal override bool HitTest(Point pt, Func? filter)
{
- if (DrawList == null)
+ var custom = Visual as ICustomHitTest;
+ if (DrawList == null && custom == null)
return false;
if (filter != null && !filter(Visual))
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);
- foreach (var op in DrawList)
+ }
+
+ foreach (var op in DrawList!)
if (op.Item.HitTest(pt))
return true;
return false;
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
index 25bbd4dc88..01b2d0d5d9 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -62,7 +62,7 @@ namespace Avalonia.Rendering.Composition
bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
{
- var m = visual.TryGetServerTransform();
+ var m = visual.TryGetServerGlobalTransform();
if (m == null)
{
matrix = default;
@@ -73,52 +73,44 @@ namespace Avalonia.Rendering.Composition
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))
{
- v = v * m;
+ v = globalPoint * m;
return true;
}
return false;
}
- bool HitTestCore(CompositionVisual visual, Point point, PooledList result,
+ void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList result,
Func? filter)
{
- //TODO: Check readback too
if (visual.Visible == false)
- return false;
- if (!TryTransformTo(visual, ref point))
- return false;
+ return;
+ if (!TryTransformTo(visual, globalPoint, out var point))
+ return;
if (visual.ClipToBounds
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
- return false;
- if (visual.Clip?.FillContains(point) == false)
- return false;
+ return;
- bool success = false;
- // Hit-test the current node
- if (visual.HitTest(point, filter))
- {
- result.Add(visual);
- success = true;
- }
-
- // Inspect children too
+ if (visual.Clip?.FillContains(point) == false)
+ return;
+
+ // Inspect children
if (visual is CompositionContainerVisual cv)
for (var c = cv.Children.Count - 1; c >= 0; c--)
{
var ch = cv.Children[c];
- var hit = HitTestCore(ch, point, result, filter);
- if (hit)
- return true;
+ HitTestCore(ch, globalPoint, result, filter);
}
-
- return success;
-
+
+ // Hit-test the current node
+ if (visual.HitTest(point, filter))
+ result.Add(visual);
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
new file mode 100644
index 0000000000..045a4f8cc6
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
@@ -0,0 +1,6 @@
+namespace Avalonia.Rendering.Composition;
+
+internal interface ICompositionTargetDebugEvents
+{
+ void RectInvalidated(Rect rc);
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
index 0fde86e484..882b66bf70 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
@@ -33,7 +33,7 @@ namespace Avalonia.Rendering.Composition.Server
private HashSet _attachedVisuals = new();
private Queue _adornerUpdateQueue = new();
-
+ public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; }
@@ -173,6 +173,7 @@ namespace Avalonia.Rendering.Composition.Server
if(rect.IsEmpty)
return;
var snapped = SnapToDevicePixels(rect, Scaling);
+ DebugEvents?.RectInvalidated(rect);
_dirtyRect = _dirtyRect.Union(snapped);
_redrawRequested = true;
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 6fdf105e58..c0e487f209 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -119,14 +119,15 @@ namespace Avalonia.Rendering.Composition.Server
var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
-
- var dirtyOldBounds = false;
+
if (_parent?.IsDirtyComposition == true)
{
IsDirtyComposition = true;
_isDirtyForUpdate = true;
- dirtyOldBounds = true;
}
+
+ var invalidateOldBounds = _isDirtyForUpdate;
+ var invalidateNewBounds = _isDirtyForUpdate;
GlobalTransformMatrix = newTransform;
@@ -157,30 +158,20 @@ namespace Avalonia.Rendering.Composition.Server
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
- IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface &&
+ IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
!_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)
- _isDirtyForUpdate = true;
+ if (wasVisible != IsVisibleInFrame || positionChanged)
+ {
+ invalidateOldBounds |= wasVisible;
+ invalidateNewBounds |= IsVisibleInFrame;
}
-
+
// Invalidate new bounds
- if (IsVisibleInFrame && _isDirtyForUpdate)
- {
- dirtyOldBounds = true;
+ if (invalidateNewBounds)
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds));
- }
- if (dirtyOldBounds && wasVisible)
+ if (invalidateOldBounds)
AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds));
@@ -190,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server
var i = Root!.Readback;
ref var readback = ref GetReadback(i.WriteIndex);
readback.Revision = root.Revision;
- readback.Matrix = CombinedTransformMatrix;
+ readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame;
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
index 32b4ed3026..b0a89c6f92 100644
--- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
+using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition.Transport;
@@ -17,6 +17,7 @@ internal abstract class BatchStreamPoolBase : IDisposable
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
+ bool _reclaimImmediately;
public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count;
@@ -27,7 +28,10 @@ internal abstract class BatchStreamPoolBase : IDisposable
GC.SuppressFinalize(needsFinalize);
var updateRef = new WeakReference>(this);
- StartUpdateTimer(startTimer, updateRef);
+ if (AvaloniaLocator.Current.GetService() == null)
+ _reclaimImmediately = true;
+ else
+ StartUpdateTimer(startTimer, updateRef);
}
static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef)
@@ -90,7 +94,7 @@ internal abstract class BatchStreamPoolBase : IDisposable
lock (_pool)
{
_usage--;
- if (!_disposed)
+ if (!_disposed && !_reclaimImmediately)
{
_pool.Push(item);
return;
diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs
index f9e1eae2ab..7ebbb0aa96 100644
--- a/src/Avalonia.Base/Rendering/Composition/Visual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition
}
}
- internal Matrix4x4? TryGetServerTransform()
+ internal Matrix4x4? TryGetServerGlobalTransform()
{
if (Root == null)
return null;
diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index bd012f74d6..002a0162c2 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -333,7 +333,7 @@ namespace Avalonia
throw new InvalidOperationException("BeginInit was not called.");
}
- if (--_initCount == 0 && _logicalRoot != null)
+ if (--_initCount == 0 && _logicalRoot is not null)
{
ApplyStyling();
InitializeIfNeeded();
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
new file mode 100644
index 0000000000..5c7d7d8605
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
@@ -0,0 +1,358 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// Stores values with as key.
+ ///
+ /// Stored value type.
+ ///
+ /// This struct implements the most commonly-used part of the dictionary API, but does
+ /// not implement . In particular, this struct
+ /// is not enumerable. Enumeration is intended to be done by index for better performance.
+ ///
+ internal struct AvaloniaPropertyDictionary
+ {
+ private const int DefaultInitialCapacity = 4;
+ private Entry[]? _entries;
+ private int _entryCount;
+
+ ///
+ /// Initializes a new instance of the
+ /// class that is empty and has the default initial capacity.
+ ///
+ public AvaloniaPropertyDictionary()
+ {
+ _entries = null;
+ _entryCount = 0;
+ }
+
+ ///
+ /// Initializes a new instance of the
+ /// class that is empty and has the specified initial capacity.
+ ///
+ ///
+ /// The initial number of elements that the collection can contain.
+ ///
+ public AvaloniaPropertyDictionary(int capactity)
+ {
+ _entries = new Entry[capactity];
+ _entryCount = 0;
+ }
+
+ ///
+ /// Gets the number of key/value pairs contained in the collection.
+ ///
+ public int Count => _entryCount;
+
+ ///
+ /// Gets or sets the value associated with the specified key.
+ ///
+ /// The key to get or set.
+ ///
+ /// The value associated with the specified key. If the key is not found, a get operation
+ /// throws a , and a set operation creates a
+ /// new element for the specified key.
+ ///
+ ///
+ /// The key does not exist in the collection.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets the value at the specified index.
+ ///
+ ///
+ /// The index of the entry, between 0 and - 1.
+ ///
+ public TValue this[int index]
+ {
+ get
+ {
+ if (index >= _entryCount)
+ ThrowOutOfRange();
+ return _entries![index].Value;
+ }
+ }
+
+ ///
+ /// Adds the specified key and value to the dictionary.
+ ///
+ /// The key.
+ /// The value of the element to add.
+ public void Add(AvaloniaProperty property, TValue value)
+ {
+ if (TryGetEntry(property.Id, out var index))
+ ThrowDuplicate();
+ InsertEntry(new Entry(property, value), index);
+ }
+
+ ///
+ /// Removes all keys and values from the collection.
+ ///
+ ///
+ /// The Count property is set to 0, and references to other objects from elements of the
+ /// collection are also released. The capacity remains unchanged.
+ ///
+ public void Clear()
+ {
+ if (_entries is not null)
+ {
+ Array.Clear(_entries, 0, _entries.Length);
+ _entryCount = 0;
+ }
+ }
+
+ ///
+ /// Determines whether the collection contains the specified key.
+ ///
+ /// The key.
+ public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _);
+
+ ///
+ /// Gets the key and value at the specified index.
+ ///
+ ///
+ /// The index of the entry, between 0 and - 1.
+ ///
+ ///
+ /// When this method returns, contains the key at the specified index.
+ ///
+ ///
+ /// When this method returns, contains the value at the specified index.
+ ///
+ 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;
+ }
+
+ ///
+ /// Removes the value of the specified key from the collection.
+ ///
+ /// The key.
+ ///
+ /// true if the element is successfully found and removed; otherwise, false. This method
+ /// returns false if key is not found in the collection.
+ ///
+ 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;
+ }
+
+ ///
+ /// Removes the value of the specified key from the collection, and copies the element to
+ /// the value parameter.
+ ///
+ /// The key.
+ /// The removed element.
+ ///
+ /// true if the element is successfully found and removed; otherwise, false. This method
+ /// returns false if key is not found in the collection.
+ ///
+ 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;
+ }
+
+ ///
+ /// Attempts to add the specified key and value to the collection.
+ ///
+ /// The key.
+ /// The value of the element to add.
+ ///
+ public bool TryAdd(AvaloniaProperty property, TValue value)
+ {
+ if (TryGetEntry(property.Id, out var index))
+ return false;
+ InsertEntry(new Entry(property, value), index);
+ return true;
+ }
+
+ ///
+ /// Gets the value associated with the specified key.
+ ///
+ /// The property key.
+ ///
+ /// When this method returns, contains the value associated with the specified key,
+ /// if the property is found; otherwise, null. This parameter is passed uninitialized.
+ ///
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
deleted file mode 100644
index d9cb57baea..0000000000
--- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-
-namespace Avalonia.Utilities
-{
- ///
- /// Stores values with as key.
- ///
- /// Stored value type.
- internal sealed class AvaloniaPropertyValueStore
- {
- // 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;
- }
- }
-}
diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs
index 20c8dff8ea..41a1abd838 100644
--- a/src/Avalonia.Controls/TrayIcon.cs
+++ b/src/Avalonia.Controls/TrayIcon.cs
@@ -61,6 +61,10 @@ namespace Avalonia.Controls
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.");
@@ -123,9 +127,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner();
- 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);
///
/// Gets or sets the property of a TrayIcon.
diff --git a/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
index 81bd8f39c5..3b0019eea1 100644
--- a/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
+++ b/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
@@ -14,6 +14,7 @@