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