From 394a5be4026afcf41202d876fb29dcf4beb8ab7c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 17 Dec 2021 14:52:22 +0100 Subject: [PATCH 001/176] OSX handle CMD+key up combinations in Avalonia --- native/Avalonia.Native/src/OSX/app.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..05b129baca 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,6 +73,11 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } From 213cc3429b7d91a2e84c1c2978a28d82ccb2f1ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:27:34 +0300 Subject: [PATCH 002/176] Visual now uses WeakEvent too --- src/Avalonia.Visuals/Visual.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 324b253a0f..41c9f76dbc 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -91,11 +91,17 @@ namespace Avalonia /// public static readonly StyledProperty ZIndexProperty = AvaloniaProperty.Register(nameof(ZIndex)); + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); private Rect _bounds; private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private IVisual? _visualParent; + private WeakEventSubscriber? _affectsRenderWeakSubscriber; /// /// Initializes static members of the class. @@ -352,12 +358,21 @@ namespace Avalonia { if (e.OldValue is IAffectsRender oldValue) { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); } if (e.NewValue is IAffectsRender newValue) { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new WeakEventSubscriber(); + sender._affectsRenderWeakSubscriber.Event += delegate + { + sender.InvalidateVisual(); + }; + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); } sender.InvalidateVisual(); @@ -608,8 +623,6 @@ namespace Avalonia OnVisualParentChanged(old, value); } - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - /// /// Called when the collection changes. /// From 7e1d9dbc72eb9c6371f8bf33800caab19916cf20 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:28:49 +0300 Subject: [PATCH 003/176] Optimized WeakEvent (9332ms to 5ms using #6660 bench) --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 0b32015a8a..21c165afae 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (!_subscriptions.TryGetValue(target, out var subscription)) _subscriptions.Add(target, subscription = new Subscription(this, target)); - subscription.Add(new WeakReference>(subscriber)); + subscription.Add(subscriber); } public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) @@ -51,11 +52,61 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly TSender _target; private readonly Action _compact; - private WeakReference>?[] _data = - new WeakReference>[16]; + struct Entry + { + WeakReference>? _reference; + int _hashCode; + + public Entry(IWeakEventSubscriber r) + { + if (r == null) + { + _reference = null; + _hashCode = 0; + return; + } + + _hashCode = r.GetHashCode(); + _reference = new WeakReference>(r); + } + + public bool IsEmpty + { + get + { + if (_reference == null) + return false; + if (_reference.TryGetTarget(out var target)) + return true; + _reference = null; + return false; + } + } + + public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) + { + if (_reference == null) + { + target = null!; + return false; + } + return _reference.TryGetTarget(out target); + } + + public bool Equals(IWeakEventSubscriber r) + { + if (_reference == null || r.GetHashCode() != _hashCode) + return false; + return _reference.TryGetTarget(out var target) && target == r; + } + } + + private Entry[] _data = + new Entry[16]; private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; + private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -71,17 +122,17 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _ev._subscriptions.Remove(_target); } - public void Add(WeakReference> s) + public void Add(IWeakEventSubscriber s) { if (_count == _data.Length) { //Extend capacity - var extendedData = new WeakReference>?[_data.Length * 2]; + var extendedData = new Entry[_data.Length * 2]; Array.Copy(_data, extendedData, _data.Length); _data = extendedData; } - _data[_count] = s; + _data[_count] = new(s); _count++; } @@ -93,16 +144,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { var reference = _data[c]; - if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + if (reference.Equals(s)) { - _data[c] = null; + _data[c] = default; removed = true; + break; } } if (removed) { + _removedSinceLastCompact++; ScheduleCompact(); + + if (_removedSinceLastCompact > 500) + Compact(); } } @@ -116,18 +172,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { + if(!_compactScheduled || _removedSinceLastCompact == 0) + return; _compactScheduled = false; + _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { - var r = _data[c]; + ref var r = ref _data[c]; //Mark current index as first empty - if (r == null && empty == -1) + if (r.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (r != null && empty != -1) + if (!r.IsEmpty && empty != -1) { - _data[c] = null; + _data[c] = default; _data[empty] = r; empty++; } @@ -145,7 +204,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event for (var c = 0; c < _count; c++) { var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) + if (r.TryGetTarget(out var sub)) sub!.OnEvent(_target, _ev, eventArgs); else needCompact = true; From dca2ca5940f0c768529f97b89535ed172ef44403 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:29:14 +0300 Subject: [PATCH 004/176] Don't implement IWeakEventSubscriber directly on public types --- .../Utilities/IWeakEventSubscriber.cs | 10 +++++++ .../Repeater/ItemsRepeater.cs | 30 ++++++++++--------- src/Avalonia.Controls/TopLevel.cs | 16 +++++----- src/Avalonia.Visuals/Media/Pen.cs | 25 +++++++++------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index e48c0cb111..2a24376592 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -9,4 +9,14 @@ namespace Avalonia.Utilities; public interface IWeakEventSubscriber where TEventArgs : EventArgs { void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} + +public class WeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + public event Action? Event; + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + + } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index b40cf26df5..6d2f4144eb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -68,6 +68,7 @@ namespace Avalonia.Controls private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + private WeakEventSubscriber _layoutWeakSubscriber = new(); /// /// Initializes a new instance of the class. @@ -77,6 +78,15 @@ namespace Avalonia.Controls _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + + _layoutWeakSubscriber.Event += (_, ev, e) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + }; + OnLayoutChanged(null, Layout); } @@ -723,8 +733,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -742,8 +752,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -793,15 +803,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index eaee5bdb50..76de113290 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -33,8 +33,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -90,6 +89,7 @@ namespace Avalonia.Controls private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; + private WeakEventSubscriber _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -184,7 +184,12 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new(); + _resourcesChangesSubscriber.Event += (_, __, e) => + { + ((ILogical)this).NotifyResourcesChanged(e); + }; + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; @@ -289,11 +294,6 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 65ba851100..f0a0d24248 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber + public sealed class Pen : AvaloniaObject, IPen { /// /// Defines the property. @@ -48,7 +48,8 @@ namespace Avalonia.Media private EventHandler? _invalidated; private IAffectsRender? _subscribedToBrush; private IAffectsRender? _subscribedToDashes; - + private WeakEventSubscriber? _weakSubscriber; + /// /// Initializes a new instance of the class. /// @@ -207,13 +208,23 @@ namespace Avalonia.Media { if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(field, this); + if (_weakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); field = null; } if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { - InvalidatedWeakEvent.Subscribe(affectsRender, this); + if (_weakSubscriber == null) + { + _weakSubscriber = new WeakEventSubscriber(); + _weakSubscriber.Event += (_, ev, __) => + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + }; + } + InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); field = affectsRender; } } @@ -223,11 +234,5 @@ namespace Avalonia.Media UpdateSubscription(ref _subscribedToBrush, Brush); UpdateSubscription(ref _subscribedToDashes, DashStyle); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if (ev == InvalidatedWeakEvent) - _invalidated?.Invoke(this, EventArgs.Empty); - } } } From bb9f5e75f33f460dcb729678b2c3a8a41fc81fb0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 19:55:46 +0300 Subject: [PATCH 005/176] Fixed WeakEvent Compact --- src/Avalonia.Base/Utilities/WeakEvent.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 21c165afae..5d5a1cb55b 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -75,7 +75,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event get { if (_reference == null) - return false; + return true; if (_reference.TryGetTarget(out var target)) return true; _reference = null; @@ -179,15 +179,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event int empty = -1; for (var c = 0; c < _count; c++) { - ref var r = ref _data[c]; + ref var currentRef = ref _data[c]; //Mark current index as first empty - if (r.IsEmpty && empty == -1) + if (currentRef.IsEmpty && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (!r.IsEmpty && empty != -1) + if (!currentRef.IsEmpty && empty != -1) { - _data[c] = default; - _data[empty] = r; + _data[empty] = currentRef; + currentRef = default; empty++; } } From c73fcd25a9cfcaa17e7295b6b01080c585fe3c78 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:09:24 +0300 Subject: [PATCH 006/176] Fixed tests --- src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs | 2 +- src/Avalonia.Base/Utilities/WeakEvent.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index 2a24376592..57060853c8 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -17,6 +17,6 @@ public class WeakEventSubscriber : IWeakEventSubscriber void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) { - + Event?.Invoke(sender, ev, e); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 5d5a1cb55b..bcee7f89f7 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -76,10 +76,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (_reference == null) return true; - if (_reference.TryGetTarget(out var target)) - return true; + if (_reference.TryGetTarget(out _)) + return false; _reference = null; - return false; + return true; } } From a4fa74977f2a9c7c8cb6fc1f8b86c88f00543463 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 20:17:56 +0300 Subject: [PATCH 007/176] Removed immediate compact since it makes things worse --- src/Avalonia.Base/Utilities/WeakEvent.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index bcee7f89f7..b27ea9f455 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -106,7 +106,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private int _count; private readonly Action _unsubscribe; private bool _compactScheduled; - private int _removedSinceLastCompact; public Subscription(WeakEvent ev, TSender target) { @@ -154,11 +153,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - _removedSinceLastCompact++; ScheduleCompact(); - - if (_removedSinceLastCompact > 500) - Compact(); } } @@ -172,10 +167,9 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { - if(!_compactScheduled || _removedSinceLastCompact == 0) + if(!_compactScheduled) return; _compactScheduled = false; - _removedSinceLastCompact = 0; int empty = -1; for (var c = 0; c < _count; c++) { From 8103f2a0b1c7dbe5f0c620229442ce8a7fe619d3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 16 Jan 2022 23:47:12 +0300 Subject: [PATCH 008/176] Use Dictionary for more than 8 WeakEvent subscribers --- src/Avalonia.Base/Utilities/WeakEvent.cs | 85 ++----- src/Avalonia.Base/Utilities/WeakHashList.cs | 236 ++++++++++++++++++++ 2 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/WeakHashList.cs diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index b27ea9f455..1335d7e9b8 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -101,11 +101,10 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } } - private Entry[] _data = - new Entry[16]; - private int _count; private readonly Action _unsubscribe; + private readonly WeakHashList> _list = new(); private bool _compactScheduled; + private bool _destroyed; public Subscription(WeakEvent ev, TSender target) { @@ -117,49 +116,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Destroy() { + if(_destroyed) + return; + _destroyed = true; _unsubscribe(); _ev._subscriptions.Remove(_target); } - public void Add(IWeakEventSubscriber s) - { - if (_count == _data.Length) - { - //Extend capacity - var extendedData = new Entry[_data.Length * 2]; - Array.Copy(_data, extendedData, _data.Length); - _data = extendedData; - } - - _data[_count] = new(s); - _count++; - } + public void Add(IWeakEventSubscriber s) => _list.Add(s); public void Remove(IWeakEventSubscriber s) { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - - if (reference.Equals(s)) - { - _data[c] = default; - removed = true; - break; - } - } - - if (removed) - { + _list.Remove(s); + if(_list.IsEmpty) + Destroy(); + else if(_list.NeedCompact && _compactScheduled) ScheduleCompact(); - } } void ScheduleCompact() { - if(_compactScheduled) + if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); @@ -170,42 +147,24 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(!_compactScheduled) return; _compactScheduled = false; - int empty = -1; - for (var c = 0; c < _count; c++) - { - ref var currentRef = ref _data[c]; - //Mark current index as first empty - if (currentRef.IsEmpty && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (!currentRef.IsEmpty && empty != -1) - { - _data[empty] = currentRef; - currentRef = default; - empty++; - } - } - - if (empty != -1) - _count = empty; - if (_count == 0) + _list.Compact(); + if (_list.IsEmpty) Destroy(); } void OnEvent(object? sender, TEventArgs eventArgs) { - var needCompact = false; - for (var c = 0; c < _count; c++) + var alive = _list.GetAlive(); + if(alive == null) + Destroy(); + else { - var r = _data[c]; - if (r.TryGetTarget(out var sub)) - sub!.OnEvent(_target, _ev, eventArgs); - else - needCompact = true; + foreach(var item in alive) + item.OnEvent(_target, _ev, eventArgs); + WeakHashList>.ReturnToSharedPool(alive); + if(_list.NeedCompact && !_compactScheduled) + ScheduleCompact(); } - - if (needCompact) - ScheduleCompact(); } } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs new file mode 100644 index 0000000000..32668872da --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Utilities; + +internal class WeakHashList where T : class +{ + private struct Key + { + public WeakReference? Weak; + public T? Strong; + public int HashCode; + + public static Key MakeStrong(T r) => new() + { + HashCode = r.GetHashCode(), + Strong = r + }; + + public static Key MakeWeak(T r) => new() + { + HashCode = r.GetHashCode(), + Weak = new WeakReference(r) + }; + + public override int GetHashCode() => HashCode; + } + + class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.HashCode != y.HashCode) + return false; + if (x.Strong != null) + { + if (y.Strong != null) + return x.Strong == y.Strong; + if (y.Weak == null) + return false; + return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; + } + else if (y.Strong != null) + { + if (x.Weak == null) + return false; + return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; + } + else + { + if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) + return y.Weak?.TryGetTarget(out _) != true; + return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; + } + } + + public int GetHashCode(Key obj) => obj.HashCode; + public static KeyComparer Instance = new(); + } + + Dictionary? _dic; + WeakReference?[]? _arr; + int _arrCount; + + public bool IsEmpty => _dic == null || _dic.Count == 0; + public bool NeedCompact { get; private set; } + + public void Add(T item) + { + if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + if (_dic.TryGetValue(strongKey, out var cnt)) + _dic[strongKey] = cnt + 1; + else + _dic[Key.MakeWeak(item)] = 1; + return; + } + + if (_arr == null) + _arr = new WeakReference[8]; + + if (_arrCount < _arr.Length) + { + _arr[_arrCount] = new WeakReference(item); + _arrCount++; + return; + } + + // Check if something is dead + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]!.TryGetTarget(out _) == false) + { + _arr[c] = new WeakReference(item); + return; + } + } + + _dic = new Dictionary(KeyComparer.Instance); + foreach (var existing in _arr) + { + if (existing!.TryGetTarget(out var target)) + Add(target); + } + _arr = null; + + } + + public void Remove(T item) + { + if (_arr != null) + { + for (var c = 0; c < _arr.Length; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true && target == item) + { + _arr[c] = null; + Compact(); + return; + } + } + } + else if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + + if (_dic.TryGetValue(strongKey, out var cnt)) + { + if (cnt > 1) + { + _dic[strongKey] = cnt - 1; + return; + } + } + + _dic.Remove(strongKey); + } + } + + private void ArrCompact() + { + if (_arr != null) + { + int empty = -1; + for (var c = 0; c < _arrCount; c++) + { + var r = _arr[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _arr[c] = null; + _arr[empty] = r; + empty++; + } + } + + if (empty != -1) + _arrCount = empty; + } + } + + public void Compact() + { + if (_dic != null) + { + PooledList? toRemove = null; + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out _) != true) + (toRemove ??= new PooledList()).Add(kvp.Key); + } + + if (toRemove != null) + { + foreach (var k in toRemove) + _dic.Remove(k); + toRemove.Dispose(); + } + } + } + + private static readonly Stack> s_listPool = new(); + + public static void ReturnToSharedPool(PooledList list) + { + list.Clear(); + s_listPool.Push(list); + } + + public PooledList? GetAlive(Func>? factory = null) + { + PooledList? pooled = null; + if (_arr != null) + { + bool needCompact = false; + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())).Add(target!); + else + { + _arr[c] = null; + needCompact = true; + } + } + if(needCompact) + ArrCompact(); + return pooled; + } + if (_dic != null) + { + + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())) + .Add(target!); + else + NeedCompact = true; + } + } + + return pooled; + } +} \ No newline at end of file From 27ecb055086d437516ec6685fc22016d77ebed94 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 17 Jan 2022 11:38:20 +0300 Subject: [PATCH 009/176] Use PooledList.Span for enumeration --- src/Avalonia.Base/Utilities/WeakEvent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 1335d7e9b8..e72606bf70 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -159,7 +159,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); else { - foreach(var item in alive) + foreach(var item in alive.Span) item.OnEvent(_target, _ev, eventArgs); WeakHashList>.ReturnToSharedPool(alive); if(_list.NeedCompact && !_compactScheduled) From b2b4014b1c6e5805e7c2330582124aa2f73f34ca Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 3 Feb 2022 14:58:28 +0300 Subject: [PATCH 010/176] Make DispatcherPriority to be a struct with static readonly field values In future versions this will allow us to extend priority lists without breaking binary compatibility --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 +- .../Threading/AvaloniaScheduler.cs | 2 +- src/Avalonia.Base/Threading/Dispatcher.cs | 12 +-- .../Threading/DispatcherPriority.cs | 97 ++++++++++++++----- .../Threading/DispatcherTimer.cs | 4 +- src/Avalonia.Base/Threading/IDispatcher.cs | 12 +-- .../Diagnostics/ViewModels/MainViewModel.cs | 3 +- .../Avalonia.UnitTests/ImmediateDispatcher.cs | 12 +-- 8 files changed, 96 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt index 4701a83175..7f378d2f65 100644 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -1,3 +1,4 @@ Compat issues with assembly Avalonia.Base: +MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Threading.DispatcherPriority.value__' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. -Total Issues: 1 +Total Issues: 2 diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 397826df53..6423d86e7c 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -46,7 +46,7 @@ namespace Avalonia.Threading { composite.Add(action(this, state)); } - }, DispatcherPriority.DataBind); + }, DispatcherPriority.Background); composite.Add(cancellation); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 49cee441d0..2eb2e7c01f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -83,42 +83,42 @@ namespace Avalonia.Threading _jobRunner.HasJobsWithPriority(minimumPriority); /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); return _jobRunner.InvokeAsync(action, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, priority); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, arg, priority); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a2b4b86bac..a93e4f406d 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,74 +1,121 @@ +using System; + namespace Avalonia.Threading { /// /// Defines the priorities with which jobs can be invoked on a . /// - // TODO: These are copied from WPF - many won't apply to Avalonia. - public enum DispatcherPriority + public readonly struct DispatcherPriority : IEquatable, IComparable { + /// + /// The integer value of the priority + /// + public int Value { get; } + + private DispatcherPriority(int value) + { + Value = value; + } + /// /// Minimum possible priority /// - MinValue = 1, - + public static readonly DispatcherPriority MinValue = new(0); + /// /// The job will be processed when the system is idle. /// - SystemIdle = 1, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; /// /// The job will be processed when the application is idle. /// - ApplicationIdle = 2, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; /// /// The job will be processed after background operations have completed. /// - ContextIdle = 3, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; /// - /// The job will be processed after other non-idle operations have completed. + /// The job will be processed with normal priority. /// - Background = 4, + public static readonly DispatcherPriority Normal = MinValue; /// - /// The job will be processed with the same priority as input. + /// The job will be processed after other non-idle operations have completed. /// - Input = 5, + public static readonly DispatcherPriority Background = new(1); /// - /// The job will be processed after layout and render but before input. + /// The job will be processed with the same priority as input. /// - Loaded = 6, + public static readonly DispatcherPriority Input = new(2); /// - /// The job will be processed with the same priority as render. + /// The job will be processed after layout and render but before input. /// - Render = 7, + public static readonly DispatcherPriority Loaded = new(3); /// /// The job will be processed with the same priority as render. /// - Layout = 8, - + public static readonly DispatcherPriority Render = new(5); + /// - /// The job will be processed with the same priority as data binding. + /// The job will be processed with the same priority as render. /// - DataBind = 9, + public static readonly DispatcherPriority Layout = new(6); /// - /// The job will be processed with normal priority. + /// The job will be processed with the same priority as data binding. /// - Normal = 10, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; /// /// The job will be processed before other asynchronous operations. /// - Send = 11, - + public static readonly DispatcherPriority Send = new(7); + /// /// Maximum possible priority /// - MaxValue = 11 + public static readonly DispatcherPriority MaxValue = Send; + + // Note: unlike ctor this one is validating + public static DispatcherPriority FromValue(int value) + { + if (value < MinValue.Value || value > MaxValue.Value) + throw new ArgumentOutOfRangeException(nameof(value)); + return new DispatcherPriority(value); + } + + public static implicit operator int(DispatcherPriority priority) => priority.Value; + + public static implicit operator DispatcherPriority(int value) => FromValue(value); + + /// + public bool Equals(DispatcherPriority other) => Value == other.Value; + + /// + public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other); + + /// + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(DispatcherPriority left, DispatcherPriority right) => left.Value == right.Value; + + public static bool operator !=(DispatcherPriority left, DispatcherPriority right) => left.Value != right.Value; + + public static bool operator <(DispatcherPriority left, DispatcherPriority right) => left.Value < right.Value; + + public static bool operator >(DispatcherPriority left, DispatcherPriority right) => left.Value > right.Value; + + public static bool operator <=(DispatcherPriority left, DispatcherPriority right) => left.Value <= right.Value; + + public static bool operator >=(DispatcherPriority left, DispatcherPriority right) => left.Value >= right.Value; + + /// + public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 93023b90a5..0c25d89722 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -123,7 +123,7 @@ namespace Avalonia.Threading /// The interval at which to tick. /// The priority to use. /// An used to cancel the timer. - public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) { var timer = new DispatcherTimer(priority) { Interval = interval }; @@ -152,7 +152,7 @@ namespace Avalonia.Threading public static IDisposable RunOnce( Action action, TimeSpan interval, - DispatcherPriority priority = DispatcherPriority.Normal) + DispatcherPriority priority = default) { interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index cd5add70d4..eccd42bd4e 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -24,7 +24,7 @@ namespace Avalonia.Threading /// /// The method. /// The priority with which to invoke the method. - void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, DispatcherPriority priority = default); /// /// Posts an action that will be invoked on the dispatcher thread. @@ -33,7 +33,7 @@ namespace Avalonia.Threading /// The method to call. /// The argument of method to call. /// The priority with which to invoke the method. - void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, T arg, DispatcherPriority priority = default); /// /// Invokes a action on the dispatcher thread. @@ -41,7 +41,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Action action, DispatcherPriority priority = default); /// /// Invokes a method on the dispatcher thread. @@ -49,7 +49,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -58,7 +58,7 @@ namespace Avalonia.Threading /// The work to execute asynchronously. /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -67,6 +67,6 @@ namespace Avalonia.Threading /// The work to execute asynchronously. /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func> function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..140515eb40 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -163,8 +163,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch { } }, - TimeSpan.FromMilliseconds(0), - DispatcherPriority.ApplicationIdle); + TimeSpan.FromMilliseconds(0)); } RaiseAndSetIfChanged(ref _content, value); diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs index 5f0d41590f..03c89732f3 100644 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs @@ -16,39 +16,39 @@ namespace Avalonia.UnitTests } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority) { action(); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority) { action(arg); } /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority) { action(); return Task.CompletedTask; } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { var result = function(); return Task.FromResult(result); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { return function(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority) { return function(); } From 3eddf5cac4d081481675412ad7bdf0cc500e0b60 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Fri, 11 Mar 2022 14:03:42 +0200 Subject: [PATCH 011/176] first work --- samples/ControlCatalog/MainView.xaml | 8 +++ samples/ControlCatalog/MainView.xaml.cs | 9 +++ src/Avalonia.Controls/Control.cs | 74 +++++++++++++++++++++++++ src/Avalonia.Controls/TextBlock.cs | 2 + src/Avalonia.Controls/TextBox.cs | 2 + 5 files changed, 95 insertions(+) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index facce2aa82..35fe45a25f 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -187,6 +187,14 @@ Mica + + + LeftToRight + RightToLeft + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 79cf07c8d9..e8ea39abbb 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -76,6 +76,15 @@ namespace ControlCatalog } }; + var flowDirections = this.Find("FlowDirection"); + flowDirections.SelectionChanged += (sender, e) => + { + if (flowDirections.SelectedItem is FlowDirection flowDirection) + { + this.FlowDirection = flowDirection; + } + }; + var decorations = this.Find("Decorations"); decorations.SelectionChanged += (sender, e) => { diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index cec662aad8..265f46be5e 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -309,5 +309,79 @@ namespace Avalonia.Controls } } } + + static Control() + { + AffectsArrange(FlowDirectionProperty); + } + + private bool _mirrorApplied; + + protected virtual bool ShouldBeMirroredIfRightToLeft() + { + if (Parent is Control parent) + { + return parent.ShouldBeMirroredIfRightToLeft(); + } + else + { + return true; + } + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + FlowDirection parentFD = FlowDirection.LeftToRight; + FlowDirection thisFD = FlowDirection; + bool shouldBeMirroredIfRightToLeft = ShouldBeMirroredIfRightToLeft(); + + if (Parent is Control control) + { + parentFD = control.FlowDirection; + } + + bool shouldMirror; + if (shouldBeMirroredIfRightToLeft) + { + shouldMirror = ShuoldApplyMirrorTransform(parentFD, thisFD); + if (this is PopupRoot && thisFD == FlowDirection.RightToLeft) + { + shouldMirror = true; + } + } + else + { + shouldMirror = ShuoldApplyMirrorTransform(parentFD, FlowDirection.LeftToRight); + } + + if (shouldMirror) + { + ApplyMirrorTransform(); + } + else + { + //RenderTransform = null; + } + } + + private void ApplyMirrorTransform() + { + if (_mirrorApplied) + { + return; + } + + var transform = new MatrixTransform(new Avalonia.Matrix(-1, 0, 0, 1, 0.0, 0.0)); + RenderTransform = transform; + _mirrorApplied = true; + } + + internal static bool ShuoldApplyMirrorTransform(FlowDirection parentFD, FlowDirection thisFD) + { + return ((parentFD == FlowDirection.LeftToRight && thisFD == FlowDirection.RightToLeft) || + (parentFD == FlowDirection.RightToLeft && thisFD == FlowDirection.LeftToRight)); + } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 09f22612de..d29f094c38 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -612,5 +612,7 @@ namespace Avalonia.Controls { InvalidateTextLayout(); } + + protected override bool ShouldBeMirroredIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 4d71717776..76f7a185fe 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1504,5 +1504,7 @@ namespace Avalonia.Controls } } } + + protected override bool ShouldBeMirroredIfRightToLeft() => false; } } From baf77db493e29c0e880e37dd686dde693cddc015 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Sat, 12 Mar 2022 20:02:19 +0200 Subject: [PATCH 012/176] Merge branch 'master' into feature/flowDirectionImpl --- samples/ControlCatalog/Pages/ScreenPage.cs | 2 ++ src/Avalonia.Controls/Control.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index caad8b0854..1fc37d903d 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -78,5 +78,7 @@ namespace ControlCatalog.Pages return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Green); } + + protected override bool ShouldBeMirroredIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 265f46be5e..16d6c3f4c5 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -346,7 +346,7 @@ namespace Avalonia.Controls if (shouldBeMirroredIfRightToLeft) { shouldMirror = ShuoldApplyMirrorTransform(parentFD, thisFD); - if (this is PopupRoot && thisFD == FlowDirection.RightToLeft) + if (Parent is Popup && thisFD == FlowDirection.RightToLeft) { shouldMirror = true; } From d5fd84ebc018927aceaf365808efa3a49bb87fbe Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 15 Mar 2022 13:58:27 +0200 Subject: [PATCH 013/176] continue working --- samples/ControlCatalog/Pages/ScreenPage.cs | 2 +- src/Avalonia.Controls/Control.cs | 170 ++++++++++++++---- .../Presenters/TextPresenter.cs | 2 + src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.Controls/TextBox.cs | 2 - src/Avalonia.Controls/TopLevel.cs | 2 + .../Controls/CheckBox.xaml | 1 + src/Avalonia.Visuals/Visual.cs | 2 +- 8 files changed, 142 insertions(+), 41 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 1fc37d903d..f65566a1e9 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -79,6 +79,6 @@ namespace ControlCatalog.Pages Typeface.Default, 12, Brushes.Green); } - protected override bool ShouldBeMirroredIfRightToLeft() => false; + protected override bool ShouldGetMirrored() => false; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 16d6c3f4c5..9b41e9ee64 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,10 +67,17 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + + /// + /// Defines the property. + /// + public static new readonly StyledProperty RenderTransformProperty = + Visual.RenderTransformProperty.AddOwner(); + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; + private bool _hasMirrorTransform; /// /// Gets or sets the control's focus adorner. @@ -126,6 +133,21 @@ namespace Avalonia.Controls set => SetValue(FlowDirectionProperty, value); } + /// + public override ITransform? RenderTransform + { + get => base.RenderTransform; + set + { + if (_hasMirrorTransform) + { + value = MargeTransforms(MirrorTrasform(), value); + } + + base.RenderTransform = value; + } + } + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -312,76 +334,152 @@ namespace Avalonia.Controls static Control() { - AffectsArrange(FlowDirectionProperty); + //var m = new StyledPropertyMetadata(coerce: (s, e) => null); + //RenderTransformProperty.OverrideMetadata(m); + + //AffectsRender(FlowDirectionProperty); + //FlowDirectionProperty.Changed.AddClassHandler((s, e) => + //{ + // s.InvalidateFlowDirection(); + // foreach (var logical in LogicalTree.LogicalExtensions.GetLogicalDescendants(s)) + // { + // if (logical is Control control) + // { + // //if (control) + // //control.InvalidateFlowDirection(); + // } + // } + //}); } - private bool _mirrorApplied; - - protected virtual bool ShouldBeMirroredIfRightToLeft() + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (Parent is Control parent) - { - return parent.ShouldBeMirroredIfRightToLeft(); - } - else + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) { - return true; + // Avoid inherit value change to invoke this method + if (!GetBaseValue(FlowDirectionProperty, change.Priority).HasValue) + { + return; + } + + InvalidateFlowDirection(); } } - protected override void ArrangeCore(Rect finalRect) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.ArrangeCore(finalRect); + base.OnAttachedToVisualTree(e); + + InvalidateFlowDirection(); + } + protected override void OnAttachedToLogicalTree(LogicalTree.LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + //InvalidateFlowDirection(); + } + + protected virtual bool ShouldGetMirrored() => true; + + private void InvalidateFlowDirection() + { FlowDirection parentFD = FlowDirection.LeftToRight; FlowDirection thisFD = FlowDirection; - bool shouldBeMirroredIfRightToLeft = ShouldBeMirroredIfRightToLeft(); - if (Parent is Control control) + bool parentShouldGetMirrored = true; + bool thisShouldGetMirrored = ShouldGetMirrored(); + + if (((Visual)this).GetVisualParent() is Control control) { parentFD = control.FlowDirection; + parentShouldGetMirrored = control.ShouldGetMirrored(); } - - bool shouldMirror; - if (shouldBeMirroredIfRightToLeft) + else if (Parent is Control logicalControl) { - shouldMirror = ShuoldApplyMirrorTransform(parentFD, thisFD); - if (Parent is Popup && thisFD == FlowDirection.RightToLeft) - { - shouldMirror = true; - } + parentFD = logicalControl.FlowDirection; + parentShouldGetMirrored = logicalControl.ShouldGetMirrored(); + } + + bool shouldBeMirrored = thisFD == FlowDirection.RightToLeft && thisShouldGetMirrored; + bool parentMirrored = parentFD == FlowDirection.RightToLeft && parentShouldGetMirrored; + + bool shouldApplyMirrorTransform = shouldBeMirrored != parentMirrored; + + if (shouldApplyMirrorTransform) + { + AddMirrorTransform(); } else { - shouldMirror = ShuoldApplyMirrorTransform(parentFD, FlowDirection.LeftToRight); + RemoveMirrorTransform(); } - if (shouldMirror) + foreach (var visual in VisualChildren) { - ApplyMirrorTransform(); + if (visual is Control child) + { + child.InvalidateFlowDirection(); + } } - else + } + + private void AddMirrorTransform() + { + if (_hasMirrorTransform) { - //RenderTransform = null; + return; } + + var mirrorTransform = MirrorTrasform(); + + ITransform? finalTransform = mirrorTransform; + if (RenderTransform != null) + { + finalTransform = MargeTransforms(RenderTransform, mirrorTransform); + } + + RenderTransform = finalTransform; + _hasMirrorTransform = true; } - private void ApplyMirrorTransform() + private void RemoveMirrorTransform() { - if (_mirrorApplied) + if (!_hasMirrorTransform) { return; } - var transform = new MatrixTransform(new Avalonia.Matrix(-1, 0, 0, 1, 0.0, 0.0)); - RenderTransform = transform; - _mirrorApplied = true; + var mirrorTransform = MirrorTrasform(); + + ITransform? finalTransform = MargeTransforms(RenderTransform, mirrorTransform); + if (finalTransform!.Value == Matrix.Identity) + { + finalTransform = null; + } + + _hasMirrorTransform = false; + RenderTransform = finalTransform; } - internal static bool ShuoldApplyMirrorTransform(FlowDirection parentFD, FlowDirection thisFD) + static ITransform? MargeTransforms(ITransform? iTransform1, ITransform? iTransform2) { - return ((parentFD == FlowDirection.LeftToRight && thisFD == FlowDirection.RightToLeft) || - (parentFD == FlowDirection.RightToLeft && thisFD == FlowDirection.LeftToRight)); + // don't know how to marge ITransform + if (iTransform1 is Transform transform1 && iTransform2 is Transform transform2) + { + TransformGroup groupTransform = new TransformGroup(); + + groupTransform.Children.Add(transform1); + groupTransform.Children.Add(transform2); + + return groupTransform; + } + + return iTransform1; } + + static ITransform MirrorTrasform() => + new MatrixTransform(new Avalonia.Matrix(-1, 0, 0, 1, 0.0, 0.0)); } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 10ce31088a..858544a872 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -798,5 +798,7 @@ namespace Avalonia.Controls.Presenters } } } + + protected override bool ShouldGetMirrored() => false; } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index d29f094c38..a5b1d44dc8 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -613,6 +613,6 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected override bool ShouldBeMirroredIfRightToLeft() => false; + protected override bool ShouldGetMirrored() => false; } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 76f7a185fe..4d71717776 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1504,7 +1504,5 @@ namespace Avalonia.Controls } } } - - protected override bool ShouldBeMirroredIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index a4fe154515..da85824457 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -529,5 +529,7 @@ namespace Avalonia.Controls ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; + + protected override bool ShouldGetMirrored() => false; } } diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index ef28593711..66dc17a417 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -152,6 +152,7 @@ + diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 324b253a0f..fcb4298895 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -222,7 +222,7 @@ namespace Avalonia /// /// Gets or sets the render transform of the control. /// - public ITransform? RenderTransform + public virtual ITransform? RenderTransform { get { return GetValue(RenderTransformProperty); } set { SetValue(RenderTransformProperty, value); } From 86a4ee86fb7d6022e4386e69d1e923f530027a9a Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 15 Mar 2022 17:24:57 +0200 Subject: [PATCH 014/176] continure working --- src/Avalonia.Controls/Control.cs | 52 ++++++++------------------------ 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 9b41e9ee64..0b74516c33 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -332,39 +332,17 @@ namespace Avalonia.Controls } } - static Control() - { - //var m = new StyledPropertyMetadata(coerce: (s, e) => null); - //RenderTransformProperty.OverrideMetadata(m); - - //AffectsRender(FlowDirectionProperty); - //FlowDirectionProperty.Changed.AddClassHandler((s, e) => - //{ - // s.InvalidateFlowDirection(); - // foreach (var logical in LogicalTree.LogicalExtensions.GetLogicalDescendants(s)) - // { - // if (logical is Control control) - // { - // //if (control) - // //control.InvalidateFlowDirection(); - // } - // } - //}); - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == FlowDirectionProperty) { - // Avoid inherit value change to invoke this method - if (!GetBaseValue(FlowDirectionProperty, change.Priority).HasValue) + // Avoid inherited value change to call this method + if (GetBaseValue(FlowDirectionProperty, change.Priority).HasValue) { - return; + InvalidateFlowDirection(); } - - InvalidateFlowDirection(); } } @@ -375,14 +353,6 @@ namespace Avalonia.Controls InvalidateFlowDirection(); } - protected override void OnAttachedToLogicalTree(LogicalTree.LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - //InvalidateFlowDirection(); - } - - protected virtual bool ShouldGetMirrored() => true; - private void InvalidateFlowDirection() { FlowDirection parentFD = FlowDirection.LeftToRight; @@ -391,7 +361,7 @@ namespace Avalonia.Controls bool parentShouldGetMirrored = true; bool thisShouldGetMirrored = ShouldGetMirrored(); - if (((Visual)this).GetVisualParent() is Control control) + if (this.GetVisualParent() is Control control) { parentFD = control.FlowDirection; parentShouldGetMirrored = control.ShouldGetMirrored(); @@ -433,14 +403,15 @@ namespace Avalonia.Controls } var mirrorTransform = MirrorTrasform(); + var rendertransform = RenderTransform; ITransform? finalTransform = mirrorTransform; - if (RenderTransform != null) + if (rendertransform != null) { - finalTransform = MargeTransforms(RenderTransform, mirrorTransform); + finalTransform = MargeTransforms(rendertransform, mirrorTransform); } - RenderTransform = finalTransform; + base.RenderTransform = finalTransform; _hasMirrorTransform = true; } @@ -452,17 +423,20 @@ namespace Avalonia.Controls } var mirrorTransform = MirrorTrasform(); + var rendertransform = RenderTransform; - ITransform? finalTransform = MargeTransforms(RenderTransform, mirrorTransform); + ITransform? finalTransform = MargeTransforms(rendertransform, mirrorTransform); if (finalTransform!.Value == Matrix.Identity) { finalTransform = null; } _hasMirrorTransform = false; - RenderTransform = finalTransform; + base.RenderTransform = finalTransform; } + protected virtual bool ShouldGetMirrored() => true; + static ITransform? MargeTransforms(ITransform? iTransform1, ITransform? iTransform2) { // don't know how to marge ITransform From 031ff2f49888076424636c6c8473eb039f86c1a0 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 15 Mar 2022 17:34:09 +0200 Subject: [PATCH 015/176] continue --- src/Avalonia.Controls/Image.cs | 2 ++ src/Avalonia.Themes.Default/Controls/CheckBox.xaml | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 3d67880638..cbde2bd338 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -127,5 +127,7 @@ namespace Avalonia.Controls return new Size(); } } + + protected override bool ShouldGetMirrored() => false; } } diff --git a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml index 75d6f853be..6cb991ba1b 100644 --- a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml @@ -26,6 +26,7 @@ Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" + FlowDirection="LeftToRight" Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/> Date: Wed, 16 Mar 2022 11:07:04 +0200 Subject: [PATCH 016/176] avoid re-calling the method --- samples/ControlCatalog/Pages/ScreenPage.cs | 2 +- src/Avalonia.Controls/Control.cs | 30 +++++++++++-------- src/Avalonia.Controls/Image.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index f65566a1e9..14e9587ca4 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -79,6 +79,6 @@ namespace ControlCatalog.Pages Typeface.Default, 12, Brushes.Green); } - protected override bool ShouldGetMirrored() => false; + protected override bool ShouldGetInvertedIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 0b74516c33..a1b5ced48b 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -338,10 +338,12 @@ namespace Avalonia.Controls if (change.Property == FlowDirectionProperty) { - // Avoid inherited value change to call this method + // A change in value inherited should be prevented from calling this method + // Because it will be handled from here by NotifyDescendantFlowDirection if (GetBaseValue(FlowDirectionProperty, change.Priority).HasValue) { InvalidateFlowDirection(); + NotifyDescendantFlowDirection(); } } } @@ -359,17 +361,13 @@ namespace Avalonia.Controls FlowDirection thisFD = FlowDirection; bool parentShouldGetMirrored = true; - bool thisShouldGetMirrored = ShouldGetMirrored(); + bool thisShouldGetMirrored = ShouldGetInvertedIfRightToLeft(); - if (this.GetVisualParent() is Control control) + var parent = this.FindAncestorOfType(); + if (parent != null) { - parentFD = control.FlowDirection; - parentShouldGetMirrored = control.ShouldGetMirrored(); - } - else if (Parent is Control logicalControl) - { - parentFD = logicalControl.FlowDirection; - parentShouldGetMirrored = logicalControl.ShouldGetMirrored(); + parentFD = parent.FlowDirection; + parentShouldGetMirrored = parent.ShouldGetInvertedIfRightToLeft(); } bool shouldBeMirrored = thisFD == FlowDirection.RightToLeft && thisShouldGetMirrored; @@ -385,8 +383,11 @@ namespace Avalonia.Controls { RemoveMirrorTransform(); } + } - foreach (var visual in VisualChildren) + private void NotifyDescendantFlowDirection() + { + foreach (var visual in this.GetVisualDescendants()) { if (visual is Control child) { @@ -435,7 +436,12 @@ namespace Avalonia.Controls base.RenderTransform = finalTransform; } - protected virtual bool ShouldGetMirrored() => true; + + /// + /// Determines whether the element should be inverted if the + /// flow direction is RightToLeft + /// + protected virtual bool ShouldGetInvertedIfRightToLeft() => true; static ITransform? MargeTransforms(ITransform? iTransform1, ITransform? iTransform2) { diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index cbde2bd338..621bf61a1d 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -128,6 +128,6 @@ namespace Avalonia.Controls } } - protected override bool ShouldGetMirrored() => false; + protected override bool ShouldGetInvertedIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 858544a872..6d566a710c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -799,6 +799,6 @@ namespace Avalonia.Controls.Presenters } } - protected override bool ShouldGetMirrored() => false; + protected override bool ShouldGetInvertedIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 45915b134a..376665a5cd 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -613,6 +613,6 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected override bool ShouldGetMirrored() => false; + protected override bool ShouldGetInvertedIfRightToLeft() => false; } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index da85824457..4a66b8d6e6 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -530,6 +530,6 @@ namespace Avalonia.Controls ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; - protected override bool ShouldGetMirrored() => false; + protected override bool ShouldGetInvertedIfRightToLeft() => false; } } From 0f5e78d70f2486edfa2b1d2798afcb2322439ede Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 16 Mar 2022 14:11:55 +0200 Subject: [PATCH 017/176] improve algorithm and typo fixes --- samples/ControlCatalog/Pages/ScreenPage.cs | 2 +- src/Avalonia.Controls/Control.cs | 38 +++++++++---------- src/Avalonia.Controls/Image.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 14e9587ca4..66e0ae6846 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -79,6 +79,6 @@ namespace ControlCatalog.Pages Typeface.Default, 12, Brushes.Green); } - protected override bool ShouldGetInvertedIfRightToLeft() => false; + protected override bool ShouldApplyMirrorTransform() => false; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a1b5ced48b..88e14adff1 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls { if (_hasMirrorTransform) { - value = MargeTransforms(MirrorTrasform(), value); + value = MergeTransforms(MirrorTrasform(), value); } base.RenderTransform = value; @@ -357,23 +357,16 @@ namespace Avalonia.Controls private void InvalidateFlowDirection() { - FlowDirection parentFD = FlowDirection.LeftToRight; - FlowDirection thisFD = FlowDirection; - - bool parentShouldGetMirrored = true; - bool thisShouldGetMirrored = ShouldGetInvertedIfRightToLeft(); + bool parentShouldGetMirrored = false; + bool thisShouldGetMirrored = ShouldPresentedMirrored(); var parent = this.FindAncestorOfType(); if (parent != null) { - parentFD = parent.FlowDirection; - parentShouldGetMirrored = parent.ShouldGetInvertedIfRightToLeft(); + parentShouldGetMirrored = parent.ShouldPresentedMirrored(); } - bool shouldBeMirrored = thisFD == FlowDirection.RightToLeft && thisShouldGetMirrored; - bool parentMirrored = parentFD == FlowDirection.RightToLeft && parentShouldGetMirrored; - - bool shouldApplyMirrorTransform = shouldBeMirrored != parentMirrored; + bool shouldApplyMirrorTransform = thisShouldGetMirrored != parentShouldGetMirrored; if (shouldApplyMirrorTransform) { @@ -404,12 +397,12 @@ namespace Avalonia.Controls } var mirrorTransform = MirrorTrasform(); - var rendertransform = RenderTransform; + var renderTransform = RenderTransform; ITransform? finalTransform = mirrorTransform; - if (rendertransform != null) + if (renderTransform != null) { - finalTransform = MargeTransforms(rendertransform, mirrorTransform); + finalTransform = MergeTransforms(renderTransform, mirrorTransform); } base.RenderTransform = finalTransform; @@ -424,9 +417,9 @@ namespace Avalonia.Controls } var mirrorTransform = MirrorTrasform(); - var rendertransform = RenderTransform; + var renderTransform = RenderTransform; - ITransform? finalTransform = MargeTransforms(rendertransform, mirrorTransform); + ITransform? finalTransform = MergeTransforms(renderTransform, mirrorTransform); if (finalTransform!.Value == Matrix.Identity) { finalTransform = null; @@ -438,12 +431,15 @@ namespace Avalonia.Controls /// - /// Determines whether the element should be inverted if the - /// flow direction is RightToLeft + /// Determines whether the element should be presented mirrored + /// by FlowDirection system /// - protected virtual bool ShouldGetInvertedIfRightToLeft() => true; + protected virtual bool ShouldPresentedMirrored() + { + return FlowDirection == FlowDirection.RightToLeft; + } - static ITransform? MargeTransforms(ITransform? iTransform1, ITransform? iTransform2) + static ITransform? MergeTransforms(ITransform? iTransform1, ITransform? iTransform2) { // don't know how to marge ITransform if (iTransform1 is Transform transform1 && iTransform2 is Transform transform2) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 621bf61a1d..2630c55d84 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -128,6 +128,6 @@ namespace Avalonia.Controls } } - protected override bool ShouldGetInvertedIfRightToLeft() => false; + protected override bool ShouldApplyMirrorTransform() => false; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 6d566a710c..bb31dcae10 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -799,6 +799,6 @@ namespace Avalonia.Controls.Presenters } } - protected override bool ShouldGetInvertedIfRightToLeft() => false; + protected override bool ShouldApplyMirrorTransform() => false; } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 376665a5cd..cc20850673 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -613,6 +613,6 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected override bool ShouldGetInvertedIfRightToLeft() => false; + protected override bool ShouldApplyMirrorTransform() => false; } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 4a66b8d6e6..1e1433e20e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -530,6 +530,6 @@ namespace Avalonia.Controls ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; - protected override bool ShouldGetInvertedIfRightToLeft() => false; + protected override bool ShouldApplyMirrorTransform() => false; } } From feec27e39fd1b8d1af3bd5dbbdee84713438d66c Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 16 Mar 2022 14:17:56 +0200 Subject: [PATCH 018/176] some fixes --- samples/ControlCatalog/Pages/ScreenPage.cs | 2 +- src/Avalonia.Controls/Image.cs | 2 +- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 66e0ae6846..d9fda2e70a 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -79,6 +79,6 @@ namespace ControlCatalog.Pages Typeface.Default, 12, Brushes.Green); } - protected override bool ShouldApplyMirrorTransform() => false; + protected override bool ShouldPresentedMirrored() => false; } } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 2630c55d84..b3275f6369 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -128,6 +128,6 @@ namespace Avalonia.Controls } } - protected override bool ShouldApplyMirrorTransform() => false; + protected override bool ShouldPresentedMirrored() => false; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bb31dcae10..e125acbcfa 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -799,6 +799,6 @@ namespace Avalonia.Controls.Presenters } } - protected override bool ShouldApplyMirrorTransform() => false; + protected override bool ShouldPresentedMirrored() => false; } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index cc20850673..624f0b671a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -613,6 +613,6 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected override bool ShouldApplyMirrorTransform() => false; + protected override bool ShouldPresentedMirrored() => false; } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1e1433e20e..71b8c09e28 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -530,6 +530,6 @@ namespace Avalonia.Controls ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; - protected override bool ShouldApplyMirrorTransform() => false; + protected override bool ShouldPresentedMirrored() => false; } } From 343163d3e4befe48bafe4df4f63c9651fed2959b Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 16 Mar 2022 14:25:20 +0200 Subject: [PATCH 019/176] typo --- src/Avalonia.Controls/Control.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 88e14adff1..9d22eaff18 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -432,7 +432,7 @@ namespace Avalonia.Controls /// /// Determines whether the element should be presented mirrored - /// by FlowDirection system + /// if FlowDirection is RightToLeft /// protected virtual bool ShouldPresentedMirrored() { From fb5f6d25465c1e1490e240add7514d64753624d0 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 16 Mar 2022 16:28:53 +0200 Subject: [PATCH 020/176] implement mergeTransforms --- src/Avalonia.Controls/Control.cs | 33 +++++++------------ src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 +- .../Media/TransformExtensions.cs | 18 ++++++++++ src/Avalonia.Visuals/Media/TransformGroup.cs | 18 ++++++++-- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 9d22eaff18..fd2f28f361 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -141,7 +141,14 @@ namespace Avalonia.Controls { if (_hasMirrorTransform) { - value = MergeTransforms(MirrorTrasform(), value); + if (value == null) + { + value = MirrorTrasform(); + } + else + { + value = MirrorTrasform().MergeTransforms(value); + } } base.RenderTransform = value; @@ -402,7 +409,7 @@ namespace Avalonia.Controls ITransform? finalTransform = mirrorTransform; if (renderTransform != null) { - finalTransform = MergeTransforms(renderTransform, mirrorTransform); + finalTransform = mirrorTransform.MergeTransforms(renderTransform); } base.RenderTransform = finalTransform; @@ -416,10 +423,10 @@ namespace Avalonia.Controls return; } - var mirrorTransform = MirrorTrasform(); - var renderTransform = RenderTransform; + ITransform mirrorTransform = MirrorTrasform(); + ITransform renderTransform = RenderTransform!; - ITransform? finalTransform = MergeTransforms(renderTransform, mirrorTransform); + ITransform? finalTransform = mirrorTransform.MergeTransforms(renderTransform); if (finalTransform!.Value == Matrix.Identity) { finalTransform = null; @@ -439,22 +446,6 @@ namespace Avalonia.Controls return FlowDirection == FlowDirection.RightToLeft; } - static ITransform? MergeTransforms(ITransform? iTransform1, ITransform? iTransform2) - { - // don't know how to marge ITransform - if (iTransform1 is Transform transform1 && iTransform2 is Transform transform2) - { - TransformGroup groupTransform = new TransformGroup(); - - groupTransform.Children.Add(transform1); - groupTransform.Children.Add(transform2); - - return groupTransform; - } - - return iTransform1; - } - static ITransform MirrorTrasform() => new MatrixTransform(new Avalonia.Matrix(-1, 0, 0, 1, 0.0, 0.0)); } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index b725993b44..b9b3d1906d 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -176,4 +176,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract. -Total Issues: 177 +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Media.Transforms' does not inherit from base type 'Avalonia.Collections.AvaloniaList' in the implementation but it does in the contract. +Total Issues: 178 diff --git a/src/Avalonia.Visuals/Media/TransformExtensions.cs b/src/Avalonia.Visuals/Media/TransformExtensions.cs index ccf2231ce2..faea406c6d 100644 --- a/src/Avalonia.Visuals/Media/TransformExtensions.cs +++ b/src/Avalonia.Visuals/Media/TransformExtensions.cs @@ -22,5 +22,23 @@ namespace Avalonia.Media return (transform as Transform)?.ToImmutable() ?? new ImmutableTransform(transform.Value); } + + /// + /// Merge two transforms to an one transform. + /// + /// The original transform. + /// The additional transform to be added. + /// + /// One ITransform that includes both. + /// + public static ITransform MergeTransforms(this ITransform srcTransform, ITransform additionalTransform) + { + TransformGroup groupTransform = new TransformGroup(); + + groupTransform.Children.Add(srcTransform); + groupTransform.Children.Add(additionalTransform); + + return groupTransform; + } } } diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 0465efd5a5..0c31d3fdcb 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -18,8 +18,20 @@ namespace Avalonia.Media Children.CollectionChanged += delegate { Children.ForEachItem( - (tr) => tr.Changed += ChildTransform_Changed, - (tr) => tr.Changed -= ChildTransform_Changed, + (tr) => + { + if (tr is IMutableTransform mutTr) + { + mutTr.Changed += ChildTransform_Changed; + } + }, + (tr) => + { + if (tr is IMutableTransform mutTr) + { + mutTr.Changed -= ChildTransform_Changed; + } + }, () => { }); }; } @@ -61,7 +73,7 @@ namespace Avalonia.Media } } - public sealed class Transforms : AvaloniaList + public sealed class Transforms : AvaloniaList { } } From b025186af0dd59b867cccbd1c9bb772d561170a8 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 09:40:58 +0200 Subject: [PATCH 021/176] refactor mirror engine --- src/Avalonia.Controls/Control.cs | 85 ++----------------- src/Avalonia.Visuals/ApiCompatBaseline.txt | 5 +- .../Media/TransformExtensions.cs | 18 ---- src/Avalonia.Visuals/Media/TransformGroup.cs | 18 +--- .../Rendering/SceneGraph/SceneBuilder.cs | 6 ++ src/Avalonia.Visuals/Visual.cs | 23 ++++- src/Avalonia.Visuals/VisualExtensions.cs | 6 ++ src/Avalonia.Visuals/VisualTree/IVisual.cs | 5 ++ 8 files changed, 51 insertions(+), 115 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index fd2f28f361..f0d5b342e7 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,17 +67,10 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - - /// - /// Defines the property. - /// - public static new readonly StyledProperty RenderTransformProperty = - Visual.RenderTransformProperty.AddOwner(); private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; - private bool _hasMirrorTransform; /// /// Gets or sets the control's focus adorner. @@ -133,28 +126,6 @@ namespace Avalonia.Controls set => SetValue(FlowDirectionProperty, value); } - /// - public override ITransform? RenderTransform - { - get => base.RenderTransform; - set - { - if (_hasMirrorTransform) - { - if (value == null) - { - value = MirrorTrasform(); - } - else - { - value = MirrorTrasform().MergeTransforms(value); - } - } - - base.RenderTransform = value; - } - } - /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -375,13 +346,15 @@ namespace Avalonia.Controls bool shouldApplyMirrorTransform = thisShouldGetMirrored != parentShouldGetMirrored; + if (this is IRenderRoot) shouldApplyMirrorTransform = false; + if (shouldApplyMirrorTransform) { - AddMirrorTransform(); + IsMirrorTransform = true; } else { - RemoveMirrorTransform(); + IsMirrorTransform = false; } } @@ -396,57 +369,15 @@ namespace Avalonia.Controls } } - private void AddMirrorTransform() - { - if (_hasMirrorTransform) - { - return; - } - - var mirrorTransform = MirrorTrasform(); - var renderTransform = RenderTransform; - - ITransform? finalTransform = mirrorTransform; - if (renderTransform != null) - { - finalTransform = mirrorTransform.MergeTransforms(renderTransform); - } - - base.RenderTransform = finalTransform; - _hasMirrorTransform = true; - } - - private void RemoveMirrorTransform() - { - if (!_hasMirrorTransform) - { - return; - } - - ITransform mirrorTransform = MirrorTrasform(); - ITransform renderTransform = RenderTransform!; - - ITransform? finalTransform = mirrorTransform.MergeTransforms(renderTransform); - if (finalTransform!.Value == Matrix.Identity) - { - finalTransform = null; - } - - _hasMirrorTransform = false; - base.RenderTransform = finalTransform; - } - - /// - /// Determines whether the element should be presented mirrored - /// if FlowDirection is RightToLeft + /// Determines whether the element should be presented mirrored, this + /// method related to FlowDirection system and as return true if FlowDirection + /// is RightToLeft. For controls that want to avoid this behavior, it is + /// possible to override this method and return false. /// protected virtual bool ShouldPresentedMirrored() { return FlowDirection == FlowDirection.RightToLeft; } - - static ITransform MirrorTrasform() => - new MatrixTransform(new Avalonia.Matrix(-1, 0, 0, 1, 0.0, 0.0)); } } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index b9b3d1906d..300603b297 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -176,5 +176,6 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Media.Transforms' does not inherit from base type 'Avalonia.Collections.AvaloniaList' in the implementation but it does in the contract. -Total Issues: 178 +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.IsMirrorTransform' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.IsMirrorTransform.get()' is present in the implementation but not in the contract. +Total Issues: 179 diff --git a/src/Avalonia.Visuals/Media/TransformExtensions.cs b/src/Avalonia.Visuals/Media/TransformExtensions.cs index faea406c6d..ccf2231ce2 100644 --- a/src/Avalonia.Visuals/Media/TransformExtensions.cs +++ b/src/Avalonia.Visuals/Media/TransformExtensions.cs @@ -22,23 +22,5 @@ namespace Avalonia.Media return (transform as Transform)?.ToImmutable() ?? new ImmutableTransform(transform.Value); } - - /// - /// Merge two transforms to an one transform. - /// - /// The original transform. - /// The additional transform to be added. - /// - /// One ITransform that includes both. - /// - public static ITransform MergeTransforms(this ITransform srcTransform, ITransform additionalTransform) - { - TransformGroup groupTransform = new TransformGroup(); - - groupTransform.Children.Add(srcTransform); - groupTransform.Children.Add(additionalTransform); - - return groupTransform; - } } } diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 0c31d3fdcb..0465efd5a5 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -18,20 +18,8 @@ namespace Avalonia.Media Children.CollectionChanged += delegate { Children.ForEachItem( - (tr) => - { - if (tr is IMutableTransform mutTr) - { - mutTr.Changed += ChildTransform_Changed; - } - }, - (tr) => - { - if (tr is IMutableTransform mutTr) - { - mutTr.Changed -= ChildTransform_Changed; - } - }, + (tr) => tr.Changed += ChildTransform_Changed, + (tr) => tr.Changed -= ChildTransform_Changed, () => { }); }; } @@ -73,7 +61,7 @@ namespace Avalonia.Media } } - public sealed class Transforms : AvaloniaList + public sealed class Transforms : AvaloniaList { } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 63c22efc3f..4eb1c0528f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -195,6 +195,12 @@ namespace Avalonia.Rendering.SceneGraph renderTransform = (-offset) * visual.RenderTransform.Value * (offset); } + if (visual.IsMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + renderTransform *= mirrorMatrix; + } + m = renderTransform * m; using (contextImpl.BeginUpdate(node)) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index fcb4298895..80adfd31a8 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -68,6 +68,12 @@ namespace Avalonia public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsMirrorTransformProperty = + AvaloniaProperty.RegisterDirect(nameof(IsMirrorTransform), o => o.IsMirrorTransform); + /// /// Defines the property. /// @@ -96,6 +102,7 @@ namespace Avalonia private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private IVisual? _visualParent; + private bool _isMirrorTransform; /// /// Initializes static members of the class. @@ -107,7 +114,8 @@ namespace Avalonia ClipProperty, ClipToBoundsProperty, IsVisibleProperty, - OpacityProperty); + OpacityProperty, + IsMirrorTransformProperty); RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); ZIndexProperty.Changed.Subscribe(ZIndexChanged); } @@ -119,7 +127,7 @@ namespace Avalonia { // Disable transitions until we're added to the visual tree. DisableTransitions(); - + var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; visualChildren.Validate = visual => ValidateVisualChild(visual); @@ -219,10 +227,19 @@ namespace Avalonia set { SetValue(OpacityMaskProperty, value); } } + /// + /// Gets or sets a value indicating whether this control presented as mirror. + /// + public bool IsMirrorTransform + { + get { return _isMirrorTransform; } + protected set { SetAndRaise(IsMirrorTransformProperty, ref _isMirrorTransform, value); } + } + /// /// Gets or sets the render transform of the control. /// - public virtual ITransform? RenderTransform + public ITransform? RenderTransform { get { return GetValue(RenderTransformProperty); } set { SetValue(RenderTransformProperty, value); } diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs index ff8a515db3..b726c1f547 100644 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualExtensions.cs @@ -110,6 +110,12 @@ namespace Avalonia result *= renderTransform; } + if (v.IsMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0); + result *= mirrorMatrix; + } + var topLeft = v.Bounds.TopLeft; if (topLeft != default) diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index 97c4554de6..95fe665e89 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -75,6 +75,11 @@ namespace Avalonia.VisualTree /// IBrush? OpacityMask { get; set; } + /// + /// Gets a value indicating whether this control presented as mirror. + /// + bool IsMirrorTransform { get; } + /// /// Gets or sets the render transform of the control. /// From ce6c8297aac39f7aef2545393860a71fb61b2965 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 09:46:33 +0200 Subject: [PATCH 022/176] typo --- src/Avalonia.Controls/Control.cs | 2 +- src/Avalonia.Visuals/Visual.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index f0d5b342e7..e237eed67b 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 80adfd31a8..d266fbd42c 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -127,7 +127,7 @@ namespace Avalonia { // Disable transitions until we're added to the visual tree. DisableTransitions(); - + var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; visualChildren.Validate = visual => ValidateVisualChild(visual); From fea93b99ccf316fec5b04673503b0d79b410b298 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 09:48:50 +0200 Subject: [PATCH 023/176] typo --- src/Avalonia.Controls/Control.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index e237eed67b..91713f8ac4 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; @@ -371,7 +371,7 @@ namespace Avalonia.Controls /// /// Determines whether the element should be presented mirrored, this - /// method related to FlowDirection system and as return true if FlowDirection + /// method related to FlowDirection system and returns true if FlowDirection /// is RightToLeft. For controls that want to avoid this behavior, it is /// possible to override this method and return false. /// From 37370d0cc6b9c2099496c820bcc28bf30233cb6e Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 09:50:21 +0200 Subject: [PATCH 024/176] more fixes --- src/Avalonia.Controls/Control.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 91713f8ac4..8660e0a3af 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -346,8 +346,6 @@ namespace Avalonia.Controls bool shouldApplyMirrorTransform = thisShouldGetMirrored != parentShouldGetMirrored; - if (this is IRenderRoot) shouldApplyMirrorTransform = false; - if (shouldApplyMirrorTransform) { IsMirrorTransform = true; From aa6531b53c7ece44ffca20d2c23d3a4b88c37644 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 09:52:51 +0200 Subject: [PATCH 025/176] typo --- src/Avalonia.Controls/Control.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 8660e0a3af..bd0ae14b6d 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; From 0653521a103089cdba15c4dff867c67243d39788 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 10:33:02 +0200 Subject: [PATCH 026/176] some fixes --- src/Avalonia.Controls/Control.cs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index bd0ae14b6d..18ca1437f8 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; @@ -335,25 +335,18 @@ namespace Avalonia.Controls private void InvalidateFlowDirection() { - bool parentShouldGetMirrored = false; - bool thisShouldGetMirrored = ShouldPresentedMirrored(); + bool parentShouldPresentedMirrored = false; + bool thisShouldPresentedMirrored = ShouldPresentedMirrored(); var parent = this.FindAncestorOfType(); if (parent != null) { - parentShouldGetMirrored = parent.ShouldPresentedMirrored(); + parentShouldPresentedMirrored = parent.ShouldPresentedMirrored(); } - bool shouldApplyMirrorTransform = thisShouldGetMirrored != parentShouldGetMirrored; - - if (shouldApplyMirrorTransform) - { - IsMirrorTransform = true; - } - else - { - IsMirrorTransform = false; - } + bool shouldApplyMirrorTransform = thisShouldPresentedMirrored != parentShouldPresentedMirrored; + + IsMirrorTransform = shouldApplyMirrorTransform; } private void NotifyDescendantFlowDirection() From d78f8e495e7813efeaadbb28ba20a4d487cde1bd Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 10:34:07 +0200 Subject: [PATCH 027/176] typo --- src/Avalonia.Controls/Control.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 18ca1437f8..99bf77cf8c 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; @@ -345,7 +345,7 @@ namespace Avalonia.Controls } bool shouldApplyMirrorTransform = thisShouldPresentedMirrored != parentShouldPresentedMirrored; - + IsMirrorTransform = shouldApplyMirrorTransform; } From 1cc2de198482b5679df70fdcdadd999077f873f3 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 17 Mar 2022 12:15:57 +0200 Subject: [PATCH 028/176] calling InvalidateVisual() only when needed --- src/Avalonia.Controls/Control.cs | 2 ++ src/Avalonia.Visuals/Visual.cs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 99bf77cf8c..852b6e2580 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -322,6 +322,8 @@ namespace Avalonia.Controls { InvalidateFlowDirection(); NotifyDescendantFlowDirection(); + + InvalidateVisual(); } } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index d266fbd42c..8d06f3c65d 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -114,8 +114,7 @@ namespace Avalonia ClipProperty, ClipToBoundsProperty, IsVisibleProperty, - OpacityProperty, - IsMirrorTransformProperty); + OpacityProperty); RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); ZIndexProperty.Changed.Subscribe(ZIndexChanged); } From 55d03d94d795cd78d261a973658364132e2f2380 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 19 Mar 2022 16:54:15 -0400 Subject: [PATCH 029/176] Add new DropDownButton control --- src/Avalonia.Controls/DropDownButton.cs | 15 +++ .../Controls/Button.xaml | 4 +- .../Controls/DropDownButton.xaml | 95 +++++++++++++++++++ .../Controls/FluentControls.xaml | 1 + 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/DropDownButton.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml diff --git a/src/Avalonia.Controls/DropDownButton.cs b/src/Avalonia.Controls/DropDownButton.cs new file mode 100644 index 0000000000..8dc40f42af --- /dev/null +++ b/src/Avalonia.Controls/DropDownButton.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls +{ + /// + /// A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + /// + public class DropDownButton : Button + { + /// + /// Initializes a new instance of the class. + /// + public DropDownButton() + { + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 533fabfb44..80343c0e7b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -78,7 +78,7 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml new file mode 100644 index 0000000000..7c3a3a6756 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 16a6cc9c14..74acee68ff 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -12,6 +12,7 @@ + From 25fb47b4eef685107201855d07d440f9335446c3 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 19 Mar 2022 16:54:42 -0400 Subject: [PATCH 030/176] Button style formatting fixes --- src/Avalonia.Themes.Fluent/Controls/Button.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 80343c0e7b..4fb6b9659a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -4,7 +4,7 @@